Minimal API クイック リファレンス

このドキュメントでは、

Minimal API には次が含まれます。

WebApplication

次のコードが ASP.NET Core テンプレートによって生成されます。

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

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードは、コマンド ラインで dotnet new web を実行するか、Visual Studio で空の Web テンプレートを選択することによって作成できます。

次のコードにより、WebApplication (app) が作成されます。WebApplicationBuilder は明示的に作成されません。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create は、事前構成された既定値を使用して WebApplication クラスの新しいインスタンスを初期化します。

WebApplication では、特定の条件に応じて、Minimal API applications に次のミドルウェアが自動的に追加されます。

  • UseDeveloperExceptionPage は、HostingEnvironment"Development" である場合、最初に追加されます。
  • UseRouting は、ユーザー コードによって UseRouting がまだ呼び出されておらず、エンドポイントが構成されている (app.MapGet など) 場合、2 番目に追加されます。
  • UseEndpoints は、エンドポイントが構成されている場合、ミドルウェア パイプラインの最後に追加されます。
  • UseAuthentication は、ユーザー コードによって UseAuthentication がまだ呼び出されておらず、サービス プロバイダーで IAuthenticationSchemeProvider が検出できる場合、UseRouting の直後に追加されます。 IAuthenticationSchemeProvider は、AddAuthentication を使用するときに既定で追加され、サービスは IServiceProviderIsService を使用して検出されます。
  • UseAuthorization は、ユーザー コードによって UseAuthorization がまだ呼び出されておらず、サービス プロバイダーで IAuthorizationHandlerProvider が検出できる場合、次に追加されます。 IAuthorizationHandlerProvider は、AddAuthorization を使用するときに既定で追加され、サービスは IServiceProviderIsService を使用して検出されます。
  • ユーザーが構成したミドルウェアとエンドポイントは、UseRoutingUseEndpoints の間に追加されます。

次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCorsUseAuthenticationUseAuthorization の前に呼び出される必要があります。 UseCors を呼び出す場合、アプリでは UseAuthenticationUseAuthorization を呼び出す必要があります。

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

ターミナル ミドルウェアを追加する場合:

  • このミドルウェアは、UseEndpoints の後に追加される必要があります。
  • ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで UseRoutingUseEndpoints を呼び出す必要があります。
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

ターミナル ミドルウェアは、いずれのエンドポイントによっても要求が処理されない場合に実行されるミドルウェアです。

ポートの設定

Visual Studio または dotnet new で Web アプリが作成されると、アプリの応答先となるポートを指定する Properties/launchSettings.json ファイルが作成されます。 続くポート設定サンプルで、Visual Studio からアプリを実行すると Unable to connect to web server 'AppName' エラー ダイアログが返されます。 Properties/launchSettings.json で指定されたポートが予期されますが、アプリでは app.Run("http://localhost:3000") で指定されたポートが使用されているため、Visual Studio からエラーが返されます。 コマンド ラインから次のポート変更サンプルを実行してください。

次のセクションでは、アプリが応答するポートを設定します。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

上記のコードの場合、アプリは 3000 ポートに応答します。

複数のポート

次のコードの場合、アプリは 30004000 ポートに応答します。

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

コマンド ラインからポートを設定する

次のコマンドにより、アプリは 7777 ポートに応答するようになります。

dotnet run --urls="https://localhost:7777"

appsettings.json ファイルで Kestrel エンドポイントも構成されている場合、 appsettings.json ファイルで指定されている URL が使用されます。 詳細については、「Kestrelエンドポイント構成」を参照してください。

環境からポートを読み取る

次のコードでは、環境からポートを読み取ります。

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

環境からポートを設定する際の推奨される方法は、次のセクションに示されているように、ASPNETCORE_URLS 環境変数を使用することです。

ASPNETCORE_URLS 環境変数を使用してポートを設定する

ASPNETCORE_URLS 環境変数は、ポートを設定するために使用できます。

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS は複数の URL をサポートしています。

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

すべてのインターフェイスでリッスンする

次のサンプルは、すべてのインターフェイスでリッスンする方法を示しています

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

ASPNETCORE_URLS を使用して、すべてのインターフェイスでリッスンする

上記のサンプルでは、ASPNETCORE_URLS を使用できます

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

ASPNETCORE_HTTPS_PORTS を使用して、すべてのインターフェイスでリッスンする

上記のサンプルでは、ASPNETCORE_HTTPS_PORTS および ASPNETCORE_HTTP_PORTS を使用できます。

ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000

詳細については、「ASP.NET Core Kestrel Web サーバーのエンドポイントを構成する」を参照してください

開発証明書を使用して HTTPS を指定する

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

開発証明書の詳細については、「Trust the ASP.NET Core HTTPS development certificate on Windows and macOS」(Windows および macOS で ASP.NET Core HTTPS 開発証明書を信頼する) を参照してください。

カスタム証明書を使用して HTTPS を指定する

次のセクションは、appsettings.json ファイルを使用してカスタム証明書を指定する方法と、構成により指定する方法を示しています。

カスタム証明書を appsettings.json で指定する

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

構成によりカスタム証明書を指定する

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

証明書 API を使用する

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

環境を読み取る

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

環境の使用の詳細については、「ASP.NET Core で複数の環境を使用する」を参照してください

構成

次のコードでは、環境システムから読み取ります。

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

詳細については、「ASP.NET Core の構成」を参照してください

ログの記録

次のコードは、アプリケーションの起動時にログにメッセージを書き込みます。

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

詳細については、「.NET Core および ASP.NET Core でのログ記録」を参照してください

依存関係の挿入 (DI) コンテナーにアクセスする

次のコードは、アプリケーションの起動時に DI コンテナーからサービスを取得する方法を示しています。


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

次のコードは、[FromKeyedServices] 属性を使用して DI コンテナーからキーにアクセスする方法を示しています。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

DI の詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。

WebApplicationBuilder

このセクションには、WebApplicationBuilder を使用するサンプル コードが含まれています。

コンテンツ ルート、アプリケーション名、環境を変更する

次のコードは、コンテンツ ルート、アプリケーション名、環境を設定します。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder は、事前に構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。

詳細については、「ASP.NET Core の基礎の概要」を参照してください

環境変数またはコマンド ラインを使ったコンテンツ ルート、アプリ名、環境の変更

次の表は、コンテンツ ルート、アプリ名、環境を変更するために使用される環境変数とコマンド ライン引数を示しています。

の機能 環境変数 コマンドライン引数
アプリケーション名 ASPNETCORE_APPLICATIONNAME --applicationName
環境名 ASPNETCORE_ENVIRONMENT --environment
コンテンツ ルート ASPNETCORE_CONTENTROOT --contentRoot

構成プロバイダーの追加

次のサンプルでは、INI 構成プロバイダーが追加されます。

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

詳細については、「ASP.NET Core の構成」の「ファイル構成プロバイダー」を参照してください。

構成を読み取る

既定では、WebApplicationBuilder は次を含む複数のソースから構成を読み取ります。

  • appSettings.json および appSettings.{environment}.json
  • 環境変数
  • コマンド ライン

読み取る構成ソースの完全な一覧については、「ASP.NET Core の構成」の「既定の構成」を参照してください。

次のコードは構成から HelloKey を読み取り、/ エンドポイントの値を表示します。 構成値が null 値の場合、message には "Hello" が代入されます。

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

環境を読み取る

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

ログ プロバイダーを追加する

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

サービスの追加

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder をカスタマイズする

IHostBuilder の既存の拡張メソッドは、Host プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

IWebHostBuilder をカスタマイズする

IWebHostBuilder の拡張メソッドは、WebApplicationBuilder.WebHost プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Web ルートを変更する

既定では、Web ルートは、wwwroot フォルダーのコンテンツ ルートに対して相対的です。 静的ファイル ミドルウェアは、Web ルートで静的ファイルを探します。 Web ルートは、WebHostOptions、コマンド ライン、UseWebRoot メソッドを使用して変更できます。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

カスタムの依存関係の挿入 (DI) コンテナー

次の例では、Autofac を使用しています。

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

ミドルウェアを追加する

既存の ASP.NET Core のミドルウェアは、WebApplication で構成できます。

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

詳細については、「ASP.NET Core のミドルウェア」を参照してください

開発者例外ページ

WebApplication.CreateBuilder は、事前構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。 開発者例外ページは、事前に構成された既定値で有効化されています。 開発環境で次のコードを実行して、/ にアクセスすると、例外を示すページが表示されます。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core のミドルウェア

次の表に示されているのは、Minimal API でよく使用されるミドルウェアの一部です。

