Skip to content

Commit 559d530

Browse files
authored
Merge pull request #296 from microsoft/main
Merge main to release/v3
2 parents bcbc5d7 + b6bade5 commit 559d530

8 files changed

+314
-47
lines changed

src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public IFeatureManagementBuilder AddFeatureFilter<T>() where T : IFeatureFilterM
3737

3838
if (!Services.Any(descriptor => descriptor.ServiceType == serviceType && descriptor.ImplementationType == implementationType))
3939
{
40-
Services.AddSingleton(typeof(IFeatureFilterMetadata), typeof(T));
40+
Services.AddSingleton(serviceType, implementationType);
4141
}
4242

4343
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using Microsoft.FeatureManagement.FeatureFilters;
6+
7+
namespace Microsoft.FeatureManagement
8+
{
9+
/// <summary>
10+
/// Extensions used to add feature management functionality.
11+
/// </summary>
12+
public static class FeatureManagementBuilderExtensions
13+
{
14+
/// <summary>
15+
/// Adds an <see cref="ITargetingContextAccessor"/> to be used for targeting and registers the targeting filter to the feature management system.
16+
/// </summary>
17+
/// <param name="builder">The <see cref="IFeatureManagementBuilder"/> used to customize feature management functionality.</param>
18+
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
19+
public static IFeatureManagementBuilder WithTargeting<T>(this IFeatureManagementBuilder builder) where T : ITargetingContextAccessor
20+
{
21+
builder.Services.TryAddSingleton(typeof(ITargetingContextAccessor), typeof(T));
22+
23+
builder.AddFeatureFilter<TargetingFilter>();
24+
25+
return builder;
26+
}
27+
}
28+
}

src/Microsoft.FeatureManagement/FeatureManager.cs

+69-35
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,17 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
141141
continue;
142142
}
143143

144-
IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name);
144+
IFeatureFilterMetadata filter;
145+
146+
if (useAppContext)
147+
{
148+
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, typeof(TContext)) ??
149+
GetFeatureFilterMetadata(featureFilterConfiguration.Name);
150+
}
151+
else
152+
{
153+
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name);
154+
}
145155

146156
if (filter == null)
147157
{
@@ -163,14 +173,14 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
163173
Parameters = featureFilterConfiguration.Parameters
164174
};
165175

176+
BindSettings(filter, context, filterIndex);
177+
166178
//
167179
// IContextualFeatureFilter
168180
if (useAppContext)
169181
{
170182
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext));
171183

172-
BindSettings(filter, context, filterIndex);
173-
174184
if (contextualFilter != null &&
175185
await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation)
176186
{
@@ -184,9 +194,8 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false)
184194
// IFeatureFilter
185195
if (filter is IFeatureFilter featureFilter)
186196
{
187-
BindSettings(filter, context, filterIndex);
188-
189-
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) {
197+
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation)
198+
{
190199
enabled = targetEvaluation;
191200

192201
break;
@@ -267,48 +276,39 @@ private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluation
267276
context.Settings = settings;
268277
}
269278

