Filtros en las aplicaciones de API mínimas
Por Fiyaz Bin Hasan, Martin Costelloy Rick Anderson
Los filtros de API mínimas permiten a los desarrolladores implementar lógica de negocios que admita:
- Ejecución de código antes y después del controlador de ruta.
- La inspección y modificación de parámetros proporcionados durante una invocación del controlador de ruta.
- La interceptación del comportamiento de respuesta de un controlador de ruta.
Los filtros pueden ser útiles en los escenarios siguientes:
- Validación de los parámetros de solicitud y el cuerpo que se envían a un punto de conexión.
- Registro de información sobre la solicitud y la respuesta.
- Validación de que una solicitud tiene como destino una versión de API compatible.
Los filtros se pueden registrar proporcionando un delegado que toma EndpointFilterInvocationContext
y devuelve EndpointFilterDelegate
. EndpointFilterInvocationContext
proporciona acceso a HttpContext
de la solicitud y a una lista Arguments
que indica los argumentos pasados al controlador en el orden en que aparecen en la declaración del controlador.
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();
El código anterior:
- Llama al método de extensión
AddEndpointFilter
para agregar un filtro al punto de conexión/colorSelector/{color}
. - Devuelve el color especificado excepto para el valor
"Red"
. - Devuelve Results.Problem cuando se solicita
/colorSelector/Red
. - Usa
next
comoEndpointFilterDelegate
yinvocationContext
comoEndpointFilterInvocationContext
para invocar el siguiente filtro en la canalización o el delegado de solicitud si se ha invocado el último filtro.
El filtro se ejecuta antes del controlador del punto de conexión. Cuando se realizan varias invocaciones AddEndpointFilter
en un controlador:
- El código de filtro al que se llama antes de llamar a
EndpointFilterDelegate
(next
) se ejecuta en orden de primero en entrar, primero en salir (FIFO). - El código de filtro al que se llama después de llamar a
EndpointFilterDelegate
(next
) se ejecuta en orden de primero en entrar, último en salir (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();
En el código anterior, los filtros y el punto de conexión registran la siguiente salida:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
El código siguiente usa filtros que implementan la interfaz 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();
En el código anterior, los registros de filtros y controladores muestran el orden en que se ejecutan:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Los filtros que implementan la interfaz IEndpointFilter
se muestran en el ejemplo siguiente:
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) { }
}
Validación de un objeto con un filtro
Considere un filtro que valide un objeto 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);
});
En el código anterior:
- El objeto
EndpointFilterInvocationContext
proporciona acceso a los parámetros asociados a una solicitud determinada emitida al punto de conexión mediante el métodoGetArguments
. - El filtro se registra mediante un objeto
delegate
que tomaEndpointFilterInvocationContext
y devuelveEndpointFilterDelegate
.
Además de pasarse como delegados, los filtros se pueden registrar mediante la implementación de la interfaz IEndpointFilter
. El código siguiente muestra el filtro anterior encapsulado en una clase que 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);
}
}
Los filtros que implementan la interfaz IEndpointFilter
pueden resolver las dependencias de la inserción de dependencias, como se muestra en el código anterior. Aunque los filtros pueden resolver las dependencias de la inserción de dependencias, los propios filtros no se pueden resolver desde la inserción de dependencias.
ToDoIsValidFilter
se aplica a los siguientes puntos de conexión:
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>();
El siguiente filtro valida el objeto Todo
y modifica la propiedad 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);
}
}
Registro de un filtro mediante una fábrica de filtros de punto de conexión
En algunos escenarios, es posible que sea necesario almacenar en caché parte de la información proporcionada en MethodInfo
en un filtro. Por ejemplo, supongamos que queríamos comprobar que el controlador al que está asociado un filtro de punto de conexión tiene un primer parámetro que se evalúa como un tipo 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);
});
En el código anterior:
- El objeto
EndpointFilterFactoryContext
proporciona acceso a la claseMethodInfo
asociada al controlador del punto de conexión. - La firma del controlador se examina inspeccionando
MethodInfo
para la firma de tipo esperada. Si se encuentra la firma esperada, el filtro de validación se registra en el punto de conexión. Este patrón de fábrica resulta útil para registrar un filtro que depende de la firma del controlador de ruta de destino. - Si no se encuentra una firma coincidente, se registra un filtro de tránsito.
Registro de un filtro en las acciones del controlador
En algunos escenarios, podría ser necesario aplicar la misma lógica de filtro para los puntos de conexión basados en controladores de ruta y las acciones del controlador. En este escenario, es posible invocar AddEndpointFilter
en ControllerActionEndpointConventionBuilder
para admitir la ejecución de la misma lógica de filtro en acciones y puntos de conexión.
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();
Recursos adicionales
Comentarios
https://aka.ms/ContentUserFeedback.
Próximamente: A lo largo de 2024 iremos eliminando gradualmente las Cuestiones de GitHub como mecanismo de retroalimentación para el contenido y lo sustituiremos por un nuevo sistema de retroalimentación. Para más información, consulta:Enviar y ver comentarios de