ミドルウェア 説明 API
認証 認証のサポートを提供します。 UseAuthentication
承認 承認のサポートを提供します。 UseAuthorization
CORS クロス オリジン リソース共有を構成します。 UseCors
例外ハンドラー ミドルウェア パイプラインがスローする例外をグローバルに処理します。 UseExceptionHandler
転送されるヘッダー プロキシされたヘッダーを現在の要求に転送します。 UseForwardedHeaders
HTTPS リダイレクト すべての HTTP 要求を HTTPS にリダイレクトします。 UseHttpsRedirection
HTTP Strict Transport Security (HSTS) 特殊な応答ヘッダーを追加するセキュリティ拡張機能のミドルウェア。 UseHsts
要求ログ HTTP 要求と応答のログのサポートを提供します。 UseHttpLogging
要求のタイムアウト グローバルな既定値として、およびエンドポイントごとに、要求のタイムアウトを構成するサポートを提供します。 UseRequestTimeouts
W3C 要求ログ W3C 形式の HTTP 要求と応答のログのサポートを提供します。 UseW3CLogging
応答キャッシュ 応答のキャッシュのサポートを提供します。 UseResponseCaching
応答圧縮 応答の圧縮のサポートを提供します。 UseResponseCompression
セッション ユーザー セッションの管理のサポートを提供します。 UseSession
静的ファイル 静的ファイルとディレクトリ参照に対応するサポートを提供します。 UseStaticFilesUseFileServer
WebSocket WebSocket プロトコルを有効にします。 UseWebSockets

以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。

ルーティング

構成された 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

パラメーターのバインド

パラメーター バインドとは、要求データを、ルート ハンドラーで表現された厳密に型指定されたパラメーターに変換するプロセスです。 バインディング ソースは、パラメーターのバインド元を指定します。 バインディング ソースは明示的に指定するか、HTTP メソッドとパラメーターの型に基づいて推測できます。

サポートされているバインディング ソース:

  • ルート値
  • クエリ文字列
  • ヘッダー
  • Body (JSON として)
  • フォーム値
  • 依存関係の挿入によって指定されるサービス
  • Custom

次の GET ルート ハンドラーは、これらのパラメーター バインディング ソースの一部を使用しています。

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

次の表は、前の例で使用したパラメーターと、関連付けられているバインディング ソースとの関係を示しています。

パラメーター バインディング ソース
id ルート値
page クエリ文字列
customHeader header
service 依存関係の挿入によって指定

HTTP メソッド GETHEADOPTIONSDELETE は、本文から暗黙的にバインドしません。 これらの HTTP メソッドの本文から (JSON として) バインドするには、[FromBody]明示的にバインドするか、HttpRequest から読み取ります。

次の例の POST ルート ハンドラーは、person パラメーターに本文のバインディング ソースを (JSON として) 使用しています。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

前の例のパラメーターはすべて、要求データから自動的にバインドされます。 パラメーター バインディングが提供する便利さを示すために、次のルート ハンドラーは、要求からどのように直接要求データを読み取るかを示しています。

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

明示的なパラメーター バインド

属性を使用すると、パラメーターのバインド元を明示的に宣言できます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
パラメーター バインディング ソース
id 名前が id のルート値
page 名前が "p" のクエリ文字列
service 依存関係の挿入によって指定
contentType 名前が "Content-Type" のヘッダー

フォーム値からの明示的なバインド

[FromForm] 属性によってフォーム値がバインドされます。

app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.

別の方法には、[FromForm] で注釈がつけられたプロパティを持つカスタム型で [AsParameters] 属性を使用する方法があります。 たとえば、次のコードによって、フォーム値から NewTodoRequest レコード構造体のプロパティへのバインドが行われます。

app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);

詳細については、この記事で後述する AsParameters に関するセクションを参照してください。

完全なサンプル コードは、AspNetCore.Docs.Samples リポジトリにあります。

IFormFile と IFormFileCollection からのバインドをセキュリティで保護する

複雑なフォームのバインドは、[FromForm] を使用する IFormFileIFormFileCollection 使用してサポートされます。

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();

[FromForm] を使用して要求にバインドされたパラメーターには、偽造防止トークンが含まれます。 偽造防止トークンは、要求が処理されるときに検証されます。 詳細については、「最小限の API を使用した偽造防止」を参照してください。

詳細については、「最小限の API でのフォーム バインド」を参照してください。

完全なサンプル コードは、AspNetCore.Docs.Samples リポジトリにあります。

依存関係の挿入を使用したパラメーター バインド

Minimal API のパラメーター バインドでは、型がサービスとして構成されているときに、依存関係の挿入によってパラメーターをバインドします。 パラメーターに [FromServices] 属性を明示的に適用する必要はありません。 次のコードでは、どちらのアクションでも時刻が返されます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

省略可能なパラメーター

ルート ハンドラーで宣言されたパラメーターは、必要に応じて処理されます。

  • 要求がルートに一致する場合、すべての必須のパラメーターが要求で指定されている場合にのみルート ハンドラーが実行されます。
  • すべての必須のパラメーターが指定されていない場合は、エラーが発生します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products BadHttpRequestException: 必須のパラメーター "int pageNumber" が、クエリ文字列から提供されていません。
/products/1 HTTP 404 エラー、一致するルートなし

pageNumber を省略可能にするには、型を省略可能として定義するか、既定値を指定します。

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products2 1 が返される

上記の null 値の許容または既定値は、すべてのソースに適用されます。

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

app.MapPost("/products", (Product? product) => { });

app.Run();

上記のコードでは、要求本文が送信されていない場合、null 値の product でメソッドが呼び出されます。

: 無効なデータが指定され、パラメーターが null 値を許容する場合、ルート ハンドラーは実行されません

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products?pageNumber=two BadHttpRequestException: "two" からパラメーター "Nullable<int> pageNumber" をバインドできませんでした。
/products/two HTTP 404 エラー、一致するルートなし

詳細については、「バインドの失敗」セクションを参照してください。

特殊な型

次の型は、明示的な属性なしでバインドされます。

  • HttpContext: 現在の HTTP 要求または応答に関するすべての情報を持つコンテキスト:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 要求と HTTP 応答:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 現在の HTTP 要求に関連付けられているキャンセル トークン:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User からバインドされた、要求に関連付けられているユーザー:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

要求本文を Stream または PipeReader としてバインドする

ユーザーがデータを処理して次のようにする必要がある場合は、シナリオを効率的にサポートするために、要求本文を Stream または PipeReader としてバインドできます。

  • データを Blob Storage に格納するか、キュー プロバイダーにデータをエンキューします。
  • ワーカー プロセスまたはクラウド関数で、格納されたデータを処理します。

たとえば、データは Azure Queue Storage にエンキューされるか、Azure Blob Storage に格納される場合があります。

次のコードでは、バックグラウンド キューが実装されています。

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

次のコードでは、要求本文が Stream にバインドされています。

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

次に示すコードは、完全な Program.cs ファイルです。

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • データを読み取るとき、StreamHttpRequest.Body と同じオブジェクトです。
  • 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
  • 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では StreamPipeReader は使用できません。

IFormFile と IFormFileCollection を使用したファイルのアップロード

次のコードでは、IFormFileIFormFileCollection を使用して、ファイルをアップロードしています。

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

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

承認ヘッダークライアント証明書、または cookie ヘッダーを使用した認証されたファイルのアップロード要求がサポートされています。

IFormCollection、IFormFile、IFormFileCollection を使ったフォームへのバインディング

IFormCollectionIFormFileIFormFileCollection を使ったフォームベースのパラメーターからのバインディングがサポートされています。 Swagger UI との統合をサポートするために、フォーム パラメーターに対して OpenAPI メタデータが推論されます。

次のコードは、IFormFile 型から推論されたバインディングを使ってファイルをアップロードします。

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

警告: フォームを実装する場合、アプリはクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃防ぐ必要があります 先ほどのコードでは、IAntiforgery サービスを使って、偽造防止トークンを生成して検証することで XSRF 攻撃を防いでいます。

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

XSRF 攻撃の詳細については、「Minimal API を使用した偽造防止」を参照してください

詳細については、「最小限の API でのフォーム バインド」を参照してください。

フォームからコレクションと複合型にバインドする

バインディングは、次の場合にサポートされています。

  • コレクション (例: ListDictionary など)
  • 複合型 (例: Todo または Project など)

このコードには、次の項目が示されています。

  • マルチパート フォーム入力を複雑なオブジェクトにバインドする最小限のエンドポイント。
  • 偽造防止サービスを使用して、偽造防止トークンの生成と検証をサポートする方法。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid anti-forgery token");
    }
});

app.Run();

class Todo
{
    public string Name { get; set; } = string.Empty;
    public bool IsCompleted { get; set; } = false;
    public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}

