Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds default http targeting context accessor #416

Closed
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides a default implementation of <see cref="ITargetingContextAccessor"/> that creates <see cref="TargetingContext"/> using info from the current HTTP request.
/// </summary>
public sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor
{
/// <summary>
/// The key used to store and retrieve the TargetingContext from the HttpContext items.
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public const string TargetingContextLookup = $"Microsoft.FeatureManagement.TargetingContext";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's debatable who should own this cache key. It's odd to see the more generic initializer and middleware reference the cache key from a very specific targeting context accessor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed... I still am unable to come up with a satisfying place to put this. I suppose the owner could be the middleware- because the middleware's whole goal is to ensure that the TargetingContext is available at that key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The targeting context accessor doesn't need to share a key.

private static object _cacheKey = new object(); should work fine.

Copy link
Contributor Author

@rossgrambo rossgrambo Jun 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's 3 classes that interact with HttpContext.Items for targeting context. Initializer, Middleware, and the TargetingContextAccessor.

Initializer depends upon the Middleware. The Middleware depends upon a TargetingContextAccessor existing. The Middleware and Initializer are tightly coupled- so they definitely need to share a key. The TargetingContextAccessor doesn't have to share the same key, but it reduces our HttpContext.Items footprint if it does- and is a clear shared cache location.

So for Initializer and Middleware, we are going to have a shared key- it could be either an object ref or the string key "Microsoft.FeatureManagement.TargetingContext". I slightly prefer the string key since it's public (on the HttpContext.Items and as a public property). The key needs a home, and if it was just these two classes it would either be on the middleware or a shared constants class.

Now the DefaultTargetingContextAccessor comes in- which is our default implementation. It needs a cache key as well. It could either define its own key (as a string or object ref) or used a shared constants class.

I think the shared constants class makes things the most clear- as FM is defining where TargetingContext should live on HttpContext.Items. I ended up not liking adding the constants class because it introduces yet another public class, and I ended up going with the DefaultTargetingContextAccessor.


private readonly IHttpContextAccessor _httpContextAccessor;

/// <summary>
/// Creates an instance of the DefaultHttpTargetingContextAccessor
/// </summary>
public DefaultHttpTargetingContextAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}

/// <summary>
/// Gets <see cref="TargetingContext"/> from the current HTTP request.
/// </summary>
public ValueTask<TargetingContext> GetContextAsync()
{
HttpContext httpContext = _httpContextAccessor.HttpContext;

//
// Try cache lookup
if (httpContext.Items.TryGetValue(TargetingContextLookup, out object value))
{
return new ValueTask<TargetingContext>((TargetingContext)value);
}

//
// Treat user identity name as user id
ClaimsPrincipal user = httpContext.User;

string userId = user?.Identity?.Name;
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved

//
// Treat claims of type Role as groups
IEnumerable<string> 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>(targetingContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extensions used to add feature management functionality.
/// </summary>
public static class FeatureManagementBuilderExtensions
{
/// <summary>
/// Adds the <see cref="DefaultHttpTargetingContextAccessor"/> to be used for targeting and registers the targeting filter to the feature management system.
/// </summary>
/// <param name="builder">The <see cref="IFeatureManagementBuilder"/> used to customize feature management functionality.</param>
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
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<ITargetingContextAccessor, DefaultHttpTargetingContextAccessor>();
}
else
{
builder.Services.TryAddSingleton<ITargetingContextAccessor, DefaultHttpTargetingContextAccessor>();
}

builder.AddFeatureFilter<TargetingFilter>();

return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ public class TargetingHttpContextMiddleware
private readonly RequestDelegate _next;
private readonly ILogger _logger;

private const string TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId";

/// <summary>
/// Creates an instance of the TargetingHttpContextMiddleware
/// </summary>
Expand Down Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

<ItemGroup>
<ProjectReference Include="..\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj" />
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved
<ProjectReference Include="..\Microsoft.FeatureManagement.AspNetCore\Microsoft.FeatureManagement.AspNetCore.csproj" />
<ProjectReference Include="..\Microsoft.FeatureManagement.Telemetry.ApplicationInsights\Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -14,8 +15,6 @@ namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore
/// </summary>
public class TargetingTelemetryInitializer : TelemetryInitializerBase
{
private const string TargetingIdKey = $"Microsoft.FeatureManagement.TargetingId";

/// <summary>
/// Creates an instance of the TargetingTelemetryInitializer
/// </summary>
Expand All @@ -37,27 +36,23 @@ 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
TargetingContext targetingContext = httpContext.Items[DefaultHttpTargetingContextAccessor.TargetingContextLookup] as TargetingContext;
rossgrambo marked this conversation as resolved.
Show resolved Hide resolved

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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down