Skip to content

Commit ff414b8

Browse files
feat: add AddHandler extension method to Dependency Injection package (#462)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR <!-- add the description of the PR here --> - Introduces a new extension method in the OpenFeature.DependencyInjection package which allows consumers to add, at a global/api level, a handler. Example usage: ```csharp builder.Services.AddOpenFeature(feature => { feature.AddHostedFeatureLifecycle() .AddInMemoryProvider() .AddHandler(ProviderEventTypes.ProviderReady, (@event) => { Console.WriteLine("{0}", @event!.ProviderName); }) .AddHandler(ProviderEventTypes.ProviderReady, sp => (@event) => { var logger = sp.GetRequiredService<ILogger<Program>>(); logger.LogInformation("Provider Ready"); }); }); ``` ### Related Issues <!-- add here the GitHub issue that this PR resolves if applicable --> Fixes #457 ### Notes <!-- any additional notes for this PR --> ### Follow-up Tasks <!-- anything that is related to this PR but not done here should be noted under this section --> <!-- if there is a need for a new issue, please link it here --> ### How to test <!-- if applicable, add testing instructions under this section --> --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
1 parent 0a5ab0c commit ff414b8

File tree

7 files changed

+267
-8
lines changed

7 files changed

+267
-8
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,9 +433,18 @@ For a basic configuration, you can use the InMemoryProvider. This provider is si
433433
builder.Services.AddOpenFeature(featureBuilder => {
434434
featureBuilder
435435
.AddHostedFeatureLifecycle() // From Hosting package
436+
.AddInMemoryProvider();
437+
});
438+
```
439+
440+
You can add EvaluationContext, hooks, and handlers at a global/API level as needed.
441+
442+
```csharp
443+
builder.Services.AddOpenFeature(featureBuilder => {
444+
featureBuilder
436445
.AddContext((contextBuilder, serviceProvider) => { /* Custom context configuration */ })
437446
.AddHook<LoggingHook>()
438-
.AddInMemoryProvider();
447+
.AddHandler(ProviderEventTypes.ProviderReady, (eventDetails) => { /* Handle event */ });
439448
});
440449
```
441450

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using OpenFeature.Constant;
2+
using OpenFeature.Model;
3+
4+
namespace OpenFeature.DependencyInjection.Internal;
5+
6+
internal record EventHandlerDelegateWrapper(
7+
ProviderEventTypes ProviderEventType,
8+
EventHandlerDelegate EventHandlerDelegate);

src/OpenFeature.DependencyInjection/Internal/FeatureLifecycleManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public async ValueTask EnsureInitializedAsync(CancellationToken cancellationToke
4343
}
4444

4545
_featureApi.AddHooks(hooks);
46+
47+
var handlers = _serviceProvider.GetServices<EventHandlerDelegateWrapper>();
48+
foreach (var handler in handlers)
49+
{
50+
_featureApi.AddHandler(handler.ProviderEventType, handler.EventHandlerDelegate);
51+
}
4652
}
4753

4854
/// <inheritdoc />

src/OpenFeature.DependencyInjection/OpenFeatureBuilderExtensions.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.DependencyInjection.Extensions;
33
using Microsoft.Extensions.Options;
4+
using OpenFeature.Constant;
45
using OpenFeature.DependencyInjection;
6+
using OpenFeature.DependencyInjection.Internal;
57
using OpenFeature.Model;
68

79
namespace OpenFeature;
@@ -303,4 +305,34 @@ public static OpenFeatureBuilder AddHook<THook>(this OpenFeatureBuilder builder,
303305

304306
return builder;
305307
}
308+
309+
/// <summary>
310+
/// 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
311+
/// </summary>
312+
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
313+
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
314+
/// <param name="eventHandlerDelegate">The handler which reacts to <see cref="ProviderEventTypes"/>.</param>
315+
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
316+
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, EventHandlerDelegate eventHandlerDelegate)
317+
{
318+
return AddHandler(builder, type, _ => eventHandlerDelegate);
319+
}
320+
321+
/// <summary>
322+
/// 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
323+
/// </summary>
324+
/// <param name="builder">The <see cref="OpenFeatureBuilder"/> instance.</param>
325+
/// <param name="type">The type <see cref="ProviderEventTypes"/> to handle.</param>
326+
/// <param name="implementationFactory">The handler factory for creating a handler which reacts to <see cref="ProviderEventTypes"/>.</param>
327+
/// <returns>The <see cref="OpenFeatureBuilder"/> instance.</returns>
328+
public static OpenFeatureBuilder AddHandler(this OpenFeatureBuilder builder, ProviderEventTypes type, Func<IServiceProvider, EventHandlerDelegate> implementationFactory)
329+
{
330+
builder.Services.AddSingleton((serviceProvider) =>
331+
{
332+
var handler = implementationFactory(serviceProvider);
333+
return new EventHandlerDelegateWrapper(type, handler);
334+
});
335+
336+
return builder;
337+
}
306338
}

test/OpenFeature.DependencyInjection.Tests/FeatureLifecycleManagerTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.DependencyInjection.Extensions;
33
using Microsoft.Extensions.Logging.Abstractions;
4+
using OpenFeature.Constant;
45
using OpenFeature.DependencyInjection.Internal;
6+
using OpenFeature.Model;
57
using Xunit;
68

79
namespace OpenFeature.DependencyInjection.Tests;
@@ -81,4 +83,43 @@ public async Task EnsureInitializedAsync_ShouldSetHook_WhenHooksAreRegistered()
8183
var actual = Api.Instance.GetHooks().FirstOrDefault();
8284
Assert.Equal(hook, actual);
8385
}
86+
87+
[Fact]
88+
public async Task EnsureInitializedAsync_ShouldSetHandler_WhenHandlersAreRegistered()
89+
{
90+
// Arrange
91+
EventHandlerDelegate eventHandlerDelegate = (_) => { };
92+
var featureProvider = new NoOpFeatureProvider();
93+
var handler = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate);
94+
95+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
96+
.AddSingleton(_ => handler);
97+
98+
var serviceProvider = _serviceCollection.BuildServiceProvider();
99+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
100+
101+
// Act
102+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
103+
}
104+
105+
[Fact]
106+
public async Task EnsureInitializedAsync_ShouldSetHandler_WhenMultipleHandlersAreRegistered()
107+
{
108+
// Arrange
109+
EventHandlerDelegate eventHandlerDelegate1 = (_) => { };
110+
EventHandlerDelegate eventHandlerDelegate2 = (_) => { };
111+
var featureProvider = new NoOpFeatureProvider();
112+
var handler1 = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate1);
113+
var handler2 = new EventHandlerDelegateWrapper(ProviderEventTypes.ProviderReady, eventHandlerDelegate2);
114+
115+
_serviceCollection.AddSingleton<FeatureProvider>(featureProvider)
116+
.AddSingleton(_ => handler1)
117+
.AddSingleton(_ => handler2);
118+
119+
var serviceProvider = _serviceCollection.BuildServiceProvider();
120+
var sut = new FeatureLifecycleManager(Api.Instance, serviceProvider, NullLogger<FeatureLifecycleManager>.Instance);
121+
122+
// Act
123+
await sut.EnsureInitializedAsync().ConfigureAwait(true);
124+
}
84125
}

test/OpenFeature.DependencyInjection.Tests/OpenFeatureBuilderExtensionsTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Options;
3+
using OpenFeature.DependencyInjection.Internal;
34
using OpenFeature.Model;
45
using Xunit;
56

@@ -301,4 +302,58 @@ public void AddHook_WithSpecifiedNameAndImplementationFactory_AsKeyedService()
301302
// Assert
302303
Assert.NotNull(hook);
303304
}
305+
306+
[Fact]
307+
public void AddHandler_AddsEventHandlerDelegateWrapperAsKeyedService()
308+
{
309+
// Arrange
310+
EventHandlerDelegate eventHandler = (eventDetails) => { };
311+
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler);
312+
313+
var serviceProvider = _services.BuildServiceProvider();
314+
315+
// Act
316+
var handler = serviceProvider.GetService<EventHandlerDelegateWrapper>();
317+
318+
// Assert
319+
Assert.NotNull(handler);
320+
Assert.Equal(eventHandler, handler.EventHandlerDelegate);
321+
}
322+
323+
[Fact]
324+
public void AddHandlerTwice_MultipleEventHandlerDelegateWrappersAsKeyedServices()
325+
{
326+
// Arrange
327+
EventHandlerDelegate eventHandler1 = (eventDetails) => { };
328+
EventHandlerDelegate eventHandler2 = (eventDetails) => { };
329+
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler1);
330+
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, eventHandler2);
331+
332+
var serviceProvider = _services.BuildServiceProvider();
333+
334+
// Act
335+
var handler = serviceProvider.GetServices<EventHandlerDelegateWrapper>();
336+
337+
// Assert
338+
Assert.NotEmpty(handler);
339+
Assert.Equal(eventHandler1, handler.ElementAt(0).EventHandlerDelegate);
340+
Assert.Equal(eventHandler2, handler.ElementAt(1).EventHandlerDelegate);
341+
}
342+
343+
[Fact]
344+
public void AddHandler_WithImplementationFactory_AddsEventHandlerDelegateWrapperAsKeyedService()
345+
{
346+
// Arrange
347+
EventHandlerDelegate eventHandler = (eventDetails) => { };
348+
_systemUnderTest.AddHandler(Constant.ProviderEventTypes.ProviderReady, _ => eventHandler);
349+
350+
var serviceProvider = _services.BuildServiceProvider();
351+
352+
// Act
353+
var handler = serviceProvider.GetService<EventHandlerDelegateWrapper>();
354+
355+
// Assert
356+
Assert.NotNull(handler);
357+
Assert.Equal(eventHandler, handler.EventHandlerDelegate);
358+
}
304359
}

test/OpenFeature.IntegrationTests/FeatureFlagIntegrationTest.cs

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using Microsoft.AspNetCore.TestHost;
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Extensions.Logging;
89
using Microsoft.Extensions.Logging.Testing;
10+
using OpenFeature.Constant;
11+
using OpenFeature.DependencyInjection;
912
using OpenFeature.DependencyInjection.Providers.Memory;
1013
using OpenFeature.Hooks;
1114
using OpenFeature.IntegrationTests.Services;
@@ -29,8 +32,7 @@ public class FeatureFlagIntegrationTest
2932
public async Task VerifyFeatureFlagBehaviorAcrossServiceLifetimesAsync(string userId, bool expectedResult, ServiceLifetime serviceLifetime)
3033
{
3134
// Arrange
32-
var logger = new FakeLogger();
33-
using var server = await CreateServerAsync(serviceLifetime, logger, services =>
35+
using var server = await CreateServerAsync(serviceLifetime, services =>
3436
{
3537
switch (serviceLifetime)
3638
{
@@ -67,10 +69,17 @@ public async Task VerifyLoggingHookIsRegisteredAsync()
6769
{
6870
// Arrange
6971
var logger = new FakeLogger();
70-
using var server = await CreateServerAsync(ServiceLifetime.Transient, logger, services =>
72+
Action<IServiceCollection> configureServices = services =>
7173
{
7274
services.AddTransient<IFeatureFlagConfigurationService, FlagConfigurationService>();
73-
}).ConfigureAwait(true);
75+
};
76+
77+
Action<OpenFeatureBuilder> openFeatureBuilder = cfg =>
78+
{
79+
cfg.AddHook(_ => new LoggingHook(logger));
80+
};
81+
82+
using var server = await CreateServerAsync(ServiceLifetime.Transient, configureServices, openFeatureBuilder).ConfigureAwait(true);
7483

7584
var client = server.CreateClient();
7685
var requestUri = $"/features/{TestUserId}/flags/{FeatureA}";
@@ -89,8 +98,103 @@ public async Task VerifyLoggingHookIsRegisteredAsync()
8998
});
9099
}
91100

92-
private static async Task<TestServer> CreateServerAsync(ServiceLifetime serviceLifetime, FakeLogger logger,
93-
Action<IServiceCollection>? configureServices = null)
101+
[Fact]
102+
public async Task VerifyHandlerIsRegisteredAsync()
103+
{
104+
// Arrange
105+
Action<IServiceCollection> configureServices = services =>
106+
{
107+
services.AddTransient<IFeatureFlagConfigurationService, FlagConfigurationService>();
108+
};
109+
110+
var handlerSuccess = false;
111+
Action<OpenFeatureBuilder> openFeatureBuilder = cfg =>
112+
{
113+
cfg.AddHandler(ProviderEventTypes.ProviderReady, (_) => { handlerSuccess = true; });
114+
};
115+
116+
using var server = await CreateServerAsync(ServiceLifetime.Transient, configureServices, openFeatureBuilder)
117+
.ConfigureAwait(true);
118+
119+
var client = server.CreateClient();
120+
var requestUri = $"/features/{TestUserId}/flags/{FeatureA}";
121+
122+
// Act
123+
var response = await client.GetAsync(requestUri).ConfigureAwait(true);
124+
125+
// Assert
126+
Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK.");
127+
Assert.True(handlerSuccess);
128+
}
129+
130+
[Fact]
131+
public async Task VerifyMultipleHandlersAreRegisteredAsync()
132+
{
133+
// Arrange
134+
Action<IServiceCollection> configureServices = services =>
135+
{
136+
services.AddTransient<IFeatureFlagConfigurationService, FlagConfigurationService>();
137+
};
138+
139+
var @lock = new Lock();
140+
var counter = 0;
141+
Action<OpenFeatureBuilder> openFeatureBuilder = cfg =>
142+
{
143+
cfg.AddHandler(ProviderEventTypes.ProviderReady, (_) => { lock (@lock) { counter++; } });
144+
cfg.AddHandler(ProviderEventTypes.ProviderReady, (_) => { lock (@lock) { counter++; } });
145+
};
146+
147+
using var server = await CreateServerAsync(ServiceLifetime.Transient, configureServices, openFeatureBuilder)
148+
.ConfigureAwait(true);
149+
150+
var client = server.CreateClient();
151+
var requestUri = $"/features/{TestUserId}/flags/{FeatureA}";
152+
153+
// Act
154+
var response = await client.GetAsync(requestUri).ConfigureAwait(true);
155+
156+
// Assert
157+
Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK.");
158+
Assert.Equal(2, counter);
159+
}
160+
161+
[Fact]
162+
public async Task VerifyHandlersAreRegisteredWithServiceProviderAsync()
163+
{
164+
// Arrange
165+
var logs = string.Empty;
166+
Action<IServiceCollection> configureServices = services =>
167+
{
168+
services.AddFakeLogging(a => a.OutputSink = log => logs = string.Join('|', logs, log));
169+
services.AddTransient<IFeatureFlagConfigurationService, FlagConfigurationService>();
170+
};
171+
172+
Action<OpenFeatureBuilder> openFeatureBuilder = cfg =>
173+
{
174+
cfg.AddHandler(ProviderEventTypes.ProviderReady, sp => (@event) =>
175+
{
176+
var innerLoger = sp.GetService<ILogger<FeatureFlagIntegrationTest>>();
177+
innerLoger!.LogInformation("Handler invoked from builder!");
178+
});
179+
};
180+
181+
using var server = await CreateServerAsync(ServiceLifetime.Transient, configureServices, openFeatureBuilder)
182+
.ConfigureAwait(true);
183+
184+
var client = server.CreateClient();
185+
var requestUri = $"/features/{TestUserId}/flags/{FeatureA}";
186+
187+
// Act
188+
var response = await client.GetAsync(requestUri).ConfigureAwait(true);
189+
190+
// Assert
191+
Assert.True(response.IsSuccessStatusCode, "Expected HTTP status code 200 OK.");
192+
Assert.Contains("Handler invoked from builder!", logs);
193+
}
194+
195+
private static async Task<TestServer> CreateServerAsync(ServiceLifetime serviceLifetime,
196+
Action<IServiceCollection>? configureServices = null,
197+
Action<OpenFeatureBuilder>? openFeatureBuilder = null)
94198
{
95199
var builder = WebApplication.CreateBuilder();
96200
builder.WebHost.UseTestServer();
@@ -125,7 +229,11 @@ private static async Task<TestServer> CreateServerAsync(ServiceLifetime serviceL
125229
return flagService.GetFlags();
126230
}
127231
});
128-
cfg.AddHook(serviceProvider => new LoggingHook(logger));
232+
233+
if (openFeatureBuilder is not null)
234+
{
235+
openFeatureBuilder(cfg);
236+
}
129237
});
130238

131239
var app = builder.Build();

0 commit comments

Comments
 (0)