上のコードでは以下の操作が行われます。

  • JS ON 本文から読み取る必要がある パラメーターから曖昧さを解消するには、ターゲット パラメーターに [FromForm] 属性で注釈を付ける必要があります
  • 要求デリゲート ジェネレーターを使ってコンパイルした最小限の API の場合、複合型またはコレクション型からのバインドはサポートされていません
  • マークアップには、isCompleted という名前の追加の非表示入力と、false の値が表示されます。 フォームの送信時に isCompleted チェック ボックスをオンにすると、値 truefalse の両方が値として送信されます。 チェックボックスをオフにすると、非表示の入力値 false のみが送信されます。 ASP.NET Core モデルバインド プロセスは、bool 値にバインドするときに最初の値のみを読み取ります。これにより、チェックボックスがオンの場合は true になり、チェックボックスがオフの場合は false になります。

前のエンドポイントに送信されたフォーム データの例を次に示します。

__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false

ヘッダーとクエリ文字列から配列と文字列値をバインドする

次のコードは、クエリ文字列をプリミティブ型の配列、文字列配列、StringValues にバインドする方法を示しています。

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

クエリ文字列またはヘッダー値を複合型の配列にバインドすることは、その型で TryParse が実装されている場合にサポートされます。 次のコードでは、文字列配列にバインドし、指定したタグを持つすべての項目を返します。

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

次のコードは、モデルと必要な TryParse の実装を示しています。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

次のコードでは、int 配列にバインドします。

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

上記のコードをテストするには、次のエンドポイントを追加して、データベースに Todo 項目を入力します。

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

HttpRepl などのツールを使って、次のデータを上記のエンドポイントに渡します。

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

次のコードでは、ヘッダー キー X-Todo-Id にバインドし、一致する Id 値を持つ Todo 項目を返します。

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

注意

クエリ文字列から string[] をバインドするとき、一致するクエリ文字列がないと null 値ではなく空の配列になります。

[AsParameters] を使用した引数リストのパラメーター バインド

AsParametersAttribute を使うと、複雑な、または再帰的なモデル バインドではなく、型へのシンプルなパラメーター バインドが可能になります。

次のコードがあるとします。

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

次の GET エンドポイントを考えてみます。

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

次の struct を使って、上記の強調表示されたパラメーターを置き換えることができます。

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

リファクタリングされた GET エンドポイントでは、上記の structAsParameters 属性と共に使用します。

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

次のコードは、アプリ内の追加のエンドポイントを示しています。

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

次のクラスは、パラメーター リストをリファクタリングするために使用されます。

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

次のコードは、AsParameters と上記の struct とクラスを使ってリファクタリングされたエンドポイントを示しています。

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

次の record 型を使って、上記のパラメーターを置き換えることができます。

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

structAsParameters と共に使うと、record 型を使うよりもパフォーマンスが向上します。

完全なサンプル コードAspNetCore.Docs.Samples リポジトリにあります。

カスタム バインド

パラメーター バインドは、2 つの方法でカスタマイズできます。

  1. ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な TryParse メソッドを追加することにより、カスタムの型をバインドします。
  2. 型に対して BindAsync メソッドを実装することにより、バインディング プロセスを制御します。

TryParse

TryParse には 2 つの API があります。

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

次のコードは、URI /map?Point=12.3,10.1 に対して Point: 12.3, 10.1 を表示します。

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync には次の API があります。

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

次のコードは、URI /products?SortBy=xyz&SortDir=Desc&Page=99 に対して SortBy:xyz, SortDirection:Desc, CurrentPage:99 を表示します。

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

バインドの失敗

バインドが失敗すると、フレームワークはデバッグ メッセージをログし、失敗の種類に応じてクライアントに様々さまざまな状態コードを返します。

障害モード null 値を許容するパラメーター型 バインディング ソース 状態コード
{ParameterType}.TryParsefalse を返します。 はい ルート/クエリ/ヘッダー 400
{ParameterType}.BindAsyncnull を返します。 はい custom 400
{ParameterType}.BindAsync がスローされる 問題ありません custom 500
JSON 本文を逆シリアル化できない 問題ありません body 400
コンテンツの型が正しくない (application/json でない) 問題ありません body 415

バインディングの優先順位

パラメーターからバインディング ソースを決定するルールは次の通りです。

  1. パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
    1. ルート値: [FromRoute]
    2. クエリ文字列: [FromQuery]
    3. ヘッダー: [FromHeader]
    4. 本文: [FromBody]
    5. フォーム: [FromForm]
    6. サービス: [FromServices]
    7. パラメーター値: [AsParameters]
  2. 特殊な型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. パラメーターの型に有効な静的 BindAsync メソッドがある。
  4. パラメーターの型が文字列であるか、有効な静的 TryParse メソッドがある。
    1. パラメーター名が app.Map("/todo/{id}", (int id) => {}); などのルート テンプレートにある場合、ルートからバインドされる。
    2. クエリ文字列からバインドされる。
  5. パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
  6. パラメーターが本文からのものである。

ボディ バインドの JSON 逆シリアル化オプションを構成する

ボディ バインド ソースでは、System.Text.Json を使用して逆シリアル化を行います。 この既定値は変更 "できません" が、JSON シリアル化と逆シリアル化のオプションを構成することはできます。

JSON 逆シリアル化オプションをグローバルに構成する

アプリにグローバルに適用されるオプションは、ConfigureHttpJsonOptions を呼び出すことによって構成できます。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

サンプル コードではシリアル化と逆シリアル化の両方を構成するため、出力 JSON に NameField の読み取りと NameField のインクルードを行うことができます。

エンドポイントの JSON 逆シリアル化オプションを構成する

ReadFromJsonAsync には、JsonSerializerOptions オブジェクトを受け入れるオーバーロードが用意されています。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

上記のコードでは、カスタマイズされたオプションが逆シリアル化にのみ適用されるため、出力 JSON では NameField が除外されます。

要求本文を読み取る

要求本文を直接読み取るには、HttpContext または HttpRequest パラメーターを使用します。

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

上記のコードでは次の操作が行われます。

  • HttpRequest.BodyReader を使用して要求本文にアクセスします。
  • 要求本文をローカル ファイルにコピーします。

応答

ルート ハンドラーは、次の型の戻り値をサポートしています。

  1. IResult ベース - これには Task<IResult>ValueTask<IResult> が含まれます
  2. string - これには Task<string>ValueTask<string> が含まれます
  3. T (その他の型) - これには Task<T>ValueTask<T> が含まれます
戻り値 動作 Content-Type
IResult フレームワークは IResult.ExecuteAsync を呼び出す IResult の実装によって決まる
string フレームワークは、文字列を直接応答に書き込む text/plain
T (その他の型) フレームワークは応答を JSON シリアル化する application/json

ルート ハンドラーの戻り値の詳細なガイドについては、Minimal API アプリケーションで応答を作成する方法に関するページを参照してください

戻り値の例

文字列の戻り値

app.MapGet("/hello", () => "Hello World");

JSON の戻り値

app.MapGet("/hello", () => new { Message = "Hello World" });

TypedResults を返す

次のコードは、TypedResults を返します。

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Results を返すより、TypedResults を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。

IResult の戻り値

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

次の例は、組み込みの結果の型を使用して応答をカスタマイズします。

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

カスタムの状態コード

app.MapGet("/405", () => Results.StatusCode(405));

Text

app.MapGet("/text", () => Results.Text("This is some text"));

ストリーム

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

その他の例については、Minimal API アプリで応答を作成する方法に関するページを参照してください。

リダイレクト

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

ファイル

app.MapGet("/download", () => Results.File("myfile.text"));

組み込みの結果

共通の結果ヘルパーは、ResultsTypedResults 静的クラスに含まれます。 Results を返すより、TypedResults を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。

結果のカスタマイズ

アプリケーションは、カスタムの IResult 型を実装することにより、応答を制御します。 次のコードは、HTML 結果型の例です。

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

拡張メソッドを Microsoft.AspNetCore.Http.IResultExtensions に追加して、これらのカスタムの結果をより見つけやすくすることを推奨します。

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

型指定された結果

IResult インターフェイスは、返されたオブジェクトを HTTP 応答にシリアル化する JSON の暗黙的なサポートを利用しない Minimal API から返される値を表すことができます。 異なる型の応答を表すさまざまな IResult オブジェクトを作成するには、静的な Results クラスを使います。 たとえば、応答状態コードを設定したり、別の URL にリダイレクトしたりします。

IResult を実装する型はパブリックであり、テスト時に型のアサーションを使用できます。 次に例を示します。

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

静的な TypedResults クラスの対応するメソッドの戻り値の型を調べて、キャスト先の正しいパブリック IResult 型を見つけることができます。

その他の例については、Minimal API アプリで応答を作成する方法に関するページを参照してください。

フィルター

参照トピック

承認

ルートは、承認ポリシーを使用して保護できます。 これらは、[Authorize] 属性により、または RequireAuthorization メソッドを使用して宣言できます。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

