diff --git a/.github/actions/publish/action.yml b/.github/actions/publish/action.yml
index 8a6684d..49232fb 100644
--- a/.github/actions/publish/action.yml
+++ b/.github/actions/publish/action.yml
@@ -25,6 +25,7 @@ runs:
run: |
dotnet restore
dotnet pack --no-build --output nupkgs --configuration Release src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj
+ dotnet pack --no-build --output nupkgs --configuration Release src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj
- name: Publish Package
if: ${{ inputs.dry_run == 'false' }}
diff --git a/.github/actions/release-build/action.yml b/.github/actions/release-build/action.yml
index 6eff9d2..42a2046 100644
--- a/.github/actions/release-build/action.yml
+++ b/.github/actions/release-build/action.yml
@@ -16,4 +16,6 @@ runs:
- name: Build
shell: bash
# For a release we do not want to build the test project. The internals are not visible in release and it will fail.
- run: dotnet build /p:Configuration=Release ./src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj
+ run: |
+ dotnet build /p:Configuration=Release ./src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj
+ dotnet build /p:Configuration=Release ./src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2b3e3e8..2cf3d26 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,10 +36,18 @@ Or, to build for only one target framework (in this example, .NET Standard 2.0):
dotnet build src/LaunchDarkly.OpenFeature.ServerProvider -f netstandard2.0
```
+The dependency injection and hosting support is a separate package, `src/LaunchDarkly.OpenFeature.ServerProvider.Hosting`, which is versioned and released together with the provider. Dependencies used only by that integration belong there, so that the provider package remains usable without them.
+
### Testing
To run all unit tests:
+```bash
+dotnet test
+```
+
+Or, to run the tests for a single project:
+
```bash
dotnet test test/LaunchDarkly.OpenFeature.ServerProvider.Tests/LaunchDarkly.OpenFeature.ServerProvider.Tests.csproj
```
diff --git a/LaunchDarkly.OpenFeature.ServerProvider.sln b/LaunchDarkly.OpenFeature.ServerProvider.sln
index 71d0189..9229f17 100644
--- a/LaunchDarkly.OpenFeature.ServerProvider.sln
+++ b/LaunchDarkly.OpenFeature.ServerProvider.sln
@@ -7,6 +7,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.Se
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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{80D014BF-F653-436F-9F7D-9A81FB2AE17F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.Hosting", "src\LaunchDarkly.OpenFeature.ServerProvider.Hosting\LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj", "{1B406D33-B27C-42AA-BBA4-431FA53105DF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6DAE4E12-F3FD-4E83-9BFB-AE8C923881A9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests", "test\LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests\LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests.csproj", "{93ADE451-57AE-4622-8048-C255217E4AD5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -28,5 +36,17 @@ Global
{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
+ {1B406D33-B27C-42AA-BBA4-431FA53105DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B406D33-B27C-42AA-BBA4-431FA53105DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B406D33-B27C-42AA-BBA4-431FA53105DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B406D33-B27C-42AA-BBA4-431FA53105DF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {93ADE451-57AE-4622-8048-C255217E4AD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {93ADE451-57AE-4622-8048-C255217E4AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {93ADE451-57AE-4622-8048-C255217E4AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {93ADE451-57AE-4622-8048-C255217E4AD5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1B406D33-B27C-42AA-BBA4-431FA53105DF} = {80D014BF-F653-436F-9F7D-9A81FB2AE17F}
+ {93ADE451-57AE-4622-8048-C255217E4AD5} = {6DAE4E12-F3FD-4E83-9BFB-AE8C923881A9}
EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 3011175..c7714c7 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,54 @@ Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side
For information on using the OpenFeature client please refer to the [OpenFeature Documentation](https://docs.openfeature.dev/docs/reference/concepts/evaluation-api/).
+### Dependency injection
+
+Support for the OpenFeature dependency injection and hosting integration is in a separate package, so that applications which do not use dependency injection do not take a dependency on `Microsoft.Extensions.*`.
+
+```bash
+dotnet add package LaunchDarkly.OpenFeature.ServerProvider.Hosting
+```
+
+```csharp
+using LaunchDarkly.OpenFeature.ServerProvider.Hosting;
+
+builder.Services.AddOpenFeature(openFeature => openFeature
+ .AddLaunchDarklyProvider(options =>
+ {
+ options.SdkKey = "my-sdk-key";
+ options.ConfigureSdk = config => config.StartWaitTime(TimeSpan.FromSeconds(10));
+ }));
+```
+
+Injected `IFeatureClient` instances then evaluate flags using LaunchDarkly, and the provider is initialized and shut down with the host.
+
+The `ILdClient` used by the provider is also registered, for use-cases that OpenFeature does not support such as migration flags and track events:
+
+```csharp
+public class MyService
+{
+ public MyService(IFeatureClient featureClient, ILdClient ldClient) { }
+}
+```
+
+Providers may also be registered for a specific OpenFeature domain, in which case the `ILdClient` for that provider is registered as a keyed service using the domain as the key:
+
+```csharp
+builder.Services.AddOpenFeature(openFeature => openFeature
+ .AddLaunchDarklyProvider("beta", options => options.SdkKey = "my-sdk-key"));
+```
+
+```csharp
+public class MyService
+{
+ public MyService([FromKeyedServices("beta")] ILdClient ldClient) { }
+}
+```
+
+Each registration uses a single provider, and therefore a single `ILdClient`, no matter how many times it is resolved.
+
+The OpenFeature dependency injection integration does not use `OpenFeature.Api.Instance`, so a provider registered this way is not visible to code which evaluates flags using that global instance.
+
## OpenFeature Specific Considerations
LaunchDarkly evaluates contexts, and it can either evaluate a single-context, or a multi-context. When using OpenFeature both single and multi-contexts must be encoded into a single `EvaluationContext`. This is accomplished by looking for an attribute named `kind` in the `EvaluationContext`.
diff --git a/release-please-config.json b/release-please-config.json
index 5a3e292..8f0f83b 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -7,7 +7,8 @@
"include-v-in-tag": false,
"include-component-in-tag": false,
"extra-files": [
- "src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj"
+ "src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj",
+ "src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj"
]
}
}
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/FeatureBuilderExtensions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/FeatureBuilderExtensions.cs
new file mode 100644
index 0000000..497eec2
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/FeatureBuilderExtensions.cs
@@ -0,0 +1,116 @@
+using System;
+using LaunchDarkly.Sdk.Server.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using OpenFeature;
+using OpenFeature.Hosting;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.Hosting
+{
+ ///
+ /// Extension methods which register the LaunchDarkly provider with the OpenFeature dependency
+ /// injection integration.
+ ///
+ ///
+ ///
+ /// In addition to the provider these methods register the used by the provider,
+ /// which can be used for LaunchDarkly functionality that OpenFeature does not expose, such as migration
+ /// flags and tracking. For a provider registered with a domain the client is registered as a keyed service
+ /// using that domain as the key.
+ ///
+ ///
+ /// The provider, and its client, are shut down when the host stops.
+ ///
+ ///
+ public static class FeatureBuilderExtensions
+ {
+ ///
+ /// Registers the LaunchDarkly provider as the default OpenFeature provider using options configured
+ /// elsewhere, for instance by binding to configuration.
+ ///
+ /// The OpenFeature builder
+ /// The OpenFeature builder
+ public static OpenFeatureBuilder AddLaunchDarklyProvider(this OpenFeatureBuilder builder)
+ {
+ if (builder == null) throw new ArgumentNullException(nameof(builder));
+
+ return AddDefaultProvider(builder);
+ }
+
+ ///
+ /// Registers the LaunchDarkly provider as the default OpenFeature provider.
+ ///
+ /// The OpenFeature builder
+ /// Configures the LaunchDarkly provider options
+ /// The OpenFeature builder
+ public static OpenFeatureBuilder AddLaunchDarklyProvider(this OpenFeatureBuilder builder,
+ Action configureOptions)
+ {
+ if (builder == null) throw new ArgumentNullException(nameof(builder));
+ if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
+
+ builder.Services.Configure(ProviderRegistry.DefaultDomain, configureOptions);
+ return AddDefaultProvider(builder);
+ }
+
+ ///
+ /// Registers the LaunchDarkly provider for the given domain using options configured elsewhere,
+ /// for instance by binding named to configuration.
+ ///
+ /// The OpenFeature builder
+ /// The OpenFeature domain the provider is bound to, which is also the name of
+ /// the options used to configure it
+ /// The OpenFeature builder
+ public static OpenFeatureBuilder AddLaunchDarklyProvider(this OpenFeatureBuilder builder, string domain)
+ {
+ if (builder == null) throw new ArgumentNullException(nameof(builder));
+ if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain));
+
+ return AddDomainProvider(builder, domain);
+ }
+
+ ///
+ /// Registers the LaunchDarkly provider for the given domain.
+ ///
+ /// The OpenFeature builder
+ /// The OpenFeature domain the provider is bound to
+ /// Configures the LaunchDarkly provider options
+ /// The OpenFeature builder
+ public static OpenFeatureBuilder AddLaunchDarklyProvider(this OpenFeatureBuilder builder, string domain,
+ Action configureOptions)
+ {
+ if (builder == null) throw new ArgumentNullException(nameof(builder));
+ if (string.IsNullOrWhiteSpace(domain)) throw new ArgumentNullException(nameof(domain));
+ if (configureOptions == null) throw new ArgumentNullException(nameof(configureOptions));
+
+ builder.Services.Configure(domain, configureOptions);
+ return AddDomainProvider(builder, domain);
+ }
+
+ private static OpenFeatureBuilder AddDefaultProvider(OpenFeatureBuilder builder)
+ {
+ AddRegistry(builder);
+ builder.Services.TryAddSingleton(sp =>
+ sp.GetRequiredService().Get(ProviderRegistry.DefaultDomain).GetClient());
+
+ return builder.AddProvider(sp =>
+ sp.GetRequiredService().Get(ProviderRegistry.DefaultDomain));
+ }
+
+ private static OpenFeatureBuilder AddDomainProvider(OpenFeatureBuilder builder, string domain)
+ {
+ AddRegistry(builder);
+ builder.Services.TryAddKeyedSingleton(domain, (sp, key) =>
+ sp.GetRequiredService().Get(key.ToString()).GetClient());
+
+ return builder.AddProvider(domain,
+ (sp, providerDomain) => sp.GetRequiredService().Get(providerDomain));
+ }
+
+ private static void AddRegistry(OpenFeatureBuilder builder)
+ {
+ builder.Services.AddOptions();
+ builder.Services.TryAddSingleton();
+ }
+ }
+}
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj
new file mode 100644
index 0000000..170f9fb
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarkly.OpenFeature.ServerProvider.Hosting.csproj
@@ -0,0 +1,48 @@
+
+
+
+
+ 2.3.0
+
+
+ netstandard2.0;net471;net6.0;net8.0
+ $(BUILDFRAMEWORKS)
+
+ portable
+ LaunchDarkly.OpenFeature.ServerProvider.Hosting
+ Library
+ LaunchDarkly.OpenFeature.ServerProvider.Hosting
+ LaunchDarkly.OpenFeature.ServerProvider.Hosting
+ 7.3
+ Dependency injection and hosting support for the LaunchDarkly OpenFeature Provider for the Server-Side SDK for .NET
+ LaunchDarkly
+ LaunchDarkly
+ LaunchDarkly
+ Copyright 2026 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.Hosting.xml
+
+
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarklyProviderOptions.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarklyProviderOptions.cs
new file mode 100644
index 0000000..a14614e
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/LaunchDarklyProviderOptions.cs
@@ -0,0 +1,34 @@
+using System;
+using LaunchDarkly.Sdk.Server;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.Hosting
+{
+ ///
+ /// Options used to configure a LaunchDarkly registered through dependency injection.
+ ///
+ public sealed class LaunchDarklyProviderOptions
+ {
+ ///
+ /// The LaunchDarkly SDK key.
+ ///
+ public string SdkKey { get; set; }
+
+ ///
+ /// An optional delegate which can further configure the LaunchDarkly SDK.
+ ///
+ public Action ConfigureSdk { get; set; }
+
+ internal Configuration BuildConfiguration()
+ {
+ if (string.IsNullOrWhiteSpace(SdkKey))
+ {
+ throw new InvalidOperationException(
+ $"{nameof(LaunchDarklyProviderOptions)}.{nameof(SdkKey)} must be configured.");
+ }
+
+ var configurationBuilder = Configuration.Builder(SdkKey);
+ ConfigureSdk?.Invoke(configurationBuilder);
+ return configurationBuilder.Build();
+ }
+ }
+}
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/ProviderRegistry.cs b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/ProviderRegistry.cs
new file mode 100644
index 0000000..8dddd30
--- /dev/null
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider.Hosting/ProviderRegistry.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.Options;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.Hosting
+{
+ ///
+ /// Caches a single provider instance per domain so that resolving the provider more than once does not
+ /// create additional LaunchDarkly clients.
+ ///
+ internal sealed class ProviderRegistry
+ {
+ internal const string DefaultDomain = "";
+
+ private readonly IOptionsMonitor _options;
+ private readonly Dictionary _providers = new Dictionary();
+ private readonly object _lock = new object();
+
+ public ProviderRegistry(IOptionsMonitor options)
+ {
+ _options = options;
+ }
+
+ public Provider Get(string domain)
+ {
+ var key = domain ?? DefaultDomain;
+ lock (_lock)
+ {
+ if (_providers.TryGetValue(key, out var existing))
+ {
+ return existing;
+ }
+
+ var created = new Provider(_options.Get(key).BuildConfiguration());
+ _providers[key] = created;
+ return created;
+ }
+ }
+ }
+}
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/DisposalTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/DisposalTests.cs
new file mode 100644
index 0000000..1e01b01
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/DisposalTests.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Integrations;
+using LaunchDarkly.Sdk.Server.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenFeature;
+using Xunit;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests
+{
+ public class DisposalTests
+ {
+ [Fact]
+ public async Task DisposingTheHostAfterOpenFeatureShutdownDoesNotThrow()
+ {
+ var testData = TestData.DataSource();
+ testData.Update(testData.Flag("enabled-flag").VariationForAll(true));
+
+ var host = new HostBuilder().ConfigureServices(services =>
+ services.AddOpenFeature(builder => builder.AddLaunchDarklyProvider(options =>
+ {
+ options.SdkKey = "fake-key";
+ options.ConfigureSdk = config =>
+ config.DataSource(testData).Events(Components.NoEvents);
+ }))).Build();
+
+ await host.StartAsync();
+ var ldClient = host.Services.GetRequiredService();
+ await host.StopAsync();
+
+ host.Dispose();
+
+ Assert.NotNull(ldClient);
+ }
+ }
+}
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/FeatureBuilderExtensionsTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/FeatureBuilderExtensionsTests.cs
new file mode 100644
index 0000000..6bf8c7f
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/FeatureBuilderExtensionsTests.cs
@@ -0,0 +1,141 @@
+using System.Threading.Tasks;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Integrations;
+using LaunchDarkly.Sdk.Server.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenFeature;
+using OpenFeature.Model;
+using Xunit;
+
+namespace LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests
+{
+ public class FeatureBuilderExtensionsTests
+ {
+ private static TestData FlagData(string flagKey, bool value)
+ {
+ var testData = TestData.DataSource();
+ testData.Update(testData.Flag(flagKey).VariationForAll(value));
+ return testData;
+ }
+
+ private static void ConfigureOffline(TestData testData, ConfigurationBuilder builder) =>
+ builder.DataSource(testData).Events(Components.NoEvents);
+
+ [Fact]
+ public async Task ResolvesFlagsThroughTheDefaultClient()
+ {
+ var testData = FlagData("enabled-flag", true);
+
+ var host = new HostBuilder().ConfigureServices(services =>
+ services.AddOpenFeature(builder => builder
+ .AddContext(context => context.SetTargetingKey("user-key"))
+ .AddLaunchDarklyProvider(options =>
+ {
+ options.SdkKey = "fake-key";
+ options.ConfigureSdk = config => ConfigureOffline(testData, config);
+ }))).Build();
+
+ await host.StartAsync();
+
+ using (var scope = host.Services.CreateScope())
+ {
+ var client = scope.ServiceProvider.GetRequiredService();
+ Assert.True(await client.GetBooleanValueAsync("enabled-flag", false));
+ }
+
+ await host.StopAsync();
+ }
+
+ [Fact]
+ public async Task ResolvesFlagsUsingOptionsConfiguredOutsideOfTheBuilder()
+ {
+ var testData = FlagData("enabled-flag", true);
+
+ var host = new HostBuilder().ConfigureServices(services =>
+ {
+ services.Configure(options =>
+ {
+ options.SdkKey = "fake-key";
+ options.ConfigureSdk = config => ConfigureOffline(testData, config);
+ });
+ services.AddOpenFeature(builder => builder
+ .AddContext(context => context.SetTargetingKey("user-key"))
+ .AddLaunchDarklyProvider());
+ }).Build();
+
+ await host.StartAsync();
+
+ using (var scope = host.Services.CreateScope())
+ {
+ var client = scope.ServiceProvider.GetRequiredService();
+ Assert.True(await client.GetBooleanValueAsync("enabled-flag", false));
+ }
+
+ await host.StopAsync();
+ }
+
+ [Fact]
+ public async Task SharesASingleLdClientBetweenTheProviderAndTheContainer()
+ {
+ var testData = FlagData("enabled-flag", true);
+
+ var host = new HostBuilder().ConfigureServices(services =>
+ services.AddOpenFeature(builder => builder.AddLaunchDarklyProvider(options =>
+ {
+ options.SdkKey = "fake-key";
+ options.ConfigureSdk = config => ConfigureOffline(testData, config);
+ }))).Build();
+
+ await host.StartAsync();
+
+ var provider = (Provider)host.Services.GetRequiredService();
+ var providerAgain = (Provider)host.Services.GetRequiredService();
+ var ldClient = host.Services.GetRequiredService();
+
+ Assert.Same(provider, providerAgain);
+ Assert.Same(provider.GetClient(), ldClient);
+
+ await host.StopAsync();
+ }
+
+ [Fact]
+ public async Task SupportsDomainScopedProviders()
+ {
+ var defaultData = FlagData("shared-flag", false);
+ var betaData = FlagData("shared-flag", true);
+
+ var host = new HostBuilder().ConfigureServices(services =>
+ services.AddOpenFeature(builder => builder
+ .AddContext(context => context.SetTargetingKey("user-key"))
+ .AddLaunchDarklyProvider("default", options =>
+ {
+ options.SdkKey = "fake-key-default";
+ options.ConfigureSdk = config => ConfigureOffline(defaultData, config);
+ })
+ .AddLaunchDarklyProvider("beta", options =>
+ {
+ options.SdkKey = "fake-key-beta";
+ options.ConfigureSdk = config => ConfigureOffline(betaData, config);
+ })
+ .AddPolicyName(options => options.DefaultNameSelector = _ => "default"))).Build();
+
+ await host.StartAsync();
+
+ using (var scope = host.Services.CreateScope())
+ {
+ var defaultClient = scope.ServiceProvider.GetRequiredService();
+ var betaClient = scope.ServiceProvider.GetRequiredKeyedService("beta");
+
+ Assert.False(await defaultClient.GetBooleanValueAsync("shared-flag", false));
+ Assert.True(await betaClient.GetBooleanValueAsync("shared-flag", false));
+
+ var betaProvider = (Provider)scope.ServiceProvider.GetRequiredKeyedService("beta");
+ Assert.Same(betaProvider.GetClient(),
+ scope.ServiceProvider.GetRequiredKeyedService("beta"));
+ }
+
+ await host.StopAsync();
+ }
+ }
+}
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests.csproj b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests.csproj
new file mode 100644
index 0000000..9e83ba2
--- /dev/null
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests/LaunchDarkly.OpenFeature.ServerProvider.Hosting.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net471;net6.0;net8.0
+ $(BUILDFRAMEWORKS)
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+