Skip to content
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

Support variant for FeatureTagHelper #407

Merged
merged 13 commits into from
May 19, 2024
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,31 +248,53 @@ public interface IDisabledFeaturesHandler

### View

In MVC views `<feature>` tags can be used to conditionally render content based on whether a feature is enabled or not.
In MVC views `<feature>` tags can be used to conditionally render content based on whether a feature is enabled or whether specific variant of a feature is assigned. For more information about variants, please refer to the [variants](./README.md#Variants) section.

``` HTML+Razor
<feature name="FeatureX">
<p>This can only be seen if 'FeatureX' is enabled.</p>
</feature>
```

You can also negate the tag helper evaluation to display content when a feature or set of features are disabled. By setting `negate="true"` in the example below, the content is only rendered if `FeatureX` is disabled.
``` HTML+Razor
<feature name="FeatureX" variant="Alpha">
<p>This can only be seen if variant 'Alpha' of 'FeatureX' is assigned.</p>
</feature>
```

You can also negate the tag helper evaluation to display content when a feature is disabled or when a variant is not assigned, by setting `negate="true"`.

``` HTML+Razor
<feature negate="true" name="FeatureX">
<p>This can only be seen if 'FeatureX' is disabled.</p>
</feature>
```

The `<feature>` tag can reference multiple features by specifying a comma separated list of features in the `name` attribute.
``` HTML+Razor
<feature negate="true" name="FeatureX" variant="Alpha">
<p>This can only be seen if variant 'Alpha' of 'FeatureX' is not assigned.</p>
</feature>
```

The `<feature>` tag can reference multiple features/variants by specifying a comma separated list of features/variants in the `name`/`variant` attribute.

``` HTML+Razor
<feature name="FeatureX,FeatureY">
<p>This can only be seen if 'FeatureX' and 'FeatureY' are enabled.</p>
</feature>
```

By default, all listed features must be enabled for the feature tag to be rendered. This behavior can be overidden by adding the `requirement` attribute as seen in the example below.
``` HTML+Razor
<feature name="FeatureX" variant="Alpha,Beta">
<p>This can only be seen if variant 'Alpha' or 'Beta' of 'FeatureX' is assigned.</p>
</feature>
```

**Note:** if `variant` is specified, only *one* feature should be specified.
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved

By default, all listed features must be enabled for the feature tag to be rendered. This behavior can be overridden by adding the `requirement` attribute as seen in the example below.

**Note:** If a `requirement` of `And` is used in conjunction with `variant` an error will be thrown, as multiple variants can never be assigned.

``` HTML+Razor
<feature name="FeatureX,FeatureY" requirement="Any">
Expand All @@ -281,6 +303,7 @@ By default, all listed features must be enabled for the feature tag to be render
```

The `<feature>` tag requires a tag helper to work. This can be done by adding the feature management tag helper to the _ViewImports.cshtml_ file.

``` HTML+Razor
@addTagHelper *, Microsoft.FeatureManagement.AspNetCore
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.
//
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -13,7 +14,7 @@ namespace Microsoft.FeatureManagement.Mvc.TagHelpers
/// </summary>
public class FeatureTagHelper : TagHelper
{
private readonly IFeatureManager _featureManager;
private readonly IVariantFeatureManager _featureManager;

/// <summary>
/// A feature name, or comma separated list of feature names, for which the content should be rendered. By default, all specified features must be enabled to render the content, but this requirement can be controlled by the <see cref="Requirement"/> property.
Expand All @@ -30,13 +31,19 @@ public class FeatureTagHelper : TagHelper
/// </summary>
public bool Negate { get; set; }

/// <summary>
/// A variant name, or comma separated list of variant names. If any of specified variants is assigned, the content should be rendered.
/// If variant is specified, <see cref="Name"/> must contain only one feature name and <see cref="Requirement"/> will have no effect.
/// </summary>
public string Variant { get; set; }

/// <summary>
/// Creates a feature tag helper.
/// </summary>
/// <param name="featureManager">The feature manager snapshot to use to evaluate feature state.</param>
public FeatureTagHelper(IFeatureManagerSnapshot featureManager)
public FeatureTagHelper(IVariantFeatureManagerSnapshot featureManager)
{
_featureManager = featureManager;
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
}

/// <summary>
Expand All @@ -52,11 +59,35 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu

if (!string.IsNullOrEmpty(Name))
{
IEnumerable<string> names = Name.Split(',').Select(n => n.Trim());
IEnumerable<string> features = Name.Split(',').Select(n => n.Trim());

if (string.IsNullOrEmpty(Variant))
{
enabled = Requirement == RequirementType.All ?
await features.All(async feature => await _featureManager.IsEnabledAsync(feature).ConfigureAwait(false)) :
await features.Any(async feature => await _featureManager.IsEnabledAsync(feature).ConfigureAwait(false));
}
else
{
if (features.Count() != 1)
{
throw new ArgumentException("Variant cannot be associated with multiple feature flags.", nameof(Name));
}

IEnumerable<string> variants = Variant.Split(',').Select(n => n.Trim());

if (variants.Count() != 1 && Requirement == RequirementType.All)
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
{
throw new ArgumentException("Requirement must be Any when there are multiple variants.", nameof(Requirement));
}

enabled = await variants.Any(
async variant => {
Variant assignedVariant = await _featureManager.GetVariantAsync(features.First()).ConfigureAwait(false);

enabled = Requirement == RequirementType.All ?
await names.All(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false)) :
await names.Any(async n => await _featureManager.IsEnabledAsync(n).ConfigureAwait(false));
return variant == assignedVariant?.Name;
});
}
zhiyuanliang-ms marked this conversation as resolved.
Show resolved Hide resolved
}

if (Negate)
Expand Down