最小 API 筛选器应用

作者:Fiyaz Bin HasanMartin CostelloRick 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();

其他资源