Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Microsoft.FeatureManagement.sln
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EvaluationDataToApplication
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore", "src\Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore\Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore.csproj", "{C647611B-A8E5-4AD7-9DBA-60DDE276644B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorServerApp", "examples\BlazorServerApp\BlazorServerApp.csproj", "{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerApp", "examples\BlazorServerApp\BlazorServerApp.csproj", "{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VariantServiceDemo", "examples\VariantServiceDemo\VariantServiceDemo.csproj", "{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -85,6 +87,10 @@ Global
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E}.Release|Any CPU.Build.0 = Release|Any CPU
{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -98,6 +104,7 @@ Global
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{1502529E-47E9-4306-98C4-BF6CF7C7C275} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{12BAB5A6-4EEB-4917-B5D9-4AFB6253008E} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{E8E17CB9-434E-4386-BF96-FA53BBFDCD6F} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Feature management provides a way to develop and expose application functionalit
* [ASP.NET Core Web App (MVC)](./examples/FeatureFlagDemo)
* [Blazor Server App](./examples/BlazorServerApp)
* [ASP.NET Core Web App with Feature Flag Telemetry](./examples/EvaluationDataToApplicationInsights)
* [ASP.NET Core Web App with Variant Service](./examples/VariantServiceDemo)

## Contributing

Expand Down
53 changes: 32 additions & 21 deletions examples/BlazorServerApp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,44 @@ This app demonstrates how to use the Feature Management library in Blazor apps.
This app uses two feature flags: "BrowserEnhancement" and "Beta".

``` json
"FeatureManagement": {
"BrowserEnhancement": {
"EnabledFor": [
{
"Name": "Browser",
"Parameters": {
"AllowedBrowsers": [ "Edge" ]
}
"feature_management": {
"feature_flags": [
{
"id": "BrowserEnhancement",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Browser",
"parameters": {
"AllowedBrowsers": [ "Edge" ]
}
}
]
}
},
"Beta": {
"EnabledFor": [
{
"Name": "Targeting",
"Parameters": {
"Audience": {
"DefaultRolloutPercentage": 50,
"Exclusion": {
"Groups": [
"Guests"
]
}
}
{
"id": "Beta",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "Targeting",
"parameters": {
"Audience": {
"DefaultRolloutPercentage": 50,
"Exclusion": {
"Groups": [
"Guests"
]
}
}
}
}
]
}
}
]
}
```

Expand All @@ -65,6 +75,7 @@ This app uses [cookie authentication](https://learn.microsoft.com/en-us/aspnet/c
Rather than `HttpContext`, the [`AuthenticationStateProvider`](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-8.0#authenticationstateprovider-service) service is used to obtain the user authentication state information for setting targeting context. The details can be found in the [`MyTargetingContextAccessor`](./MyTargetingContextAccessor.cs).

## Service Registration

Blazor applications like this one typically pull ambient contextual data from scoped services. For example, the `UserAgentContext`, `AuthenticationStateProvider` and `ITargetingContextAccessor` are all scoped services. This pattern *breaks* if the feature management services are added as singleton, which is typical in non-blazor web apps.

In Blazor, *avoid* the following
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
@using System.Security.Claims
@using Microsoft.AspNetCore.Http

@inject IHttpContextAccessor httpContextAccessor
@{
string username = httpContextAccessor.HttpContext.Request.Cookies["username"];
}

<!DOCTYPE html>
<html lang="en">
<head>
Expand Down
4 changes: 2 additions & 2 deletions examples/EvaluationDataToApplicationInsights/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.FeatureManagement;
using EvaluationDataToApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Telemetry.ApplicationInsights.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -55,7 +55,7 @@
app.MapRazorPages();

//
// Adds Targeting Id to HttpContext
// Add Targeting Id to HttpContext
app.UseMiddleware<TargetingHttpContextMiddleware>();

app.Run();
4 changes: 4 additions & 0 deletions examples/EvaluationDataToApplicationInsights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ These logs show what would be emitted to a connected Application Insights resour
To flow these events to Application Insights, [setup a new Application Insights resource in Azure](https://learn.microsoft.com/en-us/azure/azure-monitor/app/create-workspace-resource). Once setup, from `Overview` copy the `Connection String` and place it in `appsettings.json` at ApplicationInsights > ConnectionString. After restarting the app, events should now flow to Application Insights. This [document](https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core?tabs=netcorenew%2Cnetcore6#enable-application-insights-server-side-telemetry-no-visual-studio) provides more details on connecting a .NET application to Application Insights.

## About the App

This app uses [Application Insights for ASP.NET Core](https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core?tabs=netcorenew%2Cnetcore6). This means there is an App Insights SDK in the C# code and a separate App Insights SDK the Javascript. Lets cover what they're doing:

### Javascript App Insights SDK

See [Enable cliend-side telemetry for web applications](https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core?tabs=netcorenew%2Cnetcore6#enable-client-side-telemetry-for-web-applications)

For ASP.NET, this is added to _ViewImports.cshtml
Expand All @@ -47,11 +49,13 @@ These cookies are used to correlate telemetry from the browser with telemetry fr
*The Javascript SDK is not required, but is useful for collecting browser telemetry and generating these cookies out of the box.*

### Targeting Id

In order to connect evaluation events with other metrics from the user, a targeting id needs to be emitted. This can be done multiple ways, but the recommended way is to define a telemetry initializer. This initializer allows the app to modify all telemetry going to Application Insights before it's sent.

This example uses the provided `TargetingHttpContextMiddleware` and `TargetingTelemetryInitializer`. The middleware adds `TargetingId` (using the targeting context accessor) to the HTTP Context as a request comes in. The initializer checks for the `TargetingId` on the HTTP Context, and if it exists, adds `TargetingId` to all outgoing Application Insights Telemetry.

## Sample App Usage

Sample steps to try out the app:

1. Run the app. When the app is first started a User Id and Session Id will be generated. The username cookie will be set to a random integer, and the ai_user and ai_session cookies will be expired.
Expand Down
16 changes: 16 additions & 0 deletions examples/VariantServiceDemo/DefaultCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.FeatureManagement;

namespace VariantServiceDemo
{
[VariantServiceAlias("DefaultCalculator")]
public class DefaultCalculator : ICalculator
{
public Task<double> AddAsync(double a, double b)
{
return Task.FromResult(a + b);
}
}
}
53 changes: 53 additions & 0 deletions examples/VariantServiceDemo/HttpContextTargetingContextAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.FeatureManagement.FeatureFilters;

namespace VariantServiceDemo
{
/// <summary>
/// Provides an implementation of <see cref="ITargetingContextAccessor"/> that creates a targeting context using info from the current HTTP request.
/// </summary>
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<TargetingContext> GetContextAsync()
{
HttpContext httpContext = _httpContextAccessor.HttpContext;

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

//
// Grab username from cookie
string username = httpContext.Request.Cookies["username"];

var groups = new List<string>();

//
// Build targeting context based on user info
var targetingContext = new TargetingContext
{
UserId = username,
Groups = groups
};

//
// Cache for subsequent lookup
httpContext.Items[TargetingContextLookup] = targetingContext;

return new ValueTask<TargetingContext>(targetingContext);
}
}
}
10 changes: 10 additions & 0 deletions examples/VariantServiceDemo/ICalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace VariantServiceDemo
{
public interface ICalculator
{
public Task<double> AddAsync(double a, double b);
}
}
26 changes: 26 additions & 0 deletions examples/VariantServiceDemo/Pages/Error.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
28 changes: 28 additions & 0 deletions examples/VariantServiceDemo/Pages/Error.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;

namespace VariantServiceDemo.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

private readonly ILogger<ErrorModel> _logger;

public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}

public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

}
47 changes: 47 additions & 0 deletions examples/VariantServiceDemo/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@page
@model IndexModel
@inject IHttpContextAccessor httpContextAccessor
@{
ViewData["Title"] = "Home page";
}

<div class="text-center">
<h2> Current user @Model.Username </h2>
</div>

<calculator>
<input id="inputA" type="number" value="0"> +
<input id="inputB" type="number" value="0"> =
<span id="result">0</span>
<div>
<button id="calculateButton" onclick="calculate()">Calculate</button>
</div>
</calculator>

@section Scripts {
<script>
function calculate() {
var a = document.getElementById('inputA').value;
var b = document.getElementById('inputB').value;

var calculateButton = document.getElementById('calculateButton');
calculateButton.textContent = 'Calculating...';
calculateButton.disabled = true;

fetch(`/Index?handler=Calculate&a=${a}&b=${b}`)
.then(response => {
return response.json()
})
.then(data => {
document.getElementById('result').innerText = data;
})
.catch(error => {
console.error('Error:', error)
})
.finally(() => {
calculateButton.textContent = 'Calculate';
calculateButton.disabled = false;
});
}
</script>
}
40 changes: 40 additions & 0 deletions examples/VariantServiceDemo/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.FeatureManagement;

namespace VariantServiceDemo.Pages
{
public class IndexModel : PageModel
{
private readonly IVariantServiceProvider<ICalculator> _calculatorProvider;

public string Username { get; set; }

public IndexModel(IVariantServiceProvider<ICalculator> calculatorProvider)
{
_calculatorProvider = calculatorProvider ?? throw new ArgumentNullException(nameof(calculatorProvider));
}

public IActionResult OnGet()
{
//
// generate a new visitor
string visitor = Random.Shared.Next().ToString();

Response.Cookies.Append("username", visitor);

Username = visitor;

return Page();
}

public async Task<JsonResult> OnGetCalculate(double a, double b)
{
ICalculator calculator = await _calculatorProvider.GetServiceAsync(HttpContext.RequestAborted);

double result = await calculator.AddAsync(a, b);

return new JsonResult(result);
}
}
}
Loading