270-
private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName)
279+
private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName, Type appContextType = null)
271280
{
272-
const string filterSuffix = "filter";
273-
274281
IFeatureFilterMetadata filter = _filterMetadataCache.GetOrAdd(
275-
filterName,
282+
$"{filterName}{Environment.NewLine}{appContextType?.FullName}",
276283
(_) => {
277284

278285
IEnumerable<IFeatureFilterMetadata> matchingFilters = _featureFilters.Where(f =>
279286
{
280-
Type t = f.GetType();
281-
282-
string name = ((FilterAliasAttribute)Attribute.GetCustomAttribute(t, typeof(FilterAliasAttribute)))?.Alias;
287+
Type filterType = f.GetType();
283288

284-
if (name == null)
289+
if (!IsMatchingName(filterType, filterName))
285290
{
286-
name = t.Name.EndsWith(filterSuffix, StringComparison.OrdinalIgnoreCase) ? t.Name.Substring(0, t.Name.Length - filterSuffix.Length) : t.Name;
291+
return false;
287292
}
288293

289-
//
290-
// Feature filters can have namespaces in their alias
291-
// If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter'
292-
// If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter'
293-
if (filterName.Contains('.'))
294+
if (appContextType == null)
294295
{
295-
//
296-
// The configured filter name is namespaced. It must be an exact match.
297-
return string.Equals(name, filterName, StringComparison.OrdinalIgnoreCase);
296+
return (f is IFeatureFilter);
298297
}
299-
else
300-
{
301-
//
302-
// We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter'
303-
string simpleName = name.Contains('.') ? name.Split('.').Last() : name;
304298

305-
return string.Equals(simpleName, filterName, StringComparison.OrdinalIgnoreCase);
306-
}
299+
return ContextualFeatureFilterEvaluator.IsContextualFilter(f, appContextType);
307300
});
308301

309302
if (matchingFilters.Count() > 1)
310303
{
311-
throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple feature filters match the configured filter named '{filterName}'.");
304+
if (appContextType == null)
305+
{
306+
throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple feature filters match the configured filter named '{filterName}'.");
307+
}
308+
else
309+
{
310+
throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple contextual feature filters match the configured filter named '{filterName}' and context type '{appContextType}'.");
311+
}
312312
}
313313

314314
return matchingFilters.FirstOrDefault();
@@ -318,6 +318,37 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName)
318318
return filter;
319319
}
320320

321+
private bool IsMatchingName(Type filterType, string filterName)
322+
{
323+
const string filterSuffix = "filter";
324+
325+
string name = ((FilterAliasAttribute)Attribute.GetCustomAttribute(filterType, typeof(FilterAliasAttribute)))?.Alias;
326+
327+
if (name == null)
328+
{
329+
name = filterType.Name.EndsWith(filterSuffix, StringComparison.OrdinalIgnoreCase) ? filterType.Name.Substring(0, filterType.Name.Length - filterSuffix.Length) : filterType.Name;
330+
}
331+
332+
//
333+
// Feature filters can have namespaces in their alias
334+
// If a feature is configured to use a filter without a namespace such as 'MyFilter', then it can match 'MyOrg.MyProduct.MyFilter' or simply 'MyFilter'
335+
// If a feature is configured to use a filter with a namespace such as 'MyOrg.MyProduct.MyFilter' then it can only match 'MyOrg.MyProduct.MyFilter'
336+
if (filterName.Contains('.'))
337+
{
338+
//
339+
// The configured filter name is namespaced. It must be an exact match.
340+
return string.Equals(name, filterName, StringComparison.OrdinalIgnoreCase);
341+
}
342+
else
343+
{
344+
//
345+
// We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter'
346+
string simpleName = name.Contains('.') ? name.Split('.').Last() : name;
347+
348+
return string.Equals(simpleName, filterName, StringComparison.OrdinalIgnoreCase);
349+
}
350+
}
351+
321352
private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filterName, Type appContextType)
322353
{
323354
if (appContextType == null)
@@ -329,11 +360,14 @@ private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filte
329360
$"{filterName}{Environment.NewLine}{appContextType.FullName}",
330361
(_) => {
331362

332-
IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName);
363+
IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName, appContextType);
364+
365+
if (metadata == null)
366+
{
367+
return null;
368+
}
333369

334-
return ContextualFeatureFilterEvaluator.IsContextualFilter(metadata, appContextType) ?
335-
new ContextualFeatureFilterEvaluator(metadata, appContextType) :
336-
null;
370+
return new ContextualFeatureFilterEvaluator(metadata, appContextType);
337371
}
338372
);
339373

src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.DependencyInjection;
66
using Microsoft.Extensions.DependencyInjection.Extensions;
77
using Microsoft.Extensions.Logging;
8+
using Microsoft.FeatureManagement.FeatureFilters;
89
using System;
910

1011
namespace Microsoft.FeatureManagement
@@ -15,7 +16,7 @@ namespace Microsoft.FeatureManagement
1516
public static class ServiceCollectionExtensions
1617
{
1718
/// <summary>
18-
/// Adds required feature management services.
19+
/// Adds required feature management services and built-in feature filters.
1920
/// </summary>
2021
/// <param name="services">The service collection that feature management services are added to.</param>
2122
/// <returns>A <see cref="IFeatureManagementBuilder"/> that can be used to customize feature management functionality.</returns>
@@ -33,7 +34,17 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec
3334

3435
services.AddScoped<IFeatureManagerSnapshot, FeatureManagerSnapshot>();
3536

36-
return new FeatureManagementBuilder(services);
37+
var builder = new FeatureManagementBuilder(services);
38+
39+
//
40+
// Add built-in feature filters
41+
builder.AddFeatureFilter<PercentageFilter>();
42+
43+
builder.AddFeatureFilter<TimeWindowFilter>();
44+
45+
builder.AddFeatureFilter<ContextualTargetingFilter>();
46+
47+
return builder;
3748
}
3849

3950
/// <summary>

0 commit comments

Comments
 (0)