diff --git a/src/CodeCargo.NatsDistributedCache/NatsCache.Log.cs b/src/CodeCargo.NatsDistributedCache/NatsCache.Log.cs
index 6560393..928964d 100644
--- a/src/CodeCargo.NatsDistributedCache/NatsCache.Log.cs
+++ b/src/CodeCargo.NatsDistributedCache/NatsCache.Log.cs
@@ -1,43 +1,24 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using Microsoft.Extensions.Logging;
namespace CodeCargo.NatsDistributedCache;
public partial class NatsCache
{
- private void LogException(Exception exception)
- {
- _logger.LogError(EventIds.Exception, exception, "An exception occurred in NATS KV store.");
- }
+ private void LogException(Exception exception) =>
+ _logger.LogError(EventIds.Exception, exception, "Exception in NatsCache");
- private void LogConnectionError(Exception exception)
- {
- _logger.LogError(EventIds.ConnectionError, exception, "Error connecting to NATS KV store.");
- }
+ private void LogConnected(string bucketName) =>
+ _logger.LogInformation(EventIds.Connected, "Connected to NATS KV bucket {bucketName}", bucketName);
- private void LogConnectionIssue()
- {
- _logger.LogWarning(EventIds.ConnectionIssue, "Connection issue with NATS KV store.");
- }
-
- private void LogConnected()
- {
- _logger.LogInformation(EventIds.Connected, "Connected to NATS KV store.");
- }
-
- private void LogUpdateFailed(string key)
- {
- _logger.LogDebug(EventIds.UpdateFailed, "Sliding expiration update failed for key {Key} due to optimistic concurrency control", key);
- }
+ private void LogUpdateFailed(string key) => _logger.LogDebug(
+ EventIds.UpdateFailed,
+ "Sliding expiration update failed for key {Key} due to optimistic concurrency control",
+ key);
private static class EventIds
{
- public static readonly EventId ConnectionIssue = new EventId(100, nameof(ConnectionIssue));
- public static readonly EventId ConnectionError = new EventId(101, nameof(ConnectionError));
- public static readonly EventId Connected = new EventId(102, nameof(Connected));
- public static readonly EventId UpdateFailed = new EventId(103, nameof(UpdateFailed));
- public static readonly EventId Exception = new EventId(104, nameof(Exception));
+ public static readonly EventId Connected = new(100, nameof(Connected));
+ public static readonly EventId UpdateFailed = new(101, nameof(UpdateFailed));
+ public static readonly EventId Exception = new(102, nameof(Exception));
}
}
diff --git a/src/CodeCargo.NatsDistributedCache/NatsCache.cs b/src/CodeCargo.NatsDistributedCache/NatsCache.cs
index 8afbab7..8c0f264 100644
--- a/src/CodeCargo.NatsDistributedCache/NatsCache.cs
+++ b/src/CodeCargo.NatsDistributedCache/NatsCache.cs
@@ -1,6 +1,3 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using System.Buffers;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Caching.Distributed;
@@ -19,10 +16,10 @@ namespace CodeCargo.NatsDistributedCache;
public class CacheEntry
{
[JsonPropertyName("absexp")]
- public string? AbsoluteExpiration { get; set; }
+ public DateTimeOffset? AbsoluteExpiration { get; set; }
[JsonPropertyName("sldexp")]
- public string? SlidingExpiration { get; set; }
+ public long? SlidingExpirationTicks { get; set; }
[JsonPropertyName("data")]
public byte[]? Data { get; set; }
@@ -31,34 +28,33 @@ public class CacheEntry
///
/// Distributed cache implementation using NATS Key-Value Store.
///
-public partial class NatsCache : IBufferDistributedCache, IDisposable
+public partial class NatsCache : IBufferDistributedCache
{
// Static JSON serializer for CacheEntry
- private static readonly NatsJsonContextSerializer _cacheEntrySerializer =
- new NatsJsonContextSerializer(CacheEntryJsonContext.Default);
+ private static readonly NatsJsonContextSerializer CacheEntrySerializer =
+ new(CacheEntryJsonContext.Default);
+ private readonly string _bucketName;
+ private readonly string _keyPrefix;
private readonly ILogger _logger;
- private readonly NatsCacheOptions _options;
- private readonly string _instanceName;
private readonly INatsConnection _natsConnection;
- private readonly SemaphoreSlim _connectionLock = new(initialCount: 1, maxCount: 1);
- private NatsKVStore? _kvStore;
- private bool _disposed;
+ private Lazy> _lazyKvStore;
public NatsCache(
IOptions optionsAccessor,
ILogger logger,
INatsConnection natsConnection)
{
- ArgumentNullException.ThrowIfNull(optionsAccessor);
- ArgumentNullException.ThrowIfNull(natsConnection);
-
- _options = optionsAccessor.Value;
+ var options = optionsAccessor.Value;
+ _bucketName = !string.IsNullOrWhiteSpace(options.BucketName)
+ ? options.BucketName
+ : throw new NullReferenceException("BucketName must be set");
+ _keyPrefix = string.IsNullOrEmpty(options.CacheKeyPrefix)
+ ? string.Empty
+ : options.CacheKeyPrefix.TrimEnd('.');
+ _lazyKvStore = CreateLazyKvStore();
_logger = logger;
_natsConnection = natsConnection;
- _instanceName = _options.InstanceName ?? string.Empty;
-
- // No need to connect immediately; will connect on-demand
}
public NatsCache(IOptions optionsAccessor, INatsConnection natsConnection)
@@ -67,23 +63,25 @@ public NatsCache(IOptions optionsAccessor, INatsConnection nat
}
///
- public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => SetAsync(key, value, options).GetAwaiter().GetResult();
+ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) =>
+ SetAsync(key, value, options).GetAwaiter().GetResult();
///
- public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
+ public async Task SetAsync(
+ string key,
+ byte[] value,
+ DistributedCacheEntryOptions options,
+ CancellationToken token = default)
{
- ArgumentNullException.ThrowIfNull(key);
- ArgumentNullException.ThrowIfNull(value);
- ArgumentNullException.ThrowIfNull(options);
- token.ThrowIfCancellationRequested();
-
var ttl = GetTtl(options);
var entry = CreateCacheEntry(value, options);
- var kvStore = await GetKVStore().ConfigureAwait(false);
+ var kvStore = await GetKvStore().ConfigureAwait(false);
try
{
- await kvStore.PutAsync(GetKeyPrefix(key), entry, ttl ?? default, _cacheEntrySerializer, token).ConfigureAwait(false);
+ // todo: remove cast after https://github.com/nats-io/nats.net/pull/852 is released
+ await ((NatsKVStore)kvStore).PutAsync(GetPrefixedKey(key), entry, ttl ?? TimeSpan.Zero, CacheEntrySerializer, token)
+ .ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -93,75 +91,51 @@ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOption
}
///
- public void Set(string key, ReadOnlySequence value, DistributedCacheEntryOptions options) => SetAsync(key, value, options).GetAwaiter().GetResult();
+ public void Set(string key, ReadOnlySequence value, DistributedCacheEntryOptions options) =>
+ SetAsync(key, value, options).AsTask().GetAwaiter().GetResult();
///
- public async ValueTask SetAsync(string key, ReadOnlySequence value, DistributedCacheEntryOptions options, CancellationToken token = default)
+ public async ValueTask SetAsync(
+ string key,
+ ReadOnlySequence value,
+ DistributedCacheEntryOptions options,
+ CancellationToken token = default)
{
- ArgumentNullException.ThrowIfNull(key);
- ArgumentNullException.ThrowIfNull(options);
- token.ThrowIfCancellationRequested();
-
var array = value.IsSingleSegment ? value.First.ToArray() : value.ToArray();
await SetAsync(key, array, options, token).ConfigureAwait(false);
}
///
- public void Remove(string key) => RemoveAsync(key, null, default).GetAwaiter().GetResult();
+ public void Remove(string key) => RemoveAsync(key, null).GetAwaiter().GetResult();
///
- public async Task RemoveAsync(string key, CancellationToken token = default) => await RemoveAsync(key, null, token).ConfigureAwait(false);
-
- ///
- /// Removes the value with the given key.
- ///
- /// A string identifying the requested value.
- /// Nats KV delete options
- /// Optional. The used to propagate notifications that the operation should be canceled.
- /// The that represents the asynchronous operation.
- public async Task RemoveAsync(string key, NatsKVDeleteOpts? natsKVDeleteOpts = null, CancellationToken token = default)
- {
- ArgumentNullException.ThrowIfNull(key);
- token.ThrowIfCancellationRequested();
-
- var kvStore = await GetKVStore().ConfigureAwait(false);
- await kvStore.DeleteAsync(GetKeyPrefix(key), natsKVDeleteOpts, cancellationToken: token).ConfigureAwait(false);
- }
+ public async Task RemoveAsync(string key, CancellationToken token = default) =>
+ await RemoveAsync(key, null, token).ConfigureAwait(false);
///
public void Refresh(string key) => RefreshAsync(key).GetAwaiter().GetResult();
///
- public async Task RefreshAsync(string key, CancellationToken token = default)
- {
- ArgumentNullException.ThrowIfNull(key);
- token.ThrowIfCancellationRequested();
-
- await GetAndRefreshAsync(key, getData: false, retry: true, token: token).ConfigureAwait(false);
- }
+ public Task RefreshAsync(string key, CancellationToken token = default) =>
+ GetAndRefreshAsync(key, getData: false, retry: true, token: token);
///
public byte[]? Get(string key) => GetAsync(key).GetAwaiter().GetResult();
///
- public async Task GetAsync(string key, CancellationToken token = default)
- {
- ArgumentNullException.ThrowIfNull(key);
- token.ThrowIfCancellationRequested();
-
- return await GetAndRefreshAsync(key, getData: true, retry: true, token: token).ConfigureAwait(false);
- }
+ public Task GetAsync(string key, CancellationToken token = default) =>
+ GetAndRefreshAsync(key, getData: true, retry: true, token: token);
///
- public bool TryGet(string key, IBufferWriter destination) => TryGetAsync(key, destination).GetAwaiter().GetResult();
+ public bool TryGet(string key, IBufferWriter destination) =>
+ TryGetAsync(key, destination).AsTask().GetAwaiter().GetResult();
///
- public async ValueTask TryGetAsync(string key, IBufferWriter destination, CancellationToken token = default)
+ public async ValueTask TryGetAsync(
+ string key,
+ IBufferWriter destination,
+ CancellationToken token = default)
{
- ArgumentNullException.ThrowIfNull(key);
- ArgumentNullException.ThrowIfNull(destination);
- token.ThrowIfCancellationRequested();
-
try
{
var result = await GetAsync(key, token).ConfigureAwait(false);
@@ -179,34 +153,9 @@ public async ValueTask TryGetAsync(string key, IBufferWriter destina
return false;
}
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-
// This is the method used by hybrid caching to determine if it should use the distributed instance
internal virtual bool IsHybridCacheActive() => false;
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- // Dispose managed state (managed objects)
- _connectionLock.Dispose();
- _kvStore = null; // Set to null to ensure we don't use it after dispose
- }
-
- // Free unmanaged resources (unmanaged objects) and override finalizer
- _disposed = true;
- }
-
private static TimeSpan? GetTtl(DistributedCacheEntryOptions options)
{
if (options.AbsoluteExpiration.HasValue && options.AbsoluteExpiration.Value <= DateTimeOffset.Now)
@@ -266,76 +215,55 @@ private static CacheEntry CreateCacheEntry(byte[] value, DistributedCacheEntryOp
absoluteExpiration = DateTimeOffset.Now.Add(options.AbsoluteExpirationRelativeToNow.Value);
}
- var cacheEntry = new CacheEntry { Data = value };
-
- if (absoluteExpiration.HasValue)
- {
- cacheEntry.AbsoluteExpiration = absoluteExpiration.Value.ToUnixTimeMilliseconds().ToString();
- }
-
- if (options.SlidingExpiration.HasValue)
+ var cacheEntry = new CacheEntry
{
- cacheEntry.SlidingExpiration = options.SlidingExpiration.Value.TotalMilliseconds.ToString();
- }
+ Data = value,
+ AbsoluteExpiration = absoluteExpiration,
+ SlidingExpirationTicks = options.SlidingExpiration?.Ticks
+ };
return cacheEntry;
}
- private string GetKeyPrefix(string key) => string.IsNullOrEmpty(_instanceName)
- ? key
- : _instanceName + ":" + key;
-
- private async Task GetKVStore()
- {
- if (_kvStore != null && !_disposed)
- {
- return _kvStore;
- }
+ private string GetPrefixedKey(string key) => string.IsNullOrEmpty(_keyPrefix)
+ ? key
+ : _keyPrefix + "." + key;
- await _connectionLock.WaitAsync().ConfigureAwait(false);
- try
+ private Lazy> CreateLazyKvStore() =>
+ new(async () =>
{
- if (_kvStore == null || _disposed)
+ try
{
- ObjectDisposedException.ThrowIf(_disposed, this);
- if (string.IsNullOrEmpty(_options.BucketName))
- {
- throw new InvalidOperationException("BucketName is required and cannot be null or empty.");
- }
-
- var jsContext = _natsConnection.CreateJetStreamContext();
- var kvContext = new NatsKVContext(jsContext);
- _kvStore = (NatsKVStore)await kvContext.GetStoreAsync(_options.BucketName).ConfigureAwait(false);
- if (_kvStore == null)
- {
- throw new InvalidOperationException("Failed to create NATS KV store");
- }
-
- LogConnected();
+ var kv = _natsConnection.CreateKeyValueStoreContext();
+ var store = await kv.GetStoreAsync(_bucketName);
+ LogConnected(_bucketName);
+ return store;
}
- }
- catch (Exception ex)
- {
- LogException(ex);
- throw;
- }
- finally
- {
- _connectionLock.Release();
- }
+ catch (Exception ex)
+ {
+ // Reset the lazy initializer on failure for next attempt
+ _lazyKvStore = CreateLazyKvStore();
- return _kvStore;
- }
+ LogException(ex);
+ throw;
+ }
+ });
- private async Task GetAndRefreshAsync(string key, bool getData, bool retry, CancellationToken token = default)
- {
- token.ThrowIfCancellationRequested();
+ private Task GetKvStore() => _lazyKvStore.Value;
- var kvStore = await GetKVStore().ConfigureAwait(false);
- var prefixedKey = GetKeyPrefix(key);
+ private async Task GetAndRefreshAsync(
+ string key,
+ bool getData,
+ bool retry,
+ CancellationToken token = default)
+ {
+ var kvStore = await GetKvStore().ConfigureAwait(false);
+ var prefixedKey = GetPrefixedKey(key);
try
{
- var natsResult = await kvStore.TryGetEntryAsync(prefixedKey, serializer: _cacheEntrySerializer, cancellationToken: token).ConfigureAwait(false);
+ var natsResult = await kvStore
+ .TryGetEntryAsync(prefixedKey, serializer: CacheEntrySerializer, cancellationToken: token)
+ .ConfigureAwait(false);
if (!natsResult.Success)
{
return null;
@@ -348,8 +276,7 @@ private async Task GetKVStore()
}
// Check absolute expiration
- if (kvEntry.Value.AbsoluteExpiration != null
- && DateTimeOffset.Now > DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(kvEntry.Value.AbsoluteExpiration)))
+ if (kvEntry.Value.AbsoluteExpiration != null && DateTimeOffset.Now > kvEntry.Value.AbsoluteExpiration)
{
// NatsKVWrongLastRevisionException is caught below
var natsDeleteOpts = new NatsKVDeleteOpts { Revision = kvEntry.Revision };
@@ -368,11 +295,9 @@ private async Task GetKVStore()
{
return await GetAndRefreshAsync(key, getData, retry: false, token).ConfigureAwait(false);
}
- else
- {
- LogException(ex);
- return null;
- }
+
+ LogException(ex);
+ return null;
}
catch (Exception ex)
{
@@ -381,22 +306,24 @@ private async Task GetKVStore()
}
}
- private async Task UpdateEntryExpirationAsync(NatsKVStore kvStore, string key, NatsKVEntry kvEntry, CancellationToken token)
+ private async Task UpdateEntryExpirationAsync(
+ INatsKVStore kvStore,
+ string key,
+ NatsKVEntry kvEntry,
+ CancellationToken token)
{
- if (kvEntry.Value?.SlidingExpiration == null)
+ if (kvEntry.Value?.SlidingExpirationTicks == null)
{
return;
}
// If we have a sliding expiration, use it as the TTL
- var ttl = TimeSpan.FromMilliseconds(long.Parse(kvEntry.Value.SlidingExpiration));
+ var ttl = TimeSpan.FromTicks(kvEntry.Value.SlidingExpirationTicks.Value);
// If we also have an absolute expiration, make sure we don't exceed it
if (kvEntry.Value.AbsoluteExpiration != null)
{
- var absoluteExpiration =
- DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(kvEntry.Value.AbsoluteExpiration));
- var remainingTime = absoluteExpiration - DateTimeOffset.Now;
+ var remainingTime = kvEntry.Value.AbsoluteExpiration.Value - DateTimeOffset.Now;
// Use the minimum of sliding window or remaining absolute time
if (remainingTime > TimeSpan.Zero && remainingTime < ttl)
@@ -410,15 +337,32 @@ private async Task UpdateEntryExpirationAsync(NatsKVStore kvStore, string key, N
// Use optimistic concurrency control with the last revision
try
{
- await kvStore.UpdateAsync(key, kvEntry.Value, kvEntry.Revision, ttl, serializer: _cacheEntrySerializer, cancellationToken: token).ConfigureAwait(false);
+ // todo: remove cast after https://github.com/nats-io/nats.net/pull/852 is released
+ await ((NatsKVStore)kvStore).UpdateAsync(
+ key,
+ kvEntry.Value,
+ kvEntry.Revision,
+ ttl,
+ serializer: CacheEntrySerializer,
+ cancellationToken: token).ConfigureAwait(false);
}
catch (NatsKVWrongLastRevisionException)
{
// Someone else updated it; that's fine, we'll get the latest version next time
- LogUpdateFailed(key.Replace(GetKeyPrefix(string.Empty), string.Empty));
+ LogUpdateFailed(key.Replace(GetPrefixedKey(string.Empty), string.Empty));
}
}
}
+
+ private async Task RemoveAsync(
+ string key,
+ NatsKVDeleteOpts? natsKvDeleteOpts = null,
+ CancellationToken token = default)
+ {
+ var kvStore = await GetKvStore().ConfigureAwait(false);
+ await kvStore.DeleteAsync(GetPrefixedKey(key), natsKvDeleteOpts, cancellationToken: token)
+ .ConfigureAwait(false);
+ }
}
[JsonSerializable(typeof(CacheEntry))]
diff --git a/src/CodeCargo.NatsDistributedCache/NatsCacheImpl.cs b/src/CodeCargo.NatsDistributedCache/NatsCacheImpl.cs
index ec1d3d5..6c8233a 100644
--- a/src/CodeCargo.NatsDistributedCache/NatsCacheImpl.cs
+++ b/src/CodeCargo.NatsDistributedCache/NatsCacheImpl.cs
@@ -1,6 +1,3 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -13,17 +10,20 @@ internal sealed class NatsCacheImpl : NatsCache
{
private readonly IServiceProvider _services;
- public NatsCacheImpl(IOptions optionsAccessor, ILogger logger, IServiceProvider services, INatsConnection natsConnection)
- : base(optionsAccessor, logger, natsConnection)
- {
+ public NatsCacheImpl(
+ IOptions optionsAccessor,
+ ILogger logger,
+ IServiceProvider services,
+ INatsConnection natsConnection)
+ : base(optionsAccessor, logger, natsConnection) =>
_services = services; // important: do not check for HybridCache here due to dependency - creates a cycle
- }
- public NatsCacheImpl(IOptions optionsAccessor, IServiceProvider services, INatsConnection natsConnection)
- : base(optionsAccessor, natsConnection)
- {
+ public NatsCacheImpl(
+ IOptions optionsAccessor,
+ IServiceProvider services,
+ INatsConnection natsConnection)
+ : base(optionsAccessor, natsConnection) =>
_services = services; // important: do not check for HybridCache here due to dependency - creates a cycle
- }
internal override bool IsHybridCacheActive()
=> _services.GetService() is not null;
diff --git a/src/CodeCargo.NatsDistributedCache/NatsCacheOptions.cs b/src/CodeCargo.NatsDistributedCache/NatsCacheOptions.cs
index d481be9..a5ed557 100644
--- a/src/CodeCargo.NatsDistributedCache/NatsCacheOptions.cs
+++ b/src/CodeCargo.NatsDistributedCache/NatsCacheOptions.cs
@@ -1,6 +1,3 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using Microsoft.Extensions.Options;
namespace CodeCargo.NatsDistributedCache
@@ -16,10 +13,10 @@ public class NatsCacheOptions : IOptions
public string? BucketName { get; set; }
///
- /// The NATS instance name. Allows partitioning a single backend cache for use with multiple apps/services.
- /// If set, the cache keys are prefixed with this value.
+ /// If set, all cache keys are prefixed with this value followed by a period.
+ /// Allows partitioning a single backend cache for use with multiple apps/services.
///
- public string? InstanceName { get; set; }
+ public string? CacheKeyPrefix { get; set; }
NatsCacheOptions IOptions.Value => this;
}
diff --git a/src/CodeCargo.NatsDistributedCache/NatsCacheServiceCollectionExtensions.cs b/src/CodeCargo.NatsDistributedCache/NatsCacheServiceCollectionExtensions.cs
index 0ed59e0..0b6e299 100644
--- a/src/CodeCargo.NatsDistributedCache/NatsCacheServiceCollectionExtensions.cs
+++ b/src/CodeCargo.NatsDistributedCache/NatsCacheServiceCollectionExtensions.cs
@@ -1,7 +1,5 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -18,31 +16,42 @@ public static class NatsCacheServiceCollectionExtensions
/// Adds NATS distributed caching services to the specified .
///
/// The to add services to.
- /// An to configure the provided
+ /// An to configure the provided
/// .
+ /// If set, used keyed service to resolve
/// The so that additional calls can be chained.
- public static IServiceCollection AddNatsDistributedCache(this IServiceCollection services, Action setupAction)
+ public static IServiceCollection AddNatsDistributedCache(
+ this IServiceCollection services,
+ Action configureOptions,
+ object? connectionServiceKey = null)
{
- ArgumentNullException.ThrowIfNull(services);
- ArgumentNullException.ThrowIfNull(setupAction);
-
services.AddOptions();
- services.Configure(setupAction);
+ services.Configure(configureOptions);
services.Add(ServiceDescriptor.Singleton(serviceProvider =>
{
var optionsAccessor = serviceProvider.GetRequiredService>();
var logger = serviceProvider.GetService>();
- var natsConnection = serviceProvider.GetRequiredService();
- if (logger != null)
- {
- return new NatsCacheImpl(optionsAccessor, logger, serviceProvider, natsConnection);
- }
+ var natsConnection = connectionServiceKey == null
+ ? serviceProvider.GetRequiredService()
+ : serviceProvider.GetRequiredKeyedService(connectionServiceKey);
- return new NatsCacheImpl(optionsAccessor, serviceProvider, natsConnection);
+ return logger != null
+ ? new NatsCacheImpl(optionsAccessor, logger, serviceProvider, natsConnection)
+ : new NatsCacheImpl(optionsAccessor, serviceProvider, natsConnection);
}));
return services;
}
+
+ ///
+ /// Creates an that uses the provided
+ /// to perform serialization.
+ ///
+ /// The instance
+ /// The instance
+ public static IHybridCacheSerializerFactory ToHybridCacheSerializerFactory(
+ this INatsSerializerRegistry serializerRegistry) =>
+ new NatsHybridCacheSerializerFactory(serializerRegistry);
}
}
diff --git a/src/CodeCargo.NatsDistributedCache/NatsHybridCacheSerializer.cs b/src/CodeCargo.NatsDistributedCache/NatsHybridCacheSerializer.cs
new file mode 100644
index 0000000..0bc9010
--- /dev/null
+++ b/src/CodeCargo.NatsDistributedCache/NatsHybridCacheSerializer.cs
@@ -0,0 +1,34 @@
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Caching.Hybrid;
+using NATS.Client.Core;
+
+namespace CodeCargo.NatsDistributedCache;
+
+public readonly struct NatsHybridCacheSerializer(INatsSerialize serializer, INatsDeserialize deserializer)
+ : IHybridCacheSerializer
+{
+ public T Deserialize(ReadOnlySequence source) => deserializer.Deserialize(source)!;
+
+ public void Serialize(T value, IBufferWriter target) => serializer.Serialize(target, value);
+}
+
+public readonly struct NatsHybridCacheSerializerFactory(INatsSerializerRegistry serializerRegistry)
+ : IHybridCacheSerializerFactory
+{
+ public bool TryCreateSerializer([NotNullWhen(true)] out IHybridCacheSerializer? serializer)
+ {
+ try
+ {
+ var natsSerializer = serializerRegistry.GetSerializer();
+ var natsDeserializer = serializerRegistry.GetDeserializer();
+ serializer = new NatsHybridCacheSerializer(natsSerializer, natsDeserializer);
+ return true;
+ }
+ catch (Exception)
+ {
+ serializer = null;
+ return false;
+ }
+ }
+}
diff --git a/test/IntegrationTests/Cache/NatsCacheSetAndRemoveTests.cs b/test/IntegrationTests/Cache/NatsCacheSetAndRemoveTests.cs
new file mode 100644
index 0000000..1b282e0
--- /dev/null
+++ b/test/IntegrationTests/Cache/NatsCacheSetAndRemoveTests.cs
@@ -0,0 +1,122 @@
+using System.Text;
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace CodeCargo.NatsDistributedCache.IntegrationTests.Cache;
+
+public class NatsCacheSetAndRemoveTests(NatsIntegrationFixture fixture) : TestBase(fixture)
+{
+ [Fact]
+ public void GetMissingKeyReturnsNull()
+ {
+ var key = MethodKey();
+ var result = Cache.Get(key);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void SetAndGetReturnsObject()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value);
+
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+ }
+
+ [Fact]
+ public void SetAndGetWorksWithCaseSensitiveKeys()
+ {
+ var key1 = MethodKey().ToUpper();
+ var key2 = key1.ToLower();
+ var value1 = new byte[] { 1 };
+ var value2 = new byte[] { 2 };
+
+ Cache.Set(key1, value1);
+ Cache.Set(key2, value2);
+
+ var result1 = Cache.Get(key1);
+ var result2 = Cache.Get(key2);
+
+ Assert.Equal(value1, result1);
+ Assert.Equal(value2, result2);
+ }
+
+ [Fact]
+ public void SetAlwaysOverwrites()
+ {
+ var key = MethodKey();
+ var value1 = new byte[] { 1 };
+ var value2 = new byte[] { 2 };
+
+ Cache.Set(key, value1);
+ Cache.Set(key, value2);
+
+ var result = Cache.Get(key);
+ Assert.Equal(value2, result);
+ }
+
+ [Fact]
+ public void RemoveRemoves()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value);
+ Cache.Remove(key);
+
+ var result = Cache.Get(key);
+ Assert.Null(result);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("abc")]
+ [InlineData("abc def ghi jkl mno pqr stu vwx yz!")]
+ public void SetGetNonNullString(string payload)
+ {
+ var key = MethodKey();
+ Cache.Remove(key); // known state
+ Assert.Null(Cache.Get(key)); // expect null
+ Cache.SetString(key, payload);
+
+ // check raw bytes
+ var raw = Cache.Get(key);
+ Assert.NotNull(raw);
+ Assert.Equal(Hex(payload), Hex(raw));
+
+ // check via string API
+ var value = Cache.GetString(key);
+ Assert.NotNull(value);
+ Assert.Equal(payload, value);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("abc")]
+ [InlineData("abc def ghi jkl mno pqr stu vwx yz!")]
+ public async Task SetGetNonNullStringAsync(string payload)
+ {
+ var key = MethodKey();
+ await Cache.RemoveAsync(key); // known state
+ Assert.Null(await Cache.GetAsync(key)); // expect null
+ await Cache.SetStringAsync(key, payload);
+
+ // check raw bytes
+ var raw = await Cache.GetAsync(key);
+ Assert.NotNull(raw);
+ Assert.Equal(Hex(payload), Hex(raw));
+
+ // check via string API
+ var value = await Cache.GetStringAsync(key);
+ Assert.NotNull(value);
+ Assert.Equal(payload, value);
+ }
+
+ private static string Hex(byte[] value) => BitConverter.ToString(value);
+
+ private static string Hex(string value) => Hex(Encoding.UTF8.GetBytes(value));
+}
diff --git a/test/IntegrationTests/Cache/TimeExpirationAsyncTests.cs b/test/IntegrationTests/Cache/TimeExpirationAsyncTests.cs
new file mode 100644
index 0000000..153d3c2
--- /dev/null
+++ b/test/IntegrationTests/Cache/TimeExpirationAsyncTests.cs
@@ -0,0 +1,121 @@
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace CodeCargo.NatsDistributedCache.IntegrationTests.Cache;
+
+public class TimeExpirationAsyncTests(NatsIntegrationFixture fixture) : TestBase(fixture)
+{
+ [Fact]
+ public async Task AbsoluteExpirationExpiresAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
+
+ var result = await Cache.GetAsync(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4 && result != null; i++)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5));
+ result = await Cache.GetAsync(key);
+ }
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task RelativeExpirationExpiresAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
+
+ var result = await Cache.GetAsync(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4 && result != null; i++)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5));
+ result = await Cache.GetAsync(key);
+ }
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task SlidingExpirationExpiresIfNotAccessedAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
+
+ var result = await Cache.GetAsync(key);
+ Assert.Equal(value, result);
+
+ await Task.Delay(TimeSpan.FromSeconds(3));
+
+ result = await Cache.GetAsync(key);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task SlidingExpirationRenewedByAccessAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
+
+ var result = await Cache.GetAsync(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 5; i++)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5));
+
+ result = await Cache.GetAsync(key);
+ Assert.NotNull(result);
+ Assert.Equal(value, result);
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(3));
+
+ result = await Cache.GetAsync(key);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public async Task SlidingExpirationRenewedByAccessUntilAbsoluteExpirationAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions()
+ .SetSlidingExpiration(TimeSpan.FromSeconds(1.1))
+ .SetAbsoluteExpiration(TimeSpan.FromSeconds(4)));
+
+ var setTime = DateTime.Now;
+ var result = await Cache.GetAsync(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4; i++)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5));
+
+ result = await Cache.GetAsync(key);
+ Assert.NotNull(result);
+ Assert.Equal(value, result);
+ }
+
+ while ((DateTime.Now - setTime).TotalSeconds < 4)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(0.5));
+ }
+
+ result = await Cache.GetAsync(key);
+ Assert.Null(result);
+ }
+}
diff --git a/test/IntegrationTests/Cache/TimeExpirationTests.cs b/test/IntegrationTests/Cache/TimeExpirationTests.cs
new file mode 100644
index 0000000..1765045
--- /dev/null
+++ b/test/IntegrationTests/Cache/TimeExpirationTests.cs
@@ -0,0 +1,121 @@
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace CodeCargo.NatsDistributedCache.IntegrationTests.Cache;
+
+public class TimeExpirationTests(NatsIntegrationFixture fixture) : TestBase(fixture)
+{
+ [Fact]
+ public void AbsoluteExpirationExpires()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
+
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4 && result != null; i++)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(0.5));
+ result = Cache.Get(key);
+ }
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void RelativeExpirationExpires()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
+
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4 && result != null; i++)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(0.5));
+ result = Cache.Get(key);
+ }
+
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void SlidingExpirationExpiresIfNotAccessed()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
+
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+
+ Thread.Sleep(TimeSpan.FromSeconds(3));
+
+ result = Cache.Get(key);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void SlidingExpirationRenewedByAccess()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
+
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 5; i++)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(0.5));
+
+ result = Cache.Get(key);
+ Assert.NotNull(result);
+ Assert.Equal(value, result);
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(3));
+
+ result = Cache.Get(key);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void SlidingExpirationRenewedByAccessUntilAbsoluteExpiration()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ Cache.Set(key, value, new DistributedCacheEntryOptions()
+ .SetSlidingExpiration(TimeSpan.FromSeconds(1.1))
+ .SetAbsoluteExpiration(TimeSpan.FromSeconds(4)));
+
+ var setTime = DateTime.Now;
+ var result = Cache.Get(key);
+ Assert.Equal(value, result);
+
+ for (var i = 0; i < 4; i++)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(0.5));
+
+ result = Cache.Get(key);
+ Assert.NotNull(result);
+ Assert.Equal(value, result);
+ }
+
+ while ((DateTime.Now - setTime).TotalSeconds < 4)
+ {
+ Thread.Sleep(TimeSpan.FromSeconds(0.5));
+ }
+
+ result = Cache.Get(key);
+ Assert.Null(result);
+ }
+}
diff --git a/test/IntegrationTests/CacheServiceExtensionsTests.cs b/test/IntegrationTests/CacheServiceExtensionsTests.cs
deleted file mode 100644
index 90a22e9..0000000
--- a/test/IntegrationTests/CacheServiceExtensionsTests.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Microsoft.Extensions.Caching.Distributed;
-
-namespace CodeCargo.NatsDistributedCache.IntegrationTests;
-
-[Collection(NatsCollection.Name)]
-public class CacheServiceExtensionsTests : TestBase
-{
- private readonly NatsIntegrationFixture _fixture;
-
- public CacheServiceExtensionsTests(NatsIntegrationFixture fixture)
- : base(fixture)
- {
- _fixture = fixture;
- }
-
- // All tests moved to UnitTests/CacheServiceExtensionsUnitTests.cs
- private class FakeDistributedCache : IDistributedCache
- {
- public byte[]? Get(string key) => throw new NotImplementedException();
-
- public Task GetAsync(string key, CancellationToken token = default) => throw new NotImplementedException();
-
- public void Refresh(string key) => throw new NotImplementedException();
-
- public Task RefreshAsync(string key, CancellationToken token = default) => throw new NotImplementedException();
-
- public void Remove(string key) => throw new NotImplementedException();
-
- public Task RemoveAsync(string key, CancellationToken token = default) => throw new NotImplementedException();
-
- public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => throw new NotImplementedException();
-
- public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => throw new NotImplementedException();
- }
-}
diff --git a/test/IntegrationTests/NatsCacheSetAndRemoveTests.cs b/test/IntegrationTests/NatsCacheSetAndRemoveTests.cs
deleted file mode 100644
index 3132767..0000000
--- a/test/IntegrationTests/NatsCacheSetAndRemoveTests.cs
+++ /dev/null
@@ -1,150 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Text;
-using Microsoft.Extensions.Caching.Distributed;
-
-namespace CodeCargo.NatsDistributedCache.IntegrationTests;
-
-[Collection(NatsCollection.Name)]
-public class NatsCacheSetAndRemoveTests : TestBase
-{
- public NatsCacheSetAndRemoveTests(NatsIntegrationFixture fixture)
- : base(fixture)
- {
- }
-
- [Fact]
- public void GetMissingKeyReturnsNull()
- {
- var cache = CreateCacheInstance();
- var key = "non-existent-key";
-
- var result = cache.Get(key);
- Assert.Null(result);
- }
-
- [Fact]
- public void SetAndGetReturnsObject()
- {
- var cache = CreateCacheInstance();
- var value = new byte[1];
- var key = "myKey";
-
- cache.Set(key, value);
-
- var result = cache.Get(key);
- Assert.Equal(value, result);
- }
-
- [Fact]
- public void SetAndGetWorksWithCaseSensitiveKeys()
- {
- var cache = CreateCacheInstance();
- var value1 = new byte[1] { 1 };
- var value2 = new byte[1] { 2 };
- var key1 = "myKey";
- var key2 = "Mykey";
-
- cache.Set(key1, value1);
- cache.Set(key2, value2);
-
- var result1 = cache.Get(key1);
- var result2 = cache.Get(key2);
-
- Assert.Equal(value1, result1);
- Assert.Equal(value2, result2);
- }
-
- [Fact]
- public void SetAlwaysOverwrites()
- {
- var cache = CreateCacheInstance();
- var value1 = new byte[1] { 1 };
- var value2 = new byte[1] { 2 };
- var key = "myKey";
-
- cache.Set(key, value1);
- cache.Set(key, value2);
-
- var result = cache.Get(key);
- Assert.Equal(value2, result);
- }
-
- [Fact]
- public void RemoveRemoves()
- {
- var cache = CreateCacheInstance();
- var value = new byte[1];
- var key = "myKey";
-
- cache.Set(key, value);
- cache.Remove(key);
-
- var result = cache.Get(key);
- Assert.Null(result);
- }
-
- // SetNullValueThrows test moved to UnitTests/NatsCacheSetAndRemoveUnitTests.cs
- [Theory]
- [InlineData("")]
- [InlineData(" ")]
- [InlineData("abc")]
- [InlineData("abc def ghi jkl mno pqr stu vwx yz!")]
- public void SetGetNonNullString(string payload)
- {
- var cache = CreateCacheInstance();
- var key = Me();
- cache.Remove(key); // known state
- Assert.Null(cache.Get(key)); // expect null
- cache.SetString(key, payload);
-
- // check raw bytes
- var raw = cache.Get(key);
- Assert.NotNull(raw);
- Assert.Equal(Hex(payload), Hex(raw));
-
- // check via string API
- var value = cache.GetString(key);
- Assert.NotNull(value);
- Assert.Equal(payload, value);
- }
-
- [Theory]
- [InlineData("")]
- [InlineData(" ")]
- [InlineData("abc")]
- [InlineData("abc def ghi jkl mno pqr stu vwx yz!")]
- public async Task SetGetNonNullStringAsync(string payload)
- {
- var cache = CreateCacheInstance();
- var key = Me();
- await cache.RemoveAsync(key); // known state
- Assert.Null(await cache.GetAsync(key)); // expect null
- await cache.SetStringAsync(key, payload);
-
- // check raw bytes
- var raw = await cache.GetAsync(key);
- Assert.NotNull(raw);
- Assert.Equal(Hex(payload), Hex(raw));
-
- // check via string API
- var value = await cache.GetStringAsync(key);
- Assert.NotNull(value);
- Assert.Equal(payload, value);
- }
-
- private static string Hex(byte[] value) => BitConverter.ToString(value);
-
- private static string Hex(string value) => Hex(Encoding.UTF8.GetBytes(value));
-
- private static string Me([CallerMemberName] string caller = "") => caller;
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- NatsConnection);
- }
-}
diff --git a/test/IntegrationTests/NatsIntegrationFixture.cs b/test/IntegrationTests/NatsIntegrationFixture.cs
index 754fe85..b4aaaea 100644
--- a/test/IntegrationTests/NatsIntegrationFixture.cs
+++ b/test/IntegrationTests/NatsIntegrationFixture.cs
@@ -13,6 +13,7 @@ namespace CodeCargo.NatsDistributedCache.IntegrationTests;
public class NatsIntegrationFixture : IAsyncLifetime
{
private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(30);
+ private readonly Dictionary _registeredServiceTypes = new();
private DistributedApplication? _app;
private int _disposed;
private ServiceProvider? _serviceProvider;
@@ -51,7 +52,14 @@ public async ValueTask InitializeAsync()
{
builder.AddConsole();
});
- services.AddNatsTestServices(natsConnectionString);
+ services.AddNatsTestClient(natsConnectionString);
+
+ // Track registered singleton services before building the service provider
+ foreach (var descriptor in services)
+ {
+ _registeredServiceTypes[descriptor.ServiceType] = descriptor.Lifetime;
+ }
+
_serviceProvider = services.BuildServiceProvider();
// create the KV store
@@ -65,7 +73,28 @@ await kvContext.CreateOrUpdateStoreAsync(
/// Configures the services with the NATS connection
///
/// The service collection to configure
- public void ConfigureServices(IServiceCollection services) => services.AddSingleton(NatsConnection);
+ public void ConfigureServices(IServiceCollection services)
+ {
+ if (_serviceProvider == null)
+ {
+ throw new InvalidOperationException("InitializeAsync must be called before ConfigureServices");
+ }
+
+ // Register all singleton services from our service provider
+ // Filter out open generic types only
+ foreach (var serviceType in _registeredServiceTypes
+ .Where(t => t.Value == ServiceLifetime.Singleton)
+ .Select(t => t.Key)
+ .Where(type => !type.IsGenericTypeDefinition))
+ {
+ // Get the service instance from our provider and register it with the provided services
+ var instance = _serviceProvider.GetService(serviceType);
+ if (instance != null)
+ {
+ services.AddSingleton(serviceType, instance);
+ }
+ }
+ }
///
/// Disposes the fixture by shutting down the NATS server
@@ -77,6 +106,11 @@ public async ValueTask DisposeAsync()
return;
}
+ if (_serviceProvider != null)
+ {
+ await _serviceProvider.DisposeAsync();
+ }
+
if (_app != null)
{
var stopCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
@@ -84,11 +118,6 @@ public async ValueTask DisposeAsync()
await _app.DisposeAsync();
}
- if (_serviceProvider != null)
- {
- await _serviceProvider.DisposeAsync();
- }
-
GC.SuppressFinalize(this);
}
}
diff --git a/test/IntegrationTests/TestBase.cs b/test/IntegrationTests/TestBase.cs
index 28c51ca..9d524af 100644
--- a/test/IntegrationTests/TestBase.cs
+++ b/test/IntegrationTests/TestBase.cs
@@ -1,13 +1,18 @@
+using System.Runtime.CompilerServices;
using CodeCargo.NatsDistributedCache.TestUtils.Services.Logging;
+using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
+using NATS.Client.JetStream.Models;
+using NATS.Net;
namespace CodeCargo.NatsDistributedCache.IntegrationTests;
///
/// Base class for NATS integration tests that provides test output logging and fixture access
///
-public abstract class TestBase : IAsyncDisposable
+[Collection(NatsCollection.Name)]
+public abstract class TestBase : IAsyncLifetime
{
private int _disposed;
@@ -19,7 +24,9 @@ protected TestBase(NatsIntegrationFixture fixture)
{
// Get the test output helper from the current test context
var testContext = TestContext.Current;
- var output = testContext.TestOutputHelper ?? throw new InvalidOperationException("TestOutputHelper was not available in the current test context");
+ var output = testContext.TestOutputHelper ??
+ throw new InvalidOperationException(
+ "TestOutputHelper was not available in the current test context");
// Create a service collection and configure logging
var services = new ServiceCollection();
@@ -32,6 +39,9 @@ protected TestBase(NatsIntegrationFixture fixture)
// Configure the service collection with NATS connection
fixture.ConfigureServices(services);
+ // Add the cache
+ services.AddNatsDistributedCache(options => options.BucketName = "cache");
+
// Build service provider
ServiceProvider = services.BuildServiceProvider();
}
@@ -47,7 +57,20 @@ protected TestBase(NatsIntegrationFixture fixture)
protected INatsConnection NatsConnection => ServiceProvider.GetRequiredService();
///
- /// Cleanup after the test
+ /// Gets the cache from the service provider
+ ///
+ protected IDistributedCache Cache => ServiceProvider.GetRequiredService();
+
+ ///
+ /// Purge stream before test run
+ ///
+ public virtual async ValueTask InitializeAsync() =>
+ await NatsConnection
+ .CreateJetStreamContext()
+ .PurgeStreamAsync("KV_cache", new StreamPurgeRequest(), TestContext.Current.CancellationToken);
+
+ ///
+ /// Dispose
///
public virtual async ValueTask DisposeAsync()
{
@@ -59,4 +82,9 @@ public virtual async ValueTask DisposeAsync()
await ServiceProvider.DisposeAsync();
GC.SuppressFinalize(this);
}
+
+ ///
+ /// Gets the key for the current test method
+ ///
+ protected string MethodKey([CallerMemberName] string caller = "") => caller;
}
diff --git a/test/IntegrationTests/TestHelpers/ExceptionAssert.cs b/test/IntegrationTests/TestHelpers/ExceptionAssert.cs
deleted file mode 100644
index 563026a..0000000
--- a/test/IntegrationTests/TestHelpers/ExceptionAssert.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace CodeCargo.NatsDistributedCache.IntegrationTests.TestHelpers;
-
-public static class ExceptionAssert
-{
- public static void ThrowsArgumentOutOfRange(
- Action testCode,
- string paramName,
- string message,
- object actualValue)
- {
- var ex = Assert.Throws(testCode);
-
- if (paramName is not null)
- {
- Assert.Equal(paramName, ex.ParamName);
- }
-
- if (message is not null)
- {
- Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
- }
-
- if (actualValue is not null)
- {
- Assert.Equal(actualValue, ex.ActualValue);
- }
- }
-}
diff --git a/test/IntegrationTests/TimeExpirationAsyncTests.cs b/test/IntegrationTests/TimeExpirationAsyncTests.cs
deleted file mode 100644
index e6c455f..0000000
--- a/test/IntegrationTests/TimeExpirationAsyncTests.cs
+++ /dev/null
@@ -1,176 +0,0 @@
-using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Caching.Distributed;
-
-namespace CodeCargo.NatsDistributedCache.IntegrationTests;
-
-[Collection(NatsCollection.Name)]
-public class TimeExpirationAsyncTests : TestBase
-{
- public TimeExpirationAsyncTests(NatsIntegrationFixture fixture)
- : base(fixture)
- {
- }
-
- // AbsoluteExpirationInThePastThrowsAsync test moved to UnitTests/TimeExpirationAsyncUnitTests.cs
- [Fact]
- public async Task AbsoluteExpirationExpiresAsync()
- {
- var cache = CreateCacheInstance();
- var key = await GetNameAndReset(cache);
- var value = new byte[1];
-
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
-
- var result = await cache.GetAsync(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4 && result != null; i++)
- {
- await Task.Delay(TimeSpan.FromSeconds(0.5));
- result = await cache.GetAsync(key);
- }
-
- Assert.Null(result);
- }
-
- // NegativeRelativeExpirationThrowsAsync test moved to UnitTests/TimeExpirationAsyncUnitTests.cs
-
- // ZeroRelativeExpirationThrowsAsync test moved to UnitTests/TimeExpirationAsyncUnitTests.cs
- [Fact]
- public async Task RelativeExpirationExpiresAsync()
- {
- var cache = CreateCacheInstance();
- var key = await GetNameAndReset(cache);
- var value = new byte[1];
-
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
-
- var result = await cache.GetAsync(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4 && result != null; i++)
- {
- await Task.Delay(TimeSpan.FromSeconds(0.5));
- result = await cache.GetAsync(key);
- }
-
- Assert.Null(result);
- }
-
- // NegativeSlidingExpirationThrowsAsync test moved to UnitTests/TimeExpirationAsyncUnitTests.cs
-
- // ZeroSlidingExpirationThrowsAsync test moved to UnitTests/TimeExpirationAsyncUnitTests.cs
- [Fact]
- public async Task SlidingExpirationExpiresIfNotAccessedAsync()
- {
- var cache = CreateCacheInstance();
- var key = await GetNameAndReset(cache);
- var value = new byte[1];
-
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
-
- var result = await cache.GetAsync(key);
- Assert.Equal(value, result);
-
- await Task.Delay(TimeSpan.FromSeconds(3));
-
- result = await cache.GetAsync(key);
- Assert.Null(result);
- }
-
- [Fact]
- public async Task SlidingExpirationRenewedByAccessAsync()
- {
- var cache = CreateCacheInstance();
- var key = await GetNameAndReset(cache);
- var value = new byte[1];
-
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
-
- var result = await cache.GetAsync(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 5; i++)
- {
- await Task.Delay(TimeSpan.FromSeconds(0.5));
-
- result = await cache.GetAsync(key);
- Assert.NotNull(result);
- Assert.Equal(value, result);
- }
-
- await Task.Delay(TimeSpan.FromSeconds(3));
-
- result = await cache.GetAsync(key);
- Assert.Null(result);
- }
-
- [Fact]
- public async Task SlidingExpirationRenewedByAccessUntilAbsoluteExpirationAsync()
- {
- var cache = CreateCacheInstance();
- var key = await GetNameAndReset(cache);
- var value = new byte[1];
-
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions()
- .SetSlidingExpiration(TimeSpan.FromSeconds(1.1))
- .SetAbsoluteExpiration(TimeSpan.FromSeconds(4)));
-
- var setTime = DateTime.Now;
- var result = await cache.GetAsync(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4; i++)
- {
- await Task.Delay(TimeSpan.FromSeconds(0.5));
-
- result = await cache.GetAsync(key);
- Assert.NotNull(result);
- Assert.Equal(value, result);
- }
-
- while ((DateTime.Now - setTime).TotalSeconds < 4)
- {
- await Task.Delay(TimeSpan.FromSeconds(0.5));
- }
-
- result = await cache.GetAsync(key);
- Assert.Null(result);
- }
-
- private static async Task GetNameAndReset(IDistributedCache cache, [CallerMemberName] string caller = "")
- {
- await cache.RemoveAsync(caller);
- return caller;
- }
-
- // async twin to ExceptionAssert.ThrowsArgumentOutOfRange
- private static async Task ThrowsArgumentOutOfRangeAsync(Func test, string paramName, string message, object actualValue)
- {
- var ex = await Assert.ThrowsAsync(test);
- if (paramName is not null)
- {
- Assert.Equal(paramName, ex.ParamName);
- }
-
- if (message is not null)
- {
- Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
- }
-
- if (actualValue is not null)
- {
- Assert.Equal(actualValue, ex.ActualValue);
- }
- }
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- NatsConnection);
- }
-}
diff --git a/test/IntegrationTests/TimeExpirationTests.cs b/test/IntegrationTests/TimeExpirationTests.cs
deleted file mode 100644
index 16e8cc9..0000000
--- a/test/IntegrationTests/TimeExpirationTests.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Caching.Distributed;
-
-namespace CodeCargo.NatsDistributedCache.IntegrationTests;
-
-[Collection(NatsCollection.Name)]
-public class TimeExpirationTests : TestBase
-{
- public TimeExpirationTests(NatsIntegrationFixture fixture)
- : base(fixture)
- {
- }
-
- // AbsoluteExpirationInThePastThrows test moved to UnitTests/TimeExpirationUnitTests.cs
- [Fact]
- public void AbsoluteExpirationExpires()
- {
- var cache = CreateCacheInstance();
- var key = GetNameAndReset(cache);
- var value = new byte[1];
-
- cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
-
- var result = cache.Get(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4 && result != null; i++)
- {
- Thread.Sleep(TimeSpan.FromSeconds(0.5));
- result = cache.Get(key);
- }
-
- Assert.Null(result);
- }
-
- // NegativeRelativeExpirationThrows test moved to UnitTests/TimeExpirationUnitTests.cs
-
- // ZeroRelativeExpirationThrows test moved to UnitTests/TimeExpirationUnitTests.cs
- [Fact]
- public void RelativeExpirationExpires()
- {
- var cache = CreateCacheInstance();
- var key = GetNameAndReset(cache);
- var value = new byte[1];
-
- cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1.1)));
-
- var result = cache.Get(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4 && result != null; i++)
- {
- Thread.Sleep(TimeSpan.FromSeconds(0.5));
- result = cache.Get(key);
- }
-
- Assert.Null(result);
- }
-
- // NegativeSlidingExpirationThrows test moved to UnitTests/TimeExpirationUnitTests.cs
-
- // ZeroSlidingExpirationThrows test moved to UnitTests/TimeExpirationUnitTests.cs
- [Fact]
- public void SlidingExpirationExpiresIfNotAccessed()
- {
- var cache = CreateCacheInstance();
- var key = GetNameAndReset(cache);
- var value = new byte[1];
-
- cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
-
- var result = cache.Get(key);
- Assert.Equal(value, result);
-
- Thread.Sleep(TimeSpan.FromSeconds(3));
-
- result = cache.Get(key);
- Assert.Null(result);
- }
-
- [Fact]
- public void SlidingExpirationRenewedByAccess()
- {
- var cache = CreateCacheInstance();
- var key = GetNameAndReset(cache);
- var value = new byte[1];
-
- cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
-
- var result = cache.Get(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 5; i++)
- {
- Thread.Sleep(TimeSpan.FromSeconds(0.5));
-
- result = cache.Get(key);
- Assert.NotNull(result);
- Assert.Equal(value, result);
- }
-
- Thread.Sleep(TimeSpan.FromSeconds(3));
-
- result = cache.Get(key);
- Assert.Null(result);
- }
-
- [Fact]
- public void SlidingExpirationRenewedByAccessUntilAbsoluteExpiration()
- {
- var cache = CreateCacheInstance();
- var key = GetNameAndReset(cache);
- var value = new byte[1];
-
- cache.Set(key, value, new DistributedCacheEntryOptions()
- .SetSlidingExpiration(TimeSpan.FromSeconds(1.1))
- .SetAbsoluteExpiration(TimeSpan.FromSeconds(4)));
-
- var setTime = DateTime.Now;
- var result = cache.Get(key);
- Assert.Equal(value, result);
-
- for (var i = 0; i < 4; i++)
- {
- Thread.Sleep(TimeSpan.FromSeconds(0.5));
-
- result = cache.Get(key);
- Assert.NotNull(result);
- Assert.Equal(value, result);
- }
-
- while ((DateTime.Now - setTime).TotalSeconds < 4)
- {
- Thread.Sleep(TimeSpan.FromSeconds(0.5));
- }
-
- result = cache.Get(key);
- Assert.Null(result);
- }
-
- private static string GetNameAndReset(IDistributedCache cache, [CallerMemberName] string caller = "")
- {
- cache.Remove(caller);
- return caller;
- }
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- NatsConnection);
- }
-}
diff --git a/test/IntegrationTests/packages.linux-x64.lock.json b/test/IntegrationTests/packages.linux-x64.lock.json
index 12b8e06..ac4796a 100644
--- a/test/IntegrationTests/packages.linux-x64.lock.json
+++ b/test/IntegrationTests/packages.linux-x64.lock.json
@@ -1066,7 +1066,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.linux-x64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.linux-x64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.linux-x64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {
diff --git a/test/IntegrationTests/packages.osx-arm64.lock.json b/test/IntegrationTests/packages.osx-arm64.lock.json
index 7bdc2d8..e536f38 100644
--- a/test/IntegrationTests/packages.osx-arm64.lock.json
+++ b/test/IntegrationTests/packages.osx-arm64.lock.json
@@ -1066,7 +1066,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.osx-arm64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.osx-arm64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.osx-arm64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {
diff --git a/test/IntegrationTests/packages.win-x64.lock.json b/test/IntegrationTests/packages.win-x64.lock.json
index 6f377cb..b33a4d2 100644
--- a/test/IntegrationTests/packages.win-x64.lock.json
+++ b/test/IntegrationTests/packages.win-x64.lock.json
@@ -1066,7 +1066,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.win-x64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.win-x64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.win-x64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {
diff --git a/test/TestUtils/Assertions/ExceptionAssert.cs b/test/TestUtils/Assertions/ExceptionAssert.cs
new file mode 100644
index 0000000..9910482
--- /dev/null
+++ b/test/TestUtils/Assertions/ExceptionAssert.cs
@@ -0,0 +1,53 @@
+using Xunit;
+
+namespace CodeCargo.NatsDistributedCache.TestUtils.Assertions;
+
+public static class ExceptionAssert
+{
+ public static void ThrowsArgumentOutOfRange(
+ Action testCode,
+ string paramName,
+ string message,
+ object actualValue)
+ {
+ var ex = Assert.Throws(testCode);
+
+ if (paramName is not null)
+ {
+ Assert.Equal(paramName, ex.ParamName);
+ }
+
+ if (message is not null)
+ {
+ Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
+ }
+
+ if (actualValue is not null)
+ {
+ Assert.Equal(actualValue, ex.ActualValue);
+ }
+ }
+
+ public static async Task ThrowsArgumentOutOfRangeAsync(
+ Func test,
+ string paramName,
+ string message,
+ object actualValue)
+ {
+ var ex = await Assert.ThrowsAsync(test);
+ if (paramName is not null)
+ {
+ Assert.Equal(paramName, ex.ParamName);
+ }
+
+ if (message is not null)
+ {
+ Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
+ }
+
+ if (actualValue is not null)
+ {
+ Assert.Equal(actualValue, ex.ActualValue);
+ }
+ }
+}
diff --git a/test/TestUtils/NatsTestExtensions.cs b/test/TestUtils/NatsTestExtensions.cs
index 58e1bcc..4d3ca3e 100644
--- a/test/TestUtils/NatsTestExtensions.cs
+++ b/test/TestUtils/NatsTestExtensions.cs
@@ -6,7 +6,7 @@ namespace CodeCargo.NatsDistributedCache.TestUtils;
public static class NatsTestExtensions
{
- public static IServiceCollection AddNatsTestServices(this IServiceCollection services, string natsConnectionString) =>
+ public static IServiceCollection AddNatsTestClient(this IServiceCollection services, string natsConnectionString) =>
services.AddNats(configureOpts: options =>
options with
{
diff --git a/test/UnitTests/Cache/TimeExpirationAsyncUnitTests.cs b/test/UnitTests/Cache/TimeExpirationAsyncUnitTests.cs
new file mode 100644
index 0000000..1875731
--- /dev/null
+++ b/test/UnitTests/Cache/TimeExpirationAsyncUnitTests.cs
@@ -0,0 +1,88 @@
+using CodeCargo.NatsDistributedCache.TestUtils.Assertions;
+using Microsoft.Extensions.Caching.Distributed;
+
+namespace CodeCargo.NatsDistributedCache.UnitTests.Cache;
+
+public class TimeExpirationAsyncUnitTests : TestBase
+{
+ [Fact]
+ public async Task AbsoluteExpirationInThePastThrowsAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ var expected = DateTimeOffset.Now - TimeSpan.FromMinutes(1);
+ await ExceptionAssert.ThrowsArgumentOutOfRangeAsync(
+ async () =>
+ {
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(expected));
+ },
+ nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
+ "The absolute expiration value must be in the future.",
+ expected);
+ }
+
+ [Fact]
+ public async Task NegativeRelativeExpirationThrowsAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await ExceptionAssert.ThrowsArgumentOutOfRangeAsync(
+ async () =>
+ {
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(-1)));
+ },
+ nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
+ "The relative expiration value must be positive.",
+ TimeSpan.FromMinutes(-1));
+ }
+
+ [Fact]
+ public async Task ZeroRelativeExpirationThrowsAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await ExceptionAssert.ThrowsArgumentOutOfRangeAsync(
+ async () =>
+ {
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.Zero));
+ },
+ nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
+ "The relative expiration value must be positive.",
+ TimeSpan.Zero);
+ }
+
+ [Fact]
+ public async Task NegativeSlidingExpirationThrowsAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await ExceptionAssert.ThrowsArgumentOutOfRangeAsync(
+ async () =>
+ {
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(-1)));
+ },
+ nameof(DistributedCacheEntryOptions.SlidingExpiration),
+ "The sliding expiration value must be positive.",
+ TimeSpan.FromMinutes(-1));
+ }
+
+ [Fact]
+ public async Task ZeroSlidingExpirationThrowsAsync()
+ {
+ var key = MethodKey();
+ var value = new byte[1];
+
+ await ExceptionAssert.ThrowsArgumentOutOfRangeAsync(
+ async () =>
+ {
+ await Cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.Zero));
+ },
+ nameof(DistributedCacheEntryOptions.SlidingExpiration),
+ "The sliding expiration value must be positive.",
+ TimeSpan.Zero);
+ }
+}
diff --git a/test/UnitTests/TimeExpirationUnitTests.cs b/test/UnitTests/Cache/TimeExpirationUnitTests.cs
similarity index 52%
rename from test/UnitTests/TimeExpirationUnitTests.cs
rename to test/UnitTests/Cache/TimeExpirationUnitTests.cs
index bdbaa17..d2a5f64 100644
--- a/test/UnitTests/TimeExpirationUnitTests.cs
+++ b/test/UnitTests/Cache/TimeExpirationUnitTests.cs
@@ -1,38 +1,21 @@
-using CodeCargo.NatsDistributedCache.UnitTests.TestHelpers;
+using CodeCargo.NatsDistributedCache.TestUtils.Assertions;
using Microsoft.Extensions.Caching.Distributed;
-using Microsoft.Extensions.Logging;
-using Moq;
-using NATS.Client.Core;
-namespace CodeCargo.NatsDistributedCache.UnitTests;
+namespace CodeCargo.NatsDistributedCache.UnitTests.Cache;
-public class TimeExpirationUnitTests
+public class TimeExpirationUnitTests : TestBase
{
- private readonly Mock _mockNatsConnection;
-
- public TimeExpirationUnitTests()
- {
- _mockNatsConnection = new Mock();
-
- // Setup the mock to properly handle the Opts property
- var opts = new NatsOpts { LoggerFactory = new LoggerFactory() };
- _mockNatsConnection.SetupGet(m => m.Opts).Returns(opts);
- var connection = new NatsConnection(opts);
- _mockNatsConnection.SetupGet(m => m.Connection).Returns(connection);
- }
-
[Fact]
public void AbsoluteExpirationInThePastThrows()
{
- var cache = CreateCacheInstance();
- var key = "AbsoluteExpirationInThePastThrows";
+ var key = MethodKey();
var value = new byte[1];
var expected = DateTimeOffset.Now - TimeSpan.FromMinutes(1);
ExceptionAssert.ThrowsArgumentOutOfRange(
() =>
{
- cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(expected));
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(expected));
},
nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
"The absolute expiration value must be in the future.",
@@ -42,14 +25,16 @@ public void AbsoluteExpirationInThePastThrows()
[Fact]
public void NegativeRelativeExpirationThrows()
{
- var cache = CreateCacheInstance();
- var key = "NegativeRelativeExpirationThrows";
+ var key = MethodKey();
var value = new byte[1];
ExceptionAssert.ThrowsArgumentOutOfRange(
() =>
{
- cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(-1)));
+ Cache.Set(
+ key,
+ value,
+ new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(-1)));
},
nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
"The relative expiration value must be positive.",
@@ -59,14 +44,13 @@ public void NegativeRelativeExpirationThrows()
[Fact]
public void ZeroRelativeExpirationThrows()
{
- var cache = CreateCacheInstance();
- var key = "ZeroRelativeExpirationThrows";
+ var key = MethodKey();
var value = new byte[1];
ExceptionAssert.ThrowsArgumentOutOfRange(
() =>
{
- cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.Zero));
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.Zero));
},
nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
"The relative expiration value must be positive.",
@@ -76,14 +60,16 @@ public void ZeroRelativeExpirationThrows()
[Fact]
public void NegativeSlidingExpirationThrows()
{
- var cache = CreateCacheInstance();
- var key = "NegativeSlidingExpirationThrows";
+ var key = MethodKey();
var value = new byte[1];
ExceptionAssert.ThrowsArgumentOutOfRange(
() =>
{
- cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(-1)));
+ Cache.Set(
+ key,
+ value,
+ new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(-1)));
},
nameof(DistributedCacheEntryOptions.SlidingExpiration),
"The sliding expiration value must be positive.",
@@ -93,27 +79,16 @@ public void NegativeSlidingExpirationThrows()
[Fact]
public void ZeroSlidingExpirationThrows()
{
- var cache = CreateCacheInstance();
- var key = "ZeroSlidingExpirationThrows";
+ var key = MethodKey();
var value = new byte[1];
ExceptionAssert.ThrowsArgumentOutOfRange(
() =>
{
- cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.Zero));
+ Cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.Zero));
},
nameof(DistributedCacheEntryOptions.SlidingExpiration),
"The sliding expiration value must be positive.",
TimeSpan.Zero);
}
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- _mockNatsConnection.Object);
- }
}
diff --git a/test/UnitTests/CacheServiceExtensionsUnitTests.cs b/test/UnitTests/Extensions/CacheServiceExtensionsTests.cs
similarity index 62%
rename from test/UnitTests/CacheServiceExtensionsUnitTests.cs
rename to test/UnitTests/Extensions/CacheServiceExtensionsTests.cs
index aaeba90..ea86376 100644
--- a/test/UnitTests/CacheServiceExtensionsUnitTests.cs
+++ b/test/UnitTests/Extensions/CacheServiceExtensionsTests.cs
@@ -4,16 +4,11 @@
using Moq;
using NATS.Client.Core;
-namespace CodeCargo.NatsDistributedCache.UnitTests;
+namespace CodeCargo.NatsDistributedCache.UnitTests.Extensions;
public class CacheServiceExtensionsUnitTests
{
- private readonly Mock _mockNatsConnection;
-
- public CacheServiceExtensionsUnitTests()
- {
- _mockNatsConnection = new Mock();
- }
+ private readonly Mock _mockNatsConnection = new();
[Fact]
public void AddNatsCache_RegistersDistributedCacheAsSingleton()
@@ -64,13 +59,13 @@ public void AddNatsCache_SetsCacheOptions()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(_mockNatsConnection.Object);
- var expectedInstanceName = "TestInstance";
+ var expectedNamespace = "TestNamespace";
// Act
services.AddNatsDistributedCache(options =>
{
options.BucketName = "cache";
- options.InstanceName = expectedInstanceName;
+ options.CacheKeyPrefix = expectedNamespace;
});
// Build the provider to verify options
@@ -78,7 +73,7 @@ public void AddNatsCache_SetsCacheOptions()
var options = provider.GetRequiredService>().Value;
// Assert
- Assert.Equal(expectedInstanceName, options.InstanceName);
+ Assert.Equal(expectedNamespace, options.CacheKeyPrefix);
Assert.Equal("cache", options.BucketName);
}
@@ -105,11 +100,42 @@ public void AddNatsCache_UsesCacheOptionsAction()
Assert.True(wasInvoked);
}
+ [Fact]
+ public void AddNatsCache_AcceptsConnectionServiceKey_Parameter()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var defaultConnection = new Mock().Object;
+ var keyedConnection = new Mock().Object;
+
+ services.AddSingleton(defaultConnection);
+ services.AddKeyedSingleton("my-key", keyedConnection);
+
+ // Act - ensure this doesn't throw an exception
+ services.AddNatsDistributedCache(
+ options => { options.BucketName = "cache"; },
+ connectionServiceKey: "my-key");
+
+ // Assert
+ // Verify the IDistributedCache registration looks correct
+ var cacheRegistration = services.FirstOrDefault(x => x.ServiceType == typeof(IDistributedCache));
+ Assert.NotNull(cacheRegistration);
+ Assert.Equal(ServiceLifetime.Singleton, cacheRegistration.Lifetime);
+ Assert.Null(cacheRegistration.ImplementationType); // Should use a factory, not a direct type
+ Assert.NotNull(cacheRegistration.ImplementationFactory); // Should use a factory registration
+
+ // Verify the NatsCacheOptions were configured
+ var optionsRegistration = services.FirstOrDefault(d =>
+ d.ServiceType == typeof(IConfigureOptions));
+ Assert.NotNull(optionsRegistration);
+ }
+
private class FakeDistributedCache : IDistributedCache
{
- public byte[]? Get(string key) => throw new NotImplementedException();
+ public byte[] Get(string key) => throw new NotImplementedException();
- public Task GetAsync(string key, CancellationToken token = default) => throw new NotImplementedException();
+ public Task GetAsync(string key, CancellationToken token = default) =>
+ throw new NotImplementedException();
public void Refresh(string key) => throw new NotImplementedException();
@@ -119,8 +145,13 @@ private class FakeDistributedCache : IDistributedCache
public Task RemoveAsync(string key, CancellationToken token = default) => throw new NotImplementedException();
- public void Set(string key, byte[] value, DistributedCacheEntryOptions options) => throw new NotImplementedException();
+ public void Set(string key, byte[] value, DistributedCacheEntryOptions options) =>
+ throw new NotImplementedException();
- public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => throw new NotImplementedException();
+ public Task SetAsync(
+ string key,
+ byte[] value,
+ DistributedCacheEntryOptions options,
+ CancellationToken token = default) => throw new NotImplementedException();
}
}
diff --git a/test/UnitTests/NatsCacheSetAndRemoveUnitTests.cs b/test/UnitTests/NatsCacheSetAndRemoveUnitTests.cs
deleted file mode 100644
index ae4bddc..0000000
--- a/test/UnitTests/NatsCacheSetAndRemoveUnitTests.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Microsoft.Extensions.Caching.Distributed;
-using Moq;
-using NATS.Client.Core;
-
-namespace CodeCargo.NatsDistributedCache.UnitTests;
-
-public class NatsCacheSetAndRemoveUnitTests
-{
- private readonly Mock _mockNatsConnection;
-
- public NatsCacheSetAndRemoveUnitTests()
- {
- _mockNatsConnection = new Mock();
- }
-
- [Fact]
- public void SetNullValueThrows()
- {
- var cache = CreateCacheInstance();
- byte[] value = null!;
- var key = "myKey";
-
- Assert.Throws(() => cache.Set(key, value));
- }
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- _mockNatsConnection.Object);
- }
-}
diff --git a/test/UnitTests/Serialization/NatsHybridCacheSerializerFactoryTests.cs b/test/UnitTests/Serialization/NatsHybridCacheSerializerFactoryTests.cs
new file mode 100644
index 0000000..b63b3b1
--- /dev/null
+++ b/test/UnitTests/Serialization/NatsHybridCacheSerializerFactoryTests.cs
@@ -0,0 +1,65 @@
+using System.Buffers;
+using Microsoft.Extensions.Caching.Hybrid;
+using Moq;
+using NATS.Client.Core;
+using NATS.Net;
+
+namespace CodeCargo.NatsDistributedCache.UnitTests.Serialization;
+
+///
+/// Tests for the NatsHybridCacheSerializerFactory
+///
+public class NatsHybridCacheSerializerFactoryTests : TestBase
+{
+ [Fact]
+ public void TryCreateSerializer_String_CreatesSerializer()
+ {
+ // Arrange
+ var serializerRegistry = NatsOpts.Default.SerializerRegistry;
+ var factory = new NatsHybridCacheSerializerFactory(serializerRegistry);
+
+ // Act
+ var result = factory.TryCreateSerializer(out var serializer);
+
+ // Assert
+ Assert.True(result);
+ Assert.NotNull(serializer);
+
+ // Test serialization and deserialization to ensure it works end-to-end
+ const string testValue = "Hello, NatsHybridCacheSerializer!";
+ var writer = new ArrayBufferWriter();
+
+ // Serialize
+ serializer.Serialize(testValue, writer);
+
+ // Deserialize
+ var sequence = new ReadOnlySequence(writer.WrittenMemory);
+ var deserializedValue = serializer.Deserialize(sequence);
+
+ // Verify
+ Assert.Equal(testValue, deserializedValue);
+ }
+
+ [Fact]
+ public void TryCreateSerializer_UnsupportedType_ReturnsFalse()
+ {
+ // Arrange - Create a mock registry that throws exceptions when accessed
+ var mockRegistry = new Mock();
+ mockRegistry.Setup(r => r.GetSerializer())
+ .Throws();
+ mockRegistry.Setup(r => r.GetDeserializer())
+ .Throws();
+
+ var factory = new NatsHybridCacheSerializerFactory(mockRegistry.Object);
+
+ // Act
+ var result = factory.TryCreateSerializer(out var serializer);
+
+ // Assert
+ Assert.False(result);
+ Assert.Null(serializer);
+
+ // Verify the serializer was requested
+ mockRegistry.Verify(r => r.GetSerializer(), Times.Once);
+ }
+}
diff --git a/test/UnitTests/TestBase.cs b/test/UnitTests/TestBase.cs
new file mode 100644
index 0000000..764fd3b
--- /dev/null
+++ b/test/UnitTests/TestBase.cs
@@ -0,0 +1,33 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using NATS.Client.Core;
+
+namespace CodeCargo.NatsDistributedCache.UnitTests;
+
+public abstract class TestBase
+{
+ protected TestBase()
+ {
+ var mockNatsConnection = new Mock();
+ var opts = new NatsOpts { LoggerFactory = new LoggerFactory() };
+ mockNatsConnection.SetupGet(m => m.Opts).Returns(opts);
+ var connection = new NatsConnection(opts);
+ mockNatsConnection.SetupGet(m => m.Connection).Returns(connection);
+ Cache = new NatsCache(
+ Options.Create(new NatsCacheOptions { BucketName = "cache" }),
+ mockNatsConnection.Object);
+ }
+
+ ///
+ /// Gets the cache
+ ///
+ protected IDistributedCache Cache { get; }
+
+ ///
+ /// Gets the key for the current test method
+ ///
+ protected string MethodKey([CallerMemberName] string caller = "") => caller;
+}
diff --git a/test/UnitTests/TestHelpers/ExceptionAssert.cs b/test/UnitTests/TestHelpers/ExceptionAssert.cs
deleted file mode 100644
index 0cbe44a..0000000
--- a/test/UnitTests/TestHelpers/ExceptionAssert.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace CodeCargo.NatsDistributedCache.UnitTests.TestHelpers;
-
-public static class ExceptionAssert
-{
- public static void ThrowsArgumentOutOfRange(
- Action testCode,
- string paramName,
- string message,
- object actualValue)
- {
- var ex = Assert.Throws(testCode);
-
- if (paramName is not null)
- {
- Assert.Equal(paramName, ex.ParamName);
- }
-
- if (message is not null)
- {
- Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
- }
-
- if (actualValue is not null)
- {
- Assert.Equal(actualValue, ex.ActualValue);
- }
- }
-}
diff --git a/test/UnitTests/TimeExpirationAsyncUnitTests.cs b/test/UnitTests/TimeExpirationAsyncUnitTests.cs
deleted file mode 100644
index befc06a..0000000
--- a/test/UnitTests/TimeExpirationAsyncUnitTests.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using Microsoft.Extensions.Caching.Distributed;
-using Moq;
-using NATS.Client.Core;
-
-namespace CodeCargo.NatsDistributedCache.UnitTests;
-
-public class TimeExpirationAsyncUnitTests
-{
- private readonly Mock _mockNatsConnection;
-
- public TimeExpirationAsyncUnitTests()
- {
- _mockNatsConnection = new Mock();
- }
-
- [Fact]
- public async Task AbsoluteExpirationInThePastThrowsAsync()
- {
- var cache = CreateCacheInstance();
- var key = "AbsoluteExpirationInThePastThrowsAsync";
- var value = new byte[1];
-
- var expected = DateTimeOffset.Now - TimeSpan.FromMinutes(1);
- await ThrowsArgumentOutOfRangeAsync(
- async () =>
- {
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(expected));
- },
- nameof(DistributedCacheEntryOptions.AbsoluteExpiration),
- "The absolute expiration value must be in the future.",
- expected);
- }
-
- [Fact]
- public async Task NegativeRelativeExpirationThrowsAsync()
- {
- var cache = CreateCacheInstance();
- var key = "NegativeRelativeExpirationThrowsAsync";
- var value = new byte[1];
-
- await ThrowsArgumentOutOfRangeAsync(
- async () =>
- {
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(-1)));
- },
- nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
- "The relative expiration value must be positive.",
- TimeSpan.FromMinutes(-1));
- }
-
- [Fact]
- public async Task ZeroRelativeExpirationThrowsAsync()
- {
- var cache = CreateCacheInstance();
- var key = "ZeroRelativeExpirationThrowsAsync";
- var value = new byte[1];
-
- await ThrowsArgumentOutOfRangeAsync(
- async () =>
- {
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.Zero));
- },
- nameof(DistributedCacheEntryOptions.AbsoluteExpirationRelativeToNow),
- "The relative expiration value must be positive.",
- TimeSpan.Zero);
- }
-
- [Fact]
- public async Task NegativeSlidingExpirationThrowsAsync()
- {
- var cache = CreateCacheInstance();
- var key = "NegativeSlidingExpirationThrowsAsync";
- var value = new byte[1];
-
- await ThrowsArgumentOutOfRangeAsync(
- async () =>
- {
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(-1)));
- },
- nameof(DistributedCacheEntryOptions.SlidingExpiration),
- "The sliding expiration value must be positive.",
- TimeSpan.FromMinutes(-1));
- }
-
- [Fact]
- public async Task ZeroSlidingExpirationThrowsAsync()
- {
- var cache = CreateCacheInstance();
- var key = "ZeroSlidingExpirationThrowsAsync";
- var value = new byte[1];
-
- await ThrowsArgumentOutOfRangeAsync(
- async () =>
- {
- await cache.SetAsync(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.Zero));
- },
- nameof(DistributedCacheEntryOptions.SlidingExpiration),
- "The sliding expiration value must be positive.",
- TimeSpan.Zero);
- }
-
- // async twin to ExceptionAssert.ThrowsArgumentOutOfRange
- private static async Task ThrowsArgumentOutOfRangeAsync(Func test, string paramName, string message, object actualValue)
- {
- var ex = await Assert.ThrowsAsync(test);
- if (paramName is not null)
- {
- Assert.Equal(paramName, ex.ParamName);
- }
-
- if (message is not null)
- {
- Assert.StartsWith(message, ex.Message); // can have "\r\nParameter name:" etc
- }
-
- if (actualValue is not null)
- {
- Assert.Equal(actualValue, ex.ActualValue);
- }
- }
-
- private IDistributedCache CreateCacheInstance()
- {
- return new NatsCache(
- Microsoft.Extensions.Options.Options.Create(new NatsCacheOptions
- {
- BucketName = "cache"
- }),
- _mockNatsConnection.Object);
- }
-}
diff --git a/util/NatsAppHost/NatsAppHost.csproj b/util/NatsAppHost/NatsAppHost.csproj
index bccee80..0ae9fc0 100644
--- a/util/NatsAppHost/NatsAppHost.csproj
+++ b/util/NatsAppHost/NatsAppHost.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/util/NatsAppHost/packages.linux-x64.lock.json b/util/NatsAppHost/packages.linux-x64.lock.json
index 7557327..2d38308 100644
--- a/util/NatsAppHost/packages.linux-x64.lock.json
+++ b/util/NatsAppHost/packages.linux-x64.lock.json
@@ -236,6 +236,14 @@
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
},
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.4"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -323,8 +331,8 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
+ "resolved": "9.0.4",
+ "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
@@ -450,10 +458,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.3",
- "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
+ "resolved": "9.0.4",
+ "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "System.Diagnostics.DiagnosticSource": "9.0.4"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -519,11 +528,11 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
+ "resolved": "9.0.4",
+ "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
- "Microsoft.Extensions.Primitives": "8.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
@@ -540,8 +549,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+ "resolved": "9.0.4",
+ "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
@@ -590,6 +599,33 @@
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
+ "NATS.Client.Core": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "6mh+fSB9Fx6QfbYRjvswbWDgD04ePBYzB2yIY5GqzyUiUboqxpuOUDO2QOTNkwIqi8IpTMKcySAY+V/DvmvtQA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Microsoft.Extensions.Primitives": "6.0.0",
+ "System.IO.Pipelines": "8.0.0",
+ "System.Text.Json": "8.0.5"
+ }
+ },
+ "NATS.Client.JetStream": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "3t5UJyN6nnsrDxWBxklNK06rnNL9aEjnzcOHCXRcZpcfFGeWFm4dG5UXtt7WiW69uWY4MHUlOcbuBKh5/rAuSQ==",
+ "dependencies": {
+ "NATS.Client.Core": "2.6.0"
+ }
+ },
+ "NATS.Client.KeyValueStore": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "/us915HAGYdGAdblH2857w3xapZupmU4vNDODMfysgcg/tLineOtQ2+FqcSrmCVJ6mILngeuHGTk5vghunTpog==",
+ "dependencies": {
+ "NATS.Client.JetStream": "2.6.0"
+ }
+ },
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.11.90",
@@ -631,6 +667,11 @@
"resolved": "1.2.0.556",
"contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ=="
},
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "Be0emq8bRmcK4eeJIFUt9+vYPf7kzuQrFs8Ef1CdGvXpq/uSve22PTSkRF09bF/J7wmYJ2DHf2v7GaT3vMXnwQ=="
+ },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -660,10 +701,24 @@
"resolved": "6.1.0",
"contentHash": "5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg=="
},
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "8.0.5",
+ "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
+ },
"YamlDotNet": {
"type": "Transitive",
"resolved": "16.3.0",
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
+ },
+ "codecargo.natsdistributedcache": {
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Logging.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Options": "[9.0.4, )",
+ "NATS.Client.KeyValueStore": "[2.6.0, )"
+ }
}
}
}
diff --git a/util/NatsAppHost/packages.osx-arm64.lock.json b/util/NatsAppHost/packages.osx-arm64.lock.json
index 8a9d389..3d56d8f 100644
--- a/util/NatsAppHost/packages.osx-arm64.lock.json
+++ b/util/NatsAppHost/packages.osx-arm64.lock.json
@@ -236,6 +236,14 @@
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
},
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.4"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -323,8 +331,8 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
+ "resolved": "9.0.4",
+ "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
@@ -450,10 +458,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.3",
- "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
+ "resolved": "9.0.4",
+ "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "System.Diagnostics.DiagnosticSource": "9.0.4"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -519,11 +528,11 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
+ "resolved": "9.0.4",
+ "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
- "Microsoft.Extensions.Primitives": "8.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
@@ -540,8 +549,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+ "resolved": "9.0.4",
+ "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
@@ -590,6 +599,33 @@
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
+ "NATS.Client.Core": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "6mh+fSB9Fx6QfbYRjvswbWDgD04ePBYzB2yIY5GqzyUiUboqxpuOUDO2QOTNkwIqi8IpTMKcySAY+V/DvmvtQA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Microsoft.Extensions.Primitives": "6.0.0",
+ "System.IO.Pipelines": "8.0.0",
+ "System.Text.Json": "8.0.5"
+ }
+ },
+ "NATS.Client.JetStream": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "3t5UJyN6nnsrDxWBxklNK06rnNL9aEjnzcOHCXRcZpcfFGeWFm4dG5UXtt7WiW69uWY4MHUlOcbuBKh5/rAuSQ==",
+ "dependencies": {
+ "NATS.Client.Core": "2.6.0"
+ }
+ },
+ "NATS.Client.KeyValueStore": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "/us915HAGYdGAdblH2857w3xapZupmU4vNDODMfysgcg/tLineOtQ2+FqcSrmCVJ6mILngeuHGTk5vghunTpog==",
+ "dependencies": {
+ "NATS.Client.JetStream": "2.6.0"
+ }
+ },
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.11.90",
@@ -631,6 +667,11 @@
"resolved": "1.2.0.556",
"contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ=="
},
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "Be0emq8bRmcK4eeJIFUt9+vYPf7kzuQrFs8Ef1CdGvXpq/uSve22PTSkRF09bF/J7wmYJ2DHf2v7GaT3vMXnwQ=="
+ },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -660,10 +701,24 @@
"resolved": "6.1.0",
"contentHash": "5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg=="
},
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "8.0.5",
+ "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
+ },
"YamlDotNet": {
"type": "Transitive",
"resolved": "16.3.0",
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
+ },
+ "codecargo.natsdistributedcache": {
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Logging.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Options": "[9.0.4, )",
+ "NATS.Client.KeyValueStore": "[2.6.0, )"
+ }
}
}
}
diff --git a/util/NatsAppHost/packages.win-x64.lock.json b/util/NatsAppHost/packages.win-x64.lock.json
index eb63d59..b6bd3e7 100644
--- a/util/NatsAppHost/packages.win-x64.lock.json
+++ b/util/NatsAppHost/packages.win-x64.lock.json
@@ -236,6 +236,14 @@
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
},
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.4"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
@@ -323,8 +331,8 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
+ "resolved": "9.0.4",
+ "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
@@ -450,10 +458,11 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.3",
- "contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
+ "resolved": "9.0.4",
+ "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "System.Diagnostics.DiagnosticSource": "9.0.4"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -519,11 +528,11 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "8.0.2",
- "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
+ "resolved": "9.0.4",
+ "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
- "Microsoft.Extensions.Primitives": "8.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
+ "Microsoft.Extensions.Primitives": "9.0.4"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
@@ -540,8 +549,8 @@
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+ "resolved": "9.0.4",
+ "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
},
"Microsoft.IdentityModel.Abstractions": {
"type": "Transitive",
@@ -590,6 +599,33 @@
"resolved": "17.8.8",
"contentHash": "rWXThIpyQd4YIXghNkiv2+VLvzS+MCMKVRDR0GAMlflsdo+YcAN2g2r5U1Ah98OFjQMRexTFtXQQ2LkajxZi3g=="
},
+ "NATS.Client.Core": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "6mh+fSB9Fx6QfbYRjvswbWDgD04ePBYzB2yIY5GqzyUiUboqxpuOUDO2QOTNkwIqi8IpTMKcySAY+V/DvmvtQA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.1",
+ "Microsoft.Extensions.Primitives": "6.0.0",
+ "System.IO.Pipelines": "8.0.0",
+ "System.Text.Json": "8.0.5"
+ }
+ },
+ "NATS.Client.JetStream": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "3t5UJyN6nnsrDxWBxklNK06rnNL9aEjnzcOHCXRcZpcfFGeWFm4dG5UXtt7WiW69uWY4MHUlOcbuBKh5/rAuSQ==",
+ "dependencies": {
+ "NATS.Client.Core": "2.6.0"
+ }
+ },
+ "NATS.Client.KeyValueStore": {
+ "type": "Transitive",
+ "resolved": "2.6.0",
+ "contentHash": "/us915HAGYdGAdblH2857w3xapZupmU4vNDODMfysgcg/tLineOtQ2+FqcSrmCVJ6mILngeuHGTk5vghunTpog==",
+ "dependencies": {
+ "NATS.Client.JetStream": "2.6.0"
+ }
+ },
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.11.90",
@@ -631,6 +667,11 @@
"resolved": "1.2.0.556",
"contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ=="
},
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "9.0.4",
+ "contentHash": "Be0emq8bRmcK4eeJIFUt9+vYPf7kzuQrFs8Ef1CdGvXpq/uSve22PTSkRF09bF/J7wmYJ2DHf2v7GaT3vMXnwQ=="
+ },
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -660,10 +701,24 @@
"resolved": "6.1.0",
"contentHash": "5o/HZxx6RVqYlhKSq8/zronDkALJZUT2Vz0hx43f0gwe8mwlM0y2nYlqdBwLMzr262Bwvpikeb/yEwkAa5PADg=="
},
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "8.0.5",
+ "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg=="
+ },
"YamlDotNet": {
"type": "Transitive",
"resolved": "16.3.0",
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
+ },
+ "codecargo.natsdistributedcache": {
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Logging.Abstractions": "[9.0.4, )",
+ "Microsoft.Extensions.Options": "[9.0.4, )",
+ "NATS.Client.KeyValueStore": "[2.6.0, )"
+ }
}
}
}
diff --git a/util/PerfTest/Program.cs b/util/PerfTest/Program.cs
index d21fac8..28b5643 100644
--- a/util/PerfTest/Program.cs
+++ b/util/PerfTest/Program.cs
@@ -39,7 +39,7 @@
// Add services directly to the builder
builder.ConfigureServices(services =>
{
- services.AddNatsTestServices(natsConnectionString);
+ services.AddNatsTestClient(natsConnectionString);
services.AddScoped();
});
diff --git a/util/PerfTest/packages.linux-x64.lock.json b/util/PerfTest/packages.linux-x64.lock.json
index 1a35dfe..92eb4d9 100644
--- a/util/PerfTest/packages.linux-x64.lock.json
+++ b/util/PerfTest/packages.linux-x64.lock.json
@@ -958,7 +958,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.linux-x64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.linux-x64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.linux-x64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {
diff --git a/util/PerfTest/packages.osx-arm64.lock.json b/util/PerfTest/packages.osx-arm64.lock.json
index a2b9378..ef89a8b 100644
--- a/util/PerfTest/packages.osx-arm64.lock.json
+++ b/util/PerfTest/packages.osx-arm64.lock.json
@@ -958,7 +958,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.osx-arm64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.osx-arm64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.osx-arm64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {
diff --git a/util/PerfTest/packages.win-x64.lock.json b/util/PerfTest/packages.win-x64.lock.json
index c776a8d..a5570e6 100644
--- a/util/PerfTest/packages.win-x64.lock.json
+++ b/util/PerfTest/packages.win-x64.lock.json
@@ -958,7 +958,8 @@
"dependencies": {
"Aspire.Dashboard.Sdk.win-x64": "[9.2.1, )",
"Aspire.Hosting.AppHost": "[9.2.1, )",
- "Aspire.Hosting.Orchestration.win-x64": "[9.2.1, )"
+ "Aspire.Hosting.Orchestration.win-x64": "[9.2.1, )",
+ "CodeCargo.NatsDistributedCache": "[1.0.0, )"
}
},
"testutils": {