From 195d300778ad4b602c3aa21a5f6815aecd76c411 Mon Sep 17 00:00:00 2001 From: Caleb Lloyd Date: Sun, 18 May 2025 01:54:28 -0400 Subject: [PATCH 1/4] update docs and pass hc opts Signed-off-by: Caleb Lloyd --- README.md | 86 +++++++++++++------ .../NatsHybridCacheExtensions.cs | 13 ++- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 1be3603..a19a111 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -29,13 +30,26 @@ 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; @@ -43,39 +57,62 @@ 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(); +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(); var kvContext = natsConnection.CreateKeyValueStoreContext(); await kvContext.CreateOrUpdateStoreAsync(new NatsKVConfig("cache") @@ -83,7 +120,6 @@ await kvContext.CreateOrUpdateStoreAsync(new NatsKVConfig("cache") LimitMarkerTTL = TimeSpan.FromSeconds(1) }); -// Start the host await host.RunAsync(); ``` diff --git a/src/NatsHybridCache/NatsHybridCacheExtensions.cs b/src/NatsHybridCache/NatsHybridCacheExtensions.cs index d5e8c49..dc2a4a4 100644 --- a/src/NatsHybridCache/NatsHybridCacheExtensions.cs +++ b/src/NatsHybridCache/NatsHybridCacheExtensions.cs @@ -16,16 +16,23 @@ public static class NatsHybridCacheExtensions /// use the serializer registry from the configured . /// /// The to add services to. - /// An action to configure . + /// An action to configure . + /// An optional action to configure . /// If set, resolves a keyed instance. /// The configured . public static IHybridCacheBuilder AddNatsHybridCache( this IServiceCollection services, - Action configureOptions, + Action configureNatsOptions, + Action? 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(sp => { var natsConnection = connectionServiceKey == null From 1476b5eab86d9eb0cb47fe8d5dc1c26cc3876efb Mon Sep 17 00:00:00 2001 From: Caleb Lloyd Date: Sun, 18 May 2025 21:18:18 -0400 Subject: [PATCH 2/4] split files Signed-off-by: Caleb Lloyd --- util/ReadmeExample/DistributedCache.cs | 45 +++++++++++++++++ util/ReadmeExample/HybridCache.cs | 40 +++++++++++++++ util/ReadmeExample/Program.cs | 67 ++++++-------------------- 3 files changed, 101 insertions(+), 51 deletions(-) create mode 100644 util/ReadmeExample/DistributedCache.cs create mode 100644 util/ReadmeExample/HybridCache.cs diff --git a/util/ReadmeExample/DistributedCache.cs b/util/ReadmeExample/DistributedCache.cs new file mode 100644 index 0000000..e11b6aa --- /dev/null +++ b/util/ReadmeExample/DistributedCache.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace CodeCargo.ReadmeExample; + +public class DistributedCacheService +{ + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + + public DistributedCacheService(IDistributedCache cache, ILogger logger) + { + _cache = cache; + _logger = logger; + } + + public async Task Run() + { + _logger.LogInformation("------------------------------------------"); + _logger.LogInformation("DistributedCache example"); + + // CALLBACK: Begin SetStringAsync example + // 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) }); + _logger.LogInformation("Set value in cache: {Value}", value); + // CALLBACK: End SetStringAsync example + + // CALLBACK: Begin GetStringAsync example + // Retrieve the value + var retrievedValue = await _cache.GetStringAsync(cacheKey); + _logger.LogInformation("Retrieved value from cache: {Value}", retrievedValue); + // CALLBACK: End GetStringAsync example + + // CALLBACK: Begin RemoveAsync example + // Remove the value + await _cache.RemoveAsync(cacheKey); + _logger.LogInformation("Removed value from cache"); + // CALLBACK: End RemoveAsync example + } +} diff --git a/util/ReadmeExample/HybridCache.cs b/util/ReadmeExample/HybridCache.cs new file mode 100644 index 0000000..0c93745 --- /dev/null +++ b/util/ReadmeExample/HybridCache.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Logging; + +namespace CodeCargo.ReadmeExample; + +public class HybridCacheService +{ + private readonly HybridCache _cache; + private readonly ILogger _logger; + + public HybridCacheService(HybridCache cache, ILogger logger) + { + _cache = cache; + _logger = logger; + } + + public async Task Run() + { + _logger.LogInformation("------------------------------------------"); + _logger.LogInformation("HybridCache example"); + + // Define key to use + const string key = "hybrid-cache-greeting"; + + // CALLBACK: Begin GetOrCreateAsync example + // Use GetOrCreateAsync to either get the value from cache or create it if not present + var result = await _cache.GetOrCreateAsync( + key, + _ => ValueTask.FromResult("Hello from NATS Hybrid Cache!"), + new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(1) }); + _logger.LogInformation("Got/created value from cache: {Result}", result); + // CALLBACK: End GetOrCreateAsync example + + // CALLBACK: Begin RemoveAsync example + // Remove the value from cache + await _cache.RemoveAsync(key); + _logger.LogInformation("Removed value from cache"); + // CALLBACK: End RemoveAsync example + } +} diff --git a/util/ReadmeExample/Program.cs b/util/ReadmeExample/Program.cs index 535efbc..6192fc5 100644 --- a/util/ReadmeExample/Program.cs +++ b/util/ReadmeExample/Program.cs @@ -1,10 +1,12 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Testing; using CodeCargo.NatsDistributedCache; +using CodeCargo.ReadmeExample; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using NATS.Client.Core; using NATS.Client.Hosting; using NATS.Client.KeyValueStore; @@ -49,12 +51,16 @@ options.BucketName = "cache"; }); - // (Optional) Add HybridCache + // Add HybridCache var hybridCacheServices = services.AddHybridCache(); - // (Optional) Use NATS Serializer for HybridCache + // Use NATS Serializer for HybridCache hybridCacheServices.AddSerializerFactory( NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory()); + + // Register our cache services + services.AddScoped(); + services.AddScoped(); }); // Build the host @@ -90,9 +96,13 @@ await kvContext.CreateOrUpdateStoreAsync( await WaitForApplicationStartAsync(lifetime, appStartupTimeout); Console.WriteLine("App started"); - // Run the examples - await DistributedCacheExample(host.Services); - await HybridCacheExample(host.Services); + // Run the examples using the injected services + using var scope = host.Services.CreateScope(); + var distributedCacheService = scope.ServiceProvider.GetRequiredService(); + var hybridCacheService = scope.ServiceProvider.GetRequiredService(); + + await distributedCacheService.Run(); + await hybridCacheService.Run(); // Shut down gracefully await appCts.CancelAsync(); @@ -134,49 +144,4 @@ static async Task WaitForApplicationStartAsync(IHostApplicationLifetime lifetime } } -static async Task DistributedCacheExample(IServiceProvider serviceProvider) -{ - Console.WriteLine("------------------------------------------"); - Console.WriteLine("DistributedCache example"); - using var scope = serviceProvider.CreateScope(); - var cache = scope.ServiceProvider.GetRequiredService(); - - // 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(); - - // 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( - 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"); -} +// Methods have been moved to their respective service classes From bdb47e9bb797fb135c16b449b4c93e36557f9bc5 Mon Sep 17 00:00:00 2001 From: Caleb Lloyd Date: Sun, 18 May 2025 21:57:28 -0400 Subject: [PATCH 3/4] split files Signed-off-by: Caleb Lloyd --- util/ReadmeExample/DistributedCache.cs | 117 ++++++++++++++++++++ util/ReadmeExample/HybridCache.cs | 116 +++++++++++++++++++ util/ReadmeExample/Program.cs | 147 +------------------------ 3 files changed, 235 insertions(+), 145 deletions(-) diff --git a/util/ReadmeExample/DistributedCache.cs b/util/ReadmeExample/DistributedCache.cs index e11b6aa..e26929b 100644 --- a/util/ReadmeExample/DistributedCache.cs +++ b/util/ReadmeExample/DistributedCache.cs @@ -1,8 +1,122 @@ +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(); + var aspireApp = await aspireAppHost.BuildAsync(); + await aspireApp.StartAsync(); + + var resourceNotificationService = aspireApp.Services.GetRequiredService(); + 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(); + }); + + var host = builder.Build(); + var lifetime = host.Services.GetRequiredService(); + + Console.WriteLine("Creating KV store..."); + var natsConnection = host.Services.GetRequiredService(); + 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(); + 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; @@ -28,18 +142,21 @@ await _cache.SetStringAsync( value, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) }); _logger.LogInformation("Set value in cache: {Value}", value); + // CALLBACK: End SetStringAsync example // CALLBACK: Begin GetStringAsync example // Retrieve the value var retrievedValue = await _cache.GetStringAsync(cacheKey); _logger.LogInformation("Retrieved value from cache: {Value}", retrievedValue); + // CALLBACK: End GetStringAsync example // CALLBACK: Begin RemoveAsync example // Remove the value await _cache.RemoveAsync(cacheKey); _logger.LogInformation("Removed value from cache"); + // CALLBACK: End RemoveAsync example } } diff --git a/util/ReadmeExample/HybridCache.cs b/util/ReadmeExample/HybridCache.cs index 0c93745..144062c 100644 --- a/util/ReadmeExample/HybridCache.cs +++ b/util/ReadmeExample/HybridCache.cs @@ -1,8 +1,122 @@ +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Testing; +using CodeCargo.NatsHybridCache; using Microsoft.Extensions.Caching.Hybrid; +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 HybridCacheStartup +{ + 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(); + var aspireApp = await aspireAppHost.BuildAsync(); + await aspireApp.StartAsync(); + + var resourceNotificationService = aspireApp.Services.GetRequiredService(); + 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.AddNatsHybridCache(options => + { + options.BucketName = "cache"; + }); + + services.AddScoped(); + }); + + var host = builder.Build(); + var lifetime = host.Services.GetRequiredService(); + + Console.WriteLine("Creating KV store..."); + var natsConnection = host.Services.GetRequiredService(); + 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(); + 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 HybridCacheService { private readonly HybridCache _cache; @@ -29,12 +143,14 @@ public async Task Run() _ => ValueTask.FromResult("Hello from NATS Hybrid Cache!"), new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(1) }); _logger.LogInformation("Got/created value from cache: {Result}", result); + // CALLBACK: End GetOrCreateAsync example // CALLBACK: Begin RemoveAsync example // Remove the value from cache await _cache.RemoveAsync(key); _logger.LogInformation("Removed value from cache"); + // CALLBACK: End RemoveAsync example } } diff --git a/util/ReadmeExample/Program.cs b/util/ReadmeExample/Program.cs index 6192fc5..5f36385 100644 --- a/util/ReadmeExample/Program.cs +++ b/util/ReadmeExample/Program.cs @@ -1,147 +1,4 @@ -using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Testing; -using CodeCargo.NatsDistributedCache; using CodeCargo.ReadmeExample; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Hybrid; -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; -// 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(); -var aspireApp = await aspireAppHost.BuildAsync(); -await aspireApp.StartAsync(); - -// Wait for the NATS resource to be healthy before proceeding -var resourceNotificationService = aspireApp.Services.GetRequiredService(); -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"; - }); - - // Add HybridCache - var hybridCacheServices = services.AddHybridCache(); - - // Use NATS Serializer for HybridCache - hybridCacheServices.AddSerializerFactory( - NatsOpts.Default.SerializerRegistry.ToHybridCacheSerializerFactory()); - - // Register our cache services - services.AddScoped(); - services.AddScoped(); -}); - -// Build the host -var host = builder.Build(); -var lifetime = host.Services.GetRequiredService(); - -// Create KV store -Console.WriteLine("Creating KV store..."); -var natsConnection = host.Services.GetRequiredService(); -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 using the injected services - using var scope = host.Services.CreateScope(); - var distributedCacheService = scope.ServiceProvider.GetRequiredService(); - var hybridCacheService = scope.ServiceProvider.GetRequiredService(); - - await distributedCacheService.Run(); - await hybridCacheService.Run(); - - // 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 - } -} - -// Methods have been moved to their respective service classes +await DistributedCacheStartup.RunAsync(args); +await HybridCacheStartup.RunAsync(args); From e640afd1a7baf15e44b0b1ff62b9b758448310be Mon Sep 17 00:00:00 2001 From: Caleb Lloyd Date: Sun, 18 May 2025 22:05:05 -0400 Subject: [PATCH 4/4] remove extraneous comments Signed-off-by: Caleb Lloyd --- util/ReadmeExample/DistributedCache.cs | 12 ------------ util/ReadmeExample/HybridCache.cs | 9 --------- 2 files changed, 21 deletions(-) diff --git a/util/ReadmeExample/DistributedCache.cs b/util/ReadmeExample/DistributedCache.cs index e26929b..82f7ecf 100644 --- a/util/ReadmeExample/DistributedCache.cs +++ b/util/ReadmeExample/DistributedCache.cs @@ -133,8 +133,6 @@ public async Task Run() _logger.LogInformation("------------------------------------------"); _logger.LogInformation("DistributedCache example"); - // CALLBACK: Begin SetStringAsync example - // Set a value const string cacheKey = "distributed-cache-greeting"; const string value = "Hello from NATS Distributed Cache!"; await _cache.SetStringAsync( @@ -143,20 +141,10 @@ await _cache.SetStringAsync( new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) }); _logger.LogInformation("Set value in cache: {Value}", value); - // CALLBACK: End SetStringAsync example - - // CALLBACK: Begin GetStringAsync example - // Retrieve the value var retrievedValue = await _cache.GetStringAsync(cacheKey); _logger.LogInformation("Retrieved value from cache: {Value}", retrievedValue); - // CALLBACK: End GetStringAsync example - - // CALLBACK: Begin RemoveAsync example - // Remove the value await _cache.RemoveAsync(cacheKey); _logger.LogInformation("Removed value from cache"); - - // CALLBACK: End RemoveAsync example } } diff --git a/util/ReadmeExample/HybridCache.cs b/util/ReadmeExample/HybridCache.cs index 144062c..ef22e7c 100644 --- a/util/ReadmeExample/HybridCache.cs +++ b/util/ReadmeExample/HybridCache.cs @@ -133,24 +133,15 @@ public async Task Run() _logger.LogInformation("------------------------------------------"); _logger.LogInformation("HybridCache example"); - // Define key to use const string key = "hybrid-cache-greeting"; - // CALLBACK: Begin GetOrCreateAsync example - // Use GetOrCreateAsync to either get the value from cache or create it if not present var result = await _cache.GetOrCreateAsync( key, _ => ValueTask.FromResult("Hello from NATS Hybrid Cache!"), new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(1) }); _logger.LogInformation("Got/created value from cache: {Result}", result); - // CALLBACK: End GetOrCreateAsync example - - // CALLBACK: Begin RemoveAsync example - // Remove the value from cache await _cache.RemoveAsync(key); _logger.LogInformation("Removed value from cache"); - - // CALLBACK: End RemoveAsync example } }