基于 filter 实现条件路由
Intro
在我们的项目有几个测试用的接口,有的接口我们往往不想在生产环境上使用,于是会在代码里判断当前环境是不是生产环境,如果不是生产环境才允许执行,否则就返回一个错误,这样的接口多了之后就会发现很多重复的代码,我们此时就可以使用一个 filter 来实现 API 接口的检查,如果是生产环境就不执行 API 接口的逻辑
Filter V1
MVC filter 有几种类型,AuthorizationFilter
、ResourceFilter
、ActionFilter
、ResultFilter
、ExceptionFilter
, 首先我们要选择合适的类型,最合适的莫过于 ResourceFilter
和 ActionFilter
,可能大多小伙伴对于 ActionFilter
更为熟悉一些,但是我觉得这种场景下 ResourceFilter
更好一些,从 MVC filter 的执行流程上来说,会依次执行 AuthorizationFilter
、ResourceFilter
、ActionFilter
,而我们的条件并非一种授权,所以个人感觉 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_GREATERResults.NotFound()
#elsenew 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_GREATERpublic 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 来测试一下返回结果:
我们启动的时候默认的环境是 Development
,所以 Production
返回的都是 404,而 Development
相关的 API 则是正常返回了~
References
https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
https://github.com/WeihanLi/WeihanLi.Web.Extensions
https://github.com/WeihanLi/WeihanLi.Web.Extensions/tree/dev/samples/WeihanLi.Web.Extensions.Samples