Обработчики маршрутов в минимальных приложениях API

Примечание.

Это не последняя версия этой статьи. В текущей версии см. версию .NET 10 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущей версии см. версию .NET 10 этой статьи.

Настроенный WebApplication поддерживает Map{Verb} и MapMethods, где {Verb} — это метод HTTP с использованным регистром Pascal, например, Get, Post, Put или Delete.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Аргументы Delegate , передаваемые этим методам, называются обработчиками маршрутов.

В этой статье описывается использование обработчиков маршрутов, включая примеры, параметры, группы маршрутов и ограничения маршрутов.

Работа с обработчиками маршрутов

Обработчики маршрутов — это методы, которые выполняются, когда маршрут совпадает. Обработчики маршрутов могут быть лямбда-выражением, локальной функцией, методом экземпляра или статическим методом. Обработчики маршрутов могут быть синхронными или асинхронными.

В следующих разделах приведены примеры различных обработчиков маршрутов.

Лямбда-выражение

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Локальная функция

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Метод экземпляра

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Статический метод

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Конечная точка, определяемая вне Program.cs

Минимальные API не должны находиться в файле Program.cs . Например, можно настроить структуру в файле Program.cs и определить конечную точку в отдельном файле:

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

Дополнительные сведения см. в разделе "Группы маршрутов " далее в этой статье.

Вы можете указать имя конечных точек для создания URL-адресов, предназначенных для конечной точки. Использование именованной конечной точки позволяет избежать сложных путей кода в приложении:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

В предыдущем коде отображается сообщение The link to the hello route is /hello из конечной точки / (косая черта).

Критерии для имен конечных точек

Имена конечных точек должны соответствовать следующим критериям:

  • Имена конечных точек чувствительны к регистру.
  • Имена конечных точек должны быть глобально уникальными.
  • Имена конечных точек используются в качестве идентификатора операции OpenAPI при включенной поддержке OpenAPI. Дополнительные сведения см. в разделе "Создание документов OpenAPI".

Параметры маршрута

Параметры маршрута могут быть захвачены в составе определения шаблона маршрута:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

Предыдущий код возвращает сообщение Идентификатор пользователя равен 3, а идентификатор книги — 7 из URI /users/3/books/7.

Обработчик маршрута может объявлять параметры, которые нужно захватывать. Когда запрос посылается на маршрут с параметрами, объявленными для захвата, параметры анализируются и передаются обработчику. Такой подход упрощает захват значений в типобезопасном способе. В приведенном выше коде параметры userId и bookId оба имеют тип int.

В приведенном выше коде, если любое значение маршрута не может быть преобразовано в int, создается исключение. Запрос GET /users/hello/books/3 вызывает следующее исключение:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Использование подстановочных знаков и перехват всех маршрутов

Следующий перехват всех маршрутов возвращает маршрутизацию для приветствия из конечной /posts/hello точки:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Ограничения маршрута

Ограничения маршрутов определяют правила соответствия маршрута.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

В приведенной ниже таблице перечислены представленные выше примеры шаблонов маршрутов и их поведение.

Шаблон маршрута Пример сопоставления URI
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Дополнительные сведения см. в разделе Справочник по ограничениям маршрутов в статье Маршрутизация в ASP.NET Core.

Группы маршрутов

Метод MapGroup расширения помогает упорядочивать группы конечных точек с общим префиксом и уменьшает повторяющийся код. Используйте этот метод для настройки целых групп конечных точек с одним вызовом таких методов RequireAuthorization и WithMetadata добавления метаданных конечной точки.

Например, следующий код создает две аналогичные группы конечных точек:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

В этом сценарии можно использовать относительный адрес для Location заголовка в 201 Created результате:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

Первой группе конечных точек соответствуют только запросы с префиксом /public/todos, и они доступные без какой-либо проверки подлинности. Вторая группа конечных точек соответствует только запросам, которые начинаются с /private/todos, и требуют проверки подлинности.

QueryPrivateTodos — это локальная функция, которая изменяет TodoDb параметры обработчика маршрутов, чтобы разрешить им доступ к частным данным todo и хранить их. QueryPrivateTodos служит фабрикой фильтров конечных точек.

Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с user группой, может захватывать параметры маршрута {org} и {group}, определенные во внешних префиксах группы.

Префикс также может быть пустым. Этот подход может быть полезен для добавления метаданных конечной точки или фильтров в группу конечных точек без изменения шаблона маршрута.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Добавление фильтров или метаданных в группу приводит к тому же поведению, что и их добавление в каждую конечную точку (перед добавлением дополнительных фильтров или метаданных, которые могут существовать во внутренней группе или конкретной конечной точке).

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

В предыдущем примере внешний фильтр регистрирует входящий запрос раньше внутреннего фильтра, хотя он добавляется позже. Так как фильтры применяются к разным группам, порядок их добавления относительно друг друга не имеет значения. Порядок, в котором добавляются фильтры, имеет значение при применении к одной и той же группе или конкретной конечной точке.

Запрос к /outer/inner/ регистрирует следующие данные:

/outer group filter
/inner group filter
MapGet filter

Привязка параметров в обработчике маршрутов

Привязка параметров в приложениях Minimal API подробно описывает правила заполнения параметров обработчика маршрутов.

Обработайте ответ от маршрутизатора

Создание ответов в приложениях Minimal API подробно описывает, как значения, возвращаемые обработчиками маршрутов, преобразуются в ответы.