.NET基于 filter 实现条件路由

Intro

在我们的项目有几个测试用的接口,有的接口我们往往不想在生产环境上使用,于是会在代码里判断当前环境是不是生产环境,如果不是生产环境才允许执行,否则就返回一个错误,这样的接口多了之后就会发现很多重复的代码,我们此时就可以使用一个 filter 来实现 API 接口的检查,如果是生产环境就不执行 API 接口的逻辑.

Filter V1

MVC filter 有几种类型,AuthorizationFilterResourceFilterActionFilterResultFilterExceptionFilter, 首先我们要选择合适的类型,最合适的莫过于 ResourceFilter 和 ActionFilter,可能大多小伙伴对于 ActionFilter 更为熟悉一些,但是我觉得这种场景下 ResourceFilter 更好一些,从 MVC filter 的执行流程上来说,会依次执行 AuthorizationFilterResourceFilterActionFilter,而我们的条件并非一种授权,所以个人感觉 ResourceFilter 更合适一些,我们可以使用 IAsyncResourceFilter 来实现,实现代码如下:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class NonProductionOnlyFilter : Attribute, IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var environment = context.HttpContext.RequestServices.GetRequiredService<IWebHostEnvironment>();
        if (environment.IsProduction())
        {
            context.Result = new NotFoundResult();
        }
        else
        {
            await next();
        }
    }
}

Filter V2

为了更加的通用,我们可以把检查的逻辑和返回值逻辑封装成一个委托

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ConditionalFilter : Attribute, IAsyncResourceFilter
{
    public Func<HttpContext, bool> ConditionFunc { get; init; } = _ => true;

    public Func<HttpContext, IActionResult> ResultFactory { get; init; } = _ => new NotFoundResult();

    public virtual async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var condition = ConditionFunc.Invoke(context.HttpContext);
        if (condition)
        {
            await next();
        }
        else
        {
            var result = ResultFactory.Invoke(context.HttpContext);
            context.Result = result;
        }
    }
}

再在这个 ConditionalFilter 的基础上实现上面的逻辑:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class NonProductionEnvironmentFilter : ConditionalFilter
{
    public NonProductionEnvironmentFilter()
    {
        ConditionFunc = c => c.RequestServices.GetRequiredService<IWebHostEnvironment>()
            .IsProduction() == false;
    }
}

看起来是不是简单了很多,对于别的情况也比较容易扩展,比如我们实现一个指定环境生效的条件

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class EnvironmentFilter : ConditionalFilter
{
    public EnvironmentFilter(params string[] environmentNames)
    {
        Guard.NotNull(environmentNames);
        var allowedEnvironments = environmentNames.ToHashSet(StringComparer.OrdinalIgnoreCase);
        ConditionFunc = c =>
        {
            var env = c.RequestServices.GetRequiredService<IWebHostEnvironment>().EnvironmentName;
            return allowedEnvironments.Contains(env);
        };
    }
}

Filter V3

在前面的文章中我们有提到在 .NET 7 中针对于 Minimal API,引入了 EndpointFilter,我们也可以为我们的 ConditionalFilter 添加 EndpointFilter 的支持

public class ConditionalFilter : Attribute, IAsyncResourceFilter
#if NET7_0_OR_GREATER
    , IEndpointFilter
#endif

{
    public Func<HttpContext, bool> ConditionFunc { get; init; } = _ => true;

    public Func<HttpContext, object> ResultFactory { get; init; } = _ =>
#if NET7_0_OR_GREATER
       Results.NotFound()
#else
            new NotFoundResult()
#endif
        ;

    public virtual async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var condition = ConditionFunc.Invoke(context.HttpContext);
        if (condition)
        {
            await next();
        }
        else
        {
            var result = ResultFactory.Invoke(context.HttpContext);
            context.Result = result switch
            {
                IActionResult actionResult => actionResult,
                IResult httpResult => new HttpResultActionResultAdapter(httpResult),
                _ => new OkObjectResult(result)
            };
        }
    }
#if NET7_0_OR_GREATER
    public virtual async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        var result = ConditionFunc.Invoke(context.HttpContext);
        if (result)
        {
            return await next(context);
        }
        return ResultFactory.Invoke(context.HttpContext);
    }
#endif
}

这里有个需要注意的地方就是 EndpointFilter 的返回和 Resource filter 的返回值不同,返回的类型不是 IActionResult 而且不能正确的处理 IActionResult 类型,针对 IResult 会有处理,所以我们针对 .NET 7 及以上返回的是 IResult 类型,在 ResourceFilter 中处理逻辑中针对 IResult 再转成了 IActionResult, 也就是上面的 HttpResultActionResultAdapter,实现也很简单,实现如下:

internal sealed class HttpResultActionResultAdapter : IActionResult
{
    private readonly IResult _result;

    public HttpResultActionResultAdapter(IResult result)
    {
        _result = result;
    }

    public Task ExecuteResultAsync(ActionContext context)
    {
        return _result.ExecuteAsync(context.HttpContext);
    }
}

Demo

测试代码分为 Minimal API 的 endpoint API 和 MVC controller,示例代码如下:

var envGroup = app.MapGroup("/env-test");
envGroup.Map("/dev", () => "env-test")
    .AddEndpointFilter(new EnvironmentFilter(Environments.Development));
envGroup.Map("/prod", () => "env-test")
    .AddEndpointFilter(new EnvironmentFilter(Environments.Production));
[HttpGet("EnvironmentFilterTest/Dev")]
[EnvironmentFilter("Development")]
//[EnvironmentFilter("Production")]
public IActionResult EnvironmentFilterDevTest()
{
    return Ok(new { Title = ".NET is amazing!" });
}

[HttpGet("EnvironmentFilterTest/Prod")]
[EnvironmentFilter("Production")]
public IActionResult EnvironmentFilterProdTest()
{
    return Ok(new { Title = ".NET is amazing!" });
}

访问我们的 API 来测试一下返回结果:

.NET基于 filter 实现条件路由

Minimal API

.NET基于 filter 实现条件路由

controller

我们启动的时候默认的环境是 Development,所以 Production 返回的都是 404,而 Development 相关的 API 则是正常返回了~