Skip to content
Merged
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,18 @@ For a basic configuration, you can use the InMemoryProvider. This provider is si
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddHostedFeatureLifecycle() // From Hosting package
.AddInMemoryProvider();
});
```

You can add EvaluationContext, hooks, and handlers at a global/API level as needed.

```csharp
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
.AddHook<LoggingHook>()
.AddInMemoryProvider();
.AddHandler(ProviderEventTypes.ProviderReady, (eventDetails) => { /* Handle event */ });
});
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using OpenFeature.Constant;
using OpenFeature.Model;

namespace OpenFeature.DependencyInjection.Internal;

internal record EventHandlerDelegateWrapper
{
public ProviderEventTypes ProviderEventType { get; }

public EventHandlerDelegate EventHandlerDelegate { get; }

public EventHandlerDelegateWrapper(ProviderEventTypes providerEventTypes, EventHandlerDelegate eventHandlerDelegate)
{
ProviderEventType = providerEventTypes;
EventHandlerDelegate = eventHandlerDelegate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke
}

_featureApi.AddHooks(hooks);

foreach (var handlerName in options.HandlerNames)
{
var handler = _serviceProvider.GetRequiredKeyedService<EventHandlerDelegateWrapper>(handlerName);
_featureApi.AddHandler(handler.ProviderEventType, handler.EventHandlerDelegate);
}
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenFeature.Constant;
using OpenFeature.DependencyInjection;
using OpenFeature.DependencyInjection.Internal;
using OpenFeature.Model;

namespace OpenFeature;
Expand Down Expand Up @@ -303,4 +305,67 @@ public static OpenFeatureBuilder AddHook<THook>(this OpenFeatureBuilder builder,

return builder;
}

/// <summary>
/// Add a <see cref="EventHandlerDelegate"/> to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
/// <param name="eventHandlerDelegate">The handler which reacts to <see cref="ProviderEventTypes"/>.</param>
/// <returns></returns>
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
{
return AddHandler(builder, string.Empty, type, sp => eventHandlerDelegate);
}

/// <summary>
/// Add a <see cref="EventHandlerDelegate"/> to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="handlerName">The name of the <see cref="EventHandlerDelegate"/>.</param>
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
/// <param name="eventHandlerDelegate">The handler which reacts to <see cref="ProviderEventTypes"/>.</param>
/// <returns></returns>
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, string handlerName, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
{
return AddHandler(builder, handlerName, type, sp => eventHandlerDelegate);
}

/// <summary>
/// Add a <see cref="EventHandlerDelegate"/> to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
/// <param name="implementationFactory">The handler factory for creating a handler which reacts to <see cref="ProviderEventTypes"/>.</param>
/// <returns></returns>
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, Func<IServiceProvider, EventHandlerDelegate> implementationFactory)
{
return AddHandler(builder, string.Empty, type, implementationFactory);
}

/// <summary>
/// Add a <see cref="EventHandlerDelegate"/> to allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions
/// </summary>
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
/// <param name="handlerName">The name of the <see cref="EventHandlerDelegate"/>.</param>
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
/// <param name="implementationFactory">The handler factory for creating a handler which reacts to <see cref="ProviderEventTypes"/>.</param>
/// <returns></returns>
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, string handlerName, ProviderEventTypes type, Func<IServiceProvider, EventHandlerDelegate> implementationFactory)
{
if (string.IsNullOrEmpty(handlerName))
handlerName = Guid.NewGuid().ToString();

var key = string.Join(":", handlerName, type.ToString());

builder.Services.PostConfigure<OpenFeatureOptions>(options => options.AddHandlerName(key));

builder.Services.TryAddKeyedSingleton(key, (serviceProvider, _) =>
{
var handler = implementationFactory(serviceProvider);
return new EventHandlerDelegateWrapper(type, handler);
});

return builder;
}
}
12 changes: 12 additions & 0 deletions src/OpenFeature.DependencyInjection/OpenFeatureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,16 @@ internal void AddHookName(string name)
_hookNames.Add(name);
}
}

private readonly HashSet<string> _handlerNames = [];

internal IReadOnlyCollection<string> HandlerNames => _handlerNames;

internal void AddHandlerName(string name)
{
lock (_handlerNames)
{
_handlerNames.Add(name);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using OpenFeature.Constant;
using OpenFeature.DependencyInjection.Internal;
using OpenFeature.Model;
using Xunit;

namespace OpenFeature.DependencyInjection.Tests;
Expand Down Expand Up @@ -81,4 +83,26 @@ public async Task EnsureInitializedAsync_ShouldSetHook_WhenHooksAreRegistered()
var actual = Api.Instance.GetHooks().FirstOrDefault();
Assert.Equal(hook, actual);
}

[Fact]
public async Task EnsureInitializedAsync_ShouldSetHandler_WhenHandlersAreRegistered()
{
// Arrange
EventHandlerDelegate eventHandlerDelegate = (_) => { };
var featureProvider = new NoOpFeatureProvider();
var handler = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate);

_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
.AddKeyedSingleton("test:ProviderReady", (_, key) => handler)
.Configure<OpenFeatureOptions>(options =>
{
options.AddHandlerName("test:ProviderReady");
});

var serviceProvider = _serviceCollection.BuildServiceProvider();
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);

