如何从 ASP.NET 核心 3 属性路由中排除控制器

How can I exclude a controller from ASP.NET Core 3 Attribute Routing

我正在为我的网站使用 ASP.NET Core 3.1 API。我有多个控制器都使用基于属性的路由,一切都很好。

我们希望能够在应用配置中使用功能标志切换一个或多个控制器。理想情况下,如果未设置标志,则相应的控制器应在 API 眼中不复存在。我正在尝试想出最好的(或任何)方法来做到这一点。

似乎没有内置方法来配置在使用属性路由时扫描哪些控制器,也无法修改路由找到的控制器或端点的集合。这是有问题的 Startup.cs 片段:

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {            
        app.UseRouting();
        app.UseEndpoints(e =>
        {
            if (!this.FeatureEnabled)
            {
                // DO SOMETHING?
            }

            e.MapControllers();
        });
    }

我意识到我可能可以切换到更手动的手写路由并在启动中指定每个控制器、操作和参数 class,但我宁愿放弃此功能标志要求也不愿继续下去凌乱的路径。

在使用基于属性的路由时,有什么方法可以 select 在 API 中使用哪些控制器?

您可以实现自己的 ControllerFeatureProvider 并决定要在您的应用程序中使用哪些控制器。

public class CustomControllerFeatureProvider : ControllerFeatureProvider
{
    private readonly IConfiguration _configuration;

    public CustomControllerFeatureProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    protected override bool IsController(TypeInfo typeInfo)
    {
        var isController = base.IsController(typeInfo);

        if (isController)
        {
            var enabledController = _configuration.GetValue<string[]>("EnabledController");

            isController = enabledController.Any(x => typeInfo.Name.Equals(x, StringComparison.InvariantCultureIgnoreCase));
        }

        return isController;
    }
}

并添加到startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .ConfigureApplicationPartManager(manager =>
        {
            manager.FeatureProviders.Add(new CustomControllerFeatureProvider(_configuration));
        });
}

或者,您可以使用其中一种方法 the filters in ASP.NET Core 来更灵活、更易读。

而且重要的是,功能切换后面的控制器在您的应用程序中仍然是有效的控制器。也就是说,如果您想使用 特殊 测试目的请求来测试这些控制器,您仍然可以这样做。

您可以将过滤器应用于控制器级别或操作级别,以切换控制器中的所有操作或控制器中的单个操作。

这是使用 ActionFilterAttribute:

完成的示例
public class ToggledAttribute : ActionFilterAttribute
{
    // Can reuse the attribute for different controllers / actions
    // based on different configuration
    public ToggledAttribute(string configurationName = null)
    {
        ConfigurationName = configurationName;
    }

    public string ConfigurationName { get; }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var isTestRequest =
            context.HttpContext.Request.Headers["x-my-test-header"].Count > 0;

        if (isTestRequest)
        {
            return;
        }

        var configuration = (IConfiguration)context.HttpContext.RequestServices
            .GetService(typeof(IConfiguration));

        // Somehow read toggle from configuration
        var featureEnabled = ...

        if (!featureEnabled)
        {
            context.Result = new NotFoundResult();
        }
    }
}

应用于需要切换的控制器或操作:

[Route("[controller]")]
[Toggled]
public class MyToggledController : ControllerBase
{
    // OR
    [Toggled]
    [HttpGet]
    public int Get()
    {
        return 1;
    }
}

其他答案是可能的解决方案,但是我们发现了一个更简单的解决方案,它使用 Microsoft 为 ASP.NET Core 提供的特性标志功能,只需要几行代码。

https://docs.microsoft.com/en-us/azure/azure-app-configuration/use-feature-flags-dotnet-core

PM> install-package Microsoft.FeatureManagement.AspNetCore

所以我们的启动有这条线:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    
    // By default this looks at the "FeatureManagement" config section
    services.AddFeatureManagement();
}

我们的功能门控制器在顶部有一个新属性:

