Minimal API アプリでのルート ハンドラー

構成された WebApplication では、Map{Verb}MapMethods がサポートされます。ここで {Verb} は、GetPostPut または Delete などのパスカルケースの HTTP メソッドです。

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

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 endpoint is /hello を表示します。

: エンドポイント名では大文字と小文字が区別されます。

エンドポイント名:

  • 名前はグローバルに一意である必要があります。
  • OpenAPI サポートが有効な場合、名前は OpneAPI 操作 ID として使用されます。 詳細については、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();

上記のコードでは、URI /users/3/books/7 にから The user id is 3 and book id is 7 が返されます。

ルート ハンドラーは、捕捉するパラメーターを宣言できます。 キャプチャするように宣言されたパラメータを持つルートに対して要求が実行されると、パラメータが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userIdbookId は両方とも int です。

上記のコードで、どちらのルート値も int に変換できない場合、例外がスローされます。 /users/hello/books/3 への GET 要求は、次の例外をスローします。

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

ワイルドカードとキャッチ オール ルート

次のキャッチ オール ルートでは、 `/posts/hello' エンドポイントから Routing to 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 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、エンドポイント メタデータを追加する RequireAuthorizationWithMetadata のようなメソッドを 1 回呼び出すだけで、エンドポイントのグループ全体をカスタマイズできます。

たとえば、次のコードにより、2 つの似たエンドポイント グループが作成されます。

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;
}

このシナリオでは、201 Created 結果の Location ヘッダーに相対アドレスを使用できます。

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 のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 2 番目のグループは、/private/todos のプレフィックスが付いた要求にのみ一致し、認証が必要です。

QueryPrivateTodosエンドポイント フィルター ファクトリは、プライベート todo データにアクセスして格納できるようにルート ハンドラーの TodoDb パラメーターを変更するローカル関数です。

ルート グループでは、ルート パラメーターと制約を含む入れ子になったグループと複雑なプレフィックス パターンもサポートされます。 次の例で、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);
});

上記の例では、外部フィルターは、2 番目に追加された場合でも、内部フィルターの前に受信要求をログに記録します。 フィルターは異なるグループに適用されているため、互いが相対的に追加された順序は関係ありません。 同じグループまたは特定のエンドポイントに適用されている場合、追加される順序フィルターは重要です。

/outer/inner/ に対する要求によって、次がログに記録されます。

/outer group filter
/inner group filter
MapGet filter

パラメーターのバインド

Minimal API アプリでのパラメーター バインド」では、ルート ハンドラー パラメーターの設定方法に関する規則について詳しく説明します。

応答

Minimal API アプリケーションで応答を作成する方法に関するページでは、ルート ハンドラーから返される値を応答に変換する方法について詳しく説明します。