diff --git a/pkgs/dotnet-server-sdk-redis/dotnet-core b/pkgs/dotnet-server-sdk-redis/dotnet-core new file mode 160000 index 00000000..40e641b1 --- /dev/null +++ b/pkgs/dotnet-server-sdk-redis/dotnet-core @@ -0,0 +1 @@ +Subproject commit 40e641b174518be911bbbcd2d89de0f6f0223d03 diff --git a/pkgs/dotnet-server-sdk-redis/src/RedisBigSegmentStoreImpl.cs b/pkgs/dotnet-server-sdk-redis/src/RedisBigSegmentStoreImpl.cs index 000945bc..1a115cf4 100644 --- a/pkgs/dotnet-server-sdk-redis/src/RedisBigSegmentStoreImpl.cs +++ b/pkgs/dotnet-server-sdk-redis/src/RedisBigSegmentStoreImpl.cs @@ -17,10 +17,10 @@ internal sealed class RedisBigSegmentStoreImpl : RedisStoreImplBase, IBigSegment private readonly string _excludedKeyPrefix; internal RedisBigSegmentStoreImpl( - ConfigurationOptions redisConfig, + IConnectionMultiplexer redis, string prefix, Logger log - ) : base(redisConfig, prefix, log) + ) : base(redis, prefix, log) { _syncTimeKey = prefix + ":big_segments_synchronized_on"; _includedKeyPrefix = prefix + ":big_segment_include:"; diff --git a/pkgs/dotnet-server-sdk-redis/src/RedisDataStoreImpl.cs b/pkgs/dotnet-server-sdk-redis/src/RedisDataStoreImpl.cs index 4fbe5e5b..a364c343 100644 --- a/pkgs/dotnet-server-sdk-redis/src/RedisDataStoreImpl.cs +++ b/pkgs/dotnet-server-sdk-redis/src/RedisDataStoreImpl.cs @@ -40,10 +40,10 @@ internal sealed class RedisDataStoreImpl : RedisStoreImplBase, IPersistentDataSt private readonly string _initedKey; internal RedisDataStoreImpl( - ConfigurationOptions redisConfig, + IConnectionMultiplexer redis, string prefix, Logger log - ) : base(redisConfig, prefix, log) + ) : base(redis, prefix, log) { _initedKey = prefix + ":$inited"; } diff --git a/pkgs/dotnet-server-sdk-redis/src/RedisStoreBuilder.cs b/pkgs/dotnet-server-sdk-redis/src/RedisStoreBuilder.cs index 9e55b433..bcd77386 100644 --- a/pkgs/dotnet-server-sdk-redis/src/RedisStoreBuilder.cs +++ b/pkgs/dotnet-server-sdk-redis/src/RedisStoreBuilder.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; +using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Server.Subsystems; using StackExchange.Redis; @@ -57,6 +59,7 @@ namespace LaunchDarkly.Sdk.Server.Integrations public abstract class RedisStoreBuilder : IComponentConfigurer, IDiagnosticDescription { internal ConfigurationOptions _redisConfig = new ConfigurationOptions(); + internal IConnectionMultiplexer _externalConnection; internal string _prefix = Redis.DefaultPrefix; internal RedisStoreBuilder() @@ -224,6 +227,47 @@ public RedisStoreBuilder RedisConfigChanges(Action modi return this; } + /// + /// Specifies a pre-configured Redis connection multiplexer to use instead of creating one + /// from configuration options. This allows for advanced scenarios such as Azure AAD authentication. + /// + /// the pre-configured connection multiplexer + /// the builder + public RedisStoreBuilder Connection(IConnectionMultiplexer connection) + { + _externalConnection = connection ?? throw new ArgumentNullException(nameof(connection)); + return this; + } + + /// + /// Gets the connection to use - either the externally provided one or creates a new one from configuration. + /// + /// Logger for connection creation + /// The Redis connection multiplexer to use + protected IConnectionMultiplexer GetOrCreateConnection(Logger log) + { + if (_externalConnection != null) + { + log.Info("Using pre-configured Redis connection with prefix \"{0}\"", _prefix); + return _externalConnection; + } + + log.Info("Creating Redis connection to {0} with prefix \"{1}\"", + string.Join(", ", _redisConfig.EndPoints.Select(DescribeEndPoint)), _prefix); + + var redisConfigCopy = _redisConfig.Clone(); + return ConnectionMultiplexer.Connect(redisConfigCopy); + } + + private string DescribeEndPoint(EndPoint e) + { + // The default ToString() method of DnsEndPoint adds a prefix of "Unspecified", which looks + // confusing in our log messages. + return (e is DnsEndPoint de) ? + string.Format("{0}:{1}", de.Host, de.Port) : + e.ToString(); + } + /// public abstract T Build(LdClientContext context); @@ -234,13 +278,19 @@ public LdValue DescribeConfiguration(LdClientContext context) => internal sealed class BuilderForDataStore : RedisStoreBuilder { - public override IPersistentDataStore Build(LdClientContext context) => - new RedisDataStoreImpl(_redisConfig, _prefix, context.Logger.SubLogger("DataStore.Redis")); + public override IPersistentDataStore Build(LdClientContext context) + { + var connection = GetOrCreateConnection(context.Logger.SubLogger("DataStore.Redis")); + return new RedisDataStoreImpl(connection, _prefix, context.Logger.SubLogger("DataStore.Redis")); + } } internal sealed class BuilderForBigSegments : RedisStoreBuilder { - public override IBigSegmentStore Build(LdClientContext context) => - new RedisBigSegmentStoreImpl(_redisConfig, _prefix, context.Logger.SubLogger("BigSegments.Redis")); + public override IBigSegmentStore Build(LdClientContext context) + { + var connection = GetOrCreateConnection(context.Logger.SubLogger("BigSegments.Redis")); + return new RedisBigSegmentStoreImpl(connection, _prefix, context.Logger.SubLogger("BigSegments.Redis")); + } } } diff --git a/pkgs/dotnet-server-sdk-redis/src/RedisStoreImplBase.cs b/pkgs/dotnet-server-sdk-redis/src/RedisStoreImplBase.cs index d7799f61..6046b60f 100644 --- a/pkgs/dotnet-server-sdk-redis/src/RedisStoreImplBase.cs +++ b/pkgs/dotnet-server-sdk-redis/src/RedisStoreImplBase.cs @@ -10,22 +10,20 @@ namespace LaunchDarkly.Sdk.Server.Integrations { internal abstract class RedisStoreImplBase : IDisposable { - protected readonly ConnectionMultiplexer _redis; + protected readonly IConnectionMultiplexer _redis; protected readonly string _prefix; protected readonly Logger _log; protected RedisStoreImplBase( - ConfigurationOptions redisConfig, + IConnectionMultiplexer redis, string prefix, Logger log ) { _log = log; - var redisConfigCopy = redisConfig.Clone(); - _redis = ConnectionMultiplexer.Connect(redisConfigCopy); + _redis = redis; _prefix = prefix; - _log.Info("Using Redis data store at {0} with prefix \"{1}\"", - string.Join(", ", redisConfig.EndPoints.Select(DescribeEndPoint)), prefix); + _log.Info("Using Redis connection with prefix \"{0}\"", prefix); } public void Dispose() @@ -42,13 +40,5 @@ private void Dispose(bool disposing) } } - private string DescribeEndPoint(EndPoint e) - { - // The default ToString() method of DnsEndPoint adds a prefix of "Unspecified", which looks - // confusing in our log messages. - return (e is DnsEndPoint de) ? - string.Format("{0}:{1}", de.Host, de.Port) : - e.ToString(); - } } } diff --git a/pkgs/dotnet-server-sdk-redis/test/RedisBigSegmentStoreBuilderTest.cs b/pkgs/dotnet-server-sdk-redis/test/RedisBigSegmentStoreBuilderTest.cs new file mode 100644 index 00000000..3f971908 --- /dev/null +++ b/pkgs/dotnet-server-sdk-redis/test/RedisBigSegmentStoreBuilderTest.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; +using LaunchDarkly.Sdk.Server.Subsystems; +using StackExchange.Redis; +using Xunit; + +namespace LaunchDarkly.Sdk.Server.Integrations +{ + public class RedisBigSegmentStoreBuilderTest + { + [Fact] + public void ConnectionWithNull() + { + var builder = Redis.BigSegmentStore(); + + // Setting null connection should throw ArgumentNullException + Assert.Throws(() => builder.Connection(null)); + } + + [Fact] + public void ConnectionMethodExists() + { + var builder = Redis.BigSegmentStore(); + + // Initially no external connection + Assert.Null(builder._externalConnection); + + // Test that the Connection method exists by checking if we can call it + // Use reflection to verify the method exists without requiring a real connection + var connectionMethod = typeof(RedisStoreBuilder) + .GetMethod("Connection", new[] { typeof(IConnectionMultiplexer) }); + + Assert.NotNull(connectionMethod); + Assert.Equal(typeof(RedisStoreBuilder), connectionMethod.ReturnType); + } + + [Fact] + public void ConnectionWorksWithOtherBuilderMethods() + { + var builder = Redis.BigSegmentStore(); + + // Chain with other builder methods + builder.Prefix("test-prefix"); + + Assert.Equal("test-prefix", builder._prefix); + } + } +} \ No newline at end of file diff --git a/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreBuilderTest.cs b/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreBuilderTest.cs index 87d5d22e..f1a4521f 100644 --- a/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreBuilderTest.cs +++ b/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreBuilderTest.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Net; +using System.Reflection; +using LaunchDarkly.Sdk.Server.Subsystems; +using StackExchange.Redis; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations @@ -113,5 +116,46 @@ public void Prefix() builder.Prefix(null); Assert.Equal(Redis.DefaultPrefix, builder._prefix); } + + [Fact] + public void ConnectionWithNull() + { + var builder = Redis.DataStore(); + + // Setting null connection should throw ArgumentNullException + Assert.Throws(() => builder.Connection(null)); + } + + [Fact] + public void ConnectionMethodExists() + { + var builder = Redis.DataStore(); + + // Initially no external connection + Assert.Null(builder._externalConnection); + + // Test that the Connection method exists by checking if we can call it + // Use reflection to verify the method exists without requiring a real connection + var connectionMethod = typeof(RedisStoreBuilder) + .GetMethod("Connection", new[] { typeof(IConnectionMultiplexer) }); + + Assert.NotNull(connectionMethod); + Assert.Equal(typeof(RedisStoreBuilder), connectionMethod.ReturnType); + } + + [Fact] + public void ConnectionWorksWithOtherBuilderMethods() + { + var builder = Redis.DataStore(); + + // Chain with other builder methods + builder.HostAndPort("test", 9999) + .Prefix("test-prefix"); + + // Config should be set + Assert.Collection(builder._redisConfig.EndPoints, + e => Assert.Equal(new DnsEndPoint("test", 9999), e)); + Assert.Equal("test-prefix", builder._prefix); + } } } diff --git a/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreTest.cs b/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreTest.cs index 5d2f9306..4dfab0dd 100644 --- a/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreTest.cs +++ b/pkgs/dotnet-server-sdk-redis/test/RedisDataStoreTest.cs @@ -68,10 +68,16 @@ public void LogMessageAtStartup() { Assert.Equal(LogLevel.Info, m.Level); Assert.Equal("BaseLoggerName.DataStore.Redis", m.LoggerName); - Assert.Equal("Using Redis data store at localhost:6379 with prefix \"my-prefix\"", - m.Text); + Assert.Contains("Creating Redis connection to localhost:6379 with prefix \"my-prefix\"", m.Text); + }, + m => + { + Assert.Equal(LogLevel.Info, m.Level); + Assert.Equal("BaseLoggerName.DataStore.Redis", m.LoggerName); + Assert.Equal("Using Redis connection with prefix \"my-prefix\"", m.Text); }); } } + } }