Filtry w aplikacjach minimalnych interfejsów API
Przez Fiyaz Bin Hasan, Martin Costello i Rick Anderson
Minimalne filtry interfejsu API umożliwiają deweloperom implementowanie logiki biznesowej, która obsługuje:
- Uruchamianie kodu przed programem obsługi punktu końcowego i po nim.
- Sprawdzanie i modyfikowanie parametrów podanych podczas wywołania programu obsługi punktu końcowego.
- Przechwytywanie zachowania odpowiedzi programu obsługi punktu końcowego.
Filtry mogą być przydatne w następujących scenariuszach:
- Weryfikowanie parametrów żądania i treści wysyłanych do punktu końcowego.
- Rejestrowanie informacji o żądaniu i odpowiedzi.
- Weryfikowanie, czy żądanie dotyczy obsługiwanej wersji interfejsu API.
Filtry można zarejestrować, podając delegata, który przyjmuje EndpointFilterInvocationContext
element i zwraca wartość EndpointFilterDelegate
. Obiekt EndpointFilterInvocationContext
zapewnia dostęp do HttpContext
żądania i Arguments
listy wskazującej argumenty przekazane do programu obsługi w kolejności, w której są wyświetlane w deklaracji programu obsługi.
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();
Powyższy kod ma następujące działanie:
- Wywołuje metodę
AddEndpointFilter
rozszerzenia, aby dodać filtr do punktu końcowego/colorSelector/{color}
. - Zwraca określony kolor z wyjątkiem wartości
"Red"
. - Zwraca wartość Results.Problem , gdy
/colorSelector/Red
żądanie jest żądane. - Używa
next
elementu iinvocationContext
jakoEndpointFilterDelegate
EndpointFilterInvocationContext
elementu , aby wywołać następny filtr w potoku lub delegat żądania, jeśli ostatni filtr został wywołany.
Filtr jest uruchamiany przed programem obsługi punktu końcowego. Gdy na procedurze obsługi jest wykonywane wiele AddEndpointFilter
wywołań:
- Kod filtru wywoływany przed wywołaniem
EndpointFilterDelegate
(next
) jest wykonywany w kolejności od pierwszego wyjętego wyjścia (FIFO). - Kod filtru wywoływany po wywołaniu
EndpointFilterDelegate
metody (next
) jest wykonywany w kolejności od pierwszego w, ostatniego wyjścia (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();
W poprzednim kodzie filtry i dziennik punktu końcowego zawierają następujące dane wyjściowe:
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
Poniższy kod używa filtrów implementujących IEndpointFilter
interfejs:
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();
W poprzednim kodzie dzienniki filtrów i procedur obsługi pokazują kolejność ich uruchamiania:
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
Filtry implementowane interfejsu IEndpointFilter
są wyświetlane w poniższym przykładzie:
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) { }
}
Weryfikowanie obiektu za pomocą filtru
Rozważ filtr weryfikujący Todo
obiekt:
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);
});
Powyższy kod:
- Obiekt
EndpointFilterInvocationContext
zapewnia dostęp do parametrów skojarzonych z określonym żądaniem wystawionym dla punktu końcowegoGetArguments
za pośrednictwem metody . - Filtr jest rejestrowany przy użyciu elementu
delegate
, który przyjmujeEndpointFilterInvocationContext
element i zwraca wartośćEndpointFilterDelegate
.
Oprócz przekazywania jako delegatów filtry można zarejestrować przez zaimplementowanie interfejsu IEndpointFilter
. Poniższy kod przedstawia powyższy filtr hermetyzowany w klasie, która implementuje IEndpointFilter
element :
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);
}
}
Filtry, które implementują IEndpointFilter
interfejs, mogą rozpoznawać zależności od wstrzykiwania zależności (DI), jak pokazano w poprzednim kodzie. Chociaż filtry mogą rozwiązywać zależności z di, same filtry nie mogą być rozpoznawane z di.
Element ToDoIsValidFilter
jest stosowany do następujących punktów końcowych:
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>();
Następujący filtr weryfikuje Todo
obiekt i modyfikuje Name
właściwość:
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);
}
}
Rejestrowanie filtru przy użyciu fabryki filtrów punktów końcowych
W niektórych scenariuszach może być konieczne buforowanie niektórych informacji podanych w MethodInfo
filtrze. Załóżmy na przykład, że chcemy sprawdzić, czy program obsługi jest dołączony do filtru punktu końcowego Todo
, ma pierwszy parametr, który ocenia typ.
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);
});
Powyższy kod:
- Obiekt
EndpointFilterFactoryContext
zapewnia dostęp do skojarzonegoMethodInfo
z programem obsługi punktu końcowego. - Podpis procedury obsługi jest badany przez sprawdzenie
MethodInfo
pod kątem oczekiwanego podpisu typu. Jeśli zostanie znaleziony oczekiwany podpis, filtr weryfikacji zostanie zarejestrowany w punkcie końcowym. Ten wzorzec fabryki jest przydatny do rejestrowania filtru, który zależy od podpisu docelowego programu obsługi punktów końcowych. - Jeśli pasujący podpis nie zostanie znaleziony, zostanie zarejestrowany filtr przekazywania.
Rejestrowanie filtru w akcjach kontrolera
W niektórych scenariuszach może być konieczne zastosowanie tej samej logiki filtru dla punktów końcowych opartych na trasach i akcji kontrolera. W tym scenariuszu można wywołać metodę AddEndpointFilter
w ControllerActionEndpointConventionBuilder
celu obsługi wykonywania tej samej logiki filtru w akcjach i punktach końcowych.
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();
Dodatkowe zasoby
- Szczegółowe omówienie filtrów punktów końcowych
- Wyświetl lub pobierz przykładowy kod (jak pobrać)
- ValidationFilterRouteHandlerBuilderExtensions — metody rozszerzenia walidacji.
- Samouczek: tworzenie minimalnego interfejsu API przy użyciu platformy ASP.NET Core
- Uwierzytelnianie i autoryzacja w minimalnych interfejsach API
Opinia
https://aka.ms/ContentUserFeedback.
Dostępne już wkrótce: W 2024 r. będziemy stopniowo wycofywać zgłoszenia z serwisu GitHub jako mechanizm przesyłania opinii na temat zawartości i zastępować go nowym systemem opinii. Aby uzyskać więcej informacji, sprawdź:Prześlij i wyświetl opinię dla