diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..233e6128 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,39 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +## Default settings ## +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +## Formatting rule ## +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0055 +dotnet_diagnostic.IDE0055.severity = error + +# 'Using' directive preferences +dotnet_sort_system_directives_first = false + +# New line preferences +dotnet_diagnostic.IDE2002.severity = error +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +dotnet_diagnostic.IDE2004.severity = error +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false +dotnet_diagnostic.IDE2005.severity = error +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false +dotnet_diagnostic.IDE2006.severity = error +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false +dotnet_diagnostic.IDE2000.severity = error +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_diagnostic.IDE2003.severity = error +dotnet_style_allow_statement_immediately_after_block_experimental = false + +[*.csproj] +indent_size = 2 +charset = utf-8 + +[*.json] +indent_size = 2 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..e1220a8d --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,7 @@ + + + + True + + + \ No newline at end of file diff --git a/README.md b/README.md index 2c46dbe4..5c9718c4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # .NET Feature Management +[![Microsoft.FeatureManagement](https://img.shields.io/nuget/v/Microsoft.FeatureManagement?label=Microsoft.FeatureManagement)](https://www.nuget.org/packages/Microsoft.FeatureManagement) +[![Microsoft.FeatureManagement.AspNetCore](https://img.shields.io/nuget/v/Microsoft.FeatureManagement.AspNetCore?label=Microsoft.FeatureManagement.AspNetCore)](https://www.nuget.org/packages/Microsoft.FeatureManagement.AspNetCore) + Feature management provides a way to develop and expose application functionality based on features. Many applications have special requirements when a new feature is developed such as when the feature should be enabled and under what conditions. This library provides a way to define these relationships, and also integrates into common .NET code patterns to make exposing these features possible. ## Get started diff --git a/examples/BlazorServerApp/BlazorServerApp.csproj b/examples/BlazorServerApp/BlazorServerApp.csproj index eeae537e..e1ab1c73 100644 --- a/examples/BlazorServerApp/BlazorServerApp.csproj +++ b/examples/BlazorServerApp/BlazorServerApp.csproj @@ -4,7 +4,7 @@ net6.0 enable - + diff --git a/examples/BlazorServerApp/BrowserFilter.cs b/examples/BlazorServerApp/BrowserFilter.cs index 5105b8a8..376f323e 100644 --- a/examples/BlazorServerApp/BrowserFilter.cs +++ b/examples/BlazorServerApp/BrowserFilter.cs @@ -45,7 +45,7 @@ private static bool IsChromeBrowser(string userAgentContext) return false; } - return userAgentContext.Contains("chrome", StringComparison.OrdinalIgnoreCase) && + return userAgentContext.Contains("chrome", StringComparison.OrdinalIgnoreCase) && !userAgentContext.Contains("edg", StringComparison.OrdinalIgnoreCase); } diff --git a/examples/BlazorServerApp/Program.cs b/examples/BlazorServerApp/Program.cs index 4a66f68c..1dfd1c43 100644 --- a/examples/BlazorServerApp/Program.cs +++ b/examples/BlazorServerApp/Program.cs @@ -48,4 +48,4 @@ public static void Main(string[] args) app.Run(); } } -} \ No newline at end of file +} diff --git a/examples/ConsoleApp/AccountServiceContext.cs b/examples/ConsoleApp/AccountServiceContext.cs index 85114371..95f85a33 100644 --- a/examples/ConsoleApp/AccountServiceContext.cs +++ b/examples/ConsoleApp/AccountServiceContext.cs @@ -4,4 +4,4 @@ class AccountServiceContext : IAccountContext { public string AccountId { get; set; } -} \ No newline at end of file +} diff --git a/examples/ConsoleApp/Program.cs b/examples/ConsoleApp/Program.cs index 12028ca2..fb43a50e 100644 --- a/examples/ConsoleApp/Program.cs +++ b/examples/ConsoleApp/Program.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Configuration; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; @@ -49,4 +52,4 @@ // Output results Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the '{account}' account."); } -} \ No newline at end of file +} diff --git a/examples/EvaluationDataToApplicationInsights/Pages/Checkout.cshtml.cs b/examples/EvaluationDataToApplicationInsights/Pages/Checkout.cshtml.cs index eb5a8d00..ceaaef99 100644 --- a/examples/EvaluationDataToApplicationInsights/Pages/Checkout.cshtml.cs +++ b/examples/EvaluationDataToApplicationInsights/Pages/Checkout.cshtml.cs @@ -5,4 +5,4 @@ namespace EvaluationDataToApplicationInsights.Pages public class CheckoutModel : PageModel { } -} \ No newline at end of file +} diff --git a/examples/EvaluationDataToApplicationInsights/Pages/Error.cshtml.cs b/examples/EvaluationDataToApplicationInsights/Pages/Error.cshtml.cs index f34a941b..f20de344 100644 --- a/examples/EvaluationDataToApplicationInsights/Pages/Error.cshtml.cs +++ b/examples/EvaluationDataToApplicationInsights/Pages/Error.cshtml.cs @@ -24,4 +24,4 @@ public void OnGet() RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } -} \ No newline at end of file +} diff --git a/examples/EvaluationDataToApplicationInsights/Pages/Index.cshtml.cs b/examples/EvaluationDataToApplicationInsights/Pages/Index.cshtml.cs index d161697e..0fbcf0b7 100644 --- a/examples/EvaluationDataToApplicationInsights/Pages/Index.cshtml.cs +++ b/examples/EvaluationDataToApplicationInsights/Pages/Index.cshtml.cs @@ -47,7 +47,7 @@ public IActionResult OnPost() { string val = Request.Form["imageScore"]; - if (val != null && + if (val != null && int.TryParse(val, out int rating)) { _telemetry.TrackEvent( diff --git a/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs b/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs index 073b7452..7770ed3d 100644 --- a/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs +++ b/examples/FeatureFlagDemo/Authentication/QueryStringAuthenticationHandler.cs @@ -2,14 +2,10 @@ // Licensed under the MIT license. // using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; -using System.Threading.Tasks; namespace FeatureFlagDemo.Authentication { @@ -49,7 +45,7 @@ protected override Task HandleAuthenticateAsync() foreach (string group in groups) { - identity.AddClaim(new Claim(ClaimTypes.GroupName, group)); + identity.AddClaim(new Claim(ClaimTypes.Role, group)); } Logger.LogInformation($"Assigning the following groups '{string.Join(", ", groups)}' to the request."); diff --git a/examples/FeatureFlagDemo/BrowserFilter.cs b/examples/FeatureFlagDemo/BrowserFilter.cs index efeb8e70..335e6d4c 100644 --- a/examples/FeatureFlagDemo/BrowserFilter.cs +++ b/examples/FeatureFlagDemo/BrowserFilter.cs @@ -1,12 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.FeatureManagement; -using System; -using System.Linq; -using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { diff --git a/examples/FeatureFlagDemo/BrowserFilterSettings.cs b/examples/FeatureFlagDemo/BrowserFilterSettings.cs index 91b8211a..c4cce8a3 100644 --- a/examples/FeatureFlagDemo/BrowserFilterSettings.cs +++ b/examples/FeatureFlagDemo/BrowserFilterSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Collections.Generic; - namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { public class BrowserFilterSettings diff --git a/examples/FeatureFlagDemo/ClaimTypes.cs b/examples/FeatureFlagDemo/ClaimTypes.cs deleted file mode 100644 index b24dfd20..00000000 --- a/examples/FeatureFlagDemo/ClaimTypes.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace FeatureFlagDemo -{ - static class ClaimTypes - { - public static string GroupName = "http://schemas.featureflagdemo.featuremanagement.microsoft.com/claims/groupname"; - } -} diff --git a/examples/FeatureFlagDemo/Controllers/BetaController.cs b/examples/FeatureFlagDemo/Controllers/BetaController.cs index 9b69d9cf..4fed13f8 100644 --- a/examples/FeatureFlagDemo/Controllers/BetaController.cs +++ b/examples/FeatureFlagDemo/Controllers/BetaController.cs @@ -7,7 +7,7 @@ namespace FeatureFlagDemo.Controllers { - public class BetaController: Controller + public class BetaController : Controller { private readonly IFeatureManager _featureManager; diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs index 6e43fba2..e7bfcd43 100644 --- a/examples/FeatureFlagDemo/Controllers/HomeController.cs +++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; using FeatureFlagDemo.Models; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Mvc; -using System.Threading.Tasks; +using System.Diagnostics; namespace FeatureFlagDemo.Controllers { diff --git a/examples/FeatureFlagDemo/FeatureFlagDemo.csproj b/examples/FeatureFlagDemo/FeatureFlagDemo.csproj index fcf18a50..94a10cc8 100644 --- a/examples/FeatureFlagDemo/FeatureFlagDemo.csproj +++ b/examples/FeatureFlagDemo/FeatureFlagDemo.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs b/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs deleted file mode 100644 index 9f9c8964..00000000 --- a/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.AspNetCore.Http; -using Microsoft.FeatureManagement.FeatureFilters; -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace FeatureFlagDemo -{ - /// - /// Provides an implementation of that creates a targeting context using info from the current HTTP request. - /// - public class HttpContextTargetingContextAccessor : ITargetingContextAccessor - { - private const string TargetingContextLookup = "HttpContextTargetingContextAccessor.TargetingContext"; - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); - } - - public ValueTask GetContextAsync() - { - HttpContext httpContext = _httpContextAccessor.HttpContext; - - // - // Try cache lookup - if (httpContext.Items.TryGetValue(TargetingContextLookup, out object value)) - { - return new ValueTask((TargetingContext)value); - } - - ClaimsPrincipal user = httpContext.User; - - List groups = new List(); - - // - // This application expects groups to be specified in the user's claims - foreach (Claim claim in user.Claims) - { - if (claim.Type == ClaimTypes.GroupName) - { - groups.Add(claim.Value); - } - } - - // - // Build targeting context based off user info - TargetingContext targetingContext = new TargetingContext - { - UserId = user.Identity.Name, - Groups = groups - }; - - // - // Cache for subsequent lookup - httpContext.Items[TargetingContextLookup] = targetingContext; - - return new ValueTask(targetingContext); - } - } -} diff --git a/examples/FeatureFlagDemo/Models/ErrorViewModel.cs b/examples/FeatureFlagDemo/Models/ErrorViewModel.cs index 17542f32..bd9dea6c 100644 --- a/examples/FeatureFlagDemo/Models/ErrorViewModel.cs +++ b/examples/FeatureFlagDemo/Models/ErrorViewModel.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System; - namespace FeatureFlagDemo.Models { public class ErrorViewModel @@ -11,4 +9,4 @@ public class ErrorViewModel public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); } -} \ No newline at end of file +} diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs index 4c5722bd..f29f4934 100644 --- a/examples/FeatureFlagDemo/Startup.cs +++ b/examples/FeatureFlagDemo/Startup.cs @@ -45,7 +45,7 @@ public void ConfigureServices(IServiceCollection services) services.AddFeatureManagement() .AddFeatureFilter() - .WithTargeting() + .WithTargeting() .UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler()); services.AddMvc(o => diff --git a/examples/FeatureFlagDemo/SuperUserFilter.cs b/examples/FeatureFlagDemo/SuperUserFilter.cs index 25dc8e5f..4174c3c1 100644 --- a/examples/FeatureFlagDemo/SuperUserFilter.cs +++ b/examples/FeatureFlagDemo/SuperUserFilter.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using Microsoft.FeatureManagement; -using System.Threading.Tasks; namespace FeatureFlagDemo.FeatureManagement.FeatureFilters { diff --git a/examples/FeatureFlagDemo/ThirdPartyActionFilter.cs b/examples/FeatureFlagDemo/ThirdPartyActionFilter.cs index a2d3abd1..4fb4c550 100644 --- a/examples/FeatureFlagDemo/ThirdPartyActionFilter.cs +++ b/examples/FeatureFlagDemo/ThirdPartyActionFilter.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Logging; namespace FeatureFlagDemo { diff --git a/examples/FeatureFlagDemo/ThirdPartyMiddleware.cs b/examples/FeatureFlagDemo/ThirdPartyMiddleware.cs index 74907908..52bdeddf 100644 --- a/examples/FeatureFlagDemo/ThirdPartyMiddleware.cs +++ b/examples/FeatureFlagDemo/ThirdPartyMiddleware.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; - namespace FeatureFlagDemo { public class ThirdPartyMiddleware diff --git a/examples/FeatureFlagDemo/Views/Shared/Error.cshtml.cs b/examples/FeatureFlagDemo/Views/Shared/Error.cshtml.cs index d832e108..79b17264 100644 --- a/examples/FeatureFlagDemo/Views/Shared/Error.cshtml.cs +++ b/examples/FeatureFlagDemo/Views/Shared/Error.cshtml.cs @@ -24,4 +24,4 @@ public void OnGet() RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } -} \ No newline at end of file +} diff --git a/examples/RazorPages/Pages/Error.cshtml.cs b/examples/RazorPages/Pages/Error.cshtml.cs index 74050d52..149f1f23 100644 --- a/examples/RazorPages/Pages/Error.cshtml.cs +++ b/examples/RazorPages/Pages/Error.cshtml.cs @@ -24,4 +24,4 @@ public void OnGet() RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } -} \ No newline at end of file +} diff --git a/examples/RazorPages/Pages/Index.cshtml.cs b/examples/RazorPages/Pages/Index.cshtml.cs index 05dd8805..55bed039 100644 --- a/examples/RazorPages/Pages/Index.cshtml.cs +++ b/examples/RazorPages/Pages/Index.cshtml.cs @@ -18,4 +18,4 @@ public void OnGet() } } -} \ No newline at end of file +} diff --git a/examples/RazorPages/Pages/Privacy.cshtml.cs b/examples/RazorPages/Pages/Privacy.cshtml.cs index c1cd207f..cf5897c5 100644 --- a/examples/RazorPages/Pages/Privacy.cshtml.cs +++ b/examples/RazorPages/Pages/Privacy.cshtml.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.RazorPages; namespace RazorPages.Pages { @@ -16,4 +15,4 @@ public void OnGet() { } } -} \ No newline at end of file +} diff --git a/examples/TargetingConsoleApp/Identity/IUserRepository.cs b/examples/TargetingConsoleApp/Identity/IUserRepository.cs index 15fedc84..ea5a99f4 100644 --- a/examples/TargetingConsoleApp/Identity/IUserRepository.cs +++ b/examples/TargetingConsoleApp/Identity/IUserRepository.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading.Tasks; - namespace TargetingConsoleApp.Identity { interface IUserRepository diff --git a/examples/TargetingConsoleApp/Identity/InMemoryUserRepository.cs b/examples/TargetingConsoleApp/Identity/InMemoryUserRepository.cs index 450c7044..1159bda0 100644 --- a/examples/TargetingConsoleApp/Identity/InMemoryUserRepository.cs +++ b/examples/TargetingConsoleApp/Identity/InMemoryUserRepository.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace TargetingConsoleApp.Identity { class InMemoryUserRepository : IUserRepository diff --git a/examples/TargetingConsoleApp/Identity/User.cs b/examples/TargetingConsoleApp/Identity/User.cs index be27cac1..de1f4069 100644 --- a/examples/TargetingConsoleApp/Identity/User.cs +++ b/examples/TargetingConsoleApp/Identity/User.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Collections.Generic; - namespace TargetingConsoleApp.Identity { class User diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs index 5b89807a..d8ee6359 100644 --- a/examples/TargetingConsoleApp/Program.cs +++ b/examples/TargetingConsoleApp/Program.cs @@ -54,4 +54,4 @@ // Output results Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the user '{userId}'."); } -} \ No newline at end of file +} diff --git a/examples/TargetingConsoleApp/TargetingConsoleApp.csproj b/examples/TargetingConsoleApp/TargetingConsoleApp.csproj index e1ffbd53..fe29b948 100644 --- a/examples/TargetingConsoleApp/TargetingConsoleApp.csproj +++ b/examples/TargetingConsoleApp/TargetingConsoleApp.csproj @@ -5,7 +5,7 @@ net6.0 enable - + @@ -20,4 +20,5 @@ Always + diff --git a/examples/VariantServiceDemo/Pages/Error.cshtml.cs b/examples/VariantServiceDemo/Pages/Error.cshtml.cs index ea241507..4c6dfd88 100644 --- a/examples/VariantServiceDemo/Pages/Error.cshtml.cs +++ b/examples/VariantServiceDemo/Pages/Error.cshtml.cs @@ -24,5 +24,4 @@ public void OnGet() RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } - } diff --git a/examples/VariantServiceDemo/Pages/Privacy.cshtml.cs b/examples/VariantServiceDemo/Pages/Privacy.cshtml.cs index 9a1c3334..374e9bde 100644 --- a/examples/VariantServiceDemo/Pages/Privacy.cshtml.cs +++ b/examples/VariantServiceDemo/Pages/Privacy.cshtml.cs @@ -16,5 +16,4 @@ public void OnGet() { } } - } diff --git a/examples/VariantServiceDemo/Program.cs b/examples/VariantServiceDemo/Program.cs index 89e17c2c..843cd1ce 100644 --- a/examples/VariantServiceDemo/Program.cs +++ b/examples/VariantServiceDemo/Program.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using VariantServiceDemo; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Telemetry.ApplicationInsights; - +using VariantServiceDemo; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs index 742b942e..9091a4b0 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs @@ -3,9 +3,12 @@ // using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Mvc; using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.FeatureManagement { @@ -44,5 +47,28 @@ public static IFeatureManagementBuilder UseDisabledFeaturesHandler(this IFeature return builder; } + + /// + /// Enables the use of targeting within the application and adds a targeting context accessor that extracts targeting details from a request's HTTP context. + /// + /// The used to customize feature management functionality. + /// A that can be used to customize feature management functionality. + public static IFeatureManagementBuilder WithTargeting(this IFeatureManagementBuilder builder) + { + // + // Register the targeting context accessor with the same lifetime as the feature manager + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IFeatureManager) && descriptor.Lifetime == ServiceLifetime.Scoped)) + { + builder.Services.TryAddScoped(); + } + else + { + builder.Services.TryAddSingleton(); + } + + builder.AddFeatureFilter(); + + return builder; + } } } diff --git a/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs new file mode 100644 index 00000000..f2fe6201 --- /dev/null +++ b/src/Microsoft.FeatureManagement.AspNetCore/DefaultHttpTargetingContextAccessor.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.AspNetCore.Http; +using Microsoft.FeatureManagement.FeatureFilters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.FeatureManagement +{ + /// + /// Provides a default implementation of that creates using info from the current HTTP request. + /// + internal sealed class DefaultHttpTargetingContextAccessor : ITargetingContextAccessor + { + /// + /// The key used to store and retrieve the from the items. + /// + private static object _cacheKey = new object(); + + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Creates an instance of the DefaultHttpTargetingContextAccessor + /// + public DefaultHttpTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + } + + /// + /// Gets from the current HTTP request. + /// + public ValueTask GetContextAsync() + { + HttpContext httpContext = _httpContextAccessor.HttpContext; + + // + // Try cache lookup + if (httpContext.Items.TryGetValue(_cacheKey, out object value)) + { + return new ValueTask((TargetingContext)value); + } + + // + // Treat user identity name as user id + ClaimsPrincipal user = httpContext.User; + + string userId = user?.Identity?.Name; + + // + // Treat claims of type Role as groups + IEnumerable groups = httpContext.User.Claims + .Where(c => c.Type == ClaimTypes.Role) + .Select(c => c.Value) + .ToList(); + + TargetingContext targetingContext = new TargetingContext + { + UserId = userId, + Groups = groups + }; + + // + // Cache for subsequent lookup + httpContext.Items[_cacheKey] = targetingContext; + + return new ValueTask(targetingContext); + } + } +} diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs index fb15e5b1..caf30a28 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGateAttribute.cs @@ -106,9 +106,9 @@ public override async Task OnActionExecutionAsync(ActionExecutingContext context // // Enabled state is determined by either 'any' or 'all' features being enabled. - bool enabled = RequirementType == RequirementType.All ? - await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) : - await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); + bool enabled = RequirementType == RequirementType.All + ? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) + : await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); if (enabled) { @@ -134,9 +134,9 @@ public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext contex // // Enabled state is determined by either 'any' or 'all' features being enabled. - bool enabled = RequirementType == RequirementType.All ? - await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) : - await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); + bool enabled = RequirementType == RequirementType.All + ? await Features.All(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)) + : await Features.Any(async feature => await fm.IsEnabledAsync(feature).ConfigureAwait(false)); if (enabled) { diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 04878e63..bdafc42d 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -1,4 +1,5 @@ - + + @@ -31,7 +32,7 @@ https://aka.ms/AzureAppConfigurationPackageIcon © Microsoft Corporation. All rights reserved. - + diff --git a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs index 3eff1ad5..f65e62e8 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs @@ -63,9 +63,9 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu 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)); + 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 { @@ -82,10 +82,11 @@ await features.All(async feature => await _featureManager.IsEnabledAsync(feature } enabled = await variants.Any( - async variant => { + async variant => + { Variant assignedVariant = await _featureManager.GetVariantAsync(features.First()).ConfigureAwait(false); - return variant == assignedVariant?.Name; + return variant == assignedVariant?.Name; }); } } diff --git a/src/Microsoft.FeatureManagement/AssemblyInfo.cs b/src/Microsoft.FeatureManagement/AssemblyInfo.cs index 955518ef..75bfb2fa 100644 --- a/src/Microsoft.FeatureManagement/AssemblyInfo.cs +++ b/src/Microsoft.FeatureManagement/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // using System.Runtime.CompilerServices; @@ -9,4 +9,4 @@ "3ae70fbea5662f61dd9d640de2205b7bd5359a43dda006e51d83d1f5f7a7d3f849267a0a28676d" + "cf49727a32487d4c75c4aacd5febb0069e1adc66ec63bbd18ec2276091a0e3c1326aa626c9e4db" + "800714a134f2a81e405f35752b55220021923429cb61776cd2fa66d25c335f8dc27bb92292905a" + -"3798d896")] \ No newline at end of file +"3798d896")] diff --git a/src/Microsoft.FeatureManagement/ConfigurationWrapper.cs b/src/Microsoft.FeatureManagement/ConfigurationWrapper.cs index 99f0b2a6..61d921f5 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationWrapper.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationWrapper.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System; -using System.Collections.Generic; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; namespace Microsoft.FeatureManagement { @@ -27,13 +27,13 @@ public string this[string key] set => _configuration[key] = value; } - public IEnumerable GetChildren() => - _configuration.GetChildren(); + public IEnumerable GetChildren() + => _configuration.GetChildren(); - public IChangeToken GetReloadToken() => - _configuration.GetReloadToken(); + public IChangeToken GetReloadToken() + => _configuration.GetReloadToken(); - public IConfigurationSection GetSection(string key) => - _configuration.GetSection(key); + public IConfigurationSection GetSection(string key) + => _configuration.GetSection(key); } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs b/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs index 50c6743d..1fc9b667 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/ISystemClock.cs @@ -17,4 +17,4 @@ internal interface ISystemClock /// public DateTimeOffset UtcNow { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs index 4dba080a..5e8b6872 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/Recurrence/RecurrenceEvaluator.cs @@ -339,7 +339,6 @@ private static int CalculateWeeklyDayOffset(DayOfWeek day1, DayOfWeek day2) return ((int)day1 - (int)day2 + DaysPerWeek) % DaysPerWeek; } - /// /// Sorts a collection of days of week based on their offsets from a specified first day of week. /// A collection of days of week. diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs index 2b2af56c..ab07a595 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilder.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.FeatureManagement.FeatureFilters; namespace Microsoft.FeatureManagement { @@ -35,7 +35,7 @@ public IFeatureManagementBuilder AddFeatureFilter() where T : IFeatureFilterM } IEnumerable featureFilterImplementations = implementationType.GetInterfaces() - .Where(i => i == typeof(IFeatureFilter) || + .Where(i => i == typeof(IFeatureFilter) || (i.IsGenericType && i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IContextualFeatureFilter<>)))); if (featureFilterImplementations.Count() > 1) diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index 93cb60df..70a91977 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Telemetry; @@ -53,7 +53,7 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu { throw new ArgumentNullException(nameof(featureName)); } - + if (builder.Services.Any(descriptor => descriptor.ServiceType == typeof(IVariantServiceProvider))) { throw new InvalidOperationException($"A variant service of {typeof(TService).FullName} has already been added."); diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index a601f22e..40a0f0a7 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -35,7 +35,6 @@ public sealed class FeatureManager : IFeatureManager, IVariantFeatureManager private IEnumerable _sessionManagers; private TargetingEvaluationOptions _assignerOptions; - /// /// The activity source for feature management. /// @@ -291,9 +290,9 @@ private async ValueTask EvaluateFeature(string featur { if (evaluationEvent.FeatureDefinition.Allocation == null) { - evaluationEvent.VariantAssignmentReason = evaluationEvent.Enabled ? - VariantAssignmentReason.DefaultWhenEnabled : - VariantAssignmentReason.DefaultWhenDisabled; + evaluationEvent.VariantAssignmentReason = evaluationEvent.Enabled + ? VariantAssignmentReason.DefaultWhenEnabled + : VariantAssignmentReason.DefaultWhenDisabled; } else if (!evaluationEvent.Enabled) { @@ -486,6 +485,7 @@ private async ValueTask IsEnabledAsync(FeatureDefinition feature if (featureDefinition.RequirementType == RequirementType.Any) { enabled = true; + break; } diff --git a/src/Microsoft.FeatureManagement/IFilterParametersBinder.cs b/src/Microsoft.FeatureManagement/IFilterParametersBinder.cs index bd572e71..fe889893 100644 --- a/src/Microsoft.FeatureManagement/IFilterParametersBinder.cs +++ b/src/Microsoft.FeatureManagement/IFilterParametersBinder.cs @@ -18,4 +18,4 @@ public interface IFilterParametersBinder /// A settings object that is understood by the implementer of . object BindParameters(IConfiguration parameters); } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs index bd8c665b..0b78a237 100644 --- a/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs +++ b/src/Microsoft.FeatureManagement/IVariantFeatureManager.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading.Tasks; -using System.Threading; using Microsoft.FeatureManagement.FeatureFilters; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.FeatureManagement { diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 5c0dce5d..5ffcb495 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -1,4 +1,5 @@ - + + diff --git a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs index 1272b33c..f07bc694 100644 --- a/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs +++ b/src/Microsoft.FeatureManagement/MicrosoftFeatureManagementFields.cs @@ -49,4 +49,4 @@ internal static class MicrosoftFeatureManagementFields public const string Telemetry = "telemetry"; public const string Metadata = "metadata"; } -} \ No newline at end of file +} diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 346a0016..41149673 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -66,7 +66,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec services.TryAddScoped(sp => sp.GetRequiredService()); var builder = new FeatureManagementBuilder(services); - + // // Add built-in feature filters builder.AddFeatureFilter(); @@ -158,7 +158,7 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService // Add built-in feature filters builder.AddFeatureFilter(); - builder.AddFeatureFilter(sp => + builder.AddFeatureFilter(sp => new TimeWindowFilter() { Cache = sp.GetRequiredService() diff --git a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs index e642c01c..9051d761 100644 --- a/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs +++ b/src/Microsoft.FeatureManagement/Targeting/TargetingEvaluator.cs @@ -12,10 +12,10 @@ namespace Microsoft.FeatureManagement.Targeting { static class TargetingEvaluator { - private static StringComparison GetComparisonType(bool ignoreCase) => - ignoreCase ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; + private static StringComparison GetComparisonType(bool ignoreCase) + => ignoreCase + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; const string OutOfRange = "The value is out of the accepted range."; const string RequiredParameter = "Value cannot be null."; @@ -189,9 +189,9 @@ public static bool IsTargeted( if (sourceGroups != null) { - IEnumerable normalizedGroups = ignoreCase ? - sourceGroups.Select(g => g?.ToLower()) : - sourceGroups; + IEnumerable normalizedGroups = ignoreCase + ? sourceGroups.Select(g => g?.ToLower()) + : sourceGroups; foreach (string group in normalizedGroups) { @@ -231,15 +231,15 @@ public static bool IsTargeted( throw new ArgumentNullException(nameof(hint)); } - string userId = ignoreCase ? - targetingContext.UserId?.ToLower() : - targetingContext.UserId; + string userId = ignoreCase + ? targetingContext.UserId?.ToLower() + : targetingContext.UserId; if (targetingContext.Groups != null) { - IEnumerable normalizedGroups = ignoreCase ? - targetingContext.Groups.Select(g => g?.ToLower()) : - targetingContext.Groups; + IEnumerable normalizedGroups = ignoreCase + ? targetingContext.Groups.Select(g => g?.ToLower()) + : targetingContext.Groups; foreach (string group in normalizedGroups) { @@ -279,9 +279,9 @@ public static bool IsTargeted( throw new ArgumentNullException(nameof(hint)); } - string userId = ignoreCase ? - targetingContext.UserId?.ToLower() : - targetingContext.UserId; + string userId = ignoreCase + ? targetingContext.UserId?.ToLower() + : targetingContext.UserId; string defaultContextId = $"{userId}\n{hint}"; @@ -318,9 +318,9 @@ public static bool IsTargeted(ITargetingContext targetingContext, double from, d throw new ArgumentException($"Value of {nameof(from)} cannot be larger than value of {nameof(to)}."); } - string userId = ignoreCase ? - targetingContext.UserId?.ToLower() : - targetingContext.UserId; + string userId = ignoreCase + ? targetingContext.UserId?.ToLower() + : targetingContext.UserId; string contextId = $"{userId}\n{hint}"; @@ -345,7 +345,6 @@ private static bool IsTargeted(string contextId, double from, double to) hash = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(contextId)); } - // // Endianness check ensures the consistency of targeting evaluation result across different architectures if (!BitConverter.IsLittleEndian) diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs index 58ebcf01..dce12cfe 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs +++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; -using System; using System.Collections.Generic; using System.Linq; using System.Net; diff --git a/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs b/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs index 0635a289..cbf617d5 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs +++ b/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; +using System.Threading.Tasks; namespace Tests.FeatureManagement.AspNetCore { diff --git a/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj b/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj index b14bbfdb..47592cc5 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj +++ b/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj @@ -1,4 +1,4 @@ - + net6.0;net7.0;net8.0 diff --git a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs index 36579146..a0bdbace 100644 --- a/tests/Tests.FeatureManagement/CustomTargetingFilter.cs +++ b/tests/Tests.FeatureManagement/CustomTargetingFilter.cs @@ -25,7 +25,7 @@ public CustomTargetingFilter(IOptions options, ILogg public Task EvaluateAsync(FeatureFilterEvaluationContext context) { - return _contextualFilter.EvaluateAsync(context, new TargetingContext(){ UserId = "Jeff" }); + return _contextualFilter.EvaluateAsync(context, new TargetingContext() { UserId = "Jeff" }); } } } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 77a422af..e1a6efe1 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -1857,4 +1857,4 @@ public async Task TelemetryPublishing() Assert.True(result); } } -} \ No newline at end of file +} diff --git a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs index 50c6e449..184f3612 100644 --- a/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs +++ b/tests/Tests.FeatureManagement/RecurrenceEvaluation.cs @@ -594,7 +594,7 @@ public void MatchDailyRecurrenceTest() Range = new RecurrenceRange() } }, - false ), + false ) }; ConsumeEvaluationTestData(testData); @@ -1671,7 +1671,7 @@ public async void RecurrenceEvaluationThroughCacheTest() mockedTimeProvider.UtcNow = DateTimeOffset.Parse("2024-2-7T00:00:00+08:00"); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); - for (int i = 0; i < 10; i++ ) + for (int i = 0; i < 10; i++) { mockedTimeProvider.UtcNow = mockedTimeProvider.UtcNow.AddDays(1); Assert.False(await mockedTimeWindowFilter.EvaluateAsync(context)); diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index f3d7f100..367b945e 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -1,4 +1,4 @@ - + net48;net6.0;net7.0;net8.0