GlacialCache.PostgreSQL 1.0.2-alpha1

This is a prerelease version of GlacialCache.PostgreSQL.
dotnet add package GlacialCache.PostgreSQL --version 1.0.2-alpha1
                    
NuGet\Install-Package GlacialCache.PostgreSQL -Version 1.0.2-alpha1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GlacialCache.PostgreSQL" Version="1.0.2-alpha1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GlacialCache.PostgreSQL" Version="1.0.2-alpha1" />
                    
Directory.Packages.props
<PackageReference Include="GlacialCache.PostgreSQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GlacialCache.PostgreSQL --version 1.0.2-alpha1
                    
#r "nuget: GlacialCache.PostgreSQL, 1.0.2-alpha1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GlacialCache.PostgreSQL@1.0.2-alpha1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GlacialCache.PostgreSQL&version=1.0.2-alpha1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=GlacialCache.PostgreSQL&version=1.0.2-alpha1&prerelease
                    
Install as a Cake Tool

GlacialCache.PostgreSQL

A high-performance, pluggable distributed cache provider for .NET using PostgreSQL as the backend.
Designed for modern, cloud-ready applications that need reliable, scalable caching with minimal configuration.

Documentation

  • Getting started: see docs/getting-started.md for a copy-pasteable ASP.NET Core setup.
  • Concepts: see docs/concepts.md for data model, expiration semantics, and cleanup strategy.
  • Configuration: see docs/configuration.md for a full breakdown of GlacialCachePostgreSQLOptions.
  • Architecture: see docs/architecture.md for component and background service design.
  • Troubleshooting: see docs/troubleshooting.md for common issues and concrete fixes.

Features

Drop-in replacement for IDistributedCache
Advanced expiration support: sliding and absolute expiration
Binary data support: Store any byte array efficiently
Production-ready: Comprehensive error handling and logging
Auto-cleanup: Automatic removal of expired entries
High performance: Optimized SQL queries with proper indexing
Thread-safe: Concurrent operations supported
Multi-framework: Supports .NET 6.0, 8.0, and 9.0
Azure Managed Identity: Automatic token refresh for Azure PostgreSQL
Configurable serialization: Choose between JSON and MemoryPack serializers

Installation

dotnet add package GlacialCache.PostgreSQL

Quick Start

1. Basic Configuration

using GlacialCache.PostgreSQL;

var builder = WebApplication.CreateBuilder(args);

// Add GlacialCache with connection string
builder.Services.AddGlacialCachePostgreSQL(
    "Host=localhost;Database=myapp;Username=postgres;Password=mypassword");

var app = builder.Build();

2. Advanced Configuration

builder.Services.AddGlacialCachePostgreSQL(options =>
{
    options.Connection.ConnectionString = "Host=localhost;Database=myapp;Username=postgres;Password=mypassword";
    options.Cache.TableName = "my_cache_entries";
    options.Cache.SchemaName = "cache";

    // Simplified maintenance configuration
    options.Maintenance.EnableAutomaticCleanup = true;
    options.Maintenance.CleanupInterval = TimeSpan.FromMinutes(15);
    options.Maintenance.MaxCleanupBatchSize = 500;

    options.Cache.DefaultSlidingExpiration = TimeSpan.FromMinutes(20);
    options.Cache.DefaultAbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);

    // Serializer configuration
    options.Cache.Serializer = SerializerType.MemoryPack; // or SerializerType.JsonBytes
});

Migration note: Previous previews exposed a GlacialCachePostgreSQLBuilder fluent API. Configure the cache by supplying an Action<GlacialCachePostgreSQLOptions> (as shown above) instead.

3. Using the Cache

public class ProductService
{
    private readonly IDistributedCache _cache;

    public ProductService(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<Product?> GetProductAsync(int id)
    {
        var key = $"product:{id}";

        // Try to get from cache
        var cachedBytes = await _cache.GetAsync(key);
        if (cachedBytes != null)
        {
            var json = Encoding.UTF8.GetString(cachedBytes);
            return JsonSerializer.Deserialize<Product>(json);
        }

        // Get from database
        var product = await _repository.GetProductAsync(id);
        if (product != null)
        {
            // Cache for 1 hour
            var productJson = JsonSerializer.Serialize(product);
            var bytes = Encoding.UTF8.GetBytes(productJson);

            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
                SlidingExpiration = TimeSpan.FromMinutes(15)
            };

            await _cache.SetAsync(key, bytes, options);
        }

        return product;
    }
}

