Filtres dans les applications d’API minimales
Remarque
Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.
Avertissement
Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.
Important
Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.
Pour la version actuelle, consultez la version .NET 8 de cet article.
Par Fiyaz Bin Hasan, Martin Costello et Rick Anderson
Les filtres d’API minimaux permettent aux développeurs d’implémenter une logique métier qui prend en charge :
- Exécution du code avant et après le gestionnaire de point de terminaison.
- Inspection et modification des paramètres fournis lors d’un appel de gestionnaire de point de terminaison.
- Interception du comportement de réponse d’un gestionnaire de point de terminaison.
Les filtres peuvent être utiles dans les scénarios suivants :
- Validation des paramètres et du corps de la requête envoyés à un point de terminaison.
- Journalisation des informations sur la requête et la réponse.
- Validation qu’une requête cible une version d’API prise en charge.
Les filtres peuvent être inscrits en fournissant un délégué qui prend un EndpointFilterInvocationContext
et retourne un EndpointFilterDelegate
. Le EndpointFilterInvocationContext
fournit l’accès au HttpContext
de la requête et à une liste Arguments
indiquant les arguments passés au gestionnaire dans l’ordre dans lequel ils apparaissent dans la déclaration du gestionnaire.
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();
Le code précédent :
- Appelle la méthode d’extension
AddEndpointFilter
pour ajouter un filtre au point de terminaison/colorSelector/{color}
. - Retourne la couleur spécifiée à l’exception de la valeur
"Red"
. - Retourne Results.Problem lorsque
/colorSelector/Red
est demandé. - Utilise
next
commeEndpointFilterDelegate
etinvocationContext
commeEndpointFilterInvocationContext
pour appeler le filtre suivant dans le pipeline ou le délégué de requête si le dernier filtre a été appelé.
Le filtre est exécuté avant le gestionnaire de point de terminaison. Lorsque plusieurs appels AddEndpointFilter
sont effectués sur un gestionnaire :
- Le code de filtre appelé avant l’appel de
EndpointFilterDelegate
(next
) est exécuté dans l’ordre First In, First Out (FIFO). - Le code de filtre appelé après l’appel de
EndpointFilterDelegate
(next
) est exécuté dans l’ordre Premier entrant, Dernier sorti (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();
Dans le code précédent, les filtres et le point de terminaison consignent la sortie suivante :
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
Le code suivant utilise des filtres qui implémentent l’interface 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();
Dans le code précédent, les journaux des filtres et des gestionnaires indiquent l’ordre dans lequel ils sont exécutés :
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Des filtres implémentant l’interface IEndpointFilter
sont illustrés dans l’exemple suivant :
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) { }
}
Valider un objet avec un filtre
Considérez un filtre qui valide un objet 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);
});
Dans le code précédent :
- L’objet
EndpointFilterInvocationContext
fournit l’accès aux paramètres associés à une requête particulière émise au point de terminaison via la méthodeGetArguments
. - Le filtre est inscrit à l’aide d’un
delegate
qui prend unEndpointFilterInvocationContext
et retourne unEndpointFilterDelegate
.
En plus d’être passés en tant que délégués, les filtres peuvent être inscrits en implémentant l’interface IEndpointFilter
. Le code suivant montre le filtre précédent encapsulé dans une classe qui implémente 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);
}
}
Les filtres qui implémentent l’interface IEndpointFilter
peuvent résoudre les dépendances à partir de l’injection de dépendances (DI), comme indiqué dans le code précédent. Bien que les filtres puissent résoudre les dépendances à partir de la DI, les filtres eux-mêmes ne peuvent pas être résolus à partir de la DI.
Le ToDoIsValidFilter
est appliqué aux points de terminaison suivants :
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>();
Le filtre suivant valide l’objet Todo
et modifie la propriété 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);
}
}
Inscrire un filtre à l’aide d’une fabrique de filtres de point de terminaison
Dans certains scénarios, il peut être nécessaire de mettre en cache certaines informations fournies dans le MethodInfo
dans un filtre. Par exemple, supposons que nous voulions vérifier que le gestionnaire auquel un filtre de point de terminaison est attaché a un premier paramètre qui prend la valeur d’un type 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);
});
Dans le code précédent :
- L’objet
EndpointFilterFactoryContext
fournit l’accès auMethodInfo
associé au gestionnaire du point de terminaison. - La signature du gestionnaire est examinée en inspectant
MethodInfo
pour la signature de type attendue. Si la signature attendue est trouvée, le filtre de validation est inscrit sur le point de terminaison. Ce modèle de fabrique est utile pour inscrire un filtre qui dépend de la signature du gestionnaire de point de terminaison cible. - Si aucune signature correspondante n’est trouvée, un filtre pass-through est inscrit.
Inscrire un filtre sur les actions du contrôleur
Dans certains scénarios, il peut être nécessaire d’appliquer la même logique de filtre pour les points de terminaison basés sur le gestionnaire de routes et les actions du contrôleur. Pour ce scénario, il est possible d’appeler AddEndpointFilter
sur ControllerActionEndpointConventionBuilder
pour prendre en charge l’exécution de la même logique de filtre sur les actions et les points de terminaison.
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();