C# 如何从ASP.NET Core 3属性路由中排除控制器

C# 如何从ASP.NET Core 3属性路由中排除控制器,c#,asp.net-core,asp.net-mvc-routing,C#,Asp.net Core,Asp.net Mvc Routing,我正在使用ASP.NET Core 3.1作为我的web API。我有多个控制器都使用基于属性的路由,一切都很好 我们希望能够在应用程序配置中使用功能标志切换一个或多个控制器。理想情况下,如果没有设置标志,那么相应的控制器应该不再存在于API的眼中。我正试图想出最好的(或任何)方法来做到这一点 似乎没有内置的方法来配置在使用属性路由时扫描哪些控制器,也没有方法修改路由找到的控制器或端点的集合。下面是有问题的Startup.cs代码段: public void Configure(IApp

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

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

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

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

            e.MapControllers();
        });
    }
我意识到我可能可以切换到手动编写的路由,并在Startup类中指定每个控制器、操作和参数,但我宁愿放弃这个特性标志要求,也不愿走上那条混乱的道路


使用基于属性的路由时,有没有办法选择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;
    }
}

或者,您可以使用其中的一种方法来实现这一点,并且具有更大的灵活性和可读性

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

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

下面是使用
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提供的功能标志功能,只需要几行代码

所以我们的创业公司有这样一条路线:

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
  }
}
当该标志被禁用时,整个控制器对任何操作都返回404,当该标志被启用时,它工作正常

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

  • 控制器仍然显示在我们的Swagger文档和Swagger UI中。我不知道是否有可能解决这个问题
  • 当向控制器发出请求时,该控制器仍然被实例化/构造,即使功能标志被禁用,并且操作将返回404。这意味着我们的IoC系统(Autofac)正在创建控制器所需的整个对象图,尽管它实际上并不需要。也没有简单的方法来解决这个问题

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

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

对于swagger,您可以使用此筛选器,您需要在添加自定义提供程序之前删除现有提供程序:
manager.FeatureProviders.remove(manager.FeatureProviders.OfType().FirstOrDefault())
{
  "FeatureManagement": {
    "FooService" :  false
  }
}
services.AddSwaggerGen(c =>
{
    c.DocumentFilter<FeatureGateDocumentFilter>();
});
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);
            }
        }
    }
}