From bcf19c437bcfb98209fc99a1d2e6ea07d8df885c Mon Sep 17 00:00:00 2001 From: Austin Drenski Date: Tue, 16 Jan 2024 13:47:46 -0500 Subject: [PATCH 01/21] feat: Add OpenFeature.Extensions.Hosting package See: open-feature/dotnet-sdk-contrib#127 Signed-off-by: Austin Drenski --- Directory.Packages.props | 2 + OpenFeature.sln | 14 ++ build/Common.prod.props | 3 + .../Internal/OpenFeatureHostedService.cs | 36 +++++ .../OpenFeature.Extensions.Hosting.csproj | 25 +++ .../OpenFeatureBuilder.cs | 9 ++ .../OpenFeatureBuilderExtensions.cs | 145 ++++++++++++++++++ .../OpenFeatureServiceCollectionExtensions.cs | 44 ++++++ .../CallerArgumentExpressionAttribute.cs | 24 +++ src/Shared/Check.cs | 22 +++ src/Shared/IsExternalInit.cs | 24 +++ .../HostingTest.cs | 87 +++++++++++ ...penFeature.Extensions.Hosting.Tests.csproj | 25 +++ 13 files changed, 460 insertions(+) create mode 100644 src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs create mode 100644 src/OpenFeature.Extensions.Hosting/OpenFeature.Extensions.Hosting.csproj create mode 100644 src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs create mode 100644 src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs create mode 100644 src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs create mode 100644 src/Shared/CallerArgumentExpressionAttribute.cs create mode 100644 src/Shared/Check.cs create mode 100644 src/Shared/IsExternalInit.cs create mode 100644 test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs create mode 100644 test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj 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 + + + + + + + + + + + + + + + + From 0e4fe744644e15cf80230e24cf6c260521e64dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 10 Apr 2024 07:33:09 +0100 Subject: [PATCH 02/21] Removed package from merge. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0c4451e2..11e26a84 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,7 +23,6 @@ - From b39f662c532ffe44150038c33018807e72d671ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 10 Apr 2024 07:44:30 +0100 Subject: [PATCH 03/21] Try to fix build errors. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Internal/OpenFeatureHostedService.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 2 +- test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs index a43aa1e2..6d934988 100644 --- a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs +++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs @@ -17,7 +17,7 @@ async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationT { foreach (var provider in this._providers) { - await this._api.SetProviderAsync(provider.GetMetadata().Name, provider).ConfigureAwait(false); + 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); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs index 078e9a4d..8dc67944 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -133,7 +133,7 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri var api = services.GetRequiredService(); return api.GetClient( - api.GetProviderMetadata(providerName as string).Name, + api.GetProviderMetadata(providerName as string ?? string.Empty).Name, null, services.GetRequiredKeyedService(providerName), services.GetRequiredKeyedService(providerName).Build()); diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index f5e96625..a0857604 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -18,7 +18,9 @@ public async Task Can_register_no_op() 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); @@ -29,7 +31,9 @@ public async Task Can_register_no_op() Assert.Empty(app.Services.GetServices()); Assert.Empty(app.Services.GetServices()); +#pragma warning disable xUnit1030 await app.StopAsync().ConfigureAwait(false); +#pragma warning restore xUnit1030 } [Fact] @@ -48,7 +52,9 @@ public async Task Can_register_some_feature_provider() 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); @@ -60,7 +66,9 @@ public async Task Can_register_some_feature_provider() Assert.Empty(app.Services.GetServices()); Assert.NotEmpty(app.Services.GetServices()); +#pragma warning disable xUnit1030 await app.StopAsync().ConfigureAwait(false); +#pragma warning restore xUnit1030 } sealed class SomeFeatureProvider : FeatureProvider From 85d2b366cf5114ddd9c4860d886b7090733b20ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:02:46 +0100 Subject: [PATCH 04/21] Changing namespace. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs | 2 +- .../OpenFeature.Extensions.Hosting.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index a0857604..7594090d 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -5,7 +5,7 @@ using OpenFeature.Model; using Xunit; -namespace OpenFeature.Tests; +namespace OpenFeature.Extensions.Hosting.Tests; public sealed class HostingTest { diff --git a/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj b/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj index 48777c76..c3390e22 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj +++ b/test/OpenFeature.Extensions.Hosting.Tests/OpenFeature.Extensions.Hosting.Tests.csproj @@ -6,7 +6,7 @@ - OpenFeature + OpenFeature.Extensions.Hosting.Tests From c6de9e37a65377ac9256d7ef185e865b12913730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:33:26 +0100 Subject: [PATCH 05/21] Adding the documentation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeatureBuilder.cs | 4 +-- .../OpenFeatureBuilderExtensions.cs | 34 +++++++++---------- .../OpenFeatureServiceCollectionExtensions.cs | 18 +++++----- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs index 61928faf..4026721a 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs @@ -3,7 +3,7 @@ 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 index 8dc67944..84c1f433 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -8,17 +8,17 @@ 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, @@ -33,12 +33,12 @@ public static OpenFeatureBuilder AddContext( } /// - /// + /// 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, @@ -53,13 +53,13 @@ public static OpenFeatureBuilder AddContext( } /// - /// + /// 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, @@ -82,10 +82,10 @@ public static OpenFeatureBuilder AddContext( } /// - /// + /// 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); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs index edf2266a..0b14403f 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureServiceCollectionExtensions.cs @@ -6,16 +6,17 @@ 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); @@ -27,10 +28,11 @@ public static IServiceCollection AddOpenFeature(this IServiceCollection 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); From ed6fa59ad3584223a88ed57d7df0ad14644f1e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:22:47 +0100 Subject: [PATCH 06/21] reverted version. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c4ccd9b4..ad747bbb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + From 533aea8707973f5f2718e0c64b4470eebac825c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:44:15 +0100 Subject: [PATCH 07/21] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cb3c35c..9285ea07 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ dotnet add package OpenFeature public async Task Example() { // Register your feature flag provider - await Api.Instance.SetProvider(new InMemoryProvider()); + Api.Instance.SetProvider(new InMemoryProvider()); // Create a new client FeatureClient client = Api.Instance.GetClient(); @@ -67,6 +67,29 @@ public async Task Example() } ``` +### DI 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 | From cb9d0f2022bd48d7cb64a0377a4b40464a33e4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 30 Apr 2024 17:22:47 +0100 Subject: [PATCH 08/21] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Beemer Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9285ea07..d9b16e2d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ public async Task Example() } ``` -### DI Usage +### Dependency Injection Usage ```csharp // Register your feature flag provider From 38ea63ec8ddca840a216ebb83096f4b935f292c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 14 May 2024 17:29:20 +0100 Subject: [PATCH 09/21] Adding new example for the DI. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../HostingTest.cs | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index 7594090d..2534973d 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -1,6 +1,6 @@ +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using OpenFeature.Model; using Xunit; @@ -43,8 +43,9 @@ public async Task Can_register_some_feature_provider() builder.Services.AddOpenFeature(static builder => { - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); + builder.AddSomeFeatureProvider(); + // builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + // builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); }); using var app = builder.Build(); @@ -70,26 +71,41 @@ public async Task Can_register_some_feature_provider() await app.StopAsync().ConfigureAwait(false); #pragma warning restore xUnit1030 } +} - sealed class SomeFeatureProvider : FeatureProvider - { - public const string Name = "some_feature_provider"; +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 Metadata GetMetadata() => new(Name); + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext? context = null) + => Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - public override Task> ResolveBooleanValue(string flagKey, bool 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> ResolveStringValue(string flagKey, string 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> ResolveIntegerValue(string flagKey, int 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)); +} + +public static class SomeFeatureProviderExtensions +{ + public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } - public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext? context = null) - => Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + builder.Services.AddSingleton(); - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) - => Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); + return builder; } } From 942e759b8e83d1c2638972c5e6087f04ccc73ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 14 May 2024 17:50:15 +0100 Subject: [PATCH 10/21] Making class internal. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Internal/OpenFeatureHostedService.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs index 6d934988..48ee5553 100644 --- a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs +++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs @@ -5,10 +5,7 @@ namespace OpenFeature.Internal; -/// -/// -/// -public sealed class OpenFeatureHostedService(Api api, IEnumerable providers) : IHostedLifecycleService +internal sealed class OpenFeatureHostedService(Api api, IEnumerable providers) : IHostedLifecycleService { readonly Api _api = Check.NotNull(api); readonly IEnumerable _providers = Check.NotNull(providers); From 496eabfee46b16180e80aa49cfd441ff5a14c5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 15 May 2024 16:10:23 +0100 Subject: [PATCH 11/21] Renamed for clarity. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeatureBuilder.cs | 4 ++-- .../OpenFeatureBuilderExtensions.cs | 14 +++++++------- .../HostingTest.cs | 15 ++++++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs index 4026721a..80255142 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs @@ -5,5 +5,5 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// -public sealed record OpenFeatureBuilder(IServiceCollection Services); +/// +public sealed record OpenFeatureBuilder(IServiceCollection ProvidersServiceCollection); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs index 84c1f433..5410d7bd 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -69,7 +69,7 @@ public static OpenFeatureBuilder AddContext( Check.NotNull(builder); Check.NotNull(configure); - builder.Services.AddKeyedSingleton(providerName, (services, key) => + builder.ProvidersServiceCollection.AddKeyedSingleton(providerName, (services, key) => { var b = EvaluationContext.Builder(); @@ -90,9 +90,9 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri { Check.NotNull(builder); - builder.Services.AddHostedService(); + builder.ProvidersServiceCollection.AddHostedService(); - builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => + builder.ProvidersServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => { var api = providerName switch { @@ -106,13 +106,13 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return api; }); - builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch + builder.ProvidersServiceCollection.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) => + builder.ProvidersServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => { var builder = providerName switch { @@ -128,7 +128,7 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return builder; }); - builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) => + builder.ProvidersServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => { var api = services.GetRequiredService(); @@ -140,6 +140,6 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri }); if (providerName is not null) - builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); + builder.ProvidersServiceCollection.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); } } diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index 2534973d..987da207 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using OpenFeature.Model; using Xunit; @@ -41,11 +42,9 @@ public async Task Can_register_some_feature_provider() { var builder = Host.CreateApplicationBuilder(); - builder.Services.AddOpenFeature(static builder => + builder.Services.AddOpenFeature(b => { - builder.AddSomeFeatureProvider(); - // builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - // builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); + b.AddSomeFeatureProvider(); }); using var app = builder.Build(); @@ -104,8 +103,14 @@ public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddSingleton(); + builder.ProvidersServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); return builder; } + + public static void AddSomeFeatureProvider(this OpenFeatureBuilder builder, Action configure) + { + throw new NotImplementedException(); + } } From 00011c3594410bfdb0ec07f4df28d58f0e961feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 15 May 2024 16:14:08 +0100 Subject: [PATCH 12/21] Moved class. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../HostingTest.cs | 49 ++---------------- .../TestingModels/SomeFeatureProvider.cs | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index 987da207..4113c1c3 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -1,8 +1,7 @@ -using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using OpenFeature.Extensions.Hosting.Tests.TestingModels; using OpenFeature.Model; using Xunit; @@ -24,7 +23,8 @@ public async Task Can_register_no_op() #pragma warning restore xUnit1030 Assert.Equal(Api.Instance, app.Services.GetRequiredService()); - Assert.Equal(Api.Instance.GetProviderMetadata().Name, app.Services.GetRequiredService().GetMetadata().Name); + Assert.Equal(Api.Instance.GetProviderMetadata().Name, + app.Services.GetRequiredService().GetMetadata().Name); Assert.Empty(Api.Instance.GetContext().AsDictionary()); Assert.Empty(app.Services.GetRequiredService().Build().AsDictionary()); @@ -71,46 +71,3 @@ public async Task Can_register_some_feature_provider() #pragma warning restore xUnit1030 } } - -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)); -} - -public static class SomeFeatureProviderExtensions -{ - public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - builder.ProvidersServiceCollection.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/SomeFeatureProvider.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs new file mode 100644 index 00000000..8686b2f2 --- /dev/null +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs @@ -0,0 +1,50 @@ +using System; +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> 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)); +} + +public static class SomeFeatureProviderExtensions +{ + public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ProvidersServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); + + return builder; + } + + public static void AddSomeFeatureProvider(this OpenFeatureBuilder builder, Action configure) + { + throw new NotImplementedException(); + } +} From 8cd37100d1de0c00b9490c2107fa4f442e2c25f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 15 May 2024 16:18:41 +0100 Subject: [PATCH 13/21] Renamed stuff. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeatureBuilder.cs | 4 +-- .../OpenFeatureBuilderExtensions.cs | 14 ++++----- .../TestingModels/SomeFeatureProvider.cs | 2 +- .../TestingModels/SomeHook.cs | 31 +++++++++++++++++++ 4 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs index 80255142..2654a4ac 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs @@ -5,5 +5,5 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// -public sealed record OpenFeatureBuilder(IServiceCollection ProvidersServiceCollection); +/// +public sealed record OpenFeatureBuilder(IServiceCollection ServiceCollection); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs index 5410d7bd..c51c99aa 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -69,7 +69,7 @@ public static OpenFeatureBuilder AddContext( Check.NotNull(builder); Check.NotNull(configure); - builder.ProvidersServiceCollection.AddKeyedSingleton(providerName, (services, key) => + builder.ServiceCollection.AddKeyedSingleton(providerName, (services, key) => { var b = EvaluationContext.Builder(); @@ -90,9 +90,9 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri { Check.NotNull(builder); - builder.ProvidersServiceCollection.AddHostedService(); + builder.ServiceCollection.AddHostedService(); - builder.ProvidersServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => + builder.ServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => { var api = providerName switch { @@ -106,13 +106,13 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return api; }); - builder.ProvidersServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch + builder.ServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch { null => services.GetRequiredService>(), not null => services.GetRequiredService().CreateLogger($"OpenFeature.FeatureClient.{providerName}") }); - builder.ProvidersServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => + builder.ServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => { var builder = providerName switch { @@ -128,7 +128,7 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return builder; }); - builder.ProvidersServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => + builder.ServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => { var api = services.GetRequiredService(); @@ -140,6 +140,6 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri }); if (providerName is not null) - builder.ProvidersServiceCollection.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); + builder.ServiceCollection.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); } } diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs index 8686b2f2..31ff79f3 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs @@ -37,7 +37,7 @@ public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder throw new ArgumentNullException(nameof(builder)); } - builder.ProvidersServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.ServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); return builder; 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..d4c01c40 --- /dev/null +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +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.ServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); + + return builder; + } + + public static void AddSomeFeatureProvider(this OpenFeatureBuilder builder, Action configure) + { + throw new NotImplementedException(); + } +} From a9ec44b1c04a52bbe55acaee05db1892d2e5e2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 15 May 2024 16:32:57 +0100 Subject: [PATCH 14/21] Adding example for global hook. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../HostingTest.cs | 35 +++++++++++++++++++ .../TestingModels/SomeHook.cs | 14 ++------ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index 4113c1c3..4701f6a2 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -66,6 +66,41 @@ public async Task Can_register_some_feature_provider() 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.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/TestingModels/SomeHook.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs index d4c01c40..72756047 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs @@ -1,13 +1,9 @@ using System; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace OpenFeature.Extensions.Hosting.Tests.TestingModels; -public class SomeHook : Hook -{ - -} +public class SomeHook : Hook; public static class SomeHookExtensions { @@ -18,14 +14,8 @@ public static OpenFeatureBuilder AddSomeHook(this OpenFeatureBuilder builder) throw new ArgumentNullException(nameof(builder)); } - builder.ServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); - builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); + builder.ServiceCollection.TryAddSingleton(); return builder; } - - public static void AddSomeFeatureProvider(this OpenFeatureBuilder builder, Action configure) - { - throw new NotImplementedException(); - } } From a24521e5be524f261d8984e981266f95b772a8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Jul 2024 08:45:47 +0100 Subject: [PATCH 15/21] chore: Update OpenFeatureHostedService to use ShutdownAsync() method. Fix unit tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Internal/OpenFeatureHostedService.cs | 2 +- .../TestingModels/SomeFeatureProvider.cs | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs index 48ee5553..5c747e2f 100644 --- a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs +++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs @@ -29,5 +29,5 @@ async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationT Task IHostedService.StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken) => this._api.Shutdown(); + Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken) => this._api.ShutdownAsync(); } diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs index 31ff79f3..ea06e215 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -12,20 +13,20 @@ sealed class SomeFeatureProvider : FeatureProvider 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> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, + CancellationToken cancellationToken = default) => 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> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, + CancellationToken cancellationToken = default) => 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> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, + CancellationToken cancellationToken = default) => 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> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, + CancellationToken cancellationToken = default) => Task.FromResult(new ResolutionDetails(flagKey, defaultValue)); - public override Task> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext? context = null) - => 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 From 9daa33f5ad88ef1171a3acaac660a8294b79332d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 1 Jul 2024 08:49:21 +0100 Subject: [PATCH 16/21] Fix duplicate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 226a5cb9..fc6d8e5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - From bedb2f1a74f5be2f013f340974e5e2b6b6652a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 24 Jul 2024 08:39:07 +0100 Subject: [PATCH 17/21] Fix null refs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Internal/OpenFeatureHostedService.cs | 2 +- .../OpenFeatureBuilderExtensions.cs | 2 +- .../HostingTest.cs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs index 5c747e2f..e9529070 100644 --- a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs +++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs @@ -14,7 +14,7 @@ async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationT { foreach (var provider in this._providers) { - await this._api.SetProviderAsync(provider.GetMetadata().Name ?? string.Empty, provider).ConfigureAwait(false); + 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); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs index c51c99aa..6e39354c 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -133,7 +133,7 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri var api = services.GetRequiredService(); return api.GetClient( - api.GetProviderMetadata(providerName as string ?? string.Empty).Name, + api.GetProviderMetadata(providerName as string ?? string.Empty)?.Name, null, services.GetRequiredKeyedService(providerName), services.GetRequiredKeyedService(providerName).Build()); diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index 4701f6a2..f0fd775d 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -23,7 +23,7 @@ public async Task Can_register_no_op() #pragma warning restore xUnit1030 Assert.Equal(Api.Instance, app.Services.GetRequiredService()); - Assert.Equal(Api.Instance.GetProviderMetadata().Name, + Assert.Equal(Api.Instance.GetProviderMetadata()?.Name, app.Services.GetRequiredService().GetMetadata().Name); Assert.Empty(Api.Instance.GetContext().AsDictionary()); @@ -50,14 +50,14 @@ public async Task Can_register_some_feature_provider() using var app = builder.Build(); Assert.Equal(Api.Instance, app.Services.GetRequiredService()); - Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata().Name); + 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().GetProviderMetadata()?.Name); Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetMetadata().Name); Assert.Empty(Api.Instance.GetContext().AsDictionary()); @@ -85,14 +85,14 @@ public async Task Can_register_some_feature_provider_and_global_hook() using var app = builder.Build(); Assert.Equal(Api.Instance, app.Services.GetRequiredService()); - Assert.Equal("No-op Provider", app.Services.GetRequiredService().GetProviderMetadata().Name); + 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().GetProviderMetadata()?.Name); Assert.Equal(SomeFeatureProvider.Name, app.Services.GetRequiredService().GetMetadata().Name); Assert.NotEmpty(app.Services.GetServices()); From 4870d9f97535cf822d83c151fbbf25073314ce2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:48:28 +0100 Subject: [PATCH 18/21] Adding extra check. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../HostingTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs index f0fd775d..d7feda27 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/HostingTest.cs @@ -95,6 +95,44 @@ public async Task Can_register_some_feature_provider_and_global_hook() 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()); From cf1f05fd6d0c3464b9c1a9c2e7eda1b4a658a90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:53:51 +0100 Subject: [PATCH 19/21] Renamed per suggestion. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeatureBuilder.cs | 4 ++-- .../OpenFeatureBuilderExtensions.cs | 14 +++++++------- .../TestingModels/SomeFeatureProvider.cs | 2 +- .../TestingModels/SomeHook.cs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs index 2654a4ac..4026721a 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilder.cs @@ -5,5 +5,5 @@ namespace OpenFeature; /// /// Describes a backed by an . /// -/// -public sealed record OpenFeatureBuilder(IServiceCollection ServiceCollection); +/// +public sealed record OpenFeatureBuilder(IServiceCollection Services); diff --git a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs index 6e39354c..85863137 100644 --- a/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs +++ b/src/OpenFeature.Extensions.Hosting/OpenFeatureBuilderExtensions.cs @@ -69,7 +69,7 @@ public static OpenFeatureBuilder AddContext( Check.NotNull(builder); Check.NotNull(configure); - builder.ServiceCollection.AddKeyedSingleton(providerName, (services, key) => + builder.Services.AddKeyedSingleton(providerName, (services, key) => { var b = EvaluationContext.Builder(); @@ -90,9 +90,9 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri { Check.NotNull(builder); - builder.ServiceCollection.AddHostedService(); + builder.Services.AddHostedService(); - builder.ServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => + builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => { var api = providerName switch { @@ -106,13 +106,13 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return api; }); - builder.ServiceCollection.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch + builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch { null => services.GetRequiredService>(), not null => services.GetRequiredService().CreateLogger($"OpenFeature.FeatureClient.{providerName}") }); - builder.ServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => + builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) => { var builder = providerName switch { @@ -128,7 +128,7 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri return builder; }); - builder.ServiceCollection.TryAddKeyedTransient(providerName, static (services, providerName) => + builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) => { var api = services.GetRequiredService(); @@ -140,6 +140,6 @@ public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, stri }); if (providerName is not null) - builder.ServiceCollection.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); + builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService(providerName))); } } diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs index ea06e215..d7132059 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeFeatureProvider.cs @@ -38,7 +38,7 @@ public static OpenFeatureBuilder AddSomeFeatureProvider(this OpenFeatureBuilder throw new ArgumentNullException(nameof(builder)); } - builder.ServiceCollection.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); return builder; diff --git a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs index 72756047..5e4a4445 100644 --- a/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs +++ b/test/OpenFeature.Extensions.Hosting.Tests/TestingModels/SomeHook.cs @@ -14,7 +14,7 @@ public static OpenFeatureBuilder AddSomeHook(this OpenFeatureBuilder builder) throw new ArgumentNullException(nameof(builder)); } - builder.ServiceCollection.TryAddSingleton(); + builder.Services.TryAddSingleton(); return builder; } From 6bd175b58f1b2603165a34c8bfdb68ba93f54525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:54:45 +0100 Subject: [PATCH 20/21] Adding cancellation token check. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Internal/OpenFeatureHostedService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs index e9529070..83555bff 100644 --- a/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs +++ b/src/OpenFeature.Extensions.Hosting/Internal/OpenFeatureHostedService.cs @@ -12,6 +12,8 @@ internal sealed class OpenFeatureHostedService(Api api, IEnumerable Date: Fri, 6 Sep 2024 09:44:56 +0100 Subject: [PATCH 21/21] Removed duplicated entry. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 1 - 1 file changed, 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0d6a9bd6..1e147b70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ -