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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Feature management provides a way to develop and expose application functionalit

[**API reference**](https://go.microsoft.com/fwlink/?linkid=2091700): This API reference details the API surface of the libraries contained within this repository.

[**Release Notes**](https://github.com/Azure/AppConfiguration/blob/main/releaseNotes/Microsoft.Featuremanagement.md): The release notes for all versions of the feature management libraries can be found here.

## Examples

* [.NET Console App](./examples/ConsoleApp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.FeatureFilters;
namespace ParametersObjectConsoleApp
{
/// <summary>
/// A custom feature definition provider that supplies targeting filter settings
/// directly using the ParametersObject property, avoiding the need to construct IConfiguration.
/// </summary>
public class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider
{
private readonly Dictionary<string, FeatureDefinition> _featureDefinitions;

public InMemoryFeatureDefinitionProvider()
{
//
// Define feature flags with targeting settings.
// This demonstrates supplying filter parameters directly via ParametersObject
// instead of constructing an IConfiguration with key-value pairs.
_featureDefinitions = new Dictionary<string, FeatureDefinition>
{
["Beta"] = new FeatureDefinition
{
Name = "Beta",
EnabledFor = new List<FeatureFilterConfiguration>
{
new FeatureFilterConfiguration
{
Name = "Microsoft.Targeting",
ParametersObject = new TargetingFilterSettings
{
Audience = new Audience
{
Users = new List<string> { "Jeff", "Anne" },
Groups = new List<GroupRollout>
{
new GroupRollout
{
Name = "Management",
RolloutPercentage = 100
},
new GroupRollout
{
Name = "TeamMembers",
RolloutPercentage = 45
}
},
DefaultRolloutPercentage = 20
}
}
}
}
}
};
}

public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
{
_featureDefinitions.TryGetValue(featureName, out FeatureDefinition definition);

return Task.FromResult(definition);
}

public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
{
foreach (var definition in _featureDefinitions.Values)
{
yield return definition;
}

await Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions examples/ParametersObjectConsoleApp/Program.cs
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.FeatureManagement;
using Microsoft.FeatureManagement.FeatureFilters;
using ParametersObjectConsoleApp;

//
// Create a feature manager using a custom definition provider
// that supplies targeting filter settings directly via ParametersObject.
var featureManager = new FeatureManager(new InMemoryFeatureDefinitionProvider())
{
FeatureFilters = new List<IFeatureFilterMetadata> { new ContextualTargetingFilter() }
};

//
// Simulate evaluating the "Beta" feature for different users
var users = new List<(string UserId, List<string> Groups)>
{
("Jeff", new List<string> { "TeamMembers" }), // Targeted by user name
("Anne", new List<string> { "Management" }), // Targeted by user name
("Sam", new List<string> { "Management" }), // Targeted by group (100% rollout)
("Alice", new List<string> { "TeamMembers" }), // May be targeted by group (45% rollout)
("Bob", new List<string> { "External" }) // Only targeted by default rollout (20%)
};

foreach (var (userId, groups) in users)
{
const string FeatureName = "Beta";

var targetingContext = new TargetingContext
{
UserId = userId,
Groups = groups
};

bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext);

Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the user '{userId}'.");
}
33 changes: 33 additions & 0 deletions examples/ParametersObjectConsoleApp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ParametersObject Console App

This example demonstrates how to use the `ParametersObject` property on `FeatureFilterConfiguration` to supply filter settings directly from a custom `IFeatureDefinitionProvider`.

## Overview

When implementing a custom `IFeatureDefinitionProvider` that sources feature definitions from alternative backends (e.g., databases, REST APIs), you no longer need to construct an `IConfiguration` object to pass filter parameters. Instead, you can assign settings like `TargetingFilterSettings` directly to `ParametersObject`.

## Key Concept

```csharp
new FeatureFilterConfiguration
{
Name = "Microsoft.Targeting",
ParametersObject = new TargetingFilterSettings
{
Audience = new Audience
{
Users = new List<string> { "Jeff", "Anne" },
Groups = new List<GroupRollout> { ... },
DefaultRolloutPercentage = 20
}
}
}
```

This eliminates the need for verbose `IConfiguration` construction with magic string keys.

## Running

```
dotnet run --project examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj
```
7 changes: 4 additions & 3 deletions examples/VariantServiceDemo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
builder.Services.AddApplicationInsightsTelemetry();

//
// Add variant implementations of ICalculator
builder.Services.AddSingleton<ICalculator, DefaultCalculator>();
// Add variant implementations of ICalculator using keyed services so that only the
// implementation matching the assigned variant is instantiated on demand.
builder.Services.AddKeyedSingleton<ICalculator, DefaultCalculator>("DefaultCalculator");

builder.Services.AddSingleton<ICalculator, RemoteCalculator>();
builder.Services.AddKeyedSingleton<ICalculator, RemoteCalculator>("RemoteCalculator");

//
// Enter feature management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Official Version -->
<PropertyGroup>
<MajorVersion>4</MajorVersion>
<MinorVersion>5</MinorVersion>
<MinorVersion>6</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<!-- Official Version -->
<PropertyGroup>
<MajorVersion>4</MajorVersion>
<MinorVersion>5</MinorVersion>
<MinorVersion>6</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ public static IFeatureManagementBuilder WithVariantService<TService>(this IFeatu
builder.Services.AddScoped<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
sp));
}
else
{
builder.Services.AddSingleton<IVariantServiceProvider<TService>>(sp => new VariantServiceProvider<TService>(
featureName,
sp.GetRequiredService<IVariantFeatureManager>(),
sp.GetRequiredService<IEnumerable<TService>>()));
sp));
}

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Official Version -->
<PropertyGroup>
<MajorVersion>4</MajorVersion>
<MinorVersion>5</MinorVersion>
<MinorVersion>6</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>

Expand Down
40 changes: 30 additions & 10 deletions src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand All @@ -16,7 +17,7 @@ namespace Microsoft.FeatureManagement
/// </summary>
internal class VariantServiceProvider<TService> : IVariantServiceProvider<TService> where TService : class
{
private readonly IEnumerable<TService> _services;
private readonly IServiceProvider _serviceProvider;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly ConcurrentDictionary<string, TService> _variantServiceCache;
Expand All @@ -26,15 +27,15 @@ internal class VariantServiceProvider<TService> : IVariantServiceProvider<TServi
/// </summary>
/// <param name="featureName">The feature flag that should be used to determine which variant of the service should be used.</param>
/// <param name="featureManager">The feature manager to get the assigned variant of the feature flag.</param>
/// <param name="services">Implementation variants of TService.</param>
/// <param name="serviceProvider">The service provider used to resolve implementation variants of TService. If it implements <see cref="IKeyedServiceProvider"/>, keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureName"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="featureManager"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="services"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable<TService> services)
/// <exception cref="ArgumentNullException">Thrown if <paramref name="serviceProvider"/> is null.</exception>
public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider)
{
_featureName = featureName ?? throw new ArgumentNullException(nameof(featureName));
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
_services = services ?? throw new ArgumentNullException(nameof(services));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_variantServiceCache = new ConcurrentDictionary<string, TService>();
}

Expand All @@ -55,16 +56,35 @@ public async ValueTask<TService> GetServiceAsync(CancellationToken cancellationT
{
implementation = _variantServiceCache.GetOrAdd(
variant.Name,
(_) => _services.FirstOrDefault(
service => IsMatchingVariantName(
service.GetType(),
variant.Name))
);
(variantName) => ResolveVariantService(variantName));
}

return implementation;
}

private TService ResolveVariantService(string variantName)
{
//
// If the service provider supports keyed services, try to resolve the variant by its name as the key first.
// This allows lazy instantiation of the variant service.
if (_serviceProvider is IKeyedServiceProvider)
{
TService keyedService = _serviceProvider.GetKeyedService<TService>(variantName);

if (keyedService != null)
{
return keyedService;
}
}

//
// Fall back to enumerating all non-keyed registrations of TService and matching by VariantServiceAliasAttribute or the implementation type name.
IEnumerable<TService> services = _serviceProvider.GetRequiredService<IEnumerable<TService>>();

return services.FirstOrDefault(
service => IsMatchingVariantName(service.GetType(), variantName));
}

private bool IsMatchingVariantName(Type implementationType, string variantName)
{
string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias;
Expand Down
Loading
Loading