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