[ApiController]
[Route("api/v{version:apiVersion}/customers/{token}")]
// Feature.FooService is an enumeration we provide whose name is used as the feature flag
[FeatureGate(Feature.FooService)] 
public class FooController : ControllerBase
{
    ...
}

而我们的 appsettings.json 有以下部分:

{
  "FeatureManagement": {
    "FooService" :  false
  }
}

当该标志被禁用时,整个控制器 return 对任何操作都是 404,当该标志被启用时它工作得很好。

这种方法有两个突出的小问题:

  • 控制器仍然出现在我们的 Swagger 文档和 Swagger UI 中。我不知道是否可以解决这个问题。
  • 当向控制器发出请求时,控制器仍然是 instantiated/constructed,即使功能标志被禁用并且操作会 return 404。这对我们来说意味着我们的 IoC 系统(Autofac ) 正在创建控制器所需的整个对象图,即使它真的 不需要。也没有简单的方法解决这个问题。

如果您正在使用 Nathan Daniels 的答案的 FeatureManagement。 您可以使用此 DocumentFilter 来隐藏 Swashbucke 中的控制器。

services.AddSwaggerGen(c =>
{
    c.DocumentFilter<FeatureGateDocumentFilter>();
});

FeatureGateDocumentFilter.cs

using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;

namespace Portalum.Sales.WebShopApi.OperationFilters
{
    public class FeatureGateDocumentFilter : IDocumentFilter
    {
        private readonly IFeatureManager _featureManager;

        public FeatureGateDocumentFilter(IFeatureManager featureManager)
        {
            this._featureManager = featureManager;
        }

        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {      
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var filterPipeline = apiDescription.ActionDescriptor.FilterDescriptors;
                var filterMetaData = filterPipeline.Select(filterInfo => filterInfo.Filter).SingleOrDefault(filter => filter is FeatureGateAttribute);
                if (filterMetaData == default)
                {
                    continue;
                }

                var featureGateAttribute = filterMetaData as FeatureGateAttribute;
                var isActive = this._featureManager.IsEnabledAsync(featureGateAttribute.Features.Single()).GetAwaiter().GetResult();
                if (isActive)
                {
                    continue;
                }

                var apiPath = swaggerDoc.Paths.FirstOrDefault(o => o.Key.Contains(apiDescription.RelativePath));
                swaggerDoc.Paths.Remove(apiPath.Key);
            }
        }
    }
}

@live2 的例子很好;只需要进行一些修改,以便在同一控制器上处理多个功能标志:

    /// <summary>
    /// Swagger filter which allows [FeatureGate] attribute to be used to filter out 
    /// controllers and methods so feature flags take effect.
    /// </summary>
    public class FeatureGateDocumentFilter : IDocumentFilter
    {
        private readonly IFeatureManager _featureManager;

        /// <summary>
        /// Create new instance of filter.
        /// </summary>
        /// <param name="featureManager">Feature manager.</param>
        public FeatureGateDocumentFilter(IFeatureManager featureManager)
        {
            _featureManager = featureManager;
        }

        /// <inheritdoc />
        public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
        {
            foreach (var apiDescription in context.ApiDescriptions)
            {
                var filterPipeline = apiDescription.ActionDescriptor.FilterDescriptors;
                var featureAttributes = filterPipeline.Select(filterInfo => filterInfo.Filter).OfType<FeatureGateAttribute>().ToList();

                // Check the feature flags on all the [FeatureGate] attributes.
                bool allOk = true;
                foreach (var attribute in featureAttributes)
                {
                    var values = attribute.Features.Select(feature => _featureManager.IsEnabledAsync(feature).Result);
                    allOk &= (attribute.RequirementType == RequirementType.Any
                            ? values.Any(isEnabled => isEnabled)
                            : values.All(isEnabled => isEnabled)
                        );
                }

                if (!allOk)
                {
                    var apiPath = swaggerDoc.Paths.FirstOrDefault(o => o.Key.Contains(apiDescription.RelativePath!));
                    swaggerDoc.Paths.Remove(apiPath.Key);
                }
            }
        }
    }