Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 61 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[![NuGet Version](https://img.shields.io/nuget/v/CodeCargo.NatsDistributedCache?cacheSeconds=3600&color=516bf1)](https://www.nuget.org/packages/CodeCargo.NatsDistributedCache/)
[![CodeCargo.NatsDistributedCache](https://img.shields.io/nuget/v/CodeCargo.NatsDistributedCache?color=516bf1&label=CodeCargo.NatsDistributedCache)](https://www.nuget.org/packages/CodeCargo.NatsDistributedCache/) [![CodeCargo.NatsHybridCache](https://img.shields.io/nuget/v/CodeCargo.NatsHybridCache?color=516bf1&label=CodeCargo.NatsHybridCache)](https://www.nuget.org/packages/CodeCargo.NatsHybridCache/)

# CodeCargo.NatsDistributedCache

## Overview

A .NET 8+ library for integrating NATS as a distributed cache in ASP.NET Core applications. Supports the new HybridCache system for fast, scalable caching.
A .NET 8+ library for using NATS with `HybridCache` or as an `IDistributedCache` directly.

## Requirements

Expand All @@ -21,6 +21,7 @@ A .NET 8+ library for integrating NATS as a distributed cache in ASP.NET Core ap
```bash
# add NATS Distributed Cache
dotnet add package CodeCargo.NatsDistributedCache
dotnet add package CodeCargo.NatsHybridCache

# optional - add full NATS.Net (NATS Distributed Cache uses a subset of NATS.Net dependencies)
dotnet add package NATS.Net
Expand All @@ -29,61 +30,96 @@ dotnet add package NATS.Net
dotnet add package Microsoft.Extensions.Caching.Hybrid
```

## Usage
## Use with `HybridCache`

See the [Full Example here](https://github.com/code-cargo/NatsDistributedCache/tree/main/util/ReadmeExample/Program.cs).
This is the portion for registering services:
The `CodeCargo.NatsHybridCache` package provides an extension method that:

1. Adds the NATS `IDistributedCache`
2. Adds `HybridCache`
3. Configures `HybridCache` to use the NATs Connection's serializer registry

### Install

```bash
dotnet add package CodeCargo.NatsDistributedCache
dotnet add package CodeCargo.NatsHybridCache
dotnet add package NATS.Net
```

### Example

```csharp
using CodeCargo.NatsDistributedCache;
using CodeCargo.NatsHybridCache;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
using NATS.Client.Hosting;
using NATS.Client.KeyValueStore;
using NATS.Net;

// Set the NATS URL, this normally comes from configuration
const string natsUrl = "nats://localhost:4222";

// Create a host builder for a Console application
// For a Web Application you can use WebApplication.CreateBuilder(args)
var builder = Host.CreateDefaultBuilder(args);

// Add services to the container
builder.ConfigureServices(services =>
{
// Add NATS client
services.AddNats(configureOpts: options => options with { Url = natsUrl });

// Add a NATS distributed cache
services.AddNatsDistributedCache(options =>
services.AddNatsHybridCache(options =>
{
options.BucketName = "cache";
});
});

var host = builder.Build();
var natsConnection = host.Services.GetRequiredService<INatsConnection>();
var kvContext = natsConnection.CreateKeyValueStoreContext();
await kvContext.CreateOrUpdateStoreAsync(new NatsKVConfig("cache")
{
LimitMarkerTTL = TimeSpan.FromSeconds(1)
});

await host.RunAsync();
```

## Use `IDistributedCache` Directly

// (Optional) Add HybridCache
var hybridCacheServices = services.AddHybridCache();
### Install

// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory());
```bash
dotnet add package CodeCargo.NatsDistributedCache
dotnet add package NATS.Net
```

// Add other services as needed
### Example

```csharp
using CodeCargo.NatsDistributedCache;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
using NATS.Client.Hosting;
using NATS.Client.KeyValueStore;
using NATS.Net;

const string natsUrl = "nats://localhost:4222";
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices(services =>
{
services.AddNats(configureOpts: options => options with { Url = natsUrl });

services.AddNatsDistributedCache(options =>
{
options.BucketName = "cache";
});
});

// Build the host
var host = builder.Build();

// Ensure that the KV Store is created
var natsConnection = host.Services.GetRequiredService<INatsConnection>();
var kvContext = natsConnection.CreateKeyValueStoreContext();
await kvContext.CreateOrUpdateStoreAsync(new NatsKVConfig("cache")
{
LimitMarkerTTL = TimeSpan.FromSeconds(1)
});

// Start the host
await host.RunAsync();
```

Expand Down
13 changes: 10 additions & 3 deletions src/NatsHybridCache/NatsHybridCacheExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,23 @@ public static class NatsHybridCacheExtensions
/// use the serializer registry from the configured <see cref="INatsConnection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
/// <param name="configureOptions">An action to configure <see cref="NatsCacheOptions"/>.</param>
/// <param name="configureNatsOptions">An action to configure <see cref="NatsCacheOptions"/>.</param>
/// <param name="configureHybridCacheOptions">An optional action to configure <see cref="HybridCacheOptions"/>.</param>
/// <param name="connectionServiceKey">If set, resolves a keyed <see cref="INatsConnection"/> instance.</param>
/// <returns>The configured <see cref="IHybridCacheBuilder"/>.</returns>
public static IHybridCacheBuilder AddNatsHybridCache(
this IServiceCollection services,
Action<NatsCacheOptions> configureOptions,
Action<NatsCacheOptions> configureNatsOptions,
Action<HybridCacheOptions>? configureHybridCacheOptions = null,
object? connectionServiceKey = null)
{
services.AddNatsDistributedCache(configureOptions, connectionServiceKey);
services.AddNatsDistributedCache(configureNatsOptions, connectionServiceKey);
var builder = services.AddHybridCache();
if (configureHybridCacheOptions != null)
{
builder.Services.Configure(configureHybridCacheOptions);
}

builder.Services.AddSingleton<IHybridCacheSerializerFactory>(sp =>
{
var natsConnection = connectionServiceKey == null
Expand Down
150 changes: 150 additions & 0 deletions util/ReadmeExample/DistributedCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using CodeCargo.NatsDistributedCache;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.Hosting;
using NATS.Client.KeyValueStore;
using NATS.Net;

namespace CodeCargo.ReadmeExample;

public static class DistributedCacheStartup
{
public static async Task RunAsync(string[] args)
{
var aspireStartupTimeout = TimeSpan.FromSeconds(30);
var appStartupTimeout = TimeSpan.FromSeconds(30);
var appShutdownTimeout = TimeSpan.FromSeconds(10);

Console.WriteLine("Starting Aspire...");
var aspireAppHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.NatsAppHost>();
var aspireApp = await aspireAppHost.BuildAsync();
await aspireApp.StartAsync();

var resourceNotificationService = aspireApp.Services.GetRequiredService<ResourceNotificationService>();
using var startupCts = new CancellationTokenSource(aspireStartupTimeout);
await resourceNotificationService.WaitForResourceHealthyAsync("Nats", startupCts.Token);
Console.WriteLine("Aspire started");

var natsConnectionString = await aspireApp.GetConnectionStringAsync("Nats", cancellationToken: startupCts.Token);
if (string.IsNullOrEmpty(natsConnectionString))
{
throw new InvalidOperationException("Cannot find connection string for NATS");
}

var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices(services =>
{
services.AddNats(configureOpts: options => options with { Url = natsConnectionString });
services.AddNatsDistributedCache(options =>
{
options.BucketName = "cache";
});

services.AddScoped<DistributedCacheService>();
});

var host = builder.Build();
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();

Console.WriteLine("Creating KV store...");
var natsConnection = host.Services.GetRequiredService<INatsConnection>();
var kvContext = natsConnection.CreateKeyValueStoreContext();
await kvContext.CreateOrUpdateStoreAsync(
new NatsKVConfig("cache") { LimitMarkerTTL = TimeSpan.FromSeconds(1) }, startupCts.Token);
Console.WriteLine("KV store created");

Console.WriteLine("Starting app...");
using var appCts = new CancellationTokenSource();
var appTask = Task.Run(async () =>
{
try
{
await host.RunAsync(appCts.Token);
}
catch (OperationCanceledException) when (appCts.IsCancellationRequested)
{
}
});

try
{
await WaitForApplicationStartAsync(lifetime, appStartupTimeout);
Console.WriteLine("App started");

using var scope = host.Services.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<DistributedCacheService>();
await service.Run();

await appCts.CancelAsync();
await appTask;
}
finally
{
using var stopCts = new CancellationTokenSource(appShutdownTimeout);
try
{
Console.WriteLine("Stopping app...");
await aspireApp.StopAsync(stopCts.Token);
Console.WriteLine("App stopped");
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Error stopping app: {ex.Message}");
}

await aspireApp.DisposeAsync();
}
}

private static async Task WaitForApplicationStartAsync(IHostApplicationLifetime lifetime, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token,
lifetime.ApplicationStarted);
try
{
await Task.Delay(timeout, linkedCts.Token);
}
catch (OperationCanceledException) when (lifetime.ApplicationStarted.IsCancellationRequested)
{
}
}
}

public class DistributedCacheService
{
private readonly IDistributedCache _cache;
private readonly ILogger<DistributedCacheService> _logger;

public DistributedCacheService(IDistributedCache cache, ILogger<DistributedCacheService> logger)
{
_cache = cache;
_logger = logger;
}

public async Task Run()
{
_logger.LogInformation("------------------------------------------");
_logger.LogInformation("DistributedCache example");

const string cacheKey = "distributed-cache-greeting";
const string value = "Hello from NATS Distributed Cache!";
await _cache.SetStringAsync(
cacheKey,
value,
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) });
_logger.LogInformation("Set value in cache: {Value}", value);

var retrievedValue = await _cache.GetStringAsync(cacheKey);
_logger.LogInformation("Retrieved value from cache: {Value}", retrievedValue);

await _cache.RemoveAsync(cacheKey);
_logger.LogInformation("Removed value from cache");
}
}
Loading