Share via


Filtri nelle app per le API minime

Di Fiyaz Bin Hasan, Martin Costello e Rick Anderson

I filtri API minimi consentono agli sviluppatori di implementare la logica di business che supporta:

  • Esecuzione del codice prima e dopo il gestore dell'endpoint.
  • Controllo e modifica dei parametri forniti durante la chiamata di un gestore di endpoint.
  • Intercettazione del comportamento della risposta di un gestore endpoint.

I filtri possono essere utili negli scenari seguenti:

  • Convalida dei parametri della richiesta e del corpo inviati a un endpoint.
  • Registrazione delle informazioni sulla richiesta e sulla risposta.
  • Convalidare che una richiesta sia destinata a una versione dell'API supportata.

I filtri possono essere registrati fornendo un delegato che accetta un EndpointFilterInvocationContext oggetto e restituisce un oggetto EndpointFilterDelegate. EndpointFilterInvocationContext fornisce l'accesso all'oggetto HttpContext della richiesta e un Arguments elenco che indica gli argomenti passati al gestore nell'ordine in cui vengono visualizzati nella dichiarazione del gestore.

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

Il codice precedente:

  • Chiama il AddEndpointFilter metodo di estensione per aggiungere un filtro all'endpoint /colorSelector/{color} .
  • Restituisce il colore specificato, ad eccezione del valore "Red".
  • Restituisce Results.Problem quando viene richiesto ./colorSelector/Red
  • next Usa come EndpointFilterDelegate e invocationContext come EndpointFilterInvocationContext per richiamare il filtro successivo nella pipeline o il delegato di richiesta se è stato richiamato l'ultimo filtro.

Il filtro viene eseguito prima del gestore dell'endpoint. Quando vengono effettuate più AddEndpointFilter chiamate su un gestore:

  • Il codice di filtro chiamato prima della chiamata (EndpointFilterDelegatenext) viene eseguito in ordine di ordine FIFO (First In, First Out).
  • Il codice di filtro chiamato dopo la chiamata (EndpointFilterDelegatenext) viene eseguito in ordine di ordine 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();

Nel codice precedente i filtri e l'endpoint registrano l'output seguente:

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

Il codice seguente usa filtri che implementano l'interfaccia 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();

Nel codice precedente i filtri e i log dei gestori mostrano l'ordine in cui vengono eseguiti:

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

I filtri che implementano l'interfaccia IEndpointFilter sono illustrati nell'esempio seguente:


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

Convalidare un oggetto con un filtro

Si consideri un filtro che convalida un Todo oggetto:

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

Nel codice precedente:

  • L'oggetto EndpointFilterInvocationContext fornisce l'accesso ai parametri associati a una determinata richiesta inviata all'endpoint tramite il GetArguments metodo .
  • Il filtro viene registrato utilizzando un delegate oggetto che accetta un EndpointFilterInvocationContext oggetto e restituisce un oggetto EndpointFilterDelegate.

Oltre a essere passati come delegati, i filtri possono essere registrati implementando l'interfaccia IEndpointFilter . Il codice seguente mostra il filtro precedente incapsulato in una classe che implementa 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);
    }
}

I filtri che implementano l'interfaccia IEndpointFilter possono risolvere le dipendenze da Dependency Injection(DI), come illustrato nel codice precedente. Anche se i filtri possono risolvere le dipendenze dall'inserimento delle dipendenze, i filtri stessi non possono essere risolti dall'inserimento delle dipendenze.

L'oggetto ToDoIsValidFilter viene applicato agli endpoint seguenti:

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

Il filtro seguente convalida l'oggetto Todo e modifica la Name proprietà :

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

Registrare un filtro usando una factory di filtro endpoint

In alcuni scenari potrebbe essere necessario memorizzare nella cache alcune delle informazioni fornite in MethodInfo in un filtro. Si supponga, ad esempio, di voler verificare che il gestore a cui sia associato un filtro endpoint abbia un primo parametro che restituisce un Todo tipo.

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

Nel codice precedente:

  • L'oggetto EndpointFilterFactoryContext fornisce l'accesso all'oggetto MethodInfo associato al gestore dell'endpoint.
  • La firma del gestore viene esaminata controllando MethodInfo la firma del tipo previsto. Se viene trovata la firma prevista, il filtro di convalida viene registrato nell'endpoint. Questo modello factory è utile per registrare un filtro che dipende dalla firma del gestore dell'endpoint di destinazione.
  • Se non viene trovata una firma corrispondente, viene registrato un filtro pass-through.

Registrare un filtro per le azioni del controller

In alcuni scenari potrebbe essere necessario applicare la stessa logica di filtro sia per gli endpoint basati sul gestore di route che per le azioni del controller. Per questo scenario, è possibile richiamare AddEndpointFilter su ControllerActionEndpointConventionBuilder per supportare l'esecuzione della stessa logica di filtro per azioni ed endpoint.

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

Risorse aggiuntive