Skip to content

Commit 138a4e2

Browse files
authored
Adds Default Targeting Accessor and extension method WithTargeting (#466)
* Adds default targeting accessor and extension method WithTargeting variation for ASP.NET * Updates accessor to internal * Forced query evaluation * Updated description of .WithTargeting extension
1 parent 990d8fa commit 138a4e2

File tree

6 files changed

+102
-78
lines changed

6 files changed

+102
-78
lines changed

examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ protected override Task<AuthenticateResult> HandleAuthenticateAsync()
4949

5050
foreach (string group in groups)
5151
{
52-
identity.AddClaim(new Claim(ClaimTypes.GroupName, group));
52+
identity.AddClaim(new Claim(ClaimTypes.Role, group));
5353
}
5454

5555
Logger.LogInformation($"Assigning the following groups '{string.Join(", ", groups)}' to the request.");

examples/FeatureFlagDemo/ClaimTypes.cs

Lines changed: 0 additions & 10 deletions
This file was deleted.

examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs

Lines changed: 0 additions & 66 deletions
This file was deleted.

examples/FeatureFlagDemo/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void ConfigureServices(IServiceCollection services)
4545

4646
services.AddFeatureManagement()
4747
.AddFeatureFilter<BrowserFilter>()
48-
.WithTargeting<HttpContextTargetingContextAccessor>()
48+
.WithTargeting()
4949
.UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler());
5050

5151
services.AddMvc(o =>

src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
//
44
using Microsoft.AspNetCore.Mvc.Filters;
55
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.DependencyInjection.Extensions;
7+
using Microsoft.FeatureManagement.FeatureFilters;
68
using Microsoft.FeatureManagement.Mvc;
79
using System;
810
using System.Collections.Generic;
11+
using System.Linq;
912

1013
namespace Microsoft.FeatureManagement
1114
{
@@ -44,5 +47,28 @@ public static IFeatureManagementBuilder UseDisabledFeaturesHandler(this IFeature
4447

4548
return builder;
4649
}
50+
51+
/// <summary>
52+
/// Enables the use of targeting within the application and adds a targeting context accessor that extracts targeting details from a request's HTTP context.
53+
/// </summary>
54+
/// <param name="builder">The <see cref="IFeatureManagementBuilder"/> used to customize feature management functionality.</param>
55+
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
56+
public static IFeatureManagementBuilder WithTargeting(this IFeatureManagementBuilder builder)
57+
{
58+
//
59+
// Register the targeting context accessor with the same lifetime as the feature manager
60+
if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped))
61+
{
62+
builder.Services.TryAddScoped<ITargetingContextAccessor, DefaultHttpTargetingContextAccessor>();
63+
}
64+
else
65+
{
66+
builder.Services.TryAddSingleton<ITargetingContextAccessor, DefaultHttpTargetingContextAccessor>();
67+
}
68+
69+
builder.AddFeatureFilter<TargetingFilter>();
70+
71+
return builder;
72+
}
4773
}
4874
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.FeatureManagement.FeatureFilters;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Security.Claims;
10+
using System.Threading.Tasks;
11+
12+
namespace Microsoft.FeatureManagement
13+
{
14+
/// <summary>
15+
/// Provides a default implementation of <see cref="ITargetingContextAccessor"/> that creates <see cref="TargetingContext"/> using info from the current HTTP request.
16+
/// </summary>
17+
internal sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor
18+
{
19+
/// <summary>
20+
/// The key used to store and retrieve the <see cref="TargetingContext"/> from the <see cref="HttpContext"/> items.
21+
/// </summary>
22+
private static object _cacheKey = new object();
23+
24+
private readonly IHttpContextAccessor _httpContextAccessor;
25+
26+
/// <summary>
27+
/// Creates an instance of the DefaultHttpTargetingContextAccessor
28+
/// </summary>
29+
public DefaultHttpTargetingContextAccessor(IHttpContextAccessor httpContextAccessor)
30+
{
31+
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
32+
}
33+
34+
/// <summary>
35+
/// Gets <see cref="TargetingContext"/> from the current HTTP request.
36+
/// </summary>
37+
public ValueTask<TargetingContext> GetContextAsync()
38+
{
39+
HttpContext httpContext = _httpContextAccessor.HttpContext;
40+
41+
//
42+
// Try cache lookup
43+
if (httpContext.Items.TryGetValue(_cacheKey, out object value))
44+
{
45+
return new ValueTask<TargetingContext>((TargetingContext)value);
46+
}
47+
48+
//
49+
// Treat user identity name as user id
50+
ClaimsPrincipal user = httpContext.User;
51+
52+
string userId = user?.Identity?.Name;
53+
54+
//
55+
// Treat claims of type Role as groups
56+
IEnumerable<string> groups = httpContext.User.Claims
57+
.Where(c => c.Type == ClaimTypes.Role)
58+
.Select(c => c.Value)
59+
.ToList();
60+
61+
TargetingContext targetingContext = new TargetingContext
62+
{
63+
UserId = userId,
64+
Groups = groups
65+
};
66+
67+
//
68+
// Cache for subsequent lookup
69+
httpContext.Items[_cacheKey] = targetingContext;
70+
71+
return new ValueTask<TargetingContext>(targetingContext);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)