diff --git a/LaunchDarkly.OpenFeature.ServerProvider.sln b/LaunchDarkly.OpenFeature.ServerProvider.sln
index 71d0189..a6fdb7b 100644
--- a/LaunchDarkly.OpenFeature.ServerProvider.sln
+++ b/LaunchDarkly.OpenFeature.ServerProvider.sln
@@ -1,32 +1,40 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.30114.105
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36127.28 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider", "src\LaunchDarkly.OpenFeature.ServerProvider\LaunchDarkly.OpenFeature.ServerProvider.csproj", "{B61EC563-2D25-47C8-86A4-0C3A8C625109}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.Tests", "test\LaunchDarkly.OpenFeature.ServerProvider.Tests\LaunchDarkly.OpenFeature.ServerProvider.Tests.csproj", "{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection", "src\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj", "{DB523227-21AE-4B92-A263-05050CEF6C8D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests", "test\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj", "{F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B61EC563-2D25-47C8-86A4-0C3A8C625109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B61EC563-2D25-47C8-86A4-0C3A8C625109}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B61EC563-2D25-47C8-86A4-0C3A8C625109}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B61EC563-2D25-47C8-86A4-0C3A8C625109}.Release|Any CPU.Build.0 = Release|Any CPU
- {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E8CE160B-0A65-480F-AA3F-028AD9F17F6E}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B1DB34-6B22-4AA4-9974-6EE67145BDB9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB523227-21AE-4B92-A263-05050CEF6C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB523227-21AE-4B92-A263-05050CEF6C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB523227-21AE-4B92-A263-05050CEF6C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB523227-21AE-4B92-A263-05050CEF6C8D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F2C8E3A6-12D4-4B8F-9A7E-3F5C8D6B4E2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj
new file mode 100644
index 0000000..1b9467f
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.csproj
@@ -0,0 +1,52 @@
+
+
+
+
+ 2.1.1
+
+
+ net8.0
+ $(BUILDFRAMEWORKS)
+
+ portable
+ LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection
+ Library
+ LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection
+ LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection
+ 7.3
+
+ Dependency injection support for the LaunchDarkly OpenFeature .NET Server Provider.
+ Enables seamless integration of LaunchDarkly feature flagging with .NET applications via OpenFeature’s DI patterns.
+
+ LaunchDarkly
+ LaunchDarkly
+ LaunchDarkly
+ LaunchDarkly
+ LaunchDarkly
+ Copyright 2022 LaunchDarkly
+ Apache-2.0
+ https://github.com/launchdarkly/openfeature-dotnet-server
+ https://github.com/launchdarkly/openfeature-dotnet-server
+ main
+ true
+ snupkg
+
+
+ 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712
+
+
+
+
+
+
+
+
+
+
+
+ bin\$(Configuration)\$(TargetFramework)\LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.xml
+
+
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs
new file mode 100644
index 0000000..2827fbf
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection/LaunchDarklyOpenFeatureBuilderExtensions.cs
@@ -0,0 +1,165 @@
+using LaunchDarkly.Sdk.Server;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using OpenFeature;
+using OpenFeature.DependencyInjection;
+using System;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection
+{
+ ///
+ /// Provides extension methods for configuring the to use LaunchDarkly as a .
+ ///
+ public static partial class LaunchDarklyOpenFeatureBuilderExtensions
+ {
+ ///
+ /// Configures the to use LaunchDarkly as the default provider
+ /// using the specified instance.
+ ///
+ /// The instance to configure.
+ /// A pre-built LaunchDarkly .
+ /// The updated instance.
+ ///
+ /// Thrown when the argument is null.
+ ///
+ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, Configuration configuration)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null.");
+ }
+
+ return RegisterLaunchDarklyProvider(
+ builder,
+ () => configuration,
+ sp => sp.GetRequiredService()
+ );
+ }
+
+ ///
+ /// Configures the to use LaunchDarkly as a domain-scoped provider
+ /// using the specified instance.
+ ///
+ /// The instance to configure.
+ /// A domain identifier (e.g., tenant or environment).
+ /// A pre-built LaunchDarkly specific to the domain.
+ /// The updated instance.
+ ///
+ /// Thrown when the argument is null.
+ ///
+ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, Configuration configuration)
+ {
+ if (configuration == null)
+ {
+ throw new ArgumentNullException(nameof(configuration), "Configuration cannot be null.");
+ }
+
+ return RegisterLaunchDarklyProvider(
+ builder,
+ domain,
+ () => configuration,
+ (sp, key) => sp.GetRequiredKeyedService(key)
+ );
+ }
+
+ ///
+ /// Configures the to use LaunchDarkly as the default provider
+ /// using the specified SDK key and optional configuration delegate.
+ ///
+ /// The instance to configure.
+ /// The SDK key used to initialize the LaunchDarkly configuration.
+ /// An optional delegate to customize the .
+ /// The updated instance.
+ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string sdkKey, Action configure = null)
+ => RegisterLaunchDarklyProvider(
+ builder,
+ () => CreateConfiguration(sdkKey, configure),
+ sp => sp.GetRequiredService());
+
+ ///
+ /// Configures the to use LaunchDarkly as a domain-scoped provider
+ /// using the specified SDK key and optional configuration delegate.
+ ///
+ /// The instance to configure.
+ /// A domain identifier (e.g., tenant or environment).
+ /// The SDK key used to initialize the LaunchDarkly configuration.
+ /// An optional delegate to customize the .
+ /// The updated instance.
+ public static OpenFeatureBuilder UseLaunchDarkly(this OpenFeatureBuilder builder, string domain, string sdkKey, Action configure = null)
+ => RegisterLaunchDarklyProvider(
+ builder,
+ domain,
+ () => CreateConfiguration(sdkKey, configure),
+ (sp, key) => sp.GetRequiredKeyedService(key));
+
+ ///
+ /// Registers LaunchDarkly as the default feature provider using the given configuration factory and resolution logic.
+ ///
+ /// The instance to configure.
+ /// A delegate that returns a instance.
+ /// A delegate that resolves the from the service provider.
+ /// The updated instance.
+ private static OpenFeatureBuilder RegisterLaunchDarklyProvider(
+ OpenFeatureBuilder builder,
+ Func createConfiguration,
+ Func resolveConfiguration)
+ {
+ // Perform early configuration validation to ensure the provider is correctly constructed.
+ // This ensures any misconfiguration is caught during application startup rather than at runtime.
+ var config = createConfiguration();
+ builder.Services.TryAddSingleton(config);
+
+ return builder.AddProvider(serviceProvider => new Provider(resolveConfiguration(serviceProvider)));
+ }
+
+ ///
+ /// Registers LaunchDarkly as a domain-scoped feature provider using the given configuration factory and resolution logic.
+ ///
+ /// The instance to configure.
+ /// A domain identifier (e.g., tenant or environment).
+ /// A delegate that returns a domain-specific instance.
+ /// A delegate that resolves the domain-scoped from the service provider.
+ /// The updated instance.
+ private static OpenFeatureBuilder RegisterLaunchDarklyProvider(
+ OpenFeatureBuilder builder,
+ string domain,
+ Func createConfiguration,
+ Func resolveConfiguration)
+ {
+ // Perform early validation of the configuration to ensure it is valid before registration.
+ // This approach is consistent with the default (non-domain) registration path and helps fail fast on misconfiguration.
+ var config = createConfiguration();
+
+ // Register the domain-scoped configuration as a keyed singleton.
+ builder.Services.TryAddKeyedSingleton(domain, (_, key) => config);
+
+ // Register the default configuration provider, which resolves the appropriate domain-scoped configuration
+ // using the default name selection policy defined in PolicyNameOptions.
+ // This enables resolving Configuration via serviceProvider.GetRequiredService()
+ // when no specific domain key is explicitly provided.
+ builder.Services.TryAddSingleton(provider =>
+ {
+ var policy = provider.GetRequiredService>().Value;
+ var name = policy.DefaultNameSelector(provider);
+ return provider.GetRequiredKeyedService(name);
+ });
+
+ // Register the domain-scoped provider instance.
+ return builder.AddProvider(domain, (serviceProvider, key) => new Provider(resolveConfiguration(serviceProvider, key)));
+ }
+
+ ///
+ /// Creates a new using the specified SDK key and optional configuration delegate.
+ ///
+ /// The SDK key used to initialize the configuration.
+ /// An optional delegate to customize the .
+ /// A fully constructed instance.
+ private static Configuration CreateConfiguration(string sdkKey, Action configure = null)
+ {
+ var configBuilder = Configuration.Builder(sdkKey);
+ configure?.Invoke(configBuilder);
+ return configBuilder.Build();
+ }
+ }
+}
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj
new file mode 100644
index 0000000..3b5f47d
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests.csproj
@@ -0,0 +1,41 @@
+
+
+
+
+ net8.0
+ $(BUILDFRAMEWORKS)
+
+ false
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs
new file mode 100644
index 0000000..d61eea6
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyIntegrationTests.cs
@@ -0,0 +1,533 @@
+using System;
+using System.Threading.Tasks;
+using LaunchDarkly.Sdk.Server;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature;
+using OpenFeature.DependencyInjection;
+using OpenFeature.Model;
+using Xunit;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests
+{
+ public class LaunchDarklyIntegrationTests
+ {
+ private const string TestSdkKey = "test-sdk-key";
+ private const string TestDomain = "test-domain";
+ private const string TestFlagKey = "test-flag";
+
+ #region Helper Methods
+
+ ///
+ /// Configures an with OpenFeature and LaunchDarkly as the default feature provider.
+ ///
+ ///
+ /// Optional delegate to customize the LaunchDarkly during registration.
+ ///
+ ///
+ /// An initialized with LaunchDarkly configured as the default provider.
+ ///
+ private static ValueTask ConfigureLaunchDarklyAsync(Action configure = null)
+ => ConfigureOpenFeatureAsync(builder => builder.UseLaunchDarkly(TestSdkKey, configure));
+
+ ///
+ /// Configures an with OpenFeature and LaunchDarkly registered as a domain-scoped feature provider.
+ ///
+ /// The domain identifier to associate with the scoped provider (e.g., tenant or environment).
+ ///
+ /// Optional delegate to customize the LaunchDarkly for the specified domain.
+ ///
+ ///
+ /// An initialized with domain-scoped LaunchDarkly support.
+ ///
+ private static ValueTask ConfigureLaunchDarklyAsync(string domain, Action configure = null)
+ => ConfigureOpenFeatureAsync(builder => builder.UseLaunchDarkly(domain, TestSdkKey, configure));
+
+ ///
+ /// Configures an with OpenFeature and one or more feature providers.
+ ///
+ ///
+ /// Delegate to configure the with feature providers.
+ ///
+ ///
+ /// An initialized with provider lifecycle setup and validation enabled.
+ ///
+ private static async ValueTask ConfigureOpenFeatureAsync(Action configureBuilder)
+ {
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ // Register OpenFeature with the configured providers
+ services.AddOpenFeature(configureBuilder);
+
+ // Build the root service provider with scope validation enabled
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ // Ensure the feature provider lifecycle is initialized (e.g., LaunchDarkly ready for evaluations)
+ var lifecycleManager = serviceProvider.GetRequiredService();
+ await lifecycleManager.EnsureInitializedAsync();
+
+ return serviceProvider;
+ }
+
+ ///
+ /// Creates a scoped service provider for testing scoped services like IFeatureClient.
+ ///
+ /// The root service provider to create a scope from.
+ /// A scoped service provider.
+ private static IServiceProvider CreateScopedServiceProvider(IServiceProvider rootServiceProvider)
+ {
+ var scopeFactory = rootServiceProvider.GetRequiredService();
+ return scopeFactory.CreateScope().ServiceProvider;
+ }
+
+ #endregion
+
+ #region Configuration Overload Integration Tests
+
+ [Fact]
+ public async Task ConfigurationOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetBooleanValueAsync(TestFlagKey, false);
+
+ // Assert
+ Assert.False(result); // Default value should be returned in offline mode
+ }
+
+ [Fact]
+ public async Task ConfigurationOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredKeyedService(TestDomain);
+
+ // Act
+ var result = await client.GetBooleanValueAsync(TestFlagKey, true);
+
+ // Assert
+ Assert.True(result); // Default value should be returned in offline mode
+ }
+
+ [Fact]
+ public async Task ConfigurationOverload_DefaultProvider_ShouldPreserveCustomConfigurationSettings()
+ {
+ // Arrange
+ var startWaitTime = TimeSpan.FromSeconds(5);
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg
+ .Offline(true)
+ .StartWaitTime(startWaitTime));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredService();
+
+ // Assert
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ [Fact]
+ public async Task ConfigurationOverload_DomainProvider_ShouldPreserveCustomConfigurationSettings()
+ {
+ // Arrange
+ var startWaitTime = TimeSpan.FromSeconds(10);
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg
+ .Offline(true)
+ .StartWaitTime(startWaitTime));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ // Assert
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ #endregion
+
+ #region SDK Key Overload Integration Tests
+
+ [Fact]
+ public async Task SdkKeyOverload_DefaultProvider_ShouldResolveFeatureClientSuccessfully()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetBooleanValueAsync(TestFlagKey, false);
+
+ // Assert
+ Assert.False(result); // Default value should be returned in offline mode
+ }
+
+ [Fact]
+ public async Task SdkKeyOverload_DomainProvider_ShouldResolveKeyedFeatureClientSuccessfully()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredKeyedService(TestDomain);
+
+ // Act
+ var result = await client.GetBooleanValueAsync(TestFlagKey, true);
+
+ // Assert
+ Assert.True(result); // Default value should be returned in offline mode
+ }
+
+ [Fact]
+ public async Task SdkKeyOverload_DefaultProvider_ShouldApplyCustomConfigurationFromDelegate()
+ {
+ // Arrange
+ var startWaitTime = TimeSpan.FromSeconds(3);
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg
+ .Offline(true)
+ .StartWaitTime(startWaitTime));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredService();
+
+ // Assert
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ [Fact]
+ public async Task SdkKeyOverload_DomainProvider_ShouldApplyCustomConfigurationFromDelegate()
+ {
+ // Arrange
+ var startWaitTime = TimeSpan.FromSeconds(7);
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg
+ .Offline(true)
+ .StartWaitTime(startWaitTime));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ // Assert
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ #endregion
+
+ #region Multi-Provider Setup Integration Tests
+
+ [Fact]
+ public async Task MultiProvider_MixedOverloads_ShouldRegisterBothDefaultAndDomainProvidersCorrectly()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureOpenFeatureAsync(builder =>
+ {
+ // Register both default and domain-scoped providers using different overloads
+ builder
+ .UseLaunchDarkly("domain1", TestSdkKey, cfg => cfg.Offline(true))
+ .UseLaunchDarkly("domain2", TestSdkKey, cfg => cfg.Offline(true))
+ .AddPolicyName(policy =>
+ {
+ policy.DefaultNameSelector = _ => "domain1";
+ });
+ });
+
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+
+ // Act
+ var defaultClient = scopedProvider.GetRequiredService();
+ var domain1Client = scopedProvider.GetRequiredKeyedService("domain1");
+ var domain2Client = scopedProvider.GetRequiredKeyedService("domain2");
+
+ var defaultConfig = serviceProvider.GetRequiredService();
+ var domain1Config = serviceProvider.GetRequiredKeyedService("domain1");
+ var domain2Config = serviceProvider.GetRequiredKeyedService("domain2");
+
+ // Assert
+ Assert.Same(defaultClient, domain1Client);
+ Assert.NotSame(domain1Client, domain2Client);
+ Assert.NotSame(domain1Config, domain2Config);
+
+ Assert.True(domain1Config.Offline, "Expected 'domain1' LaunchDarkly config to be in offline mode.");
+ Assert.True(domain2Config.Offline, "Expected 'domain2' LaunchDarkly config to be in offline mode.");
+ }
+
+ [Fact]
+ public async Task MultiProvider_MultipleDomains_ShouldIsolateConfigurationsCorrectly()
+ {
+ // Arrange
+ const string fastDomain = "fast-domain";
+ const string slowDomain = "slow-domain";
+ var fastStartWait = TimeSpan.FromMilliseconds(100);
+ var slowStartWait = TimeSpan.FromSeconds(5);
+
+ var serviceProvider = await ConfigureOpenFeatureAsync(builder =>
+ {
+ // Register using different configurations for variety
+ builder.UseLaunchDarkly(fastDomain, TestSdkKey, cfg => cfg
+ .Offline(true)
+ .StartWaitTime(fastStartWait));
+
+ builder.UseLaunchDarkly(slowDomain, TestSdkKey, cfg => cfg
+ .Offline(true)
+ .StartWaitTime(slowStartWait));
+
+ builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => fastDomain);
+ });
+
+ // Act & Assert
+ var config1 = serviceProvider.GetRequiredKeyedService(fastDomain);
+ var config2 = serviceProvider.GetRequiredKeyedService(slowDomain);
+
+ Assert.NotSame(config1, config2);
+ Assert.Equal(fastStartWait, config1.StartWaitTime);
+ Assert.Equal(slowStartWait, config2.StartWaitTime);
+ Assert.True(config1.Offline);
+ Assert.True(config2.Offline);
+ }
+
+ #endregion
+
+ #region OpenFeature Value Type Support Tests
+
+ [Fact]
+ public async Task AllOverloads_FeatureProviders_ShouldSupportAllOpenFeatureValueTypes()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureOpenFeatureAsync(builder =>
+ {
+ // Test all SDK key overloads
+ builder.UseLaunchDarkly("domain1", TestSdkKey, cfg => cfg.Offline(true)); // Default provider
+ builder.UseLaunchDarkly("domain2", TestSdkKey, cfg => cfg.Offline(true)); // Domain provider
+ builder.UseLaunchDarkly("domain3", TestSdkKey, cfg => cfg.Offline(true)); // Domain provider
+ builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => "domain1");
+ });
+
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+
+ var defaultClient = scopedProvider.GetRequiredService();
+ var domain1Client = scopedProvider.GetRequiredKeyedService("domain1");
+ var domain2Client = scopedProvider.GetRequiredKeyedService("domain2");
+ var domain3Client = scopedProvider.GetRequiredKeyedService("domain3");
+
+ // Act & Assert - Test all supported types on all providers
+ foreach (var client in new[] { defaultClient, domain1Client, domain2Client, domain3Client })
+ {
+ var boolResult = await client.GetBooleanValueAsync("bool-flag", true);
+ Assert.True(boolResult);
+
+ var stringResult = await client.GetStringValueAsync("string-flag", "default");
+ Assert.Equal("default", stringResult);
+
+ var intResult = await client.GetIntegerValueAsync("int-flag", 42);
+ Assert.Equal(42, intResult);
+
+ var doubleResult = await client.GetDoubleValueAsync("double-flag", 3.14);
+ Assert.Equal(3.14, doubleResult);
+
+ var structureResult = await client.GetObjectValueAsync("object-flag", new Value("default"));
+ Assert.Equal("default", structureResult.AsString);
+ }
+ }
+
+ #endregion
+
+ #region OpenFeature Behavior Integration Tests
+
+ [Fact]
+ public async Task DefaultProvider_InOfflineMode_ShouldReturnCorrectReasonAndDefaultValue()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredService();
+
+ // Act
+ var result = await client.GetBooleanDetailsAsync(TestFlagKey, false);
+
+ // Assert
+ Assert.False(result.Value);
+ }
+
+ [Fact]
+ public async Task DefaultProvider_WithEvaluationContext_ShouldHandleContextCorrectly()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+ var scopedProvider = CreateScopedServiceProvider(serviceProvider);
+ var client = scopedProvider.GetRequiredService();
+
+ var context = EvaluationContext.Builder()
+ .Set("userId", "test-user")
+ .Set("email", "test@example.com")
+ .Build();
+
+ // Act
+ var result = await client.GetBooleanDetailsAsync(TestFlagKey, false, context);
+
+ // Assert
+ Assert.False(result.Value);
+ //Assert.Equal(Reason.Default, result.Reason);
+ }
+
+ #endregion
+
+ #region Service Lifetime Integration Tests
+
+ [Fact]
+ public async Task DefaultProvider_Configuration_ShouldBeRegisteredAsSingleton()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+
+ // Act
+ var config1 = serviceProvider.GetRequiredService();
+ var config2 = serviceProvider.GetRequiredService();
+
+ // Assert
+ Assert.Same(config1, config2);
+ }
+
+ [Fact]
+ public async Task DomainProvider_Configuration_ShouldBeRegisteredAsKeyedSingleton()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true));
+
+ // Act
+ var config1 = serviceProvider.GetRequiredKeyedService(TestDomain);
+ var config2 = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ // Assert
+ Assert.Same(config1, config2);
+ }
+
+ [Fact]
+ public async Task TryAddSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration()
+ {
+ // Arrange & Act
+ var serviceProvider = await ConfigureOpenFeatureAsync(builder =>
+ {
+ // First registration should win due to TryAddSingleton behavior
+ builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true));
+ builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(false)); // Should not replace the first
+ });
+
+ // Assert
+ var registeredConfig = serviceProvider.GetRequiredService();
+ Assert.True(registeredConfig.Offline); // Should still be the first configuration
+ }
+
+ [Fact]
+ public async Task TryAddKeyedSingleton_MultipleRegistrations_ShouldNotReplaceExistingRegistration()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureOpenFeatureAsync(builder =>
+ {
+ // First registration should win due to TryAddKeyedSingleton behavior
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true));
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(false)); // Should not replace the first
+ builder.AddPolicyName(policy => policy.DefaultNameSelector = _ => TestDomain);
+ });
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ // Assert
+ Assert.True(registeredConfig.Offline); // Should still be the first configuration
+ }
+
+ #endregion
+
+ #region Resource Management Integration Tests
+
+ [Fact]
+ public async Task ServiceProviderDisposal_AfterUsingProviders_ShouldNotCauseMemoryLeaks()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+
+ // Act
+ var config = serviceProvider.GetRequiredService();
+
+ // Assert
+ Assert.NotNull(config);
+ Assert.True(config.Offline);
+ }
+
+ [Fact]
+ public async Task MultipleServiceProviders_WithSameConfiguration_ShouldIsolateConfigurations()
+ {
+ // Arrange
+ var serviceProvider1 = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+ var serviceProvider2 = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(false));
+
+ // Act
+ var config1 = serviceProvider1.GetRequiredService();
+ var config2 = serviceProvider2.GetRequiredService();
+
+ // Assert
+ Assert.NotSame(config1, config2);
+ Assert.True(config1.Offline);
+ Assert.False(config2.Offline);
+ }
+
+ #endregion
+
+ #region Early Validation Integration Tests
+
+ [Fact]
+ public async Task EarlyValidation_WithValidConfiguration_ShouldPassValidationAndRegisterCorrectly()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(cfg => cfg.Offline(true));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredService();
+
+ // Assert
+ Assert.NotNull(registeredConfig);
+ Assert.True(registeredConfig.Offline);
+ }
+
+ [Fact]
+ public async Task EarlyValidation_WithValidDomainConfiguration_ShouldPassValidationAndRegisterCorrectly()
+ {
+ // Arrange
+ var serviceProvider = await ConfigureLaunchDarklyAsync(TestDomain, cfg => cfg.Offline(true));
+
+ // Act
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ // Assert
+ Assert.NotNull(registeredConfig);
+ Assert.True(registeredConfig.Offline);
+ }
+
+ [Fact]
+ public void EarlyValidation_WithPrebuiltConfiguration_ShouldPassValidationAndRegisterCorrectly()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ services.AddOpenFeature(builder =>
+ {
+ builder.UseLaunchDarkly(config);
+ });
+
+ // Act
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+
+ // Assert
+ var registeredConfig = serviceProvider.GetRequiredService();
+ Assert.NotNull(registeredConfig);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs
new file mode 100644
index 0000000..fa9249a
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests/LaunchDarklyOpenFeatureBuilderExtensionsTests.cs
@@ -0,0 +1,538 @@
+using System;
+using LaunchDarkly.Sdk.Server;
+using Microsoft.Extensions.DependencyInjection;
+using OpenFeature.DependencyInjection;
+using Xunit;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.DependencyInjection.Tests
+{
+ public class LaunchDarklyOpenFeatureBuilderExtensionsTests
+ {
+ private const string TestSdkKey = "test-sdk-key";
+ private const string TestDomain = "test-domain";
+
+ #region UseLaunchDarkly(Configuration) - Default Provider Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithConfiguration_WhenCalled_ShouldReturnSameBuilderInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ var result = builder.UseLaunchDarkly(config);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithConfiguration_WhenCalled_ShouldRegisterConfigurationAsSingleton()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ builder.UseLaunchDarkly(config);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredService();
+ Assert.NotNull(registeredConfig);
+ Assert.True(registeredConfig.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimes_ShouldShareSameConfigurationInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ builder.UseLaunchDarkly(config);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config1 = serviceProvider.GetRequiredService();
+ var config2 = serviceProvider.GetRequiredService();
+ Assert.Same(config1, config2);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithConfiguration_WhenCalledMultipleTimesWithDifferentConfigs_ShouldUseFirstConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build();
+ var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build();
+
+ // Act
+ builder.UseLaunchDarkly(config1);
+ builder.UseLaunchDarkly(config2); // Should not replace the first
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredService();
+ Assert.True(registeredConfig.Offline); // Should still be the first configuration
+ }
+
+ #endregion
+
+ #region UseLaunchDarkly(domain, Configuration) - Domain Provider Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalled_ShouldReturnSameBuilderInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestDomain, config);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalled_ShouldRegisterConfigurationAsKeyedSingleton()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ builder.UseLaunchDarkly(TestDomain, config);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.NotNull(registeredConfig);
+ Assert.True(registeredConfig.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndConfiguration_WhenCalledMultipleTimes_ShouldShareSameConfigurationInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ builder.UseLaunchDarkly(TestDomain, config);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config1 = serviceProvider.GetRequiredKeyedService(TestDomain);
+ var config2 = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.Same(config1, config2);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void UseLaunchDarklyWithDomainAndConfiguration_WhenDomainIsNullOrWhitespace_ShouldReturnBuilder(string domain)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config = Configuration.Builder(TestSdkKey).Build();
+
+ // Act
+ var result = builder.UseLaunchDarkly(domain, config);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndConfiguration_WhenDifferentDomainsUsed_ShouldRegisterSeparateConfigurations()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var config1 = Configuration.Builder(TestSdkKey).Offline(true).Build();
+ var config2 = Configuration.Builder(TestSdkKey).Offline(false).Build();
+ const string domain1 = "domain1";
+ const string domain2 = "domain2";
+
+ // Act
+ builder.UseLaunchDarkly(domain1, config1);
+ builder.UseLaunchDarkly(domain2, config2);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig1 = serviceProvider.GetRequiredKeyedService(domain1);
+ var registeredConfig2 = serviceProvider.GetRequiredKeyedService(domain2);
+ Assert.NotSame(registeredConfig1, registeredConfig2);
+ Assert.True(registeredConfig1.Offline);
+ Assert.False(registeredConfig2.Offline);
+ }
+
+ #endregion
+
+ #region UseLaunchDarkly(sdkKey) - SDK Key Default Provider Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKey_WhenCalled_ShouldReturnSameBuilderInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestSdkKey);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKey_WhenCalled_ShouldRegisterValidConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.UseLaunchDarkly(TestSdkKey);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredService();
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCalled_ShouldApplyCustomConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var configureWasCalled = false;
+ ConfigurationBuilder capturedBuilder = null;
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestSdkKey, configBuilder =>
+ {
+ configureWasCalled = true;
+ capturedBuilder = configBuilder;
+ configBuilder.Offline(true);
+ });
+
+ // Assert
+ Assert.Same(builder, result);
+
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredService();
+
+ Assert.True(configureWasCalled);
+ Assert.NotNull(capturedBuilder);
+ Assert.True(config.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenConfigurationDelegateThrows_ShouldPropagateExceptionImmediately()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var expectedException = new InvalidOperationException("Test exception");
+
+ // Act & Assert
+ // The exception should be thrown immediately during registration due to early validation
+ var actualException = Assert.Throws(() =>
+ builder.UseLaunchDarkly(TestSdkKey, _ => throw expectedException));
+ Assert.Same(expectedException, actualException);
+ }
+
+ #endregion
+
+ #region UseLaunchDarkly(domain, sdkKey) - SDK Key Domain Provider Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKey_WhenCalled_ShouldReturnSameBuilderInstance()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKey_WhenCalled_ShouldRegisterValidKeyedConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCalled_ShouldApplyCustomConfiguration()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var configureWasCalled = false;
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, configBuilder =>
+ {
+ configureWasCalled = true;
+ configBuilder.Offline(true);
+ });
+
+ // Assert
+ Assert.Same(builder, result);
+
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredKeyedService(TestDomain);
+
+ Assert.True(configureWasCalled);
+ Assert.True(config.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenConfigurationDelegateThrows_ShouldPropagateExceptionImmediately()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var expectedException = new InvalidOperationException("Test exception");
+
+ // Act
+ InvalidOperationException registerWithThrowing() => Assert.Throws(() =>
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey, _ => throw expectedException));
+
+ // Assert
+ // The exception should be thrown immediately during registration due to early validation
+ var actualException = registerWithThrowing();
+ Assert.Same(expectedException, actualException);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenNullConfigurationDelegate_ShouldNotThrowAndReturnBuilder()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act & Assert - should not throw
+ var result = builder.UseLaunchDarkly(TestDomain, TestSdkKey, null);
+
+ Assert.Same(builder, result);
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKeyAndNullDelegate_WhenNullConfigurationPassed_ShouldThrowArgumentNullException()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ Configuration configuration = null;
+
+ // Assert
+ Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey, configuration));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void UseLaunchDarklyWithDomainAndSdkKey_WhenSdkKeyIsNullOrWhitespace_ShouldReturnBuilder(string sdkKey)
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act
+ var result = builder.UseLaunchDarkly(TestDomain, sdkKey);
+
+ // Assert
+ Assert.Same(builder, result);
+ }
+
+ #endregion
+
+ #region Null Builder Validation Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithNullBuilder_WhenSdkKeyOverloadUsed_ShouldThrowNullReferenceException()
+ {
+ // Arrange
+ OpenFeatureBuilder builder = null;
+
+ // Act & Assert
+ Assert.Throws(() => builder.UseLaunchDarkly(TestSdkKey));
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithNullBuilder_WhenConfigurationOverloadUsed_ShouldThrowNullReferenceException()
+ {
+ // Arrange
+ OpenFeatureBuilder builder = null;
+ var config = Configuration.Builder(TestSdkKey).Build();
+
+ // Act & Assert
+ Assert.Throws(() => builder.UseLaunchDarkly(config));
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithNullBuilder_WhenDomainSdkKeyOverloadUsed_ShouldThrowNullReferenceException()
+ {
+ // Arrange
+ OpenFeatureBuilder builder = null;
+
+ // Act & Assert
+ Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, TestSdkKey));
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithNullBuilder_WhenDomainConfigurationOverloadUsed_ShouldThrowNullReferenceException()
+ {
+ // Arrange
+ OpenFeatureBuilder builder = null;
+ var config = Configuration.Builder(TestSdkKey).Build();
+
+ // Act & Assert
+ Assert.Throws(() => builder.UseLaunchDarkly(TestDomain, config));
+ }
+
+ #endregion
+
+ #region Configuration Property Preservation Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithConfiguration_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var originalConfig = Configuration.Builder(TestSdkKey).Offline(true).Build();
+
+ // Act
+ builder.UseLaunchDarkly(originalConfig);
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredService();
+
+ // The registered config should be a rebuilt version, not the same instance
+ // but should have the same properties
+ Assert.True(registeredConfig.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var startWaitTime = TimeSpan.FromSeconds(5);
+
+ // Act
+ builder.UseLaunchDarkly(TestSdkKey, config =>
+ {
+ config.Offline(true);
+ config.StartWaitTime(startWaitTime);
+ });
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredService();
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenCustomPropertiesSet_ShouldPreserveConfigurationProperties()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+ var startWaitTime = TimeSpan.FromSeconds(10);
+
+ // Act
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey, config =>
+ {
+ config.Offline(true);
+ config.StartWaitTime(startWaitTime);
+ });
+
+ // Assert
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var registeredConfig = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.True(registeredConfig.Offline);
+ Assert.Equal(startWaitTime, registeredConfig.StartWaitTime);
+ }
+
+ #endregion
+
+ #region Early Validation Behavior Tests
+
+ [Fact]
+ public void UseLaunchDarklyWithSdkKeyAndDelegate_WhenEarlyValidationPasses_ShouldPreventRuntimeFailures()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act - Early validation should catch configuration issues immediately
+ builder.UseLaunchDarkly(TestSdkKey, cfg => cfg.Offline(true));
+
+ // Assert - If we reach here, early validation passed
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredService();
+ Assert.NotNull(config);
+ Assert.True(config.Offline);
+ }
+
+ [Fact]
+ public void UseLaunchDarklyWithDomainAndSdkKeyAndDelegate_WhenEarlyValidationPasses_ShouldPreventRuntimeFailures()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var builder = new OpenFeatureBuilder(services);
+
+ // Act - Early validation should catch configuration issues immediately
+ builder.UseLaunchDarkly(TestDomain, TestSdkKey, cfg => cfg.Offline(true));
+
+ // Assert - If we reach here, early validation passed
+ var serviceProvider = services.BuildServiceProvider(validateScopes: true);
+ var config = serviceProvider.GetRequiredKeyedService(TestDomain);
+ Assert.NotNull(config);
+ Assert.True(config.Offline);
+ }
+
+ #endregion
+ }
+}