Configuration Options

Option Description Default
ConnectionString PostgreSQL connection string Required
TableName Cache table name glacial_cache_entries
SchemaName Database schema public
Maintenance.EnableAutomaticCleanup Enable periodic cleanup true
Maintenance.CleanupInterval Cleanup frequency 30 minutes
Maintenance.MaxCleanupBatchSize Max items per cleanup batch 1000
DefaultSlidingExpiration Default sliding expiration null
DefaultAbsoluteExpirationRelativeToNow Default absolute expiration null

Database Schema

GlacialCache automatically creates the following table structure:

CREATE TABLE public.glacial_cache_entries (
    key VARCHAR(900) PRIMARY KEY,
    value BYTEA NOT NULL,
    absolute_expiration TIMESTAMPTZ,
    sliding_interval INTERVAL,
    next_expiration TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    value_type VARCHAR(255),
    value_size INTEGER GENERATED ALWAYS AS (OCTET_LENGTH(value)) STORED
);

-- Indexes for efficient cleanup
CREATE INDEX idx_glacial_cache_entries_absolute_expiration
ON public.glacial_cache_entries (absolute_expiration)
WHERE absolute_expiration IS NOT NULL;

CREATE INDEX idx_glacial_cache_entries_next_expiration
ON public.glacial_cache_entries (next_expiration);

CREATE INDEX idx_glacial_cache_entries_value_type
ON public.glacial_cache_entries (value_type)
WHERE value_type IS NOT NULL;

CREATE INDEX idx_glacial_cache_entries_value_size
ON public.glacial_cache_entries (value_size);

Serialization Options

GlacialCache supports two serialization strategies for complex objects while maintaining optimal performance for strings and byte arrays:

Serializer Types

Serializer Type Description Performance Use Case
MemoryPack Fast binary serialization (default) Highest High-performance applications, complex objects
JsonBytes JSON serialization with optimizations High Interoperability, debugging, simple objects

String and Byte Array Optimization

Both serializers include automatic optimizations:

  • Strings: Always use direct UTF-8 encoding (no serialization overhead)
  • Byte Arrays: Pass-through without modification
  • Complex Objects: Use configured serializer

Configuration Examples

// Use MemoryPack for maximum performance (default)
builder.Services.AddGlacialCachePostgreSQL(options =>
{
    options.Connection.ConnectionString = connectionString;
    options.Cache.Serializer = SerializerType.MemoryPack;
});

// Use JSON for better interoperability
builder.Services.AddGlacialCachePostgreSQL(options =>
{
    options.Connection.ConnectionString = connectionString;
    options.Cache.Serializer = SerializerType.JsonBytes;
});

Performance Characteristics

  • MemoryPack: ~22% faster serialization, smaller payload size
  • JSON: Human-readable, better debugging, cross-platform compatibility
  • String Optimization: Both serializers use UTF-8 encoding for strings
  • Byte Array Pass-through: Both serializers pass byte arrays unchanged

Performance Considerations

  • Connection Pooling: Uses Npgsql's built-in connection pooling
  • Async Operations: All operations are fully async
  • Efficient Cleanup: Background cleanup with configurable intervals
  • Optimized Queries: Uses prepared statements and proper indexing
  • Binary Storage: Direct byte array storage without unnecessary serialization

Examples

Simple String Caching

// Store a string
await _cache.SetStringAsync("greeting", "Hello, World!", TimeSpan.FromMinutes(5));

// Retrieve a string
var greeting = await _cache.GetStringAsync("greeting");

Object Caching with JSON

public static class DistributedCacheExtensions
{
    public static async Task SetObjectAsync<T>(
        this IDistributedCache cache,
        string key,
        T value,
        DistributedCacheEntryOptions? options = null)
    {
        var json = JsonSerializer.Serialize(value);
        var bytes = Encoding.UTF8.GetBytes(json);
        await cache.SetAsync(key, bytes, options ?? new DistributedCacheEntryOptions());
    }

    public static async Task<T?> GetObjectAsync<T>(
        this IDistributedCache cache,
        string key)
    {
        var bytes = await cache.GetAsync(key);
        if (bytes == null) return default;

        var json = Encoding.UTF8.GetString(bytes);
        return JsonSerializer.Deserialize<T>(json);
    }
}

// Usage
await _cache.SetObjectAsync("user:123", user, new DistributedCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(30)
});

var user = await _cache.GetObjectAsync<User>("user:123");

