This article was originally published at: https://www.ahmetkucukoglu.com/en/asp-net-core-feature-management-en/
Feature Management provides feature management in .NET Core applications. It allows the management and querying of the active / passive status of the features of the application. For example, you can ensure that a feature you have just developed is active in a certain date range. To give another example, you can ensure that a feature you have developed is active with a certain percentage. Like A/B testing.
Let's test it by practicing. First, we need to install the package below.
dotnet add package Microsoft.FeatureManagement.AspNetCore -version 2.0.0
Let's define our features in enum.
namespace FeatureManagementSample | |
{ | |
public enum Features | |
{ | |
FeatureA, | |
FeatureB, | |
FeatureC, | |
FeatureD | |
} | |
} |
Let's activate the Feature Management in the Startup.
namespace FeatureManagementSample | |
{ | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.FeatureManagement; | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddFeatureManagement(); | |
services.AddControllers(); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await context.Response.WriteAsync("Hello World!"); | |
}); | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Because Feature Management reads features from IConfiguration, we can define features in environment variable, commandline argument or appsettings. Or, if you have a custom provider, you can define your features in the related source. We will use appsettings in our example. Let's define the feature named FeatureA.
{ | |
"FeatureManagement": { | |
"FeatureA": true | |
} | |
} |
We need to inject the "IFeatureManager" interface to query if the features are activated. By using the "IsEnabledAsync" method of this interface, we can query as follows if the "FeatureA" feature is activated.
namespace FeatureManagementSample.Controllers | |
{ | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.FeatureManagement; | |
using System.Threading.Tasks; | |
[Route("api/[controller]")] | |
[ApiController] | |
public class FeatureController : ControllerBase | |
{ | |
private readonly IFeatureManager _featureManager; | |
public FeatureController(IFeatureManager featureManager) | |
{ | |
_featureManager = featureManager; | |
} | |
[HttpGet] | |
public async Task<IActionResult> Get() | |
{ | |
var featureA = await _featureManager.IsEnabledAsync(nameof(Features.FeatureA)); | |
return Ok(featureA); | |
} | |
} | |
} |
TimeWindowFilter
If we want the feature to be active in a certain date range, we can use the internal "TimeWindowFilter". For this, we need to activate this filter in the Startup.
namespace FeatureManagementSample | |
{ | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.FeatureManagement; | |
using Microsoft.FeatureManagement.FeatureFilters; | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddFeatureManagement() | |
.AddFeatureFilter<TimeWindowFilter>(); | |
services.AddControllers(); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await context.Response.WriteAsync("Hello World!"); | |
}); | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Let's define the feature named FeatureB in appsettings.
{ | |
"FeatureManagement": { | |
"FeatureB": { | |
"EnabledFor": [ | |
{ | |
"Name": "TimeWindow", | |
"Parameters": { | |
"Start": "Wed, 20 May 2020 14:53:00 GMT", | |
"End": "Wed, 20 May 2020 15:53:00 GMT" | |
} | |
} | |
] | |
} | |
} | |
} |
UTC Date is used as the date format. If you only give the Start parameter, the feature will be active from this date. If you only give the End parameter, the feature will be active until this date.
Again, using the "IsEnabledAsync" method of the "IFeatureManager" interface, we can query if the "FeatureB" feature gets activated as follows.
namespace FeatureManagementSample.Controllers | |
{ | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.FeatureManagement; | |
using System.Threading.Tasks; | |
[Route("api/[controller]")] | |
[ApiController] | |
public class FeatureController : ControllerBase | |
{ | |
private readonly IFeatureManager _featureManager; | |
public FeatureController(IFeatureManager featureManager) | |
{ | |
_featureManager = featureManager; | |
} | |
[HttpGet] | |
public async Task<IActionResult> Get() | |
{ | |
var featureB = await _featureManager.IsEnabledAsync(nameof(Features.FeatureB)); | |
return Ok(featureB); | |
} | |
} | |
} |
PercentageFilter
If we want the feature to be active with a certain percentage, we can use the internal "PercentageFilter". For this, we need to activate this filter in the Startup.
namespace FeatureManagementSample | |
{ | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.FeatureManagement; | |
using Microsoft.FeatureManagement.FeatureFilters; | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddFeatureManagement() | |
.AddFeatureFilter<TimeWindowFilter>() | |
.AddFeatureFilter<PercentageFilter>(); | |
services.AddControllers(); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await context.Response.WriteAsync("Hello World!"); | |
}); | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Let's define the feature named FeatureC in appsettings.
{ | |
"FeatureManagement": { | |
"FeatureC": { | |
"EnabledFor": [ | |
{ | |
"Name": "Percentage", | |
"Parameters": { | |
"Value": 80 | |
} | |
} | |
] | |
} | |
} |
This feature will be active in 80% of the requests.
Again, using the "IsEnabledAsync" method of the "IFeatureManager" interface, we can query if the "FeatureC" feature gets activated as follows.
namespace FeatureManagementSample.Controllers | |
{ | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.FeatureManagement; | |
using System.Threading.Tasks; | |
[Route("api/[controller]")] | |
[ApiController] | |
public class FeatureController : ControllerBase | |
{ | |
private readonly IFeatureManager _featureManager; | |
public FeatureController(IFeatureManager featureManager) | |
{ | |
_featureManager = featureManager; | |
} | |
[HttpGet] | |
public async Task<IActionResult> Get() | |
{ | |
var featureC = await _featureManager.IsEnabledAsync(nameof(Features.FeatureC)); | |
return Ok(featureC); | |
} | |
} | |
} |
Custom Filter
Apart from these filters, you can also write your own filters. For example, a filter can be written as below for the feature to be active on a mobile device.
Let's define the feature named FeatureD in appsettings.
{ | |
"FeatureManagement": { | |
"FeatureD": { | |
"EnabledFor": [ | |
{ | |
"Name": "Mobile", | |
"Parameters": { | |
"Allowed": [ "Android" ] | |
} | |
} | |
] | |
} | |
} | |
} |
Let's create "MobileFilterSettings" class to access filter parameters.
namespace FeatureManagementSample | |
{ | |
using System.Collections.Generic; | |
public class MobileFilterSettings | |
{ | |
public IList<string> Allowed { get; set; } = new List<string>(); | |
} | |
} |
Let's write our special filter named "MobileFilter" as follows.
namespace FeatureManagementSample | |
{ | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.Configuration; | |
using Microsoft.FeatureManagement; | |
using System; | |
using System.Threading.Tasks; | |
public class MobileFilter : IFeatureFilter | |
{ | |
private readonly IHttpContextAccessor _httpContextAccessor; | |
public MobileFilter(IHttpContextAccessor httpContextAccessor) | |
{ | |
_httpContextAccessor = httpContextAccessor; | |
} | |
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context) | |
{ | |
var isEnabled = false; | |
var settings = context.Parameters.Get<MobileFilterSettings>(); | |
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString(); | |
foreach (var item in settings.Allowed) | |
{ | |
if (userAgent.Contains(item, StringComparison.OrdinalIgnoreCase)) | |
{ | |
isEnabled = true; | |
break; | |
} | |
} | |
return Task.FromResult(isEnabled); | |
} | |
} | |
} |
We need to activate this filter in the Startup.
namespace FeatureManagementSample | |
{ | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Hosting; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.DependencyInjection; | |
using Microsoft.Extensions.Hosting; | |
using Microsoft.FeatureManagement; | |
using Microsoft.FeatureManagement.FeatureFilters; | |
public class Startup | |
{ | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddFeatureManagement() | |
.AddFeatureFilter<TimeWindowFilter>() | |
.AddFeatureFilter<PercentageFilter>() | |
.AddFeatureFilter<MobileFilter>(); | |
services.AddHttpContextAccessor(); | |
services.AddControllers(); | |
} | |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseRouting(); | |
app.UseEndpoints(endpoints => | |
{ | |
endpoints.MapGet("/", async context => | |
{ | |
await context.Response.WriteAsync("Hello World!"); | |
}); | |
endpoints.MapControllers(); | |
}); | |
} | |
} | |
} |
Again, using the "IsEnabledAsync" method of the "IFeatureManager" interface, we can query if "FeatureD" feature gets activated as follows.
namespace FeatureManagementSample.Controllers | |
{ | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.FeatureManagement; | |
using System.Threading.Tasks; | |
[Route("api/[controller]")] | |
[ApiController] | |
public class FeatureController : ControllerBase | |
{ | |
private readonly IFeatureManager _featureManager; | |
public FeatureController(IFeatureManager featureManager) | |
{ | |
_featureManager = featureManager; | |
} | |
[HttpGet] | |
public async Task<IActionResult> Get() | |
{ | |
var featureD = await _featureManager.IsEnabledAsync(nameof(Features.FeatureD)); | |
return Ok(featureD); | |
} | |
} | |
} |
Mvc Action Filter Attribute
Instead of using the IFeatureManager interface, you can set the activation of the attribute based Controller or Action, thanks to ActionFilter.
namespace FeatureManagementSample.Controllers | |
{ | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.FeatureManagement.Mvc; | |
[Route("api/[controller]")] | |
[ApiController] | |
public class FeatureController : ControllerBase | |
{ | |
[HttpGet] | |
[FeatureGate(nameof(Features.FeatureA))] | |
public IActionResult Get() | |
{ | |
return Ok(); | |
} | |
} | |
} |
Razor Tag Helper
You can use the feature razor tag if you want to display the content in the View according to the activation status of the feature.
<feature name="@nameof(Features.FeatureA)"> | |
<p>FeatureA özelliği aktif ise burası görüntülenir.</p> | |
</feature> |
Middleware
If you want to add middleware according to the activation status of the feature, you can use the UseMiddlewareForFeature method.
app.UseMiddlewareForFeature<FeatureAMiddleware>(nameof(Features.FeatureA)); |
You can access the sample project from Github.
Good luck.
Top comments (0)