From 87cb9e2ad216b762166b9bdbf885dcb00d2b9810 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Wed, 3 Apr 2024 14:46:37 -0700 Subject: [PATCH 1/7] Adds default targeting context accessor and aligns caches --- .../DefaultHttpTargetingContextAccessor.cs | 68 +++++++++++++++++++ .../TargetingHttpContextMiddleware.cs | 6 +- .../TargetingTelemetryInitializer.cs | 21 +++--- .../ApplicationInsightsTelemetryPublisher.cs | 2 +- 4 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs new file mode 100644 index 00000000..7626bf98 --- /dev/null +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -0,0 +1,68 @@ +// 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 FeatureFlagDemo +{ + /// + /// Provides a default implementation of that creates using info from the current HTTP request. + /// + public class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor + { + private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; + 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(TargetingContextLookup, 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[TargetingContextLookup] = targetingContext; + + return new ValueTask(targetingContext); + } + } +} diff --git a/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs b/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs index 8dd2378f..69c65d30 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs @@ -18,7 +18,7 @@ public class TargetingHttpContextMiddleware private readonly RequestDelegate _next; private readonly ILogger _logger; - private const string TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId"; + private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; /// /// Creates an instance of the TargetingHttpContextMiddleware @@ -48,9 +48,9 @@ public async Task InvokeAsync(HttpContext context, ITargetingContextAccessor tar TargetingContext targetingContext = await targetingContextAccessor.GetContextAsync().ConfigureAwait(false); - if (targetingContext != null) + if (targetingContext != null && !context.Items.ContainsKey(TargetingContextLookup)) { - context.Items[TargetingIdKey] = targetingContext.UserId; + context.Items[TargetingContextLookup] = targetingContext; } else { diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs index e01bd9e4..5fe7306e 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs @@ -6,6 +6,7 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.AspNetCore.Http; +using Microsoft.FeatureManagement.FeatureFilters; namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore { @@ -14,7 +15,7 @@ namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore /// public class TargetingTelemetryInitializer : TelemetryInitializerBase { - private const string TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId"; + private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; /// /// Creates an instance of the TargetingTelemetryInitializer @@ -42,21 +43,15 @@ protected override void OnInitializeTelemetry(HttpContext httpContext, RequestTe throw new ArgumentNullException(nameof(httpContext)); } - // Extract the targeting id from the http context - string targetingId = null; + // + // Extract the targeting info from the http context + TargetingContext targetingContext = httpContext.Items[TargetingContextLookup] as TargetingContext; - if (httpContext.Items.TryGetValue(TargetingIdKey, out object value)) - { - targetingId = value?.ToString(); - } + string targetingId = targetingContext?.UserId ?? string.Empty; - if (!string.IsNullOrEmpty(targetingId)) + if (telemetry is ISupportProperties telemetryWithSupportProperties) { - // Telemetry.Properties is deprecated in favor of ISupportProperties - if (telemetry is ISupportProperties telemetryWithSupportProperties) - { - telemetryWithSupportProperties.Properties["TargetingId"] = targetingId; - } + telemetryWithSupportProperties.Properties["TargetingId"] = targetingId; } } } diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs index 450f95e9..a24e1722 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs @@ -44,7 +44,7 @@ public ValueTask PublishEvent(EvaluationEvent evaluationEvent, CancellationToken if (evaluationEvent.TargetingContext != null) { - properties["TargetingId"] = evaluationEvent.TargetingContext.UserId; + properties["TargetingId"] = evaluationEvent.TargetingContext.UserId ?? string.Empty; } if (evaluationEvent.VariantAssignmentReason != VariantAssignmentReason.None) From e5fe69b072dd72be848aa9f22995f12d9615f143 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Mon, 8 Apr 2024 09:05:51 -0700 Subject: [PATCH 2/7] Adding newline and adjusted namespace --- .../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 7626bf98..069890a3 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -9,7 +9,7 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace FeatureFlagDemo +namespace Microsoft.FeatureManagement { /// /// Provides a default implementation of that creates using info from the current HTTP request. @@ -44,6 +44,7 @@ public ValueTask GetContextAsync() // // Treat user identity name as user id ClaimsPrincipal user = httpContext.User; + string userId = user?.Identity?.Name; // From 387f6480c811920fed1e14336f28b5b3df754503 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Mon, 15 Apr 2024 15:48:11 -0700 Subject: [PATCH 3/7] Update src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs Co-authored-by: Weihan Li --- .../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 069890a3..e4cc8c70 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 class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor + public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor { private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; private readonly IHttpContextAccessor _httpContextAccessor; From 622095bfafcd44977a198d0e9001ad5efa1f35d8 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Tue, 28 May 2024 16:40:57 -0700 Subject: [PATCH 4/7] Adjusts to new design --- .../DefaultHttpTargetingContextAccessor.cs | 6 ++- .../FeatureManagementBuilderExtensions.cs | 40 +++++++++++++++++++ .../TargetingHttpContextMiddleware.cs | 6 +-- ...etry.ApplicationInsights.AspNetCore.csproj | 1 + .../TargetingTelemetryInitializer.cs | 14 +++---- 5 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 src/Microsoft.FeatureManagement.AspNetCore/FeatureManagementBuilderExtensions.cs diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs index e4cc8c70..060b35ab 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -16,7 +16,11 @@ namespace Microsoft.FeatureManagement /// public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor { - private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; + /// + /// The key used to store and retrieve the TargetingContext from the HttpContext items. + /// + public const string TargetingContextLookup = $"Microsoft.FeatureManagement.TargetingContext"; + private readonly IHttpContextAccessor _httpContextAccessor; /// diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureManagementBuilderExtensions.cs new file mode 100644 index 00000000..b0857451 --- /dev/null +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureManagementBuilderExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.FeatureManagement.FeatureFilters; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.FeatureManagement +{ + /// + /// Extensions used to add feature management functionality. + /// + public static class FeatureManagementBuilderExtensions + { + /// + /// 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/TargetingHttpContextMiddleware.cs b/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs index 69c65d30..22166fe5 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/TargetingHttpContextMiddleware.cs @@ -18,8 +18,6 @@ public class TargetingHttpContextMiddleware private readonly RequestDelegate _next; private readonly ILogger _logger; - private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; - /// /// Creates an instance of the TargetingHttpContextMiddleware /// @@ -48,9 +46,9 @@ public async Task InvokeAsync(HttpContext context, ITargetingContextAccessor tar TargetingContext targetingContext = await targetingContextAccessor.GetContextAsync().ConfigureAwait(false); - if (targetingContext != null && !context.Items.ContainsKey(TargetingContextLookup)) + if (targetingContext != null && !context.Items.ContainsKey(DefaultHttpTargetingContextAccessor.TargetingContextLookup)) { - context.Items[TargetingContextLookup] = targetingContext; + context.Items[DefaultHttpTargetingContextAccessor.TargetingContextLookup] = targetingContext; } else { diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj index 0c4eb847..2800037c 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs index 5fe7306e..dcaebbe4 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs @@ -15,8 +15,6 @@ namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore /// public class TargetingTelemetryInitializer : TelemetryInitializerBase { - private const string TargetingContextLookup = "FeatureManagement.TargetingContext"; - /// /// Creates an instance of the TargetingTelemetryInitializer /// @@ -38,6 +36,11 @@ protected override void OnInitializeTelemetry(HttpContext httpContext, RequestTe throw new ArgumentNullException(nameof(telemetry)); } + if (telemetry is not ISupportProperties telemetryWithSupportProperties) + { + return; + } + if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); @@ -45,14 +48,11 @@ protected override void OnInitializeTelemetry(HttpContext httpContext, RequestTe // // Extract the targeting info from the http context - TargetingContext targetingContext = httpContext.Items[TargetingContextLookup] as TargetingContext; + TargetingContext targetingContext = httpContext.Items[DefaultHttpTargetingContextAccessor.TargetingContextLookup] as TargetingContext; string targetingId = targetingContext?.UserId ?? string.Empty; - if (telemetry is ISupportProperties telemetryWithSupportProperties) - { - telemetryWithSupportProperties.Properties["TargetingId"] = targetingId; - } + telemetryWithSupportProperties.Properties["TargetingId"] = targetingId; } } } From 01ca93ab595b122c7090b384ba237d6e60fbf51d Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 30 May 2024 15:53:23 -0700 Subject: [PATCH 5/7] Update src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs Co-authored-by: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> --- .../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 060b35ab..c6fed688 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -17,7 +17,7 @@ namespace Microsoft.FeatureManagement public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor { /// - /// The key used to store and retrieve the TargetingContext from the HttpContext items. + /// The key used to store and retrieve the from the items. /// public const string TargetingContextLookup = $"Microsoft.FeatureManagement.TargetingContext"; From f41978433aa5c594e8faa26847ed7a1fe01688dd Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Fri, 31 May 2024 11:43:37 -0700 Subject: [PATCH 6/7] Removed unused import --- ...ureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj index 2800037c..ae494bf2 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj @@ -37,7 +37,6 @@ - From abc8ebf4f22d61711df8f8332297301a9b536288 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 6 Jun 2024 11:40:16 -0700 Subject: [PATCH 7/7] Avoided exeption in HttpContext.Items read --- .../TargetingTelemetryInitializer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs index dcaebbe4..4f888d6f 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs @@ -48,7 +48,8 @@ protected override void OnInitializeTelemetry(HttpContext httpContext, RequestTe // // Extract the targeting info from the http context - TargetingContext targetingContext = httpContext.Items[DefaultHttpTargetingContextAccessor.TargetingContextLookup] as TargetingContext; + httpContext.Items.TryGetValue(DefaultHttpTargetingContextAccessor.TargetingContextLookup, out object targetingContextObject); + TargetingContext targetingContext = targetingContextObject as TargetingContext; string targetingId = targetingContext?.UserId ?? string.Empty;