From 30fc53c425d57dd5b447764c2cac2ea207091b53 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 20 Jun 2024 10:48:54 -0700 Subject: [PATCH 1/4] Adds default targeting accessor and extension method WithTargeting variation for ASP.NET --- .../QueryStringAuthenticationHandler.cs | 2 +- examples/FeatureFlagDemo/ClaimTypes.cs | 10 --- .../HttpContextTargetingContextAccessor.cs | 66 ----------------- examples/FeatureFlagDemo/Startup.cs | 2 +- ...tCoreFeatureManagementBuilderExtensions.cs | 26 +++++++ .../DefaultHttpTargetingContextAccessor.cs | 73 +++++++++++++++++++ 6 files changed, 101 insertions(+), 78 deletions(-) delete mode 100644 examples/FeatureFlagDemo/ClaimTypes.cs delete mode 100644 examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs create mode 100644 src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs diff --git a/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs b/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs index 073b7452..0049cca3 100644 --- a/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs +++ b/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs @@ -49,7 +49,7 @@ protected override Task HandleAuthenticateAsync() foreach (string group in groups) { - identity.AddClaim(new Claim(ClaimTypes.GroupName, group)); + identity.AddClaim(new Claim(ClaimTypes.Role, group)); } Logger.LogInformation($"Assigning the following groups '{string.Join(", ", groups)}' to the request."); diff --git a/examples/FeatureFlagDemo/ClaimTypes.cs b/examples/FeatureFlagDemo/ClaimTypes.cs deleted file mode 100644 index b24dfd20..00000000 --- a/examples/FeatureFlagDemo/ClaimTypes.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace FeatureFlagDemo -{ - static class ClaimTypes - { - public static string GroupName = "http://schemas.featureflagdemo.featuremanagement.microsoft.com/claims/groupname"; - } -} diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs deleted file mode 100644 index 9f9c8964..00000000 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.AspNetCore.Http; -using Microsoft.FeatureManagement.FeatureFilters; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace FeatureFlagDemo -{ - /// - /// Provides an implementation of that creates a targeting context using info from the current HTTP request. - /// - public class HttpContextTargetingContextAccessor : ITargetingContextAccessor - { - private const string TargetingContextLookup = "HttpContextTargetingContextAccessor.TargetingContext"; - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - } - - public ValueTask GetContextAsync() - { - HttpContext httpContext = _httpContextAccessor.HttpContext; - - // - // Try cache lookup - if (httpContext.Items.TryGetValue(TargetingContextLookup, out object value)) - { - return new ValueTask((TargetingContext)value); - } - - ClaimsPrincipal user = httpContext.User; - - List groups = new List(); - - // - // This application expects groups to be specified in the user's claims - foreach (Claim claim in user.Claims) - { - if (claim.Type == ClaimTypes.GroupName) - { - groups.Add(claim.Value); - } - } - - // - // Build targeting context based off user info - TargetingContext targetingContext = new TargetingContext - { - UserId = user.Identity.Name, - Groups = groups - }; - - // - // Cache for subsequent lookup - httpContext.Items[TargetingContextLookup] = targetingContext; - - return new ValueTask(targetingContext); - } - } -} diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs index 4c5722bd..f29f4934 100644 --- a/examples/FeatureFlagDemo/Startup.cs +++ b/examples/FeatureFlagDemo/Startup.cs @@ -45,7 +45,7 @@ public void ConfigureServices(IServiceCollection services) services.AddFeatureManagement() .AddFeatureFilter() - .WithTargeting() + .WithTargeting() .UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler()); services.AddMvc(o => diff --git a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs index 742b942e..af681d31 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs @@ -3,9 +3,12 @@ // using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Mvc; using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.FeatureManagement { @@ -44,5 +47,28 @@ public static IFeatureManagementBuilder UseDisabledFeaturesHandler(this IFeature return builder; } + + /// + /// Adds the to be used for targeting and registers the targeting filter to the feature management system. + /// + /// The used to customize feature management functionality. + /// A that can be used to customize feature management functionality. + public static IFeatureManagementBuilder WithTargeting(this IFeatureManagementBuilder builder) + { + // + // Register the targeting context accessor with the same lifetime as the feature manager + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + builder.Services.TryAddScoped(); + } + else + { + builder.Services.TryAddSingleton(); + } + + builder.AddFeatureFilter(); + + return builder; + } } } diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs new file mode 100644 index 00000000..548b3405 --- /dev/null +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.AspNetCore.Http; +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a default implementation of that creates using info from the current HTTP request. + /// + public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor + { + /// + /// The key used to store and retrieve the from the items. + /// + private static object _cacheKey = new object(); + + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Creates an instance of the DefaultHttpTargetingContextAccessor + /// + public DefaultHttpTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + + /// + /// Gets from the current HTTP request. + /// + public ValueTask GetContextAsync() + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + // + // Try cache lookup + if (httpContext.Items.TryGetValue(_cacheKey, out object value)) + { + return new ValueTask((TargetingContext)value); + } + + // + // Treat user identity name as user id + ClaimsPrincipal user = httpContext.User; + + string userId = user?.Identity?.Name; + + // + // Treat claims of type Role as groups + IEnumerable groups = httpContext.User.Claims + .Where(c => c.Type == ClaimTypes.Role) + .Select(c => c.Value); + + TargetingContext targetingContext = new TargetingContext + { + UserId = userId, + Groups = groups + }; + + // + // Cache for subsequent lookup + httpContext.Items[_cacheKey] = targetingContext; + + return new ValueTask(targetingContext); + } + } +} \ No newline at end of file From 6142f3edaefcd33e5cea6c6c95d43f367d943468 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 20 Jun 2024 11:01:11 -0700 Subject: [PATCH 2/4] Updates accessor to internal --- .../DefaultHttpTargetingContextAccessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs index 548b3405..e010a1a3 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -14,7 +14,7 @@ namespace Microsoft.FeatureManagement /// /// Provides a default implementation of that creates using info from the current HTTP request. /// - public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor + internal sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor { /// /// The key used to store and retrieve the from the items. From 04f066d54d4d8165c2695daec0ab69a755762268 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Wed, 26 Jun 2024 11:12:28 -0700 Subject: [PATCH 3/4] Forced query evaluation --- .../DefaultHttpTargetingContextAccessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs index e010a1a3..8eb99090 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -55,7 +55,8 @@ public ValueTask GetContextAsync() // Treat claims of type Role as groups IEnumerable groups = httpContext.User.Claims .Where(c => c.Type == ClaimTypes.Role) - .Select(c => c.Value); + .Select(c => c.Value) + .ToList(); TargetingContext targetingContext = new TargetingContext { From 13519482c29ec17987b3370bb8c762a457a51ced Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Fri, 12 Jul 2024 11:44:06 -0700 Subject: [PATCH 4/4] Updated description of .WithTargeting extension --- .../AspNetCoreFeatureManagementBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs index af681d31..9091a4b0 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs @@ -49,7 +49,7 @@ public static IFeatureManagementBuilder UseDisabledFeaturesHandler(this IFeature } /// - /// Adds the to be used for targeting and registers the targeting filter to the feature management system. + /// Enables the use of targeting within the application and adds a targeting context accessor that extracts targeting details from a request's HTTP context. /// /// The used to customize feature management functionality. /// A that can be used to customize feature management functionality.