上記のコードは RequireAuthorization を使用して記述できます。

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

次のサンプルは、ポリシーベースの認可を使用しています。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

認証されていないユーザーがエンドポイントにアクセスできるようにする

[AllowAnonymous] は、認証されていないユーザーにエンドポイントへのアクセスを許可します。

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

CORS ポリシーを使用することにより、ルートに対して CORS を有効にできます。 CORS は、[EnableCors] 属性により、または RequireCors メソッドを使用して宣言できます。 次のサンプルにより、CORS が有効になります。

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

詳細については、「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」を参照してください

ValidateScopes と ValidateOnBuild

ValidateScopesValidateOnBuild は、開発環境では既定で有効になっていますが、他の環境では無効になっています。

ValidateOnBuildtrue の場合、DI コンテナーではビルド時にサービス構成を検証します。 サービス構成が無効な場合、サービスが要求されたときに実行時ではなく、アプリの起動時にビルドが失敗します。

ValidateScopestrue の場合、DI コンテナーでは、スコープ付きサービスがルート スコープから解決されていないことを検証します。 ルート スコープからスコープ付きサービスを解決すると、サービスが要求のスコープよりも長くメモリに保持されるため、メモリ リークが発生する可能性があります。

ValidateScopesValidateOnBuild は、パフォーマンス上の理由から、開発以外のモードでは既定では false です。

次のコードは、ValidateScopes が開発モードでは既定で有効になっているが、リリース モードでは無効になっていることを示しています。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }

次のコードは、ValidateOnBuild が開発モードでは既定で有効になっているが、リリース モードでは無効になっていることを示しています。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }

次のコードを使用すると、Development では ValidateScopesValidateOnBuild は無効になります。

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}

関連項目

このドキュメントでは、

Minimal API には次が含まれます。

WebApplication

次のコードが ASP.NET Core テンプレートによって生成されます。

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

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードは、コマンド ラインで dotnet new web を実行するか、Visual Studio で空の Web テンプレートを選択することによって作成できます。

次のコードにより、WebApplication (app) が作成されます。WebApplicationBuilder は明示的に作成されません。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create は、事前構成された既定値を使用して WebApplication クラスの新しいインスタンスを初期化します。

WebApplication では、特定の条件に応じて、Minimal API applications に次のミドルウェアが自動的に追加されます。

  • UseDeveloperExceptionPage は、HostingEnvironment"Development" である場合、最初に追加されます。
  • UseRouting は、ユーザー コードによって UseRouting がまだ呼び出されておらず、エンドポイントが構成されている (app.MapGet など) 場合、2 番目に追加されます。
  • UseEndpoints は、エンドポイントが構成されている場合、ミドルウェア パイプラインの最後に追加されます。
  • UseAuthentication は、ユーザー コードによって UseAuthentication がまだ呼び出されておらず、サービス プロバイダーで IAuthenticationSchemeProvider が検出できる場合、UseRouting の直後に追加されます。 IAuthenticationSchemeProvider は、AddAuthentication を使用するときに既定で追加され、サービスは IServiceProviderIsService を使用して検出されます。
  • UseAuthorization は、ユーザー コードによって UseAuthorization がまだ呼び出されておらず、サービス プロバイダーで IAuthorizationHandlerProvider が検出できる場合、次に追加されます。 IAuthorizationHandlerProvider は、AddAuthorization を使用するときに既定で追加され、サービスは IServiceProviderIsService を使用して検出されます。
  • ユーザーが構成したミドルウェアとエンドポイントは、UseRoutingUseEndpoints の間に追加されます。

次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCorsUseAuthenticationUseAuthorization の前に呼び出される必要があります。 UseCors を呼び出す場合、アプリでは UseAuthenticationUseAuthorization を呼び出す必要があります。

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

ターミナル ミドルウェアを追加する場合:

  • このミドルウェアは、UseEndpoints の後に追加される必要があります。
  • ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで UseRoutingUseEndpoints を呼び出す必要があります。
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

ターミナル ミドルウェアは、いずれのエンドポイントによっても要求が処理されない場合に実行されるミドルウェアです。

ポートの設定

Visual Studio または dotnet new で Web アプリが作成されると、アプリの応答先となるポートを指定する Properties/launchSettings.json ファイルが作成されます。 続くポート設定サンプルで、Visual Studio からアプリを実行すると Unable to connect to web server 'AppName' エラー ダイアログが返されます。 Properties/launchSettings.json で指定されたポートが予期されますが、アプリでは app.Run("http://localhost:3000") で指定されたポートが使用されているため、Visual Studio からエラーが返されます。 コマンド ラインから次のポート変更サンプルを実行してください。

次のセクションでは、アプリが応答するポートを設定します。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

上記のコードの場合、アプリは 3000 ポートに応答します。

複数のポート

次のコードの場合、アプリは 30004000 ポートに応答します。

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

コマンド ラインからポートを設定する

次のコマンドにより、アプリは 7777 ポートに応答するようになります。

dotnet run --urls="https://localhost:7777"

appsettings.json ファイルで Kestrel エンドポイントも構成されている場合、 appsettings.json ファイルで指定されている URL が使用されます。 詳細については、「Kestrelエンドポイント構成」を参照してください。

環境からポートを読み取る

次のコードでは、環境からポートを読み取ります。

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

環境からポートを設定する際の推奨される方法は、次のセクションに示されているように、ASPNETCORE_URLS 環境変数を使用することです。

ASPNETCORE_URLS 環境変数を使用してポートを設定する

ASPNETCORE_URLS 環境変数は、ポートを設定するために使用できます。

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS は複数の URL をサポートしています。

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

環境の使用の詳細については、「ASP.NET Core で複数の環境を使用する」を参照してください

すべてのインターフェイスでリッスンする

次のサンプルは、すべてのインターフェイスでリッスンする方法を示しています

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

ASPNETCORE_URLS を使用して、すべてのインターフェイスでリッスンする

上記のサンプルでは、ASPNETCORE_URLS を使用できます

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

開発証明書を使用して HTTPS を指定する

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

開発証明書の詳細については、「Trust the ASP.NET Core HTTPS development certificate on Windows and macOS」(Windows および macOS で ASP.NET Core HTTPS 開発証明書を信頼する) を参照してください。

カスタム証明書を使用して HTTPS を指定する

次のセクションは、appsettings.json ファイルを使用してカスタム証明書を指定する方法と、構成により指定する方法を示しています。

カスタム証明書を appsettings.json で指定する

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

構成によりカスタム証明書を指定する

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

証明書 API を使用する

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

構成

次のコードでは、環境システムから読み取ります。

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

詳細については、「ASP.NET Core の構成」を参照してください

ログの記録

次のコードは、アプリケーションの起動時にログにメッセージを書き込みます。

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

詳細については、「.NET Core および ASP.NET Core でのログ記録」を参照してください

依存関係の挿入 (DI) コンテナーにアクセスする

次のコードは、アプリケーションの起動時に DI コンテナーからサービスを取得する方法を示しています。


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。

WebApplicationBuilder

このセクションには、WebApplicationBuilder を使用するサンプル コードが含まれています。

コンテンツ ルート、アプリケーション名、環境を変更する

次のコードは、コンテンツ ルート、アプリケーション名、環境を設定します。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder は、事前に構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。

詳細については、「ASP.NET Core の基礎の概要」を参照してください

環境変数またはコマンド ラインを使用したコンテンツ ルート、アプリ名、環境の変更

次の表は、コンテンツ ルート、アプリ名、環境を変更するために使用される環境変数とコマンド ライン引数を示しています。

の機能 環境変数 コマンドライン引数
アプリケーション名 ASPNETCORE_APPLICATIONNAME --applicationName
環境名 ASPNETCORE_ENVIRONMENT --environment
コンテンツ ルート ASPNETCORE_CONTENTROOT --contentRoot

構成プロバイダーの追加

次のサンプルでは、INI 構成プロバイダーが追加されます。

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

詳細については、「ASP.NET Core の構成」の「ファイル構成プロバイダー」を参照してください。

構成を読み取る

既定では、WebApplicationBuilder は次を含む複数のソースから構成を読み取ります。

  • appSettings.json および appSettings.{environment}.json
  • 環境変数
  • コマンド ライン

次のコードは構成から HelloKey を読み取り、/ エンドポイントの値を表示します。 構成値が null 値の場合、message には "Hello" が代入されます。

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

読み取る構成ソースの完全な一覧については、「ASP.NET Core の構成」の「既定の構成」を参照してください

ログ プロバイダーを追加する

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

サービスの追加

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder をカスタマイズする

IHostBuilder の既存の拡張メソッドは、Host プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

IWebHostBuilder をカスタマイズする

