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
15 changes: 15 additions & 0 deletions NatsDistributedCache.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "test\UnitTests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PerfTest", "util\PerfTest\PerfTest.csproj", "{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadmeExample", "util\ReadmeExample\ReadmeExample.csproj", "{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -103,6 +105,18 @@ Global
{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29}.Release|x64.Build.0 = Release|Any CPU
{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29}.Release|x86.ActiveCfg = Release|Any CPU
{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29}.Release|x86.Build.0 = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|x64.ActiveCfg = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|x64.Build.0 = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|x86.ActiveCfg = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Debug|x86.Build.0 = Debug|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|Any CPU.Build.0 = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|x64.ActiveCfg = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|x64.Build.0 = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|x86.ActiveCfg = Release|Any CPU
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -114,5 +128,6 @@ Global
{F73CD4BD-5689-4222-BC6D-99E9A536F8D0} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
{E2375A65-5A56-4708-997F-2311D9E23FB6} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
EndGlobalSection
EndGlobal
68 changes: 51 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,78 @@ A .NET 8+ library for integrating NATS as a distributed cache in ASP.NET Core ap
```csharp
// assuming an INatsConnection natsConnection
var kvContext = natsConnection.CreateKeyValueStoreContext();
await kvContext.CreateOrUpdateStoreAsync(
new NatsKVConfig("cache") { LimitMarkerTTL = TimeSpan.FromSeconds(1), });
await kvContext.CreateOrUpdateStoreAsync(new NatsKVConfig("cache") { LimitMarkerTTL = TimeSpan.FromSeconds(1) });
```

## Installation

```bash
# add NATS Distributed Cache
dotnet add package CodeCargo.Nats.DistributedCache

# optional - add full NATS.Net (NATS Distributed Cache uses a subset of NATS.Net dependencies)
dotnet add package NATS.Net

# optional - add HybridCache
dotnet add package Microsoft.Extensions.Caching.Hybrid
```

## Usage

See the [Full Example here](https://github.com/code-cargo/NatsDistributedCache/tree/main/util/ReadmeExample/Program.cs).
This is the portion for registering services:

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

var builder = WebApplication.CreateBuilder(args);
// Set the NATS URL, this normally comes from configuration
const string natsUrl = "nats://localhost:4222";

// Add a NATS connection
var natsOpts = NatsOpts.Default with { Url = "nats://localhost:4222" }
builder.Services.AddNats(opts => natsOpts);
// Create a host builder for a Console application
// For a Web Application you can use WebApplication.CreateBuilder(args)
var builder = Host.CreateDefaultBuilder(args);

// Add a NATS distributed cache
builder.Services.AddNatsDistributedCache(options =>
// Add services to the container
builder.ConfigureServices(services =>
{
options.BucketName = "cache";
// Add NATS client
services.AddNats(configureOpts: options => options with { Url = natsUrl });

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

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

// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory());

// Add other services as needed
});

// (Optional) Add HybridCache
var hybridCacheServices = builder.Services.AddHybridCache();
// Build the host
var host = builder.Build();

// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
natsOpts.SerializerRegistry.ToHybridCacheSerializerFactory());
// 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)
});

var app = builder.Build();
app.Run();
// Start the host
await host.RunAsync();
```

## Additional Resources
Expand Down
58 changes: 58 additions & 0 deletions util/ReadmeExample/Abbreviated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using CodeCargo.Nats.DistributedCache;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
using NATS.Client.Hosting;
using NATS.Client.KeyValueStore;
using NATS.Net;

// The abbreviated example put into the README.md
// Based on Program.cs
public static class Abbreviated
{
public static async Task Run(string[] args)
{
// 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 =>
{
options.BucketName = "cache";
});

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

// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory());

// Add other services as needed
});

// 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();
}
}
182 changes: 182 additions & 0 deletions util/ReadmeExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using CodeCargo.Nats.DistributedCache;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
using NATS.Client.Hosting;
using NATS.Client.KeyValueStore;
using NATS.Net;

// Timeouts
var aspireStartupTimeout = TimeSpan.FromSeconds(30);
var appStartupTimeout = TimeSpan.FromSeconds(30);
var appShutdownTimeout = TimeSpan.FromSeconds(10);

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

// Wait for the NATS resource to be healthy before proceeding
var resourceNotificationService = aspireApp.Services.GetRequiredService<ResourceNotificationService>();
using var startupCts = new CancellationTokenSource(aspireStartupTimeout);
await resourceNotificationService.WaitForResourceHealthyAsync("Nats", startupCts.Token);
Console.WriteLine("Aspire started");

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

// Create a host builder for a console application
var builder = Host.CreateDefaultBuilder(args);

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

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

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

// (Optional) Use NATS Serializer for HybridCache
hybridCacheServices.AddSerializerFactory(
NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory());
});

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

// Create KV store
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");

// Start the host
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)
{
// Ignore expected cancellation
}
});

try
{
// Wait for the host to start
await WaitForApplicationStartAsync(lifetime, appStartupTimeout);
Console.WriteLine("App started");

// Run the examples
await DistributedCacheExample(host.Services);
await HybridCacheExample(host.Services);

// Shut down gracefully
await appCts.CancelAsync();
await appTask;
}
finally
{
// Clean up resources
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();
}

return;

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)
{
// Application started successfully
}
}

static async Task DistributedCacheExample(IServiceProvider serviceProvider)
{
Console.WriteLine("------------------------------------------");
Console.WriteLine("DistributedCache example");
using var scope = serviceProvider.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();

// Set a value
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) });
Console.WriteLine($"Set value in cache: {value}");

// Retrieve the value
var retrievedValue = await cache.GetStringAsync(cacheKey);
Console.WriteLine($"Retrieved value from cache: {retrievedValue}");

// Remove the value
await cache.RemoveAsync(cacheKey);
Console.WriteLine("Removed value from cache");
}

static async Task HybridCacheExample(IServiceProvider serviceProvider)
{
Console.WriteLine("------------------------------------------");
Console.WriteLine("HybridCache example");
using var scope = serviceProvider.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<HybridCache>();

// Define key to use
const string key = "hybrid-cache-greeting";

// Use GetOrCreateAsync to either get the value from cache or create it if not present
var result = await cache.GetOrCreateAsync<string>(
key,
_ => ValueTask.FromResult("Hello from NATS Hybrid Cache!"),
new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(1) });
Console.WriteLine($"Got/created value from cache: {result}");

// Remove the value from cache
await cache.RemoveAsync(key);
Console.WriteLine("Removed value from cache");
}
20 changes: 20 additions & 0 deletions util/ReadmeExample/ReadmeExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
<OutputType>Exe</OutputType>
<RootNamespace>CodeCargo.Nats.DistributedCache.ReadmeExample</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.2.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
<PackageReference Include="NATS.Net" Version="2.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NatsDistributedCache\NatsDistributedCache.csproj" />
<ProjectReference Include="..\NatsAppHost\NatsAppHost.csproj" />
</ItemGroup>

</Project>
Loading