最小 API 筛选器应用
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
作者:Fiyaz Bin Hasan、Martin Costello 和 Rick Anderson
使用最小 API 筛选器,开发人员可以实现支持以下操作的业务逻辑:
- 在终结点处理程序前后运行代码。
- 检查和修改终结点处理程序调用期间提供的参数。
- 截获终结点处理程序的响应行为。
在以下场景中,筛选器很有用:
- 验证已发送到终结点的请求参数和正文。
- 记录有关请求和响应的信息。
- 验证请求是否面向受支持的 API 版本。
可以通过提供采用 EndpointFilterInvocationContext
和返回 EndpointFilterDelegate
的委托来注册筛选器。 EndpointFilterInvocationContext
提供了对请求的 HttpContext
和一个 Arguments
列表的访问权限,该列表指示传递到处理程序的参数(按参数在处理程序声明中出现的顺序)。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string ColorName(string color) => $"Color specified: {color}!";
app.MapGet("/colorSelector/{color}", ColorName)
.AddEndpointFilter(async (invocationContext, next) =>
{
var color = invocationContext.GetArgument<string>(0);
if (color == "Red")
{
return Results.Problem("Red not allowed!");
}
return await next(invocationContext);
});
app.Run();
前面的代码:
- 调用
AddEndpointFilter
扩展方法,将筛选器添加到/colorSelector/{color}
终结点。 - 返回除值
"Red"
之外的指定颜色。 - 请求
/colorSelector/Red
时返回 Results.Problem。 - 如果调用了最后一个筛选器,则将
next
用作EndpointFilterDelegate
,将invocationContext
用作EndpointFilterInvocationContext
来调用管道或请求委托中的下一个筛选器。
筛选器是在终结点处理程序之前运行的。 在处理程序上执行多个 AddEndpointFilter
调用时:
- 在调用
EndpointFilterDelegate
(next
) 之前调用的筛选器代码按照先进先出 (FIFO) 的顺序执行。 - 在调用
EndpointFilterDelegate
(next
) 之后调用的筛选器代码按照先进后出 (FILO) 的顺序执行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation(" Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation("Before first filter");
var result = await next(efiContext);
app.Logger.LogInformation("After first filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 2nd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 2nd filter");
return result;
})
.AddEndpointFilter(async (efiContext, next) =>
{
app.Logger.LogInformation(" Before 3rd filter");
var result = await next(efiContext);
app.Logger.LogInformation(" After 3rd filter");
return result;
});
app.Run();
在前面的代码中,筛选器和终结点记录以下输出:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
以下代码使用实现 IEndpointFilter
接口的筛选器:
using Filters.EndpointFilters;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
app.Logger.LogInformation("Endpoint");
return "Test of multiple filters";
})
.AddEndpointFilter<AEndpointFilter>()
.AddEndpointFilter<BEndpointFilter>()
.AddEndpointFilter<CEndpointFilter>();
app.Run();
在前面的代码中,筛选器和处理程序日志显示了它们运行的顺序:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
以下示例中显示了实现 IEndpointFilter
接口的筛选器:
namespace Filters.EndpointFilters;
public abstract class ABCEndpointFilters : IEndpointFilter
{
protected readonly ILogger Logger;
private readonly string _methodName;
protected ABCEndpointFilters(ILoggerFactory loggerFactory)
{
Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
_methodName = GetType().Name;
}
public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
Logger.LogInformation("{MethodName} Before next", _methodName);
var result = await next(context);
Logger.LogInformation("{MethodName} After next", _methodName);
return result;
}
}
class AEndpointFilter : ABCEndpointFilters
{
public AEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class BEndpointFilter : ABCEndpointFilters
{
public BEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
class CEndpointFilter : ABCEndpointFilters
{
public CEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}
使用筛选器验证对象
考虑使用一个筛选器来验证 Todo
对象:
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
var tdparam = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(tdparam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
});
在上述代码中:
EndpointFilterInvocationContext
对象提供对与通过GetArguments
方法向终结点发出的特定请求关联的参数的访问权限。- 筛选器是使用
delegate
注册的,它采用了EndpointFilterInvocationContext
并返回EndpointFilterDelegate
。
除了作为委托传递之外,还可以通过实现 IEndpointFilter
接口来注册筛选器。 下面的代码显示了前面的筛选器被封装在一个实现 IEndpointFilter
的类中:
public class TodoIsValidFilter : IEndpointFilter
{
private ILogger _logger;
public TodoIsValidFilter(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todo!);
if (!string.IsNullOrEmpty(validationError))
{
_logger.LogWarning(validationError);
return Results.Problem(validationError);
}
return await next(efiContext);
}
}
实现 IEndpointFilter
接口的筛选器可以从依赖项注入 (DI) 解析依赖项,如前面的代码所示。 尽管筛选器可以从 DI 解析依赖项,但筛选器本身无法从 DI 进行解析。
ToDoIsValidFilter
应用于以下终结点:
app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilter<TodoIsValidFilter>();
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
}).AddEndpointFilter<TodoIsValidFilter>();
以下筛选器验证 Todo
对象并修改 Name
属性:
public class TodoIsValidUcFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext,
EndpointFilterDelegate next)
{
var todo = efiContext.GetArgument<Todo>(0);
todo.Name = todo.Name!.ToUpper();
var validationError = Utilities.IsValid(todo!);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(efiContext);
}
}
使用终结点筛选器工厂注册筛选器
在某些情况下,可能需要缓存筛选器的 MethodInfo
中提供的某些信息。 例如,假设我们想要验证终结点筛选器附加到的处理程序是否具有计算结果为 Todo
类型的第一个参数。
app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
var parameters = filterFactoryContext.MethodInfo.GetParameters();
if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(Todo))
{
return async invocationContext =>
{
var todoParam = invocationContext.GetArgument<Todo>(0);
var validationError = Utilities.IsValid(todoParam);
if (!string.IsNullOrEmpty(validationError))
{
return Results.Problem(validationError);
}
return await next(invocationContext);
};
}
return invocationContext => next(invocationContext);
});
在上述代码中:
EndpointFilterFactoryContext
对象提供对与终结点处理程序关联的MethodInfo
的访问权限。- 通过检查预期类型签名的
MethodInfo
来检查处理程序的签名。 如果找到预期的签名,则会将验证筛选器注册到终结点。 此工厂模式可帮助注册依赖于目标终结点处理程序签名的筛选器。 - 如果未找到匹配的签名,则会注册直通筛选器。
对控制器操作注册筛选器
在某些情况下,可能需要对基于路由处理程序的终结点和控制器操作应用相同的筛选器逻辑。 对于此场景,可以在 ControllerActionEndpointConventionBuilder
上调用 AddEndpointFilter
以支持对操作和终结点执行相同的筛选器逻辑。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapController()
.AddEndpointFilter(async (efiContext, next) =>
{
efiContext.HttpContext.Items["endpointFilterCalled"] = true;
var result = await next(efiContext);
return result;
});
app.Run();
其他资源
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