IWebHostBuilder の拡張メソッドは、WebApplicationBuilder.WebHost プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Web ルートを変更する

既定では、Web ルートは、wwwroot フォルダーのコンテンツ ルートに対して相対的です。 静的ファイル ミドルウェアは、Web ルートで静的ファイルを探します。 Web ルートは、WebHostOptions、コマンド ライン、UseWebRoot メソッドを使用して変更できます。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

カスタムの依存関係の挿入 (DI) コンテナー

次の例では、Autofac を使用しています。

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

ミドルウェアを追加する

既存の ASP.NET Core のミドルウェアは、WebApplication で構成できます。

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

詳細については、「ASP.NET Core のミドルウェア」を参照してください

開発者例外ページ

WebApplication.CreateBuilder は、事前構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。 開発者例外ページは、事前に構成された既定値で有効化されています。 開発環境で次のコードを実行して、/ にアクセスすると、例外を示すページが表示されます。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core のミドルウェア

次の表に示されているのは、Minimal API でよく使用されるミドルウェアの一部です。

ミドルウェア 説明 API
認証 認証のサポートを提供します。 UseAuthentication
承認 承認のサポートを提供します。 UseAuthorization
CORS クロス オリジン リソース共有を構成します。 UseCors
例外ハンドラー ミドルウェア パイプラインがスローする例外をグローバルに処理します。 UseExceptionHandler
転送されるヘッダー プロキシされたヘッダーを現在の要求に転送します。 UseForwardedHeaders
HTTPS リダイレクト すべての HTTP 要求を HTTPS にリダイレクトします。 UseHttpsRedirection
HTTP Strict Transport Security (HSTS) 特殊な応答ヘッダーを追加するセキュリティ拡張機能のミドルウェア。 UseHsts
要求ログ HTTP 要求と応答のログのサポートを提供します。 UseHttpLogging
要求のタイムアウト グローバルな既定値として、およびエンドポイントごとに、要求のタイムアウトを構成するサポートを提供します。 UseRequestTimeouts
W3C 要求ログ W3C 形式の HTTP 要求と応答のログのサポートを提供します。 UseW3CLogging
応答キャッシュ 応答のキャッシュのサポートを提供します。 UseResponseCaching
応答圧縮 応答の圧縮のサポートを提供します。 UseResponseCompression
セッション ユーザー セッションの管理のサポートを提供します。 UseSession
静的ファイル 静的ファイルとディレクトリ参照に対応するサポートを提供します。 UseStaticFilesUseFileServer
WebSocket WebSocket プロトコルを有効にします。 UseWebSockets

以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。

ルーティング

構成された 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

パラメーターのバインド

パラメーター バインドとは、要求データを、ルート ハンドラーで表現された厳密に型指定されたパラメーターに変換するプロセスです。 バインディング ソースは、パラメーターのバインド元を指定します。 バインディング ソースは明示的に指定するか、HTTP メソッドとパラメーターの型に基づいて推測できます。

サポートされているバインディング ソース:

  • ルート値
  • クエリ文字列
  • ヘッダー
  • Body (JSON として)
  • 依存関係の挿入によって指定されるサービス
  • Custom

フォーム値からのバインドは、.NET 6 と 7 ではネイティブにサポートされて "いません"。

次の GET ルート ハンドラーは、これらのパラメーター バインディング ソースの一部を使用しています。

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

次の表は、前の例で使用したパラメーターと、関連付けられているバインディング ソースとの関係を示しています。

パラメーター バインディング ソース
id ルート値
page クエリ文字列
customHeader header
service 依存関係の挿入によって指定

HTTP メソッド GETHEADOPTIONSDELETE は、本文から暗黙的にバインドしません。 これらの HTTP メソッドの本文から (JSON として) バインドするには、[FromBody]明示的にバインドするか、HttpRequest から読み取ります。

次の例の POST ルート ハンドラーは、person パラメーターに本文のバインディング ソースを (JSON として) 使用しています。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

前の例のパラメーターはすべて、要求データから自動的にバインドされます。 パラメーター バインディングが提供する便利さを示すために、次のルート ハンドラーは、要求からどのように直接要求データを読み取るかを示しています。

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

明示的なパラメーター バインド

属性を使用すると、パラメーターのバインド元を明示的に宣言できます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
パラメーター バインディング ソース
id 名前が id のルート値
page 名前が "p" のクエリ文字列
service 依存関係の挿入によって指定
contentType 名前が "Content-Type" のヘッダー

注意

フォーム値からのバインドは、.NET 6 と 7 ではネイティブにサポートされて "いません"。

依存関係の挿入を使用したパラメーター バインド

Minimal API のパラメーター バインドでは、型がサービスとして構成されているときに、依存関係の挿入によってパラメーターをバインドします。 パラメーターに [FromServices] 属性を明示的に適用する必要はありません。 次のコードでは、どちらのアクションでも時刻が返されます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

省略可能なパラメーター

ルート ハンドラーで宣言されたパラメーターは、必要に応じて処理されます。

  • 要求がルートに一致する場合、すべての必須のパラメーターが要求で指定されている場合にのみルート ハンドラーが実行されます。
  • すべての必須のパラメーターが指定されていない場合は、エラーが発生します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products BadHttpRequestException: 必須パラメーター "int pageNumber" が、クエリ文字列から指定されていません
/products/1 HTTP 404 エラー、一致するルートなし

pageNumber を省略可能にするには、型を省略可能として定義するか、既定値を指定します。

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products2 1 が返される

上記の null 値の許容または既定値は、すべてのソースに適用されます。

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

app.MapPost("/products", (Product? product) => { });

app.Run();

上記のコードでは、要求本文が送信されていない場合、null 値の product でメソッドが呼び出されます。

: 無効なデータが指定され、パラメーターが null 値を許容する場合、ルート ハンドラーは実行されません

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products?pageNumber=two BadHttpRequestException: "two" からパラメーター "Nullable<int> pageNumber" をバインドできませんでした。
/products/two HTTP 404 エラー、一致するルートなし

詳細については、「バインドの失敗」セクションを参照してください。

特殊な型

次の型は、明示的な属性なしでバインドされます。

  • HttpContext: 現在の HTTP 要求または応答に関するすべての情報を持つコンテキスト:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 要求と HTTP 応答:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 現在の HTTP 要求に関連付けられているキャンセル トークン:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User からバインドされた、要求に関連付けられているユーザー:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

要求本文を Stream または PipeReader としてバインドする

ユーザーがデータを処理して次のようにする必要がある場合は、シナリオを効率的にサポートするために、要求本文を Stream または PipeReader としてバインドできます。

  • データを Blob Storage に格納するか、キュー プロバイダーにデータをエンキューします。
  • ワーカー プロセスまたはクラウド関数で、格納されたデータを処理します。

たとえば、データは Azure Queue Storage にエンキューされるか、Azure Blob Storage に格納される場合があります。

次のコードでは、バックグラウンド キューが実装されています。

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

次のコードでは、要求本文が Stream にバインドされています。

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

次に示すコードは、完全な Program.cs ファイルです。

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • データを読み取るとき、StreamHttpRequest.Body と同じオブジェクトです。
  • 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
  • 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では StreamPipeReader は使用できません。

IFormFile と IFormFileCollection を使用したファイルのアップロード

次のコードでは、IFormFileIFormFileCollection を使用して、ファイルをアップロードしています。

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

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

承認ヘッダークライアント証明書、または cookie ヘッダーを使用した認証されたファイルのアップロード要求がサポートされています。

ASP.NET Core 7.0 には、偽造防止のサポートが組み込まれていません。 偽造防止は、ASP.NET Core 8.0 以降でのみ使用できます。 ただし、IAntiforgery サービスを使用して実装することはできます。

ヘッダーとクエリ文字列から配列と文字列値をバインドする

次のコードは、クエリ文字列をプリミティブ型の配列、文字列配列、StringValues にバインドする方法を示しています。

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

クエリ文字列またはヘッダー値を複合型の配列にバインドすることは、その型で TryParse が実装されている場合にサポートされます。 次のコードでは、文字列配列にバインドし、指定したタグを持つすべての項目を返します。

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

次のコードは、モデルと必要な TryParse の実装を示しています。

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

次のコードでは、int 配列にバインドします。

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

上記のコードをテストするには、次のエンドポイントを追加して、データベースに Todo 項目を入力します。

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

HttpRepl などの API テスト ツールを使って、次のデータを上記のエンドポイントに渡します。

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

次のコードでは、ヘッダー キー X-Todo-Id にバインドし、一致する Id 値を持つ Todo 項目を返します。

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

注意

クエリ文字列から string[] をバインドするとき、一致するクエリ文字列がないと null 値ではなく空の配列になります。

[AsParameters] を使用した引数リストのパラメーター バインド

