-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add OpenFeature.Extensions.Hosting package #181
Changes from 13 commits
bcf19c4
694c0ff
0e4fe74
b39f662
85d2b36
c6de9e3
1fc6d51
90f3723
ed6fa59
67b24d9
31ba188
533aea8
cb9d0f2
38ea63e
942e759
496eabf
00011c3
8cd3710
a9ec44b
eda289e
c57fb4f
a24521e
9daa33f
980f65d
4bb4cd1
bedb2f1
0b6ee68
c9c2141
ff2512b
4870d9f
cf1f05f
6bd175b
d338265
85cc629
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | |
} | ||
``` | ||
|
||
### Dependency Injection Usage | ||
|
||
```csharp | ||
// Register your feature flag provider | ||
builder.Services.AddOpenFeature(static builder => | ||
{ | ||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<FeatureProvider, SomeFeatureProvider>()); | ||
builder.TryAddOpenFeatureClient(SomeFeatureProvider.Name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to do request scoped DI? It would be really great to include an example where you sent request specific context in the client. Since it's a slightly more advanced topic, perhaps it could be included in the evaluation context section later in the doc. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like it should work. Here's an example from .NET FeatureManagement. https://github.com/microsoft/FeatureManagement-Dotnet?tab=readme-ov-file#using-httpcontext |
||
}); | ||
|
||
// 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 | | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace OpenFeature.Internal; | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
public sealed class OpenFeatureHostedService(Api api, IEnumerable<FeatureProvider> providers) : IHostedLifecycleService | ||
{ | ||
readonly Api _api = Check.NotNull(api); | ||
readonly IEnumerable<FeatureProvider> _providers = Check.NotNull(providers); | ||
|
||
async Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken) | ||
{ | ||
askpt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
foreach (var provider in this._providers) | ||
{ | ||
await this._api.SetProviderAsync(provider.GetMetadata().Name ?? string.Empty, provider).ConfigureAwait(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the meta-data name for a provider is an appropriate name here. We probably need a way to make providers that encapsulates that data. Simple example is using one provider with two different environments. But more than that the provider name is a different domain of names. It is the domain of the provider implementation, where client names are in the domain of the application. An application developer needs to know and assign that name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 |
||
|
||
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(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<Nullable>enable</Nullable> | ||
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0;net462</TargetFrameworks> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should .NET 7 be included as TFM? .NET 7 is out of official support |
||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<PackageReadmeFile>README.md</PackageReadmeFile> | ||
<RootNamespace>OpenFeature</RootNamespace> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<None Include="../../README.md" Pack="true" PackagePath="/" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="../OpenFeature/OpenFeature.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace OpenFeature; | ||
|
||
/// <summary> | ||
/// Describes a <see cref="OpenFeatureBuilder"/> backed by an <see cref="IServiceCollection"/>. | ||
/// </summary> | ||
/// <param name="Services"><see cref="IServiceCollection"/></param> | ||
public sealed record OpenFeatureBuilder(IServiceCollection Services); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the reason to go for a record and the extension methods here? I am not too deep into .NET but I have not seen that pattern formerly. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Contains extension methods for the <see cref="OpenFeatureBuilder"/> class. | ||
/// </summary> | ||
public static class OpenFeatureBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// This method is used to add a new context to the service collection. | ||
/// </summary> | ||
/// <param name="builder"><see cref="OpenFeatureBuilder"/></param> | ||
/// <param name="configure">the desired configuration</param> | ||
/// <returns> | ||
/// the <see cref="OpenFeatureBuilder"/> instance | ||
/// </returns> | ||
public static OpenFeatureBuilder AddContext( | ||
this OpenFeatureBuilder builder, | ||
Action<EvaluationContextBuilder> configure) | ||
{ | ||
Check.NotNull(builder); | ||
Check.NotNull(configure); | ||
|
||
AddContext(builder, null, (b, _, _) => configure(b)); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// This method is used to add a new context to the service collection. | ||
/// </summary> | ||
/// <param name="builder"><see cref="OpenFeatureBuilder"/></param> | ||
/// <param name="configure">the desired configuration</param> | ||
/// <returns> | ||
/// the <see cref="OpenFeatureBuilder"/> instance | ||
/// </returns> | ||
public static OpenFeatureBuilder AddContext( | ||
this OpenFeatureBuilder builder, | ||
Action<EvaluationContextBuilder, IServiceProvider> configure) | ||
{ | ||
Check.NotNull(builder); | ||
Check.NotNull(configure); | ||
|
||
AddContext(builder, null, (b, _, s) => configure(b, s)); | ||
|
||
return builder; | ||
} | ||
|
||
/// <summary> | ||
/// This method is used to add a new context to the service collection. | ||
/// </summary> | ||
/// <param name="builder"><see cref="OpenFeatureBuilder"/></param> | ||
/// <param name="providerName">the name of the provider</param> | ||
/// <param name="configure">the desired configuration</param> | ||
/// <returns> | ||
/// the <see cref="OpenFeatureBuilder"/> instance | ||
/// </returns> | ||
public static OpenFeatureBuilder AddContext( | ||
this OpenFeatureBuilder builder, | ||
string? providerName, | ||
Action<EvaluationContextBuilder, string?, IServiceProvider> 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; | ||
} | ||
|
||
/// <summary> | ||
/// This method is used to add a new feature client to the service collection. | ||
/// </summary> | ||
/// <param name="builder"><see cref="OpenFeatureBuilder"/></param> | ||
/// <param name="providerName">the name of the provider</param> | ||
public static void TryAddOpenFeatureClient(this OpenFeatureBuilder builder, string? providerName = null) | ||
{ | ||
Check.NotNull(builder); | ||
|
||
builder.Services.AddHostedService<OpenFeatureHostedService>(); | ||
|
||
builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => | ||
{ | ||
var api = providerName switch | ||
{ | ||
null => Api.Instance, | ||
not null => services.GetRequiredKeyedService<Api>(null) | ||
}; | ||
|
||
api.AddHooks(services.GetKeyedServices<Hook>(providerName)); | ||
api.SetContext(services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build()); | ||
|
||
return api; | ||
}); | ||
|
||
builder.Services.TryAddKeyedSingleton(providerName, static (services, providerName) => providerName switch | ||
{ | ||
null => services.GetRequiredService<ILogger<FeatureClient>>(), | ||
not null => services.GetRequiredService<ILoggerFactory>().CreateLogger($"OpenFeature.FeatureClient.{providerName}") | ||
}); | ||
|
||
builder.Services.TryAddKeyedTransient(providerName, static (services, providerName) => | ||
{ | ||
var builder = providerName switch | ||
{ | ||
null => EvaluationContext.Builder(), | ||
not null => services.GetRequiredKeyedService<EvaluationContextBuilder>(null) | ||
}; | ||
|
||
foreach (var c in services.GetKeyedServices<EvaluationContext>(providerName)) | ||
{ | ||
builder.Merge(c); | ||
} | ||
|
||
return builder; | ||
}); | ||
|
||
builder.Services.TryAddKeyedTransient<IFeatureClient>(providerName, static (services, providerName) => | ||
{ | ||
var api = services.GetRequiredService<Api>(); | ||
|
||
return api.GetClient( | ||
api.GetProviderMetadata(providerName as string ?? string.Empty).Name, | ||
null, | ||
services.GetRequiredKeyedService<ILogger>(providerName), | ||
services.GetRequiredKeyedService<EvaluationContextBuilder>(providerName).Build()); | ||
}); | ||
|
||
if (providerName is not null) | ||
builder.Services.Replace(ServiceDescriptor.Transient(services => services.GetRequiredKeyedService<IFeatureClient>(providerName))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Contains extension methods for the <see cref="IServiceCollection"/> class. | ||
/// </summary> | ||
public static class OpenFeatureServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// This method is used to add OpenFeature to the service collection. | ||
/// OpenFeature will be registered as a singleton. | ||
/// </summary> | ||
/// <param name="services"><see cref="IServiceCollection"/></param> | ||
/// <param name="configure">the desired configuration</param> | ||
/// <returns>the current <see cref="IServiceCollection"/> instance</returns> | ||
public static IServiceCollection AddOpenFeature(this IServiceCollection services, Action<OpenFeatureBuilder> configure) | ||
{ | ||
Check.NotNull(services); | ||
Check.NotNull(configure); | ||
|
||
configure(AddOpenFeature(services)); | ||
|
||
return services; | ||
} | ||
|
||
/// <summary> | ||
/// This method is used to add OpenFeature to the service collection. | ||
/// OpenFeature will be registered as a singleton. | ||
/// </summary> | ||
/// <param name="services"><see cref="IServiceCollection"/></param> | ||
/// <returns>the current <see cref="IServiceCollection"/> instance</returns> | ||
public static OpenFeatureBuilder AddOpenFeature(this IServiceCollection services) | ||
{ | ||
Check.NotNull(services); | ||
|
||
var builder = new OpenFeatureBuilder(services); | ||
|
||
builder.TryAddOpenFeatureClient(); | ||
|
||
return builder; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to time this merge closely with the release or move this to a separate PR that can be merged after the 2.0 release.