Custom Expiration Policies

// Absolute expiration
var absoluteOptions = new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
};

// Sliding expiration
var slidingOptions = new DistributedCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(15)
};

// Combined expiration
var combinedOptions = new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(4),
    SlidingExpiration = TimeSpan.FromMinutes(30)
};

Logging

GlacialCache uses Microsoft.Extensions.Logging for comprehensive logging:

builder.Services.AddLogging(config =>
{
    config.AddConsole().SetMinimumLevel(LogLevel.Information);
});

Log levels:

  • Information: Successful operations, cleanup statistics
  • Warning: Non-critical errors (cleanup failures, access time updates)
  • Error: Critical failures (connection issues, query failures)

Azure Managed Identity Support

GlacialCache supports Azure Managed Identity for secure PostgreSQL connections without storing credentials. This is especially useful for Azure-hosted applications where tokens expire every 24 hours.

Basic Azure Managed Identity Setup

// Simple configuration
builder.Services.AddGlacialCachePostgreSQLWithAzureManagedIdentity(
    baseConnectionString: "Host=your-server.postgres.database.azure.com;Database=yourdb;Username=your-username@your-server",
    resourceId: "https://ossrdbms-aad.database.windows.net"
);

Advanced Azure Managed Identity Configuration

builder.Services.AddGlacialCachePostgreSQLWithAzureManagedIdentity(
    azureOptions =>
    {
        azureOptions.BaseConnectionString = "Host=your-server.postgres.database.azure.com;Database=yourdb;Username=your-username@your-server";
        azureOptions.ResourceId = "https://ossrdbms-aad.database.windows.net";
        azureOptions.ClientId = "your-user-assigned-managed-identity-client-id"; // Optional
        azureOptions.TokenRefreshBuffer = TimeSpan.FromHours(1); // Refresh token 1 hour before expiration
        azureOptions.MaxRetryAttempts = 3;
        azureOptions.RetryDelay = TimeSpan.FromSeconds(1);
    },
    cacheOptions =>
    {
        cacheOptions.TableName = "app_cache";
        cacheOptions.SchemaName = "public";
        cacheOptions.Maintenance.CleanupInterval = TimeSpan.FromMinutes(5);
        cacheOptions.Maintenance.MaxCleanupBatchSize = 200;
        cacheOptions.DefaultSlidingExpiration = TimeSpan.FromMinutes(15);
    }
);

Azure Managed Identity Requirements

  1. Base Connection String: Must NOT include a password/token
  2. Azure Setup: Enable system-assigned or user-assigned managed identity
  3. Permissions: Grant managed identity access to PostgreSQL server
  4. Environment: Must run on Azure (App Service, VM, AKS, etc.)
  5. Network: Access to Azure Instance Metadata Service (IMDS)

Token Refresh Behavior

  • Automatic Refresh: Tokens are refreshed 1 hour before expiration (configurable)
  • Retry Logic: Failed token acquisition is retried with exponential backoff
  • Connection Pool: Pool is recreated when tokens are refreshed
  • Monitoring: Token refresh events are logged at Information level

Health Check Example

app.MapGet("/health/azure-cache", async (IDistributedCache cache) =>
{
    try
    {
        var testKey = $"health-check-{Guid.NewGuid()}";
        var testValue = DateTime.UtcNow.ToString("O");

        await cache.SetStringAsync(testKey, testValue, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1)
        });

        var retrievedValue = await cache.GetStringAsync(testKey);

        if (retrievedValue == testValue)
        {
            await cache.RemoveAsync(testKey);
            return Results.Ok(new { status = "healthy", message = "Azure Managed Identity cache is working" });
        }

        return Results.Problem("Cache value mismatch", statusCode: 500);
    }
    catch (Exception ex)
    {
        return Results.Problem($"Azure Managed Identity cache health check failed: {ex.Message}", statusCode: 500);
    }
});

Testing

Use the provided test container setup for integration tests:

[Fact]
public async Task CustomTest()
{
    using var postgres = new PostgreSqlBuilder()
        .WithImage("postgres:17-alpine")
        .Build();

    await postgres.StartAsync();

    var services = new ServiceCollection();
    services.AddGlacialCachePostgreSQL(postgres.GetConnectionString());

    var provider = services.BuildServiceProvider();
    var cache = provider.GetRequiredService<IDistributedCache>();

    // Your test logic here
}

License

MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2-alpha1 384 12/8/2025