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_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 来测试一下返回结果:
我们启动的时候默认的环境是 Development
,所以 Production
返回的都是 404,而 Development
相关的 API 则是正常返回了~