AsParametersAttribute を使うと、複雑な、または再帰的なモデル バインドではなく、型へのシンプルなパラメーター バインドが可能になります。

次のコードがあるとします。

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

次の GET エンドポイントを考えてみます。

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

次の struct を使って、上記の強調表示されたパラメーターを置き換えることができます。

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

リファクタリングされた GET エンドポイントでは、上記の structAsParameters 属性と共に使用します。

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

次のコードは、アプリ内の追加のエンドポイントを示しています。

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

次のクラスは、パラメーター リストをリファクタリングするために使用されます。

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

次のコードは、AsParameters と上記の struct とクラスを使ってリファクタリングされたエンドポイントを示しています。

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

次の record 型を使って、上記のパラメーターを置き換えることができます。

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

structAsParameters と共に使うと、record 型を使うよりもパフォーマンスが向上します。

完全なサンプル コードAspNetCore.Docs.Samples リポジトリにあります。

カスタム バインド

パラメーター バインドは、2 つの方法でカスタマイズできます。

  1. ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な TryParse メソッドを追加することにより、カスタムの型をバインドします。
  2. 型に対して BindAsync メソッドを実装することにより、バインディング プロセスを制御します。

TryParse

TryParse には 2 つの API があります。

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

次のコードは、URI /map?Point=12.3,10.1 に対して Point: 12.3, 10.1 を表示します。

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync には次の API があります。

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

次のコードは、URI /products?SortBy=xyz&SortDir=Desc&Page=99 に対して SortBy:xyz, SortDirection:Desc, CurrentPage:99 を表示します。

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

バインドの失敗

バインドが失敗すると、フレームワークはデバッグ メッセージをログし、失敗の種類に応じてクライアントに様々さまざまな状態コードを返します。

障害モード null 値を許容するパラメーター型 バインディング ソース 状態コード
{ParameterType}.TryParsefalse を返します。 はい ルート/クエリ/ヘッダー 400
{ParameterType}.BindAsyncnull を返します。 はい custom 400
{ParameterType}.BindAsync がスローされる どちらでもよい custom 500
JSON 本文を逆シリアル化できない どちらでもよい body 400
コンテンツの型が正しくない (application/json でない) どちらでもよい body 415

バインディングの優先順位

パラメーターからバインディング ソースを決定するルールは次の通りです。

  1. パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
    1. ルート値: [FromRoute]
    2. クエリ文字列: [FromQuery]
    3. ヘッダー: [FromHeader]
    4. 本文: [FromBody]
    5. サービス: [FromServices]
    6. パラメーター値: [AsParameters]
  2. 特殊な型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. パラメーターの型に有効な静的 BindAsync メソッドがある。
  4. パラメーターの型が文字列であるか、有効な静的 TryParse メソッドがある。
    1. パラメーター名が app.Map("/todo/{id}", (int id) => {}); などのルート テンプレートにある場合、ルートからバインドされる。
    2. クエリ文字列からバインドされる。
  5. パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
  6. パラメーターが本文からのものである。

ボディ バインドの JSON 逆シリアル化オプションを構成する

ボディ バインド ソースでは、System.Text.Json を使用して逆シリアル化を行います。 この既定値は変更 "できません" が、JSON シリアル化と逆シリアル化のオプションを構成することはできます。

JSON 逆シリアル化オプションをグローバルに構成する

アプリにグローバルに適用されるオプションは、ConfigureHttpJsonOptions を呼び出すことによって構成できます。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

サンプル コードではシリアル化と逆シリアル化の両方を構成するため、出力 JSON に NameField の読み取りと NameField のインクルードを行うことができます。

エンドポイントの JSON 逆シリアル化オプションを構成する

ReadFromJsonAsync には、JsonSerializerOptions オブジェクトを受け入れるオーバーロードが用意されています。 次の例には、パブリック フィールドと JSON 出力形式が含まれています。

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

上記のコードでは、カスタマイズされたオプションが逆シリアル化にのみ適用されるため、出力 JSON では NameField が除外されます。

要求本文を読み取る

要求本文を直接読み取るには、HttpContext または HttpRequest パラメーターを使用します。

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

上記のコードでは次の操作が行われます。

  • HttpRequest.BodyReader を使用して要求本文にアクセスします。
  • 要求本文をローカル ファイルにコピーします。

応答

ルート ハンドラーは、次の型の戻り値をサポートしています。

  1. IResult ベース - これには Task<IResult>ValueTask<IResult> が含まれます
  2. string - これには Task<string>ValueTask<string> が含まれます
  3. T (その他の型) - これには Task<T>ValueTask<T> が含まれます
戻り値 動作 Content-Type
IResult フレームワークは IResult.ExecuteAsync を呼び出す IResult の実装によって決まる
string フレームワークは、文字列を直接応答に書き込む text/plain
T (その他の型) フレームワークは応答を JSON シリアル化する application/json

ルート ハンドラーの戻り値の詳細なガイドについては、Minimal API アプリケーションで応答を作成する方法に関するページを参照してください

戻り値の例

文字列の戻り値

app.MapGet("/hello", () => "Hello World");

JSON の戻り値

app.MapGet("/hello", () => new { Message = "Hello World" });

TypedResults を返す

次のコードは、TypedResults を返します。

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Results を返すより、TypedResults を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。

IResult の戻り値

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

次の例は、組み込みの結果の型を使用して応答をカスタマイズします。

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

カスタムの状態コード

app.MapGet("/405", () => Results.StatusCode(405));

Text

app.MapGet("/text", () => Results.Text("This is some text"));

ストリーム

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

その他の例については、Minimal API アプリで応答を作成する方法に関するページを参照してください。

リダイレクト

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

ファイル

app.MapGet("/download", () => Results.File("myfile.text"));

組み込みの結果

共通の結果ヘルパーは、ResultsTypedResults 静的クラスに含まれます。 Results を返すより、TypedResults を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。

結果のカスタマイズ

アプリケーションは、カスタムの IResult 型を実装することにより、応答を制御します。 次のコードは、HTML 結果型の例です。

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

拡張メソッドを Microsoft.AspNetCore.Http.IResultExtensions に追加して、これらのカスタムの結果をより見つけやすくすることを推奨します。

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

型指定された結果

IResult インターフェイスは、返されたオブジェクトを HTTP 応答にシリアル化する JSON の暗黙的なサポートを利用しない Minimal API から返される値を表すことができます。 異なる型の応答を表すさまざまな IResult オブジェクトを作成するには、静的な Results クラスを使います。 たとえば、応答状態コードを設定したり、別の URL にリダイレクトしたりします。

IResult を実装する型はパブリックであり、テスト時に型のアサーションを使用できます。 次に例を示します。

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

静的な TypedResults クラスの対応するメソッドの戻り値の型を調べて、キャスト先の正しいパブリック IResult 型を見つけることができます。

その他の例については、Minimal API アプリで応答を作成する方法に関するページを参照してください。

フィルター

Minimal API アプリのフィルター」を参照してください。

承認

ルートは、承認ポリシーを使用して保護できます。 これらは、[Authorize] 属性により、または RequireAuthorization メソッドを使用して宣言できます。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

上記のコードは RequireAuthorization を使用して記述できます。

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

次のサンプルは、ポリシーベースの認可を使用しています。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

認証されていないユーザーがエンドポイントにアクセスできるようにする

[AllowAnonymous] は、認証されていないユーザーにエンドポイントへのアクセスを許可します。

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

CORS ポリシーを使用することにより、ルートに対して CORS を有効にできます。 CORS は、[EnableCors] 属性により、または RequireCors メソッドを使用して宣言できます。 次のサンプルにより、CORS が有効になります。

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

詳細については、「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」を参照してください

関連項目

このドキュメントでは、

Minimal API には次が含まれます。

WebApplication

次のコードが ASP.NET Core テンプレートによって生成されます。

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

app.MapGet("/", () => "Hello World!");

app.Run();

上記のコードは、コマンド ラインで dotnet new web を実行するか、Visual Studio で空の Web テンプレートを選択することによって作成できます。

次のコードにより、WebApplication (app) が作成されます。WebApplicationBuilder は明示的に作成されません。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create は、事前構成された既定値を使用して WebApplication クラスの新しいインスタンスを初期化します。

ポートの設定

Visual Studio または dotnet new で Web アプリが作成されると、アプリの応答先となるポートを指定する Properties/launchSettings.json ファイルが作成されます。 続くポート設定サンプルで、Visual Studio からアプリを実行すると Unable to connect to web server 'AppName' エラー ダイアログが返されます。 コマンド ラインから次のポート変更サンプルを実行してください。

次のセクションでは、アプリが応答するポートを設定します。

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run("http://localhost:3000");

上記のコードの場合、アプリは 3000 ポートに応答します。

複数のポート

次のコードの場合、アプリは 30004000 ポートに応答します。

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

