-
Notifications
You must be signed in to change notification settings - Fork 127
Allow different type of filters to share the same Alias #262
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
Changes from 6 commits
15d6a07
eb412f4
745ea3e
b5a10f6
8483c5e
c4192a5
6082e9f
b0bd5df
f57bd7f
d39de05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,8 +25,8 @@ class FeatureManager : IFeatureManager, IDisposable | |||||
| private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters; | ||||||
| private readonly IEnumerable<ISessionManager> _sessionManagers; | ||||||
| private readonly ILogger _logger; | ||||||
| private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache; | ||||||
| private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache; | ||||||
| private readonly ConcurrentDictionary<ValueTuple<string, Type>, IFeatureFilterMetadata> _filterMetadataCache; | ||||||
| private readonly ConcurrentDictionary<ValueTuple<string, Type>, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache; | ||||||
| private readonly FeatureManagementOptions _options; | ||||||
| private readonly IMemoryCache _parametersCache; | ||||||
|
|
||||||
|
|
@@ -48,8 +48,8 @@ public FeatureManager( | |||||
| _featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters)); | ||||||
| _sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers)); | ||||||
| _logger = loggerFactory.CreateLogger<FeatureManager>(); | ||||||
| _filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>(); | ||||||
| _contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>(); | ||||||
| _filterMetadataCache = new ConcurrentDictionary<ValueTuple<string, Type>, IFeatureFilterMetadata>(); | ||||||
| _contextualFeatureFilterCache = new ConcurrentDictionary<ValueTuple<string, Type>, ContextualFeatureFilterEvaluator>(); | ||||||
| _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); | ||||||
| _parametersCache = new MemoryCache(new MemoryCacheOptions()); | ||||||
| } | ||||||
|
|
@@ -141,7 +141,17 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |||||
| continue; | ||||||
| } | ||||||
|
|
||||||
| IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); | ||||||
| IFeatureFilterMetadata filter; | ||||||
|
|
||||||
| if (useAppContext) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're now saying a call to Just use contextual filter if its defined for the context, otherwise use the no contextual filter.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Context may be null. I think we still need to distinguish the case that users intend to pass null as the context. |
||||||
| { | ||||||
| filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, typeof(TContext)) ?? | ||||||
| GetFeatureFilterMetadata(featureFilterConfiguration.Name, null); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, null); | ||||||
| } | ||||||
|
|
||||||
| if (filter == null) | ||||||
| { | ||||||
|
|
@@ -165,7 +175,7 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |||||
|
|
||||||
| // | ||||||
| // IContextualFeatureFilter | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
At this point we should already know whether the filter is a contextual filter or not by whether filter was populated with or without an app context. |
||||||
| if (useAppContext) | ||||||
| if (useAppContext && ContextualFeatureFilterEvaluator.IsContextualFilter(filter, typeof(TContext))) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a staic method IsContextualFilter() of ContextualFeatureFilterEvaluator. |
||||||
| { | ||||||
| ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext)); | ||||||
|
|
||||||
|
|
@@ -267,48 +277,72 @@ private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluation | |||||
| context.Settings = settings; | ||||||
| } | ||||||
|
|
||||||
| private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) | ||||||
| private bool IsFilterNameMatched(Type filterType, string filterName) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs to move after the private methods that call it to preserve consistent scroll-flow.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see an update to the name |
||||||
| { | ||||||
| const string filterSuffix = "filter"; | ||||||
|
|
||||||
| string name = ((FilterAliasAttribute)Attribute.GetCustomAttribute(filterType, typeof(FilterAliasAttribute)))?.Alias; | ||||||
|
|
||||||
| if (name == null) | ||||||
| { | ||||||
| name = filterType.Name.EndsWith(filterSuffix, StringComparison.OrdinalIgnoreCase) ? filterType.Name.Substring(0, filterType.Name.Length - filterSuffix.Length) : filterType.Name; | ||||||
| } | ||||||
|
|
||||||
| // | ||||||
| // Feature filters can have namespaces in their alias | ||||||
| // 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' | ||||||
| // 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' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, what if there is a filter
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. If the configuration of a feature flag contains any filters with a namespace, the feature manager will try to find a registered filter whose alias is the same as the filter name in the feature flag configuration. |
||||||
| if (filterName.Contains('.')) | ||||||
| { | ||||||
| // | ||||||
| // The configured filter name is namespaced. It must be an exact match. | ||||||
| return string.Equals(name, filterName, StringComparison.OrdinalIgnoreCase); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| // | ||||||
| // We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter' | ||||||
| string simpleName = name.Contains('.') ? name.Split('.').Last() : name; | ||||||
|
|
||||||
| return string.Equals(simpleName, filterName, StringComparison.OrdinalIgnoreCase); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName, Type appContextType) | ||||||
|
zhiyuanliang-ms marked this conversation as resolved.
Outdated
|
||||||
| { | ||||||
| IFeatureFilterMetadata filter = _filterMetadataCache.GetOrAdd( | ||||||
| filterName, | ||||||
| (filterName, appContextType), | ||||||
| (_) => { | ||||||
|
|
||||||
| IEnumerable<IFeatureFilterMetadata> matchingFilters = _featureFilters.Where(f => | ||||||
| { | ||||||
| Type t = f.GetType(); | ||||||
| Type filterType = f.GetType(); | ||||||
|
|
||||||
|
zhiyuanliang-ms marked this conversation as resolved.
|
||||||
| string name = ((FilterAliasAttribute)Attribute.GetCustomAttribute(t, typeof(FilterAliasAttribute)))?.Alias; | ||||||
|
|
||||||
| if (name == null) | ||||||
| if (appContextType == null) | ||||||
| { | ||||||
| name = t.Name.EndsWith(filterSuffix, StringComparison.OrdinalIgnoreCase) ? t.Name.Substring(0, t.Name.Length - filterSuffix.Length) : t.Name; | ||||||
| } | ||||||
|
|
||||||
| // | ||||||
| // Feature filters can have namespaces in their alias | ||||||
| // 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' | ||||||
| // 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' | ||||||
| if (filterName.Contains('.')) | ||||||
| { | ||||||
| // | ||||||
| // The configured filter name is namespaced. It must be an exact match. | ||||||
| return string.Equals(name, filterName, StringComparison.OrdinalIgnoreCase); | ||||||
| return (f is IFeatureFilter) && IsFilterNameMatched(filterType, filterName); | ||||||
| } | ||||||
| else | ||||||
|
zhiyuanliang-ms marked this conversation as resolved.
Outdated
|
||||||
| { | ||||||
| // | ||||||
| // We take the simple name of a filter, E.g. 'MyFilter' for 'MyOrg.MyProduct.MyFilter' | ||||||
| string simpleName = name.Contains('.') ? name.Split('.').Last() : name; | ||||||
| IEnumerable<Type> contextualFilterInterfaces = filterType.GetInterfaces().Where( | ||||||
| i => i.IsGenericType && | ||||||
| i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureFilter<>)) && | ||||||
| i.GetGenericArguments()[0].IsAssignableFrom(appContextType)); | ||||||
|
|
||||||
| return string.Equals(simpleName, filterName, StringComparison.OrdinalIgnoreCase); | ||||||
| return contextualFilterInterfaces.Any() && IsFilterNameMatched(filterType, filterName); | ||||||
|
zhiyuanliang-ms marked this conversation as resolved.
Outdated
|
||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| if (matchingFilters.Count() > 1) | ||||||
| { | ||||||
| throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple feature filters match the configured filter named '{filterName}'."); | ||||||
| if (appContextType == null) | ||||||
| { | ||||||
| throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple feature filters match the configured filter named '{filterName}'."); | ||||||
| } | ||||||
| else | ||||||
| { | ||||||
| throw new FeatureManagementException(FeatureManagementError.AmbiguousFeatureFilter, $"Multiple contextual feature filters match the configured filter named '{filterName}' and context type '{appContextType}'."); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return matchingFilters.FirstOrDefault(); | ||||||
|
|
@@ -326,14 +360,12 @@ private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filte | |||||
| } | ||||||
|
|
||||||
| ContextualFeatureFilterEvaluator filter = _contextualFeatureFilterCache.GetOrAdd( | ||||||
| $"{filterName}{Environment.NewLine}{appContextType.FullName}", | ||||||
| (filterName, appContextType), | ||||||
| (_) => { | ||||||
|
|
||||||
| IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName); | ||||||
| IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName, appContextType); | ||||||
|
|
||||||
| return ContextualFeatureFilterEvaluator.IsContextualFilter(metadata, appContextType) ? | ||||||
| new ContextualFeatureFilterEvaluator(metadata, appContextType) : | ||||||
| null; | ||||||
| return new ContextualFeatureFilterEvaluator(metadata, appContextType); | ||||||
| } | ||||||
| ); | ||||||
|
|
||||||
|
|
||||||
| 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.FeatureManagement; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Tests.FeatureManagement | ||
| { | ||
| interface IDummyContext | ||
| { | ||
| string DummyProperty { get; set; } | ||
| } | ||
|
|
||
| class DummyContext : IDummyContext | ||
| { | ||
| public string DummyProperty { get; set; } | ||
| } | ||
|
|
||
| [FilterAlias(Alias)] | ||
| class DuplicatedAliasFeatureFilter1 : IFeatureFilter | ||
| { | ||
| private const string Alias = "DuplicatedFilterName"; | ||
|
|
||
| public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context) | ||
| { | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| [FilterAlias(Alias)] | ||
| class DuplicatedAliasFeatureFilter2 : IFeatureFilter | ||
| { | ||
| private const string Alias = "DuplicatedFilterName"; | ||
|
|
||
| public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context) | ||
| { | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| [FilterAlias(Alias)] | ||
| class ContextualDuplicatedAliasFeatureFilterWithAccountContext : IContextualFeatureFilter<IAccountContext> | ||
| { | ||
| private const string Alias = "DuplicatedFilterName"; | ||
|
|
||
| public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, IAccountContext accountContext) | ||
| { | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| [FilterAlias(Alias)] | ||
| class ContextualDuplicatedAliasFeatureFilterWithDummyContext1 : IContextualFeatureFilter<IDummyContext> | ||
| { | ||
| private const string Alias = "DuplicatedFilterName"; | ||
|
|
||
| public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, IDummyContext dummyContext) | ||
| { | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| [FilterAlias(Alias)] | ||
| class ContextualDuplicatedAliasFeatureFilterWithDummyContext2 : IContextualFeatureFilter<IDummyContext> | ||
| { | ||
| private const string Alias = "DuplicatedFilterName"; | ||
|
|
||
| public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, IDummyContext dummyContext) | ||
| { | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.