diff --git a/Directory.Packages.props b/Directory.Packages.props
index 97c4baa6..9b6fa335 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -6,6 +6,7 @@
+
@@ -18,6 +19,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/build/Common.prod.props b/build/Common.prod.props
index b797ea0d..4378dfeb 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..a43aa1e2
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Hosting;
+
+namespace OpenFeature.Internal;
+
+///
+///
+///
+public 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)
+ {
+ foreach (var provider in this._providers)
+ {
+ await this._api.SetProviderAsync(provider.GetMetadata().Name, 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.Shutdown();
+}
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..61928faf
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs
@@ -0,0 +1,9 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace OpenFeature;
+
+///
+///
+///
+///
+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..078e9a4d
--- /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;
+
+///
+///
+///
+public static class OpenFeatureBuilderExtensions
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static OpenFeatureBuilder AddContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ AddContext(builder, null, (b, _, _) => configure(b));
+
+ return builder;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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).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..edf2266a
--- /dev/null
+++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs
@@ -0,0 +1,44 @@
+using System;
+using OpenFeature;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+// ReSharper disable once CheckNamespace
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+///
+///
+public static class OpenFeatureServiceCollectionExtensions
+{
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action configure)
+ {
+ Check.NotNull(services);
+ Check.NotNull(configure);
+
+ configure(AddOpenFeature(services));
+
+ return services;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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..f5e96625
--- /dev/null
+++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs
@@ -0,0 +1,87 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using OpenFeature.Model;
+using Xunit;
+
+namespace OpenFeature.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();
+
+ await app.StartAsync().ConfigureAwait(false);
+
+ 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());
+
+ await app.StopAsync().ConfigureAwait(false);
+ }
+
+ [Fact]
+ public async Task Can_register_some_feature_provider()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Services.AddOpenFeature(static builder =>
+ {
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+ builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name);
+ });
+
+ using var app = builder.Build();
+
+ Assert.Equal(Api.Instance, app.Services.GetRequiredService());
+ Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata().Name);
+
+ await app.StartAsync().ConfigureAwait(false);
+
+ 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());
+
+ await app.StopAsync().ConfigureAwait(false);
+ }
+
+ sealed class SomeFeatureProvider : FeatureProvider
+ {
+ public const string Name = "some_feature_provider";
+
+ public override Metadata GetMetadata() => new(Name);
+
+ public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext? context = null)
+ => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null)
+ => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext? context = null)
+ => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null)
+ => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+
+ public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null)
+ => Task.FromResult(new ResolutionDetails(flagKey, defaultValue));
+ }
+}
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..48777c76
--- /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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+