// Act
await sut.EnsureInitializedAsync().ConfigureAwait(true);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenFeature.DependencyInjection.Internal;
using OpenFeature.Model;
using Xunit;

Expand Down Expand Up @@ -301,4 +302,105 @@ public void AddHook_WithSpecifiedNameAndImplementationFactory_AsKeyedService()
// Assert
Assert.NotNull(hook);
}

[Fact]
public void AddHandler_AddsEventHandlerDelegateWrapperAsKeyedService()
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler("test", Constant.ProviderEventTypes.ProviderReady, eventHandler);

var serviceProvider = _services.BuildServiceProvider();

// Act
var handler = serviceProvider.GetKeyedService<EventHandlerDelegateWrapper>("test:ProviderReady");

// Assert
Assert.NotNull(handler);
Assert.Equal(eventHandler, handler.EventHandlerDelegate);
}

[Fact]
public void AddHandler_SetsHandlerNameInOpenFeatureOptions()
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler("test", Constant.ProviderEventTypes.ProviderReady, eventHandler);

var serviceProvider = _services.BuildServiceProvider();

// Act
var options = serviceProvider.GetService<IOptions<OpenFeatureOptions>>();
var openFeatureOptions = options!.Value;

// Assert
Assert.Contains("test:ProviderReady", openFeatureOptions.HandlerNames);
}

[Fact]
public void AddHandler_WithoutName_AddsEventHandlerDelegateWrapperAsKeyedService()
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler);

// Act
var handlers = _services.Where(s => s.ServiceType == typeof(EventHandlerDelegateWrapper)).ToList();
var handler = handlers.First();

// Assert
Assert.True(handler.IsKeyedService);
Assert.Equal(ServiceLifetime.Singleton, handler.Lifetime);
}

[Theory]
[InlineData("")]
[InlineData(null)]
public void AddHandler_WithEmptyName_AddsEventHandlerDelegateWrapperAsKeyedService(string? handlerName)
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler(handlerName!, Constant.ProviderEventTypes.ProviderReady, eventHandler);

// Act
var handlers = _services.Where(s => s.ServiceType == typeof(EventHandlerDelegateWrapper)).ToList();
var handler = handlers.First();

// Assert
Assert.True(handler.IsKeyedService);
Assert.Equal(ServiceLifetime.Singleton, handler.Lifetime);
}

[Fact]
public void AddHandler_WithImplementationFactory_AddsEventHandlerDelegateWrapperAsKeyedService()
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler("test", Constant.ProviderEventTypes.ProviderReady, sp => eventHandler);

var serviceProvider = _services.BuildServiceProvider();

// Act
var handler = serviceProvider.GetKeyedService<EventHandlerDelegateWrapper>("test:ProviderReady");

// Assert
Assert.NotNull(handler);
Assert.Equal(eventHandler, handler.EventHandlerDelegate);
}

[Fact]
public void AddHandlerWithImplementationFactory_WithoutName_AddsEventHandlerDelegateWrapperAsKeyedService()
{
// Arrange
EventHandlerDelegate eventHandler = (eventDetails) => { };
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, sp => eventHandler);

// Act
var handlers = _services.Where(s => s.ServiceType == typeof(EventHandlerDelegateWrapper)).ToList();
var handler = handlers.First();

// Assert
Assert.True(handler.IsKeyedService);
Assert.Equal(ServiceLifetime.Singleton, handler.Lifetime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Testing;
using OpenFeature.Constant;
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Hooks;
using OpenFeature.IntegrationTests.Services;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;

namespace OpenFeature.IntegrationTests;
Expand Down Expand Up @@ -89,8 +91,33 @@ public async Task VerifyLoggingHookIsRegisteredAsync()
});
}

[Fact]
public async Task VerifyHandlerIsRegisteredAsync()
{
// Arrange
var logger = new FakeLogger();
Action<IServiceCollection> configureServices = services =>
{
services.AddTransient<IFeatureFlagConfigurationService, FlagConfigurationService>();
};
var handlerSuccess = false;

using var server = await CreateServerAsync(ServiceLifetime.Transient, logger, configureServices, (_) => { handlerSuccess = true; })
.ConfigureAwait(true);

var client = server.CreateClient();
var requestUri = $"/features/{TestUserId}/flags/{FeatureA}";

// Act
var response = await client.GetAsync(requestUri).ConfigureAwait(true);

// Assert
Assert.True(handlerSuccess);
}

private static async Task<TestServer> CreateServerAsync(ServiceLifetime serviceLifetime, FakeLogger logger,
Action<IServiceCollection>? configureServices = null)
Action<IServiceCollection>? configureServices = null,
EventHandlerDelegate? eventHandlerDelegate = null)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
Expand Down Expand Up @@ -126,6 +153,11 @@ private static async Task<TestServer> CreateServerAsync(ServiceLifetime serviceL
}
});
cfg.AddHook(serviceProvider => new LoggingHook(logger));

if (eventHandlerDelegate is not null)
{
cfg.AddHandler(ProviderEventTypes.ProviderReady, eventHandlerDelegate);
}
});

var app = builder.Build();
Expand Down