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 comeEndpointFilterDelegate
einvocationContext
comeEndpointFilterInvocationContext
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 (
EndpointFilterDelegate
next
) viene eseguito in ordine di ordine FIFO (First In, First Out). - Il codice di filtro chiamato dopo la chiamata (
EndpointFilterDelegate
next
) 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 ilGetArguments
metodo . - Il filtro viene registrato utilizzando un
delegate
oggetto che accetta unEndpointFilterInvocationContext
oggetto e restituisce un oggettoEndpointFilterDelegate
.
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'oggettoMethodInfo
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
Commenti e suggerimenti
https://aka.ms/ContentUserFeedback.
Presto disponibile: Nel corso del 2024 verranno gradualmente disattivati i problemi di GitHub come meccanismo di feedback per il contenuto e ciò verrà sostituito con un nuovo sistema di feedback. Per altre informazioni, vedereInvia e visualizza il feedback per