diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs new file mode 100644 index 00000000..c6fed688 --- /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. + /// + public const string TargetingContextLookup = $"Microsoft.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/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 8dd2378f..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 TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId"; - /// /// 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) + if (targetingContext != null && !context.Items.ContainsKey(DefaultHttpTargetingContextAccessor.TargetingContextLookup)) { - context.Items[TargetingIdKey] = targetingContext.UserId; + 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..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,7 @@ - + diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore/TargetingTelemetryInitializer.cs index e01bd9e4..4f888d6f 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,8 +15,6 @@ namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore /// public class TargetingTelemetryInitializer : TelemetryInitializerBase { - private const string TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId"; - /// /// Creates an instance of the TargetingTelemetryInitializer /// @@ -37,27 +36,24 @@ 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)); } - // Extract the targeting id from the http context - string targetingId = null; + // + // Extract the targeting info from the http context + httpContext.Items.TryGetValue(DefaultHttpTargetingContextAccessor.TargetingContextLookup, out object targetingContextObject); + TargetingContext targetingContext = targetingContextObject as TargetingContext; - if (httpContext.Items.TryGetValue(TargetingIdKey, out object value)) - { - targetingId = value?.ToString(); - } + string targetingId = targetingContext?.UserId ?? string.Empty; - if (!string.IsNullOrEmpty(targetingId)) - { - // 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 20b05299..ca201c12 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)