最小 API 應用程式中的篩選

作者:Fiyaz Bin HasanMartin Costello, and Rick Anderson

基本 API 篩選條件可讓開發人員實作下列商務邏輯,以支援:

  • 在端點處理常式前後執行程式碼。
  • 檢查和修改端點處理常式引動過程所提供的參數。
  • 攔截端點處理常式的回應行為。

篩選條件在下列情況下很有用:

  • 驗證傳送至端點的要求參數和本文。
  • 記錄要求和回應的相關資訊。
  • 驗證要求是以支援的 API 版本為目標。

提供 Delegate 來接受 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 方法存取與發出至端點的特定要求相關聯的參數。
  • 篩選條件是使用可接受 EndpointFilterInvocationContext 並傳回 EndpointFilterDelegatedelegate 來註冊。

除了傳遞為委派之外,還可藉由實作 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();

其他資源