Фильтры в приложениях с минимальным API
Авторы: Фияз Бин Хасан (Fiyaz Bin Hasan), Мартин Костелло (Martin Costello) и Рик Андерсон (Rick Anderson)
Фильтры минимальных API позволяют разработчикам реализовать бизнес-логику, которая поддерживает следующее:
- выполнение кода до и после запуска обработчика конечной точки;
- проверку и изменение параметров, предоставленных при вызове обработчика конечной точки;
- перехват ответа обработчика конечной точки.
Фильтры могут быть полезны в следующих сценариях:
- Проверка параметров и текста запросов, отправляемых в конечную точку.
- Запись сведений о запросе и ответе в журнал.
- Проверка того, что запрос предназначен для поддерживаемой версии API.
Фильтры можно зарегистрировать, предоставив делегат, который принимает EndpointFilterInvocationContext
и возвращает EndpointFilterDelegate
. EndpointFilterInvocationContext
предоставляет доступ к HttpContext
запроса и списку Arguments
, в котором переданные обработчику аргументы указаны в том порядке, в котором они отображаются в объявлении обработчика.
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();
Предыдущий код:
- Вызывает метод расширения
AddEndpointFilter
для добавления фильтра в конечную точку/colorSelector/{color}
. - Возвращает указанный цвет, за исключением значения
"Red"
. - Возвращает Results.Problem, если запрашивается
/colorSelector/Red
. - Использует
next
в качествеEndpointFilterDelegate
иinvocationContext
в качествеEndpointFilterInvocationContext
для вызова следующего фильтра в конвейере или делегата запроса, если был вызван последний фильтр.
Фильтр выполняется перед обработчиком конечной точки. При выполнении нескольких вызовов AddEndpointFilter
в обработчике:
- Код фильтра, вызываемый перед вызовом
EndpointFilterDelegate
(next
), выполняется в порядке FIFO (прибыл первым — обслужен первым). - Код фильтра, вызываемый после вызова
EndpointFilterDelegate
(next
), выполняется в порядке 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();
В приведенном выше коде фильтры и конечная точка регистрируют следующие выходные данные:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
В следующем коде используются фильтры, реализующие интерфейс 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();
В приведенном выше коде в журналах фильтров и обработчиков показан порядок их выполнения:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Фильтры, реализующие интерфейс IEndpointFilter
, показаны в следующем примере:
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) { }
}
Проверка объекта с помощью фильтра
Рассмотрим фильтр, проверяющий объект 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);
});
В предыдущем коде:
- Объект
EndpointFilterInvocationContext
предоставляет доступ к параметрам, связанным с определенным запросом, выданным конечной точке с помощьюGetArguments
метода. - Фильтр регистрируется с помощью
delegate
, который принимаетEndpointFilterInvocationContext
и возвращаетEndpointFilterDelegate
.
Кроме передачи в качестве делегатов, фильтры можно зарегистрировать, реализовав интерфейс IEndpointFilter
. В следующем коде показан предыдущий фильтр, инкапсулированный в классе, который реализует 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);
}
}
Фильтры, реализующие интерфейс IEndpointFilter
, могут разрешать зависимости, возникшие в результате внедрения зависимостей (DI), как показано в предыдущем коде. Хотя фильтры и могут разрешать зависимости, возникшие в результате DI, сами фильтры не могут быть разрешены из внедрения зависимостей.
ToDoIsValidFilter
применяется к следующим конечным точкам:
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>();
Следующий фильтр проверяет объект Todo
и изменяет свойство 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);
}
}
Регистрация фильтра с помощью фабрики фильтров конечных точек
В некоторых сценариях может потребоваться кэшировать некоторые сведения, предоставленные в MethodInfo
фильтре. Например, предположим, что мы хотели убедиться, что обработчик фильтра конечной точки подключен к первому параметру, который оценивается типом 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);
});
В предыдущем коде:
- Объект
EndpointFilterFactoryContext
предоставляет доступ к связанномуMethodInfo
обработчику конечной точки. - Подпись обработчика проверяется путем проверки
MethodInfo
ожидаемой сигнатуры типа. При обнаружении ожидаемой подписи фильтр проверки регистрируется в конечной точке. Этот шаблон фабрики полезен для регистрации фильтра, который зависит от сигнатуры целевого обработчика конечной точки. - Если соответствующая подпись не найдена, то регистрируется сквозной фильтр.
Регистрация фильтра для действий контроллера
В некоторых сценариях может потребоваться применить одну логику фильтра как для конечных точек на основе маршрутизации, так и для действий контроллера. В этом сценарии можно вызвать AddEndpointFilter
ControllerActionEndpointConventionBuilder
для поддержки выполнения той же логики фильтрации для действий и конечных точек.
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();
Дополнительные ресурсы
ASP.NET Core
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по