コマンド ラインからポートを設定する

次のコマンドにより、アプリは 7777 ポートに応答するようになります。

dotnet run --urls="https://localhost:7777"

appsettings.json ファイルで Kestrel エンドポイントも構成されている場合、 appsettings.json ファイルで指定されている URL が使用されます。 詳細については、「Kestrelエンドポイント構成」を参照してください。

環境からポートを読み取る

次のコードでは、環境からポートを読み取ります。

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

環境からポートを設定する際の推奨される方法は、次のセクションに示されているように、ASPNETCORE_URLS 環境変数を使用することです。

ASPNETCORE_URLS 環境変数を使用してポートを設定する

ASPNETCORE_URLS 環境変数は、ポートを設定するために使用できます。

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS は複数の URL をサポートしています。

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

すべてのインターフェイスでリッスンする

次のサンプルは、すべてのインターフェイスでリッスンする方法を示しています

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

ASPNETCORE_URLS を使用して、すべてのインターフェイスでリッスンする

上記のサンプルでは、ASPNETCORE_URLS を使用できます

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

開発証明書を使用して HTTPS を指定する

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

開発証明書の詳細については、「Trust the ASP.NET Core HTTPS development certificate on Windows and macOS」(Windows および macOS で ASP.NET Core HTTPS 開発証明書を信頼する) を参照してください。

カスタム証明書を使用して HTTPS を指定する

次のセクションは、appsettings.json ファイルを使用してカスタム証明書を指定する方法と、構成により指定する方法を示しています。

カスタム証明書を appsettings.json で指定する

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

構成によりカスタム証明書を指定する

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

証明書 API を使用する

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

環境を読み取る

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

環境の使用の詳細については、「ASP.NET Core で複数の環境を使用する」を参照してください

構成

次のコードでは、環境システムから読み取ります。

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

詳細については、「ASP.NET Core の構成」を参照してください

ログの記録

次のコードは、アプリケーションの起動時にログにメッセージを書き込みます。

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

詳細については、「.NET Core および ASP.NET Core でのログ記録」を参照してください

依存関係の挿入 (DI) コンテナーにアクセスする

次のコードは、アプリケーションの起動時に DI コンテナーからサービスを取得する方法を示しています。


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

詳細については、「ASP.NET Core での依存関係の挿入」を参照してください。

WebApplicationBuilder

このセクションには、WebApplicationBuilder を使用するサンプル コードが含まれています。

コンテンツ ルート、アプリケーション名、環境を変更する

次のコードは、コンテンツ ルート、アプリケーション名、環境を設定します。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder は、事前に構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。

詳細については、「ASP.NET Core の基礎の概要」を参照してください

環境変数またはコマンド ラインを使用したコンテンツ ルート、アプリ名、環境の変更

次の表は、コンテンツ ルート、アプリ名、環境を変更するために使用される環境変数とコマンド ライン引数を示しています。

の機能 環境変数 コマンドライン引数
アプリケーション名 ASPNETCORE_APPLICATIONNAME --applicationName
環境名 ASPNETCORE_ENVIRONMENT --environment
コンテンツ ルート ASPNETCORE_CONTENTROOT --contentRoot

構成プロバイダーの追加

次のサンプルでは、INI 構成プロバイダーが追加されます。

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

詳細については、「ASP.NET Core の構成」の「ファイル構成プロバイダー」を参照してください。

構成を読み取る

既定では、WebApplicationBuilder は次を含む複数のソースから構成を読み取ります。

  • appSettings.json および appSettings.{environment}.json
  • 環境変数
  • コマンド ライン

読み取る構成ソースの完全な一覧については、「ASP.NET Core の構成」の「既定の構成」を参照してください

次のコードは構成から HelloKey を読み取り、/ エンドポイントの値を表示します。 構成値が null 値の場合、message には "Hello" が代入されます。

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

環境を読み取る

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

ログ プロバイダーを追加する

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

サービスの追加

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

IHostBuilder をカスタマイズする

IHostBuilder の既存の拡張メソッドは、Host プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

IWebHostBuilder をカスタマイズする

IWebHostBuilder の拡張メソッドは、WebApplicationBuilder.WebHost プロパティを使用してアクセスできます。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Web ルートを変更する

既定では、Web ルートは、wwwroot フォルダーのコンテンツ ルートに対して相対的です。 静的ファイル ミドルウェアは、Web ルートで静的ファイルを探します。 Web ルートは、WebHostOptions、コマンド ライン、UseWebRoot メソッドを使用して変更できます。

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

カスタムの依存関係の挿入 (DI) コンテナー

次の例では、Autofac を使用しています。

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

ミドルウェアを追加する

既存の ASP.NET Core のミドルウェアは、WebApplication で構成できます。

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

詳細については、「ASP.NET Core のミドルウェア」を参照してください

開発者例外ページ

WebApplication.CreateBuilder は、事前構成された既定値を使用して WebApplicationBuilder クラスの新しいインスタンスを初期化します。 開発者例外ページは、事前に構成された既定値で有効化されています。 開発環境で次のコードを実行して、/ にアクセスすると、例外を示すページが表示されます。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core のミドルウェア

次の表に示されているのは、Minimal API でよく使用されるミドルウェアの一部です。

ミドルウェア 説明 API
認証 認証のサポートを提供します。 UseAuthentication
承認 承認のサポートを提供します。 UseAuthorization
CORS クロス オリジン リソース共有を構成します。 UseCors
例外ハンドラー ミドルウェア パイプラインがスローする例外をグローバルに処理します。 UseExceptionHandler
転送されるヘッダー プロキシされたヘッダーを現在の要求に転送します。 UseForwardedHeaders
HTTPS リダイレクト すべての HTTP 要求を HTTPS にリダイレクトします。 UseHttpsRedirection
HTTP Strict Transport Security (HSTS) 特殊な応答ヘッダーを追加するセキュリティ拡張機能のミドルウェア。 UseHsts
要求ログ HTTP 要求と応答のログのサポートを提供します。 UseHttpLogging
W3C 要求ログ W3C 形式の HTTP 要求と応答のログのサポートを提供します。 UseW3CLogging
応答キャッシュ 応答のキャッシュのサポートを提供します。 UseResponseCaching
応答圧縮 応答の圧縮のサポートを提供します。 UseResponseCompression
セッション ユーザー セッションの管理のサポートを提供します。 UseSession
静的ファイル 静的ファイルとディレクトリ参照に対応するサポートを提供します。 UseStaticFilesUseFileServer
WebSocket WebSocket プロトコルを有効にします。 UseWebSockets

要求処理

以下のセクションでは、ルーティング、パラメーター バインディング、応答について説明します。

ルーティング

構成された WebApplicationMap{Verb}MapMethods をサポートします。

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();

ルート ハンドラー

ルート ハンドラーは、ルートが一致する場合に実行されるメソッドです。 ルート ハンドラーには、同期と非同期を含む任意の形式の関数を指定できます。 ルート ハンドラーには、ラムダ式、ローカル関数、インスタンス メソッド、静的メソッドを指定できます。

ラムダ式

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

エンドポイントへの 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 のルーティング」の「ルート制約参照」を参照してください。

パラメーター バインド

パラメーター バインドとは、要求データを、ルート ハンドラーで表現された厳密に型指定されたパラメーターに変換するプロセスです。 バインディング ソースは、パラメーターのバインド元を指定します。 バインディング ソースは明示的に指定するか、HTTP メソッドとパラメーターの型に基づいて推測できます。

サポートされているバインディング ソース:

  • ルート値
  • クエリ文字列
  • ヘッダー
  • Body (JSON として)
  • 依存関係の挿入によって指定されるサービス
  • Custom

注意

フォーム値からのバインドは、.NET ではネイティブにサポートされて "いません"。

次の例の GET ルート ハンドラーは、これらのパラメーター バインディング ソースの一部を使用しています。

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

次の表は、前の例で使用したパラメーターと、関連付けられているバインディング ソースとの関係を示しています。

パラメーター バインディング ソース
id ルート値
page クエリ文字列
customHeader header
service 依存関係の挿入によって指定

HTTP メソッド GETHEADOPTIONSDELETE は、本文から暗黙的にバインドしません。 これらの HTTP メソッドの本文から (JSON として) バインドするには、[FromBody]明示的にバインドするか、HttpRequest から読み取ります。

次の例の POST ルート ハンドラーは、person パラメーターに本文のバインディング ソースを (JSON として) 使用しています。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

前の例のパラメーターはすべて、要求データから自動的にバインドされます。 パラメーター バインディングが提供する便利さを示すために、次の例のルート ハンドラーは、要求からどのように直接要求データを読み取るかを示しています。

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

明示的なパラメーター バインド

