diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7227000a..1e147b70 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
@@ -19,6 +20,7 @@
+
diff --git a/OpenFeature.sln b/OpenFeature.sln
index 6f1cce8d..c5c571ee 100644
--- a/OpenFeature.sln
+++ b/OpenFeature.sln
@@ -73,8 +73,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{65FBA159-2
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature", "src\OpenFeature\OpenFeature.csproj", "{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Extensions.Hosting", "src\OpenFeature.Extensions.Hosting\OpenFeature.Extensions.Hosting.csproj", "{F2DB11D0-15E7-4C1F-936A-37D23EECECC0}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Tests", "test\OpenFeature.Tests\OpenFeature.Tests.csproj", "{49BB42BA-10A6-4DA3-A7D5-38C968D57837}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Extensions.Hosting.Tests", "test\OpenFeature.Extensions.Hosting.Tests\OpenFeature.Extensions.Hosting.Tests.csproj", "{4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "test\OpenFeature.Benchmarks\OpenFeature.Benchmarks.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{7398C446-2630-4F8C-9278-4E807720E9E5}"
@@ -89,10 +93,18 @@ Global
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F2DB11D0-15E7-4C1F-936A-37D23EECECC0}.Release|Any CPU.Build.0 = Release|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49BB42BA-10A6-4DA3-A7D5-38C968D57837}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29}.Release|Any CPU.Build.0 = Release|Any CPU
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90E7EAD3-251E-4490-AF78-E758E33518E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -107,7 +119,9 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{07A6E6BD-FB7E-4B3B-9CBE-65AE9D0EB223} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
+ {F2DB11D0-15E7-4C1F-936A-37D23EECECC0} = {C97E9975-E10A-4817-AE2C-4DD69C3C02D4}
{49BB42BA-10A6-4DA3-A7D5-38C968D57837} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{7398C446-2630-4F8C-9278-4E807720E9E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
{C4746B8C-FE19-440B-922C-C2377F906FE8} = {2B172AA0-A5A6-4D94-BA1F-B79D59B0C2D8}
diff --git a/README.md b/README.md
index b8f25012..f57199e5 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,29 @@ public async Task Example()
}
```
+### Dependency Injection Usage
+
+```csharp
+// Register your feature flag provider
+builder.Services.AddOpenFeature(static builder =>
+{
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name);
+});
+
+// Inject the client
+app.MapGet("/flag", async ([FromServices]IFeatureClient client) =>
+ {
+ // Evaluate your feature flag
+ var flag = await client.GetBooleanValue("some_flag", true).ConfigureAwait(true);
+
+ if (flag)
+ {
+ // Do some work
+ }
+ })
+```
+
## 🌟 Features
| Status | Features | Description |
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 656f3476..033669b9 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -28,4 +28,7 @@
+
+
+
diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs
new file mode 100644
index 00000000..83555bff
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting;
+
+namespace OpenFeature.Internal;
+
+internal sealed class OpenFeatureHostedService(Api api, IEnumerable providers) : IHostedLifecycleService
+{
+ readonly Api _api = Check.NotNull(api);
+ readonly IEnumerable _providers = Check.NotNull(providers);
+
+ async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ foreach (var provider in this._providers)
+ {
+ await this._api.SetProviderAsync(provider.GetMetadata()?.Name ?? string.Empty, provider).ConfigureAwait(false);
+
+ if (this._api.GetProviderMetadata() is { Name: "No-op Provider" })
+ await this._api.SetProviderAsync(provider).ConfigureAwait(false);
+ }
+ }
+
+ Task IHostedService.StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken) => this._api.ShutdownAsync();
+}
diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeature.Extensions.Hosting.csproj b/src/OpenFeature.Extensions.Hosting/OpenFeature.Extensions.Hosting.csproj
new file mode 100644
index 00000000..cb66072d
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeature.Extensions.Hosting.csproj
@@ -0,0 +1,25 @@
+
+
+
+ enable
+ netstandard2.0;net6.0;net7.0;net8.0;net462
+
+
+
+ README.md
+ OpenFeature
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs
new file mode 100644
index 00000000..4026721a
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature;
+
+///
+/// Describes a backed by an .
+///
+///
+public sealed record OpenFeatureBuilder(IServiceCollection Services);
diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs
new file mode 100644
index 00000000..85863137
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs
@@ -0,0 +1,145 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+using OpenFeature.Internal;
+using OpenFeature.Model;
+
+namespace OpenFeature;
+
+///
+/// Contains extension methods for the class.
+///
+public static class OpenFeatureBuilderExtensions
+{
+ ///
+ /// This method is used to add a new context to the service collection.
+ ///
+ ///
+ /// the desired configuration
+ ///
+ /// the instance
+ ///
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ AddContext(builder, null, (b, _, _) => configure(b));
+
+ return builder;
+ }
+
+ ///
+ /// This method is used to add a new context to the service collection.
+ ///
+ ///
+ /// the desired configuration
+ ///
+ /// the instance
+ ///
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ AddContext(builder, null, (b, _, s) => configure(b, s));
+
+ return builder;
+ }
+
+ ///
+ /// This method is used to add a new context to the service collection.
+ ///
+ ///
+ /// the name of the provider
+ /// the desired configuration
+ ///
+ /// the instance
+ ///
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ string? providerName,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ builder.Services.AddKeyedSingleton(providerName, (services, key) =>
+ {
+ var b = EvaluationContext.Builder();
+
+ configure(b, key as string, services);
+
+ return b.Build();
+ });
+
+ return builder;
+ }
+
+ ///
+ /// This method is used to add a new feature client to the service collection.
+ ///
+ ///
+ /// the name of the provider
+ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, string? providerName = null)
+ {
+ Check.NotNull(builder);
+
+ builder.Services.AddHostedService();
+
+ builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) =>
+ {
+ var api = providerName switch
+ {
+ null => Api.Instance,
+ not null => services.GetRequiredKeyedService(null)
+ };
+
+ api.AddHooks(services.GetKeyedServices(providerName));
+ api.SetContext(services.GetRequiredKeyedService(providerName).Build());
+
+ return api;
+ });
+
+ builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch
+ {
+ null => services.GetRequiredService>(),
+ not null => services.GetRequiredService().CreateLogger($"OpenFeature.FeatureClient.{providerName}")
+ });
+
+ builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) =>
+ {
+ var builder = providerName switch
+ {
+ null => EvaluationContext.Builder(),
+ not null => services.GetRequiredKeyedService(null)
+ };
+
+ foreach (var c in services.GetKeyedServices(providerName))
+ {
+ builder.Merge(c);
+ }
+
+ return builder;
+ });
+
+ builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) =>
+ {
+ var api = services.GetRequiredService();
+
+ return api.GetClient(
+ api.GetProviderMetadata(providerName as string ?? string.Empty)?.Name,
+ null,
+ services.GetRequiredKeyedService(providerName),
+ services.GetRequiredKeyedService(providerName).Build());
+ });
+
+ if (providerName is not null)
+ builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName)));
+ }
+}
diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs
new file mode 100644
index 00000000..0b14403f
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,46 @@
+using System;
+using OpenFeature;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+// ReSharper disable once CheckNamespace
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Contains extension methods for the class.
+///
+public static class OpenFeatureServiceCollectionExtensions
+{
+ ///
+ /// This method is used to add OpenFeature to the service collection.
+ /// OpenFeature will be registered as a singleton.
+ ///
+ ///
+ /// the desired configuration
+ /// the current instance
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure)
+ {
+ Check.NotNull(services);
+ Check.NotNull(configure);
+
+ configure(AddOpenFeature(services));
+
+ return services;
+ }
+
+ ///
+ /// This method is used to add OpenFeature to the service collection.
+ /// OpenFeature will be registered as a singleton.
+ ///
+ ///
+ /// the current instance
+ public static OpenFeatureBuilder AddOpenFeature(this IServiceCollection services)
+ {
+ Check.NotNull(services);
+
+ var builder = new OpenFeatureBuilder(services);
+
+ builder.TryAddOpenFeatureClient();
+
+ return builder;
+ }
+}
diff --git a/src/Shared/CallerArgumentExpressionAttribute.cs b/src/Shared/CallerArgumentExpressionAttribute.cs
new file mode 100644
index 00000000..b8b364bf
--- /dev/null
+++ b/src/Shared/CallerArgumentExpressionAttribute.cs
@@ -0,0 +1,24 @@
+// @formatter:off
+// ReSharper disable All
+#if NETCOREAPP3_0_OR_GREATER
+// https://github.com/dotnet/runtime/issues/96197
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.CallerArgumentExpressionAttribute))]
+#else
+#pragma warning disable
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Runtime.CompilerServices
+{
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+ internal sealed class CallerArgumentExpressionAttribute : Attribute
+ {
+ public CallerArgumentExpressionAttribute(string parameterName)
+ {
+ ParameterName = parameterName;
+ }
+
+ public string ParameterName { get; }
+ }
+}
+#endif
diff --git a/src/Shared/Check.cs b/src/Shared/Check.cs
new file mode 100644
index 00000000..d5f99594
--- /dev/null
+++ b/src/Shared/Check.cs
@@ -0,0 +1,22 @@
+#nullable enable
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace OpenFeature;
+
+[DebuggerStepThrough]
+static class Check
+{
+ public static T NotNull(T? value, [CallerArgumentExpression("value")] string name = null!)
+ {
+#if NET8_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(value, name);
+#else
+ if (value is null)
+ throw new ArgumentNullException(name);
+#endif
+
+ return value;
+ }
+}
diff --git a/src/Shared/IsExternalInit.cs b/src/Shared/IsExternalInit.cs
new file mode 100644
index 00000000..a020657f
--- /dev/null
+++ b/src/Shared/IsExternalInit.cs
@@ -0,0 +1,24 @@
+// @formatter:off
+// ReSharper disable All
+#if NET5_0_OR_GREATER
+// https://github.com/dotnet/runtime/issues/96197
+[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))]
+#else
+#pragma warning disable
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+
+namespace System.Runtime.CompilerServices
+{
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ static class IsExternalInit
+ {
+ }
+}
+#endif
diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs
new file mode 100644
index 00000000..d7feda27
--- /dev/null
+++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs
@@ -0,0 +1,146 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OpenFeature.Extensions.Hosting.Tests.TestingModels;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.Extensions.Hosting.Tests;
+
+public sealed class HostingTest
+{
+ [Fact]
+ public async Task Can_register_no_op()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Services.AddOpenFeature();
+
+ using var app = builder.Build();
+
+#pragma warning disable xUnit1030
+ await app.StartAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal(Api.Instance.GetProviderMetadata()?.Name,
+ app.Services.GetRequiredService().GetMetadata().Name);
+
+ Assert.Empty(Api.Instance.GetContext().AsDictionary());
+ Assert.Empty(app.Services.GetRequiredService().Build().AsDictionary());
+ Assert.Empty(app.Services.GetServices());
+ Assert.Empty(app.Services.GetServices());
+ Assert.Empty(app.Services.GetServices());
+
+#pragma warning disable xUnit1030
+ await app.StopAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+ }
+
+ [Fact]
+ public async Task Can_register_some_feature_provider()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Services.AddOpenFeature(b =>
+ {
+ b.AddSomeFeatureProvider();
+ });
+
+ using var app = builder.Build();
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+
+#pragma warning disable xUnit1030
+ await app.StartAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetMetadata().Name);
+
+ Assert.Empty(Api.Instance.GetContext().AsDictionary());
+ Assert.Empty(app.Services.GetRequiredService().Build().AsDictionary());
+ Assert.Empty(app.Services.GetServices());
+ Assert.Empty(app.Services.GetServices());
+ Assert.NotEmpty(app.Services.GetServices());
+
+#pragma warning disable xUnit1030
+ await app.StopAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+ }
+
+ [Fact]
+ public async Task Can_register_some_feature_provider_and_global_hook()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Services.AddOpenFeature(b =>
+ {
+ b.AddSomeFeatureProvider();
+ b.AddSomeHook();
+ });
+
+ using var app = builder.Build();
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+
+#pragma warning disable xUnit1030
+ await app.StartAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetMetadata().Name);
+ Assert.NotEmpty(app.Services.GetServices());
+ Assert.NotEmpty(app.Services.GetRequiredService().GetHooks());
+ Assert.Empty(app.Services.GetRequiredService().GetClient(SomeFeatureProvider.Name).GetHooks());
+
+ Assert.Empty(Api.Instance.GetContext().AsDictionary());
+ Assert.Empty(app.Services.GetRequiredService().Build().AsDictionary());
+ Assert.Empty(app.Services.GetServices());
+ Assert.NotEmpty(app.Services.GetServices());
+
+#pragma warning disable xUnit1030
+ await app.StopAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+ }
+
+ [Fact(Skip = "In development")]
+ public async Task Can_register_some_feature_provider_and_client_hook()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Services.AddOpenFeature(b =>
+ {
+ b.AddSomeFeatureProvider();
+ b.AddSomeHook();
+ });
+
+ using var app = builder.Build();
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+
+#pragma warning disable xUnit1030
+ await app.StartAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetProviderMetadata()?.Name);
+ Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetMetadata().Name);
+ Assert.NotEmpty(app.Services.GetServices());
+ Assert.NotEmpty(app.Services.GetRequiredService().GetClient(SomeFeatureProvider.Name).GetHooks());
+
+ Assert.Empty(Api.Instance.GetContext().AsDictionary());
+ Assert.Empty(app.Services.GetRequiredService().Build().AsDictionary());
+ Assert.Empty(app.Services.GetServices());
+ Assert.NotEmpty(app.Services.GetServices());
+
+#pragma warning disable xUnit1030
+ await app.StopAsync().ConfigureAwait(false);
+#pragma warning restore xUnit1030
+ }
+}
diff --git a/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj b/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj
new file mode 100644
index 00000000..c3390e22
--- /dev/null
+++ b/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net6.0;net7.0;net8.0
+ $(TargetFrameworks);net462
+
+
+
+ OpenFeature.Extensions.Hosting.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs
new file mode 100644
index 00000000..d7132059
--- /dev/null
+++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.Hosting.Tests.TestingModels;
+
+sealed class SomeFeatureProvider : FeatureProvider
+{
+ public const string Name = "some_feature_provider";
+
+ public override Metadata GetMetadata() => new(Name);
+
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null,
+ CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null,
+ CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null,
+ CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null,
+ CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null,
+ CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+}
+
+public static class SomeFeatureProviderExtensions
+{
+ public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name);
+
+ return builder;
+ }
+
+ public static void AddSomeFeatureProvider(this OpenFeatureBuilder builder, Action configure)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs
new file mode 100644
index 00000000..5e4a4445
--- /dev/null
+++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs
@@ -0,0 +1,21 @@
+using System;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace OpenFeature.Extensions.Hosting.Tests.TestingModels;
+
+public class SomeHook : Hook;
+
+public static class SomeHookExtensions
+{
+ public static OpenFeatureBuilder AddSomeHook(this OpenFeatureBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.TryAddSingleton();
+
+ return builder;
+ }
+}