Filtry v aplikacích s minimálním rozhraním API

Fiyaz Bin Hasan, Martin Costello a Rick Anderson

Minimální filtry rozhraní API umožňují vývojářům implementovat obchodní logiku, která podporuje:

  • Spuštění kódu před a za obslužnou rutinou koncového bodu
  • Kontrola a úprava parametrů zadaných během vyvolání obslužné rutiny koncového bodu
  • Zachycení chování odpovědi obslužné rutiny koncového bodu

Filtry můžou být užitečné v následujících scénářích:

  • Ověření parametrů požadavku a textu odesílaných do koncového bodu
  • Protokolování informací o požadavku a odpovědi
  • Ověření, že požadavek cílí na podporovanou verzi rozhraní API.

Filtry lze zaregistrovat zadáním delegáta, který přebírá EndpointFilterInvocationContext a vrací hodnotu EndpointFilterDelegate. Poskytuje EndpointFilterInvocationContext přístup k HttpContext požadavku a Arguments seznam označující argumenty předané obslužné rutině v pořadí, ve kterém se zobrazí v deklaraci obslužné rutiny.

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();

Předchozí kód:

  • Zavolá metodu AddEndpointFilter rozšíření, která přidá do koncového /colorSelector/{color} bodu filtr.
  • Vrátí barvu určenou s výjimkou hodnoty "Red".
  • Vrátí hodnotu Results.Problem , pokud je /colorSelector/Red požadována.
  • Používá next se jako EndpointFilterDelegate a invocationContext jako EndpointFilterInvocationContext vyvolání dalšího filtru v kanálu nebo delegát požadavku, pokud byl vyvolán poslední filtr.

Filtr se spustí před obslužnou rutinou koncového bodu. Když se u obslužné rutiny provede více AddEndpointFilter vyvolání:

  • Kód filtru volaný před EndpointFilterDelegate zavolání (next) se provede v pořadí podle pořadí First In, First Out (FIFO).
  • Kód filtru volaný po EndpointFilterDelegate zavolání (next) se spustí v pořadí podle pořadí First In, Last Out (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();

V předchozím kódu filtry a koncový bod protokolují následující výstup:

Before first filter
    Before 2nd filter
        Before 3rd filter
            Endpoint
        After 3rd filter
    After 2nd filter
After first filter

Následující kód používá filtry, které implementují IEndpointFilter rozhraní:

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();

V předchozím kódu zobrazují filtry a obslužné rutiny pořadí jejich spuštění:

AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
      Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next

Filtry implementují IEndpointFilter rozhraní v následujícím příkladu:


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) { }
}

Ověření objektu pomocí filtru

Zvažte filtr, který ověřuje Todo objekt:

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);
});

V předchozím kódu:

  • Objekt EndpointFilterInvocationContext poskytuje přístup k parametrům přidruženým k určitému požadavku vydanému koncovému bodu prostřednictvím GetArguments metody.
  • Filtr je registrován pomocí, delegate který přebírá EndpointFilterInvocationContext a vrací EndpointFilterDelegate.

Kromě předávání jako delegáty je možné filtry zaregistrovat implementací IEndpointFilter rozhraní. Následující kód ukazuje předchozí zapouzdřený filtr ve třídě, která implementuje 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);
    }
}

Filtry, které implementují IEndpointFilter rozhraní, mohou přeložit závislosti z injektáže závislostí (DI), jak je znázorněno v předchozím kódu. I když filtry můžou vyřešit závislosti z DI, samotné filtry se nedají rozpoznat z DI.

Použije se ToDoIsValidFilter na následující koncové body:

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>();

Následující filtr ověří Todo objekt a upraví Name vlastnost:

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);
    }
}

Registrace filtru pomocí objektu pro filtrování koncových bodů

V některých scénářích může být nutné uložit některé informace uvedené ve MethodInfo filtru do mezipaměti. Předpokládejme například, že jsme chtěli ověřit, že obslužná rutina, ke které je připojený filtr koncového bodu, má první parametr, který se vyhodnotí jako Todo typ.

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); 
});

V předchozím kódu:

  • Objekt EndpointFilterFactoryContext poskytuje přístup k MethodInfo obslužné rutině koncového bodu.
  • Podpis obslužné rutiny je zkoumán kontrolou MethodInfo očekávaného podpisu typu. Pokud se najde očekávaný podpis, ověřovací filtr se zaregistruje do koncového bodu. Tento vzor továrny je užitečný k registraci filtru, který závisí na podpisu obslužné rutiny cílového koncového bodu.
  • Pokud se nenajde odpovídající podpis, zaregistruje se průchozí filtr.

Registrace filtru akcí kontroleru

V některých scénářích může být nutné použít stejnou logiku filtru pro koncové body založené na obslužné rutině tras i akce kontroleru. V tomto scénáři je možné vyvolat volání AddEndpointFilter , ControllerActionEndpointConventionBuilder které podporuje provádění stejné logiky filtru u akcí a koncových bodů.

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();

Další materiály