属性を使用すると、パラメーターのバインド元を明示的に宣言できます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
パラメーター バインディング ソース
id 名前が id のルート値
page 名前が "p" のクエリ文字列
service 依存関係の挿入によって指定
contentType 名前が "Content-Type" のヘッダー

注意

フォーム値からのバインドは、.NET ではネイティブにサポートされて "いません"。

DI によるパラメーター バインド

Minimal API のパラメーター バインドでは、型がサービスとして構成されているときに、依存関係の挿入によってパラメーターをバインドします。 パラメーターに [FromServices] 属性を明示的に適用する必要はありません。 次のコードでは、どちらのアクションでも時刻が返されます。

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

省略可能なパラメーター

ルート ハンドラーで宣言されたパラメーターは、必要に応じて処理されます。

  • 要求がルートに一致する場合、すべての必須のパラメーターが要求で指定されている場合にのみルート ハンドラーが実行されます。
  • すべての必須のパラメーターが指定されていない場合は、エラーが発生します。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products BadHttpRequestException: 必須パラメーター "int pageNumber" が、クエリ文字列から指定されていません
/products/1 HTTP 404 エラー、一致するルートなし

pageNumber を省略可能にするには、型を省略可能として定義するか、既定値を指定します。

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products2 1 が返される

上記の null 値の許容または既定値は、すべてのソースに適用されます。

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

app.MapPost("/products", (Product? product) => { });

app.Run();

上記のコードでは、要求本文が送信されていない場合、null 値の product でメソッドが呼び出されます。

: 無効なデータが指定され、パラメーターが null 値を許容する場合、ルート ハンドラーは実行されません

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

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI 結果
/products?pageNumber=3 3 が返される
/products 1 が返される
/products?pageNumber=two BadHttpRequestException: "two" からパラメーター "Nullable<int> pageNumber" をバインドできませんでした。
/products/two HTTP 404 エラー、一致するルートなし

詳細については、「バインドの失敗」セクションを参照してください。

特殊な型

次の型は、明示的な属性なしでバインドされます。

  • HttpContext: 現在の HTTP 要求または応答に関するすべての情報を持つコンテキスト:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: HTTP 要求と HTTP 応答:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: 現在の HTTP 要求に関連付けられているキャンセル トークン:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: HttpContext.User からバインドされた、要求に関連付けられているユーザー:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

カスタム バインド

パラメーター バインドは、2 つの方法でカスタマイズできます。

  1. ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な TryParse メソッドを追加することにより、カスタムの型をバインドします。
  2. 型に対して BindAsync メソッドを実装することにより、バインディング プロセスを制御します。

TryParse

TryParse には 2 つの API があります。

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

次のコードは、URI /map?Point=12.3,10.1 に対して Point: 12.3, 10.1 を表示します。

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

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync には次の API があります。

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

次のコードは、URI /products?SortBy=xyz&SortDir=Desc&Page=99 に対して SortBy:xyz, SortDirection:Desc, CurrentPage:99 を表示します。

using System.Reflection;

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

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

バインドの失敗

バインドが失敗すると、フレームワークはデバッグ メッセージをログし、失敗の種類に応じてクライアントに様々さまざまな状態コードを返します。

障害モード null 値を許容するパラメーター型 バインディング ソース 状態コード
{ParameterType}.TryParsefalse を返します。 はい ルート/クエリ/ヘッダー 400
{ParameterType}.BindAsyncnull を返します。 はい custom 400
{ParameterType}.BindAsync がスローされる どちらでもよい custom 500
JSON 本文を逆シリアル化できない どちらでもよい body 400
コンテンツの型が正しくない (application/json でない) どちらでもよい body 415

バインディングの優先順位

パラメーターからバインディング ソースを決定するルールは次の通りです。

  1. パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
    1. ルート値: [FromRoute]
    2. クエリ文字列: [FromQuery]
    3. ヘッダー: [FromHeader]
    4. 本文: [FromBody]
    5. Service: [FromServices]
  2. 特殊な型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
  3. パラメーターの型に有効な BindAsync メソッドがある。
  4. パラメーターの型が文字列であるか、有効な TryParse メソッドがある。
    1. パラメーター名が app.Map("/todo/{id}", (int id) => {}); などのルート テンプレートにある場合、ルートからバインドされる。
    2. クエリ文字列からバインドされる。
  5. パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
  6. パラメーターが本文からのものである。

JSON バインドをカスタマイズする

本文のバインディング ソースは System.Text.Json を使用して逆シリアル化を行います。 この既定値を変更することは "できません" が、既に説明した他の方法を使ってバインディングをカスタマイズすることはできます。 JSON シリアライザー オプションをカスタマイズするには、次のようなコードを使用します。

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/products", (Product product) => product);

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

上記のコードでは次の操作が行われます。

  • 入力および出力の既定の JSON オプションが構成されます。
  • 次の JSON が返されます
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    POST の場合
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

要求本文を読み取る

要求本文を直接読み取るには、HttpContext または HttpRequest パラメーターを使用します。

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

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

上記のコードでは次の操作が行われます。

  • HttpRequest.BodyReader を使用して要求本文にアクセスします。
  • 要求本文をローカル ファイルにコピーします。

応答

ルート ハンドラーは、次の型の戻り値をサポートしています。

  1. IResult ベース - これには Task<IResult>ValueTask<IResult> が含まれます
  2. string - これには Task<string>ValueTask<string> が含まれます
  3. T (その他の型) - これには Task<T>ValueTask<T> が含まれます
戻り値 動作 Content-Type
IResult フレームワークは IResult.ExecuteAsync を呼び出す IResult の実装によって決まる
string フレームワークは、文字列を直接応答に書き込む text/plain
T (その他の型) フレームワークが応答を JSON にシリアル化する application/json

戻り値の例

文字列の戻り値

app.MapGet("/hello", () => "Hello World");

JSON の戻り値

app.MapGet("/hello", () => new { Message = "Hello World" });

IResult の戻り値

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

次の例は、組み込みの結果の型を使用して応答をカスタマイズします。

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
カスタムの状態コード
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
ストリーム
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
リダイレクト
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
ファイル
app.MapGet("/download", () => Results.File("myfile.text"));

組み込みの結果

共通の結果ヘルパーは、Microsoft.AspNetCore.Http.Results 静的クラスに含まれています。

説明 応答の種類 状態コード API
詳細なオプションを使用した JSON 応答の書き込み application/json 200 Results.Json
JSON 応答の書き込み application/json 200 Results.Ok
テキスト応答の書き込み text/plain (既定値)、構成可能 200 Results.Text
応答をバイトとして書き込む application/octet-stream (既定値)、構成可能 200 Results.Bytes
応答にバイトのストリームを書き込む application/octet-stream (既定値)、構成可能 200 Results.Stream
content-disposition ヘッダーによるダウンロードのために、応答に対してファイルをストリーミングする application/octet-stream (既定値)、構成可能 200 Results.File
状態コードを 404 に設定し、オプションの JSON 応答を指定する 該当なし 404 Results.NotFound
状態コードを 204 に設定する 該当なし 204 Results.NoContent
状態コードを 422 に設定し、オプションの JSON 応答を指定する 該当なし 422 Results.UnprocessableEntity
状態コードを 400 に設定し、オプションの JSON 応答を指定する 該当なし 400 Results.BadRequest
状態コードを 409 に設定し、オプションの JSON 応答を指定する 該当なし 409 Results.Conflict
問題の詳細の JSON オブジェクトを応答に書き込む 該当なし 500 (既定値)、構成可能 Results.Problem
問題の詳細の JSON オブジェクトを検証エラーと共に応答に書き込む 該当なし 該当なし、構成可能 Results.ValidationProblem

結果のカスタマイズ

アプリケーションは、カスタムの IResult 型を実装することにより、応答を制御します。 次のコードは、HTML 結果型の例です。

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

拡張メソッドを Microsoft.AspNetCore.Http.IResultExtensions に追加して、これらのカスタムの結果をより見つけやすくすることを推奨します。

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

承認

ルートは、承認ポリシーを使用して保護できます。 これらは、[Authorize] 属性により、または RequireAuthorization メソッドを使用して宣言できます。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

上記のコードは RequireAuthorization を使用して記述できます。

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

次のサンプルは、ポリシーベースの認可を使用しています。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

認証されていないユーザーがエンドポイントにアクセスできるようにする

[AllowAnonymous] は、認証されていないユーザーにエンドポイントへのアクセスを許可します。

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

CORS ポリシーを使用することにより、ルートに対して CORS を有効にできます。 CORS は、[EnableCors] 属性により、または RequireCors メソッドを使用して宣言できます。 次のサンプルにより、CORS が有効になります。

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

詳細については、「ASP.NET Core でクロスオリジン要求 (CORS) を有効にする」を参照してください

関連項目

Minimal API での OpenAPI のサポート