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 + + + + + + + + +