diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e9ff3649..8a77fdf6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
7.0.x
- name: Run Tests
- run: dotnet test test/OpenFeature.Tests/ --configuration Release --logger GitHubActions
+ run: dotnet test --logger GitHubActions
unit-tests-windows:
runs-on: windows-latest
@@ -43,4 +43,4 @@ jobs:
7.0.x
- name: Run Tests
- run: dotnet test test\OpenFeature.Tests\ --configuration Release --logger GitHubActions
+ run: dotnet test --logger GitHubActions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb0a0f5a..99af306c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,19 +34,13 @@ jobs:
if: ${{ steps.release.outputs.releases_created }}
run: dotnet restore
- - name: Build
- if: ${{ steps.release.outputs.releases_created }}
- run: |
- dotnet build --configuration Release --no-restore -p:Deterministic=true
-
- name: Pack
if: ${{ steps.release.outputs.releases_created }}
- run: |
- dotnet pack OpenFeature.proj --configuration Release --no-build -p:PackageID=OpenFeature
+ run: dotnet pack --no-restore
- name: Publish to Nuget
if: ${{ steps.release.outputs.releases_created }}
run: |
- dotnet nuget push src/OpenFeature/bin/Release/OpenFeature.${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}.nupkg `
+ dotnet nuget push "src/**/*.nupkg" `
--api-key ${{secrets.NUGET_TOKEN}} `
--source https://api.nuget.org/v3/index.json
diff --git a/OpenFeature.sln b/OpenFeature.sln
index 5ed0e809..c7ac68a2 100644
--- a/OpenFeature.sln
+++ b/OpenFeature.sln
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
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("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{72005F60-C2E8-40BF-AE95-893635134D7D}"
ProjectSection(SolutionItems) = preProject
.github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml
@@ -38,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Benchmarks", "t
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.E2ETests", "test\OpenFeature.E2ETests\OpenFeature.E2ETests.csproj", "{90E7EAD3-251E-4490-AF78-E758E33518E5}"
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +52,10 @@ 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
@@ -56,14 +64,20 @@ Global
{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
{90E7EAD3-251E-4490-AF78-E758E33518E5}.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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}
{90E7EAD3-251E-4490-AF78-E758E33518E5} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
+ {4588FE3C-EB7E-4BA5-BD77-E15131DB3B29} = {65FBA159-23E0-4CF9-881B-F78DBFF198E9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {41F01B78-FB06-404F-8AD0-6ED6973F948F}
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 49e454c5..91afe4aa 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -2,7 +2,11 @@
+ $(DOTNET_RUNNING_IN_CONTAINER)
+ true
+ true
true
+ true
@@ -11,7 +15,6 @@
https://github.com/open-feature/dotnet-sdk
OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.
Feature;OpenFeature;Flags;
- OpenFeature
openfeature-icon.png
https://openfeature.dev
Apache-2.0
@@ -37,7 +40,7 @@
-
- true
-
+
+
+
diff --git a/build/Common.props b/build/Common.props
index 65bf984d..a4bb479c 100644
--- a/build/Common.props
+++ b/build/Common.props
@@ -1,6 +1,6 @@
- 7.3
+ latest
true
true
@@ -19,6 +19,7 @@
Please sort alphabetically.
Refer to https://docs.microsoft.com/nuget/concepts/package-versioning for semver syntax.
-->
+ [8.0.0,)
[2.0,)
[1.0.0,2.0)
diff --git a/build/Common.tests.props b/build/Common.tests.props
index 060e749d..cec4da3f 100644
--- a/build/Common.tests.props
+++ b/build/Common.tests.props
@@ -29,6 +29,7 @@
[3.1.2]
[6.7.0]
[2.3.3]
+ [8.0.0]
[17.2.0]
[5.0.0]
[2.4.3,3.0)
diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs
new file mode 100644
index 00000000..ae7f719c
--- /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.SetProvider(provider.GetMetadata().Name, provider).ConfigureAwait(false);
+
+ if (this._api.GetProviderMetadata() is { Name: "No-op Provider" })
+ await this._api.SetProvider(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..b28222a4
--- /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..1ecabdeb
--- /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 AddEvaluationContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ AddEvaluationContext(builder, null, (b, _, _) => configure(b));
+
+ return builder;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static OpenFeatureBuilder AddEvaluationContext(
+ this OpenFeatureBuilder builder,
+ Action configure)
+ {
+ Check.NotNull(builder);
+ Check.NotNull(configure);
+
+ AddEvaluationContext(builder, null, (b, _, s) => configure(b, s));
+
+ return builder;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static OpenFeatureBuilder AddEvaluationContext(
+ 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..cf74b9d9
--- /dev/null
+++ b/src/Shared/Check.cs
@@ -0,0 +1,21 @@
+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..e6758e62
--- /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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+