-
Notifications
You must be signed in to change notification settings - Fork 115
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
Allow different type of filters to share the same Alias #262
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
15d6a07
allow dup alias & target on main branch
zhiyuanliang-ms eb412f4
fix bug: non-contextual filter will not be found when there is context
zhiyuanliang-ms 745ea3e
keep using ContextualFeatureFilterEvaluator.IsContextualFilter
zhiyuanliang-ms b5a10f6
fix bug
zhiyuanliang-ms 8483c5e
Merge branch 'main' into zhiyuanliang/allow-dup-alias
zhiyuanliang-ms c4192a5
use typeof & avoid using repeated code
zhiyuanliang-ms 6082e9f
resolve comments & add testcase
zhiyuanliang-ms b0bd5df
resolve comments
zhiyuanliang-ms f57bd7f
remove unnecessary '?'
zhiyuanliang-ms d39de05
use var
zhiyuanliang-ms File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -141,7 +141,17 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |
continue; | ||
} | ||
|
||
IFeatureFilterMetadata filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); | ||
IFeatureFilterMetadata filter; | ||
|
||
if (useAppContext) | ||
{ | ||
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name, typeof(TContext)) ?? | ||
GetFeatureFilterMetadata(featureFilterConfiguration.Name); | ||
} | ||
else | ||
{ | ||
filter = GetFeatureFilterMetadata(featureFilterConfiguration.Name); | ||
} | ||
|
||
if (filter == null) | ||
{ | ||
|
@@ -163,14 +173,14 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo | |
Parameters = featureFilterConfiguration.Parameters | ||
}; | ||
|
||
BindSettings(filter, context, filterIndex); | ||
|
||
// | ||
// IContextualFeatureFilter | ||
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) | ||
{ | ||
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext)); | ||
|
||
BindSettings(filter, context, filterIndex); | ||
|
||
if (contextualFilter != null && | ||
await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation) | ||
{ | ||
|
@@ -184,9 +194,8 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) | |
// IFeatureFilter | ||
if (filter is IFeatureFilter featureFilter) | ||
{ | ||
BindSettings(filter, context, filterIndex); | ||
|
||
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) { | ||
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) | ||
{ | ||
enabled = targetEvaluation; | ||
|
||
break; | ||
|
@@ -267,48 +276,39 @@ private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluation | |
context.Settings = settings; | ||
} | ||
|
||
private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) | ||
private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName, Type appContextType = null) | ||
{ | ||
const string filterSuffix = "filter"; | ||
|
||
IFeatureFilterMetadata filter = _filterMetadataCache.GetOrAdd( | ||
filterName, | ||
$"{filterName}{Environment.NewLine}{appContextType?.FullName}", | ||
(_) => { | ||
|
||
IEnumerable<IFeatureFilterMetadata> matchingFilters = _featureFilters.Where(f => | ||
{ | ||
Type t = f.GetType(); | ||
|
||
string name = ((FilterAliasAttribute)Attribute.GetCustomAttribute(t, typeof(FilterAliasAttribute)))?.Alias; | ||
Type filterType = f.GetType(); | ||
|
||
zhiyuanliang-ms marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (name == null) | ||
if (!IsMatchingName(filterType, filterName)) | ||
{ | ||
name = t.Name.EndsWith(filterSuffix, StringComparison.OrdinalIgnoreCase) ? t.Name.Substring(0, t.Name.Length - filterSuffix.Length) : t.Name; | ||
return false; | ||
} | ||
|
||
// | ||
// 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('.')) | ||
if (appContextType == null) | ||
{ | ||
// | ||
// The configured filter name is namespaced. It must be an exact match. | ||
return string.Equals(name, filterName, StringComparison.OrdinalIgnoreCase); | ||
return (f is IFeatureFilter); | ||
} | ||
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); | ||
} | ||
return ContextualFeatureFilterEvaluator.IsContextualFilter(f, appContextType); | ||
}); | ||
|
||
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(); | ||
|
@@ -318,6 +318,37 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName) | |
return filter; | ||
} | ||
|
||
private bool IsMatchingName(Type filterType, string filterName) | ||
{ | ||
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' | ||
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 ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filterName, Type appContextType) | ||
{ | ||
if (appContextType == null) | ||
|
@@ -329,11 +360,14 @@ private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filte | |
$"{filterName}{Environment.NewLine}{appContextType.FullName}", | ||
(_) => { | ||
|
||
IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName); | ||
IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName, appContextType); | ||
|
||
if (metadata == null) | ||
{ | ||
return null; | ||
} | ||
|
||
return ContextualFeatureFilterEvaluator.IsContextualFilter(metadata, appContextType) ? | ||
new ContextualFeatureFilterEvaluator(metadata, appContextType) : | ||
null; | ||
return new ContextualFeatureFilterEvaluator(metadata, appContextType); | ||
} | ||
); | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
tests/Tests.FeatureManagement/FiltersWithDuplicatedAlias.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're now saying a call to
IsEnabledAsync("name", Context);
will use "name" even if it has no context, then I don't think theuseAppContext
bool needs to exist anymore.Just use contextual filter if its defined for the context, otherwise use the no contextual filter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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.