diff --git a/src/Healthie.StateProviding.Redis/README.md b/src/Healthie.StateProviding.Redis/README.md
index 3c78786..ee58a6c 100644
--- a/src/Healthie.StateProviding.Redis/README.md
+++ b/src/Healthie.StateProviding.Redis/README.md
@@ -28,6 +28,8 @@ If your application already registers an `IConnectionMultiplexer` — for its ow
builder.Services.AddHealthieRedis(); // uses the registered IConnectionMultiplexer
```
+Both shapes are the same method: pass a configuration string to have the connection opened for you, or leave it out to use the registered one. Name the argument when you only want a different prefix — `AddHealthieRedis(keyPrefix: "myapp:health:")`.
+
Every key is prefixed, `healthie:state:` by default, so the provider stays out of the way of whatever else lives on that server. Pass your own as the last argument.
## Optimistic concurrency
diff --git a/src/Healthie.StateProviding.Redis/StartupExtensions.cs b/src/Healthie.StateProviding.Redis/StartupExtensions.cs
index 6d85055..15af3fc 100644
--- a/src/Healthie.StateProviding.Redis/StartupExtensions.cs
+++ b/src/Healthie.StateProviding.Redis/StartupExtensions.cs
@@ -18,53 +18,47 @@ public static class StartupExtensions
///
/// The service collection to register with.
///
- /// A StackExchange.Redis configuration string, such as localhost:6379.
+ /// A StackExchange.Redis configuration string, such as localhost:6379. Leave it out to use
+ /// an the application already registers -- for its own cache,
+ /// or through AddStackExchangeRedisCache. Sharing that one is the better option where it
+ /// exists: a second connection to the same server buys nothing.
///
/// Prefixed to every key the provider owns.
/// The service collection for fluent chaining.
///
- /// Opens the connection here, once, and keeps it: an is
- /// designed to be shared for the life of the application and is expensive to build per call.
- /// Registered as a singleton so the container disposes it on shutdown.
- ///
- public static IServiceCollection AddHealthieRedis(
- this IServiceCollection services,
- string configuration,
- string keyPrefix = DefaultKeyPrefix)
- {
- ArgumentNullException.ThrowIfNull(services);
- ArgumentException.ThrowIfNullOrWhiteSpace(configuration);
-
- services.TryAddSingleton(_ => ConnectionMultiplexer.Connect(configuration));
-
- return services.AddHealthieRedis(keyPrefix);
- }
-
- ///
- /// Registers a state provider backed by a Redis connection the application already owns.
- ///
- /// The service collection to register with.
- /// Prefixed to every key the provider owns.
- /// The service collection for fluent chaining.
- ///
///
- /// For an application that already registers an -- for its
- /// own cache, or through AddStackExchangeRedisCache. Sharing it is the point: a second
- /// connection to the same server buys nothing.
+ /// One method rather than one per case, because two overloads separated only by what a
+ /// means cannot be told apart at the call site. C# resolved
+ /// AddHealthieRedis("localhost:6379") to the overload whose single string was the key
+ /// prefix, so the connection string quietly became a prefix, no connection was registered, and
+ /// the failure arrived later as a missing service -- from the very call the README documents.
+ ///
+ ///
+ /// The connection is opened once and kept: an is built to be
+ /// shared for the life of the application and is expensive to create per call. It goes in with
+ /// TryAdd, so an application that already has one keeps it.
///
///
- /// Registered with AddSingleton rather than TryAddSingleton on purpose: the
- /// built-in in-memory provider registers with TryAdd, so whichever call comes first, this
- /// one wins.
+ /// The provider is registered with AddSingleton rather than TryAddSingleton on
+ /// purpose: the built-in in-memory provider registers with TryAdd, so whichever call comes
+ /// first, this one wins.
///
///
public static IServiceCollection AddHealthieRedis(
this IServiceCollection services,
+ string? configuration = null,
string keyPrefix = DefaultKeyPrefix)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrWhiteSpace(keyPrefix);
+ if (configuration is not null)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(configuration);
+
+ services.TryAddSingleton(_ => ConnectionMultiplexer.Connect(configuration));
+ }
+
return services.AddSingleton(provider =>
new RedisStateProvider(provider.GetRequiredService(), keyPrefix));
}
diff --git a/tests/Healthie.Tests.Unit/RedisStateProviderTests.cs b/tests/Healthie.Tests.Unit/RedisStateProviderTests.cs
index d05ce18..13a7258 100644
--- a/tests/Healthie.Tests.Unit/RedisStateProviderTests.cs
+++ b/tests/Healthie.Tests.Unit/RedisStateProviderTests.cs
@@ -1,7 +1,9 @@
using Healthie.Abstractions.Enums;
using Healthie.Abstractions.Models;
using Healthie.Abstractions.StateProviding;
+using Healthie.DependencyInjection;
using Healthie.StateProviding.Redis;
+using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;
using Testcontainers.Redis;
@@ -61,6 +63,87 @@ private bool Unavailable()
return false;
}
+ ///
+ /// Through AddHealthieRedis, not by constructing the provider. Nothing did that before,
+ /// which is how a registration that never registered the connection shipped: the documented call
+ /// bound to the overload whose one string was the key prefix.
+ ///
+ [Fact]
+ public async Task AddHealthieRedis_WithAConfigurationString_ResolvesAWorkingProvider()
+ {
+ if (Unavailable())
+ {
+ return;
+ }
+
+ var services = new ServiceCollection();
+ services.AddHealthie(typeof(RedisStateProviderTests).Assembly);
+ services.AddHealthieRedis(_redis!.GetConnectionString());
+
+ await using var host = services.BuildServiceProvider();
+
+ var provider = host.GetRequiredService();
+
+ Assert.IsType(provider);
+
+ await provider.SetStateAsync("via-di", new PulseCheckerState(PulseInterval.Every30Seconds), Ct);
+ Assert.Equal(
+ PulseInterval.Every30Seconds,
+ (await provider.GetStateAsync("via-di", Ct))!.Interval);
+ }
+
+ ///
+ /// The other shape: the application owns the connection and Healthie shares it.
+ ///
+ [Fact]
+ public async Task AddHealthieRedis_WithAnExistingConnection_SharesIt()
+ {
+ if (Unavailable())
+ {
+ return;
+ }
+
+ var services = new ServiceCollection();
+ services.AddHealthie(typeof(RedisStateProviderTests).Assembly);
+ services.AddSingleton(_connection!);
+ services.AddHealthieRedis();
+
+ await using var host = services.BuildServiceProvider();
+
+ await host.GetRequiredService()
+ .SetStateAsync("shared", new PulseCheckerState(), Ct);
+
+ // Read back through the prefix the registration defaults to, over the same connection.
+ Assert.NotNull(await Provider(Healthie.StateProviding.Redis.StartupExtensions.DefaultKeyPrefix)
+ .GetStateAsync("shared", Ct));
+ }
+
+ ///
+ /// A prefix is still a prefix when it is the only thing passed, and it must be named to be that.
+ ///
+ [Fact]
+ public async Task AddHealthieRedis_WithOnlyAPrefix_UsesTheRegisteredConnection()
+ {
+ if (Unavailable())
+ {
+ return;
+ }
+
+ var services = new ServiceCollection();
+ services.AddHealthie(typeof(RedisStateProviderTests).Assembly);
+ services.AddSingleton(_connection!);
+ services.AddHealthieRedis(keyPrefix: "named-prefix:");
+
+ await using var host = services.BuildServiceProvider();
+
+ await host.GetRequiredService()
+ .SetStateAsync("k", new PulseCheckerState(), Ct);
+
+ // Written under the prefix that was asked for, and nowhere else.
+ Assert.NotNull(await Provider("named-prefix:").GetStateAsync("k", Ct));
+ Assert.Null(await Provider().GetStateAsync("k", Ct));
+ }
+
[Fact]
public async Task StateSurvivesARoundTrip()
{