注
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
このドキュメントでは、
- Minimal API のクイック リファレンスを提供します。
- 経験豊富な開発者を対象としています。 概要については、「チュートリアル: ASP.NET Core を使って最小 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
は、ユーザー コードによってUseRouting
がまだ呼び出されておらず、サービス プロバイダーでUseAuthentication
が検出できる場合、IAuthenticationSchemeProvider
の直後に追加されます。IAuthenticationSchemeProvider
は、AddAuthentication
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 -
UseAuthorization
は、ユーザー コードによってUseAuthorization
がまだ呼び出されておらず、サービス プロバイダーでIAuthorizationHandlerProvider
が検出できる場合、次に追加されます。IAuthorizationHandlerProvider
は、AddAuthorization
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 - ユーザーが構成したミドルウェアとエンドポイントは、
UseRouting
とUseEndpoints
の間に追加されます。
次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。
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 => {});
場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCors は UseAuthentication と UseAuthorization の前に呼び出される必要があります。
UseAuthentication
を呼び出す場合、アプリでは UseAuthorization
と UseCors
を呼び出す必要があります。
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting
への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
ターミナル ミドルウェアを追加する場合:
- このミドルウェアは、
UseEndpoints
の後に追加される必要があります。 - ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで
UseRouting
とUseEndpoints
を呼び出す必要があります。
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
ポートに応答します。
複数のポート
次のコードの場合、アプリは 3000
と 4000
ポートに応答します。
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"
Kestrel ファイルで appsettings.json
エンドポイントも構成されている場合、 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 | --環境 |
コンテンツ ルート | ASPNETCORE_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 |
静的ファイル | 静的ファイルとディレクトリ参照に対応するサポートを提供します。 | UseStaticFiles、UseFileServer |
WebSocket | WebSocket プロトコルを有効にします。 | UseWebSockets |
以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。
ルーティング
構成された WebApplication
では、Map{Verb}
と MapMethods をサポートします。ここで {Verb}
は、Get
、Post
、Put
または 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 route 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 The user id is 3 and book id is 7
にから /users/3/books/7
が返されます。
ルート ハンドラーは、捕捉するパラメーターを宣言できます。 キャプチャするように宣言されたパラメーターを持つルートに対して要求が実行されると、パラメーターが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userId
と bookId
は両方とも 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 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationを追加する WithMetadata や のようなメソッドを 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;
}
このシナリオでは、Location
結果の 201 Created
ヘッダーに相対アドレスを使用できます。
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
エンドポイントの最初のグループは、/public/todos
のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 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 メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- フォーム値
- 依存関係の挿入によって指定されるサービス
- 習慣
次の 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 |
ヘッダ |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの 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.
別の方法には、[AsParameters]
で注釈がつけられたプロパティを持つカスタム型で [FromForm]
属性を使用する方法があります。 たとえば、次のコードによって、フォーム値から 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 からのバインドをセキュリティで保護する
複雑なフォームのバインドは、IFormFile を使用する IFormFileCollection と [FromForm]
使用してサポートされます。
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"));
HttpRequest と HttpResponse: 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();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
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 を使ったフォームへのバインディング
IFormCollection、IFormFile、IFormFileCollection を使ったフォームベースのパラメーターからのバインディングがサポートされています。 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 でのフォーム バインド」を参照してください。
フォームからコレクションと複合型にバインドする
バインディングは、次の場合にサポートされています。
- コレクション (例: List や Dictionary など)
- 複合型 (例:
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 antiforgery 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));
}
上のコードでは以下の操作が行われます。
- JSON 本文から読み取る必要があるパラメーターから曖昧さを解消するには、ターゲット パラメーターに 属性で注釈を付ける。
- 要求デリゲート ジェネレーターを使ってコンパイルした最小限の API の場合、複合型またはコレクション型からのバインドはサポートされていません。
- マークアップには、
isCompleted
という名前の追加の非表示入力と、false
の値が表示されます。 フォームの送信時にisCompleted
チェック ボックスをオンにすると、値true
とfalse
の両方が値として送信されます。 チェックボックスをオフにすると、非表示の入力値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
にバインドし、一致する Todo
値を持つ Id
項目を返します。
// 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
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
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);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
トライパース
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI Point: 12.3, 10.1
に対して /map?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 SortBy:xyz, SortDirection:Desc, CurrentPage:99
に対して /products?SortBy=xyz&SortDir=Desc&Page=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}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | 習慣 | 400 |
{ParameterType}.BindAsync がスローされる |
問題ありません | 習慣 | 500 |
JSON 本文を逆シリアル化できない | 問題ありません | 体 | 400 |
コンテンツの型が正しくない (application/json でない) |
問題ありません | 体 | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- フォーム:
[FromForm]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormCollection
(HttpContext.Request.Form
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- パラメーター名が
app.Map("/todo/{id}", (int id) => {});
などのルート テンプレートにある場合、ルートからバインドされる。 - クエリ文字列からバインドされる。
- パラメーター名が
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの 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 を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
最小限の API での検証のサポート
最小限の API での検証のサポートが利用可能になりました。 この機能を使用すると、API エンドポイントに送信されたデータの検証を要求できます。 検証を有効にすると、ASP.NET Core ランタイムは、次に定義されているすべての検証を実行できます。
- クエリ
- ヘッダー
- リクエストの本文
検証は、 DataAnnotations
名前空間の属性を使用して定義されます。
Minimal API エンドポイントへのパラメーターがクラスまたはレコードの種類である場合、検証属性が自動的に適用されます。 次に例を示します。
public record Product(
[Required] string Name,
[Range(1, 1000)] int Quantity);
開発者は、次の方法で検証システムの動作をカスタマイズします。
- カスタム
[Validation]
属性実装の作成。 - 複雑な検証ロジック用の
IValidatableObject
インターフェイスを実装する。
検証が失敗した場合、ランタイムは検証エラーの詳細を含む 400 Bad Request 応答を返します。
最小限の API に対して組み込みの検証サポートを有効にする
AddValidation
拡張メソッドを呼び出して、アプリケーションのサービス コンテナーに必要なサービスを登録することで、最小限の API に対する組み込みの検証サポートを有効にします。
builder.Services.AddValidation();
この実装では、最小限の API ハンドラーで定義されている型、または最小限の API ハンドラーで定義された型の基本型として、自動的に検出されます。 エンドポイント フィルターは、これらの種類に対して検証を実行し、エンドポイントごとに追加されます。
次の例のように、 DisableValidation
拡張メソッドを使用して、特定のエンドポイントに対して検証を無効にすることができます。
app.MapPost("/products",
([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
=> TypedResults.Ok(productId))
.DisableValidation();
応答
ルート ハンドラーは、次の型の戻り値をサポートしています。
-
IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます -
string
- これにはTask<string>
とValueTask<string>
が含まれます -
T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます
戻り値 | 動作 | コンテンツタイプ |
---|---|---|
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!" }));
TypedResults
を返すより、Results を返すことをお勧めします。 詳しくは、「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));
テキスト
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"));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。
TypedResults
を返すより、Results
を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。
ヘッダーの変更
応答ヘッダーを変更するには、HttpResponse
オブジェクトを使用します。
app.MapGet("/", (HttpContext context) => {
// Set a custom header
context.Response.Headers["X-Custom-Header"] = "CustomValue";
// Set a known header
context.Response.Headers.CacheControl = $"public,max-age=3600";
return "Hello World";
});
結果のカスタマイズ
アプリケーションは、カスタムの 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
クラスを使います。 たとえば、応答状態コードを設定したり、別の 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) を有効にする」を参照してください
ValidateScopes と ValidateOnBuild
ValidateScopes と ValidateOnBuild は、開発環境では既定で有効になっていますが、他の環境では無効になっています。
ValidateOnBuild
が true
の場合、DI コンテナーではビルド時にサービス構成を検証します。 サービス構成が無効な場合、サービスが要求されたときに実行時ではなく、アプリの起動時にビルドが失敗します。
ValidateScopes
が true
の場合、DI コンテナーでは、スコープ付きサービスがルート スコープから解決されていないことを検証します。 ルート スコープからスコープ付きサービスを解決すると、サービスが要求のスコープよりも長くメモリに保持されるため、メモリ リークが発生する可能性があります。
ValidateScopes
と ValidateOnBuild
は、パフォーマンス上の理由から、開発以外のモードでは既定では 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 { }
次のコードを使用すると、ValidateScopes
では ValidateOnBuild
と Development
は無効になります。
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 クイック リファレンス
- OpenAPI ドキュメントを生成する
- Minimal API アプリケーションで応答を作成する
- Minimal API アプリのフィルター
- Minimal API アプリでエラーを処理する
- Minimal API での認証と認可
- Minimal API アプリをテストする
- 短絡ルーティング
- Identity API エンドポイント
- キー付きサービス依存関係挿入コンテナーのサポート
- 最小限の API エンドポイントの背後にある外観
- ASP.NET Core の最小 API の整理
- GitHub での Fluent Validation に関するディスカッション
このドキュメントでは、
- Minimal API のクイック リファレンスを提供します。
- 経験豊富な開発者を対象としています。 概要については、「チュートリアル: ASP.NET Core を使って最小 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
は、ユーザー コードによってUseRouting
がまだ呼び出されておらず、サービス プロバイダーでUseAuthentication
が検出できる場合、IAuthenticationSchemeProvider
の直後に追加されます。IAuthenticationSchemeProvider
は、AddAuthentication
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 -
UseAuthorization
は、ユーザー コードによってUseAuthorization
がまだ呼び出されておらず、サービス プロバイダーでIAuthorizationHandlerProvider
が検出できる場合、次に追加されます。IAuthorizationHandlerProvider
は、AddAuthorization
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 - ユーザーが構成したミドルウェアとエンドポイントは、
UseRouting
とUseEndpoints
の間に追加されます。
次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。
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 => {});
場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCors は UseAuthentication と UseAuthorization の前に呼び出される必要があります。
UseAuthentication
を呼び出す場合、アプリでは UseAuthorization
と UseCors
を呼び出す必要があります。
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting
への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
ターミナル ミドルウェアを追加する場合:
- このミドルウェアは、
UseEndpoints
の後に追加される必要があります。 - ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで
UseRouting
とUseEndpoints
を呼び出す必要があります。
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
ポートに応答します。
複数のポート
次のコードの場合、アプリは 3000
と 4000
ポートに応答します。
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"
Kestrel ファイルで appsettings.json
エンドポイントも構成されている場合、 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 | --環境 |
コンテンツ ルート | ASPNETCORE_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 |
静的ファイル | 静的ファイルとディレクトリ参照に対応するサポートを提供します。 | UseStaticFiles、UseFileServer |
WebSocket | WebSocket プロトコルを有効にします。 | UseWebSockets |
以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。
ルーティング
構成された WebApplication
では、Map{Verb}
と MapMethods をサポートします。ここで {Verb}
は、Get
、Post
、Put
または 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 route 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 The user id is 3 and book id is 7
にから /users/3/books/7
が返されます。
ルート ハンドラーは、捕捉するパラメーターを宣言できます。 キャプチャするように宣言されたパラメーターを持つルートに対して要求が実行されると、パラメーターが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userId
と bookId
は両方とも 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 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationを追加する WithMetadata や のようなメソッドを 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;
}
このシナリオでは、Location
結果の 201 Created
ヘッダーに相対アドレスを使用できます。
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
エンドポイントの最初のグループは、/public/todos
のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 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 メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- フォーム値
- 依存関係の挿入によって指定されるサービス
- 習慣
次の 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 |
ヘッダ |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの 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.
別の方法には、[AsParameters]
で注釈がつけられたプロパティを持つカスタム型で [FromForm]
属性を使用する方法があります。 たとえば、次のコードによって、フォーム値から 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 からのバインドをセキュリティで保護する
複雑なフォームのバインドは、IFormFile を使用する IFormFileCollection と [FromForm]
使用してサポートされます。
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"));
HttpRequest と HttpResponse: 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();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
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 を使ったフォームへのバインディング
IFormCollection、IFormFile、IFormFileCollection を使ったフォームベースのパラメーターからのバインディングがサポートされています。 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 でのフォーム バインド」を参照してください。
フォームからコレクションと複合型にバインドする
バインディングは、次の場合にサポートされています。
- コレクション (例: List や Dictionary など)
- 複合型 (例:
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 antiforgery 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));
}
上のコードでは以下の操作が行われます。
- JSON 本文から読み取る必要があるパラメーターから曖昧さを解消するには、ターゲット パラメーターに 属性で注釈を付ける。
- 要求デリゲート ジェネレーターを使ってコンパイルした最小限の API の場合、複合型またはコレクション型からのバインドはサポートされていません。
- マークアップには、
isCompleted
という名前の追加の非表示入力と、false
の値が表示されます。 フォームの送信時にisCompleted
チェック ボックスをオンにすると、値true
とfalse
の両方が値として送信されます。 チェックボックスをオフにすると、非表示の入力値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
にバインドし、一致する Todo
値を持つ Id
項目を返します。
// 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
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
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);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
トライパース
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI Point: 12.3, 10.1
に対して /map?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 SortBy:xyz, SortDirection:Desc, CurrentPage:99
に対して /products?SortBy=xyz&SortDir=Desc&Page=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}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | 習慣 | 400 |
{ParameterType}.BindAsync がスローされる |
問題ありません | 習慣 | 500 |
JSON 本文を逆シリアル化できない | 問題ありません | 体 | 400 |
コンテンツの型が正しくない (application/json でない) |
問題ありません | 体 | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- フォーム:
[FromForm]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormCollection
(HttpContext.Request.Form
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- パラメーター名が
app.Map("/todo/{id}", (int id) => {});
などのルート テンプレートにある場合、ルートからバインドされる。 - クエリ文字列からバインドされる。
- パラメーター名が
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの 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 を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
応答
ルート ハンドラーは、次の型の戻り値をサポートしています。
-
IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます -
string
- これにはTask<string>
とValueTask<string>
が含まれます -
T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます
戻り値 | 動作 | コンテンツタイプ |
---|---|---|
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!" }));
TypedResults
を返すより、Results を返すことをお勧めします。 詳しくは、「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));
テキスト
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"));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。
TypedResults
を返すより、Results
を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。
ヘッダーの変更
応答ヘッダーを変更するには、HttpResponse
オブジェクトを使用します。
app.MapGet("/", (HttpContext context) => {
// Set a custom header
context.Response.Headers["X-Custom-Header"] = "CustomValue";
// Set a known header
context.Response.Headers.CacheControl = $"public,max-age=3600";
return "Hello World";
});
結果のカスタマイズ
アプリケーションは、カスタムの 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
クラスを使います。 たとえば、応答状態コードを設定したり、別の 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) を有効にする」を参照してください
ValidateScopes と ValidateOnBuild
ValidateScopes と ValidateOnBuild は、開発環境では既定で有効になっていますが、他の環境では無効になっています。
ValidateOnBuild
が true
の場合、DI コンテナーではビルド時にサービス構成を検証します。 サービス構成が無効な場合、サービスが要求されたときに実行時ではなく、アプリの起動時にビルドが失敗します。
ValidateScopes
が true
の場合、DI コンテナーでは、スコープ付きサービスがルート スコープから解決されていないことを検証します。 ルート スコープからスコープ付きサービスを解決すると、サービスが要求のスコープよりも長くメモリに保持されるため、メモリ リークが発生する可能性があります。
ValidateScopes
と ValidateOnBuild
は、パフォーマンス上の理由から、開発以外のモードでは既定では 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 { }
次のコードを使用すると、ValidateScopes
では ValidateOnBuild
と Development
は無効になります。
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 クイック リファレンス
- OpenAPI ドキュメントを生成する
- Minimal API アプリケーションで応答を作成する
- Minimal API アプリのフィルター
- Minimal API アプリでエラーを処理する
- Minimal API での認証と認可
- Minimal API アプリをテストする
- 短絡ルーティング
- Identity API エンドポイント
- キー付きサービス依存関係挿入コンテナーのサポート
- 最小限の API エンドポイントの背後にある外観
- ASP.NET Core の最小 API の整理
- GitHub での Fluent Validation に関するディスカッション
このドキュメントでは、
- Minimal API のクイック リファレンスを提供します。
- 経験豊富な開発者を対象としています。 概要については、「チュートリアル: ASP.NET Core を使って最小 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
は、ユーザー コードによってUseRouting
がまだ呼び出されておらず、サービス プロバイダーでUseAuthentication
が検出できる場合、IAuthenticationSchemeProvider
の直後に追加されます。IAuthenticationSchemeProvider
は、AddAuthentication
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 -
UseAuthorization
は、ユーザー コードによってUseAuthorization
がまだ呼び出されておらず、サービス プロバイダーでIAuthorizationHandlerProvider
が検出できる場合、次に追加されます。IAuthorizationHandlerProvider
は、AddAuthorization
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 - ユーザーが構成したミドルウェアとエンドポイントは、
UseRouting
とUseEndpoints
の間に追加されます。
次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。
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 => {});
場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCors は UseAuthentication と UseAuthorization の前に呼び出される必要があります。
UseAuthentication
を呼び出す場合、アプリでは UseAuthorization
と UseCors
を呼び出す必要があります。
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting
への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
ターミナル ミドルウェアを追加する場合:
- このミドルウェアは、
UseEndpoints
の後に追加される必要があります。 - ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで
UseRouting
とUseEndpoints
を呼び出す必要があります。
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
ポートに応答します。
複数のポート
次のコードの場合、アプリは 3000
と 4000
ポートに応答します。
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"
Kestrel ファイルで appsettings.json
エンドポイントも構成されている場合、 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 | --環境 |
コンテンツ ルート | ASPNETCORE_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 |
静的ファイル | 静的ファイルとディレクトリ参照に対応するサポートを提供します。 | UseStaticFiles、UseFileServer |
WebSocket | WebSocket プロトコルを有効にします。 | UseWebSockets |
以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。
ルーティング
構成された WebApplication
では、Map{Verb}
と MapMethods をサポートします。ここで {Verb}
は、Get
、Post
、Put
または 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 route 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 The user id is 3 and book id is 7
にから /users/3/books/7
が返されます。
ルート ハンドラーは、捕捉するパラメーターを宣言できます。 キャプチャするように宣言されたパラメーターを持つルートに対して要求が実行されると、パラメーターが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userId
と bookId
は両方とも 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 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationを追加する WithMetadata や のようなメソッドを 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;
}
このシナリオでは、Location
結果の 201 Created
ヘッダーに相対アドレスを使用できます。
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
エンドポイントの最初のグループは、/public/todos
のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 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 メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- フォーム値
- 依存関係の挿入によって指定されるサービス
- 習慣
次の 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 |
ヘッダ |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの 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.
別の方法には、[AsParameters]
で注釈がつけられたプロパティを持つカスタム型で [FromForm]
属性を使用する方法があります。 たとえば、次のコードによって、フォーム値から 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 からのバインドをセキュリティで保護する
複雑なフォームのバインドは、IFormFile を使用する IFormFileCollection と [FromForm]
使用してサポートされます。
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"));
HttpRequest と HttpResponse: 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();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
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 を使ったフォームへのバインディング
IFormCollection、IFormFile、IFormFileCollection を使ったフォームベースのパラメーターからのバインディングがサポートされています。 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 でのフォーム バインド」を参照してください。
フォームからコレクションと複合型にバインドする
バインディングは、次の場合にサポートされています。
- コレクション (例: List や Dictionary など)
- 複合型 (例:
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 antiforgery 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));
}
上のコードでは以下の操作が行われます。
- JSON 本文から読み取る必要があるパラメーターから曖昧さを解消するには、ターゲット パラメーターに 属性で注釈を付ける。
- 要求デリゲート ジェネレーターを使ってコンパイルした最小限の API の場合、複合型またはコレクション型からのバインドはサポートされていません。
- マークアップには、
isCompleted
という名前の追加の非表示入力と、false
の値が表示されます。 フォームの送信時にisCompleted
チェック ボックスをオンにすると、値true
とfalse
の両方が値として送信されます。 チェックボックスをオフにすると、非表示の入力値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
にバインドし、一致する Todo
値を持つ Id
項目を返します。
// 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
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
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);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
トライパース
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI Point: 12.3, 10.1
に対して /map?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 SortBy:xyz, SortDirection:Desc, CurrentPage:99
に対して /products?SortBy=xyz&SortDir=Desc&Page=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}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | 習慣 | 400 |
{ParameterType}.BindAsync がスローされる |
問題ありません | 習慣 | 500 |
JSON 本文を逆シリアル化できない | 問題ありません | 体 | 400 |
コンテンツの型が正しくない (application/json でない) |
問題ありません | 体 | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- フォーム:
[FromForm]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormCollection
(HttpContext.Request.Form
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- パラメーター名が
app.Map("/todo/{id}", (int id) => {});
などのルート テンプレートにある場合、ルートからバインドされる。 - クエリ文字列からバインドされる。
- パラメーター名が
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの 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 を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
応答
ルート ハンドラーは、次の型の戻り値をサポートしています。
-
IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます -
string
- これにはTask<string>
とValueTask<string>
が含まれます -
T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます
戻り値 | 動作 | コンテンツタイプ |
---|---|---|
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!" }));
TypedResults
を返すより、Results を返すことをお勧めします。 詳しくは、「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));
テキスト
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"));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。
TypedResults
を返すより、Results
を返すことをお勧めします。 詳しくは、「TypedResults と Results」をご覧ください。
ヘッダーの変更
応答ヘッダーを変更するには、HttpResponse
オブジェクトを使用します。
app.MapGet("/", (HttpContext context) => {
// Set a custom header
context.Response.Headers["X-Custom-Header"] = "CustomValue";
// Set a known header
context.Response.Headers.CacheControl = $"public,max-age=3600";
return "Hello World";
});
結果のカスタマイズ
アプリケーションは、カスタムの 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
クラスを使います。 たとえば、応答状態コードを設定したり、別の 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) を有効にする」を参照してください
ValidateScopes と ValidateOnBuild
ValidateScopes と ValidateOnBuild は、開発環境では既定で有効になっていますが、他の環境では無効になっています。
ValidateOnBuild
が true
の場合、DI コンテナーではビルド時にサービス構成を検証します。 サービス構成が無効な場合、サービスが要求されたときに実行時ではなく、アプリの起動時にビルドが失敗します。
ValidateScopes
が true
の場合、DI コンテナーでは、スコープ付きサービスがルート スコープから解決されていないことを検証します。 ルート スコープからスコープ付きサービスを解決すると、サービスが要求のスコープよりも長くメモリに保持されるため、メモリ リークが発生する可能性があります。
ValidateScopes
と ValidateOnBuild
は、パフォーマンス上の理由から、開発以外のモードでは既定では 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 { }
次のコードを使用すると、ValidateScopes
では ValidateOnBuild
と Development
は無効になります。
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 クイック リファレンス
- OpenAPI ドキュメントを生成する
- Minimal API アプリケーションで応答を作成する
- Minimal API アプリのフィルター
- Minimal API アプリでエラーを処理する
- Minimal API での認証と認可
- Minimal API アプリをテストする
- 短絡ルーティング
- Identity API エンドポイント
- キー付きサービス依存関係挿入コンテナーのサポート
- 最小限の API エンドポイントの背後にある外観
- ASP.NET Core の最小 API の整理
- GitHub での Fluent Validation に関するディスカッション
このドキュメントでは、
- Minimal API のクイック リファレンスを提供します。
- 経験豊富な開発者を対象としています。 概要については、「チュートリアル: ASP.NET Core で Minimal Web 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
は、ユーザー コードによってUseRouting
がまだ呼び出されておらず、サービス プロバイダーでUseAuthentication
が検出できる場合、IAuthenticationSchemeProvider
の直後に追加されます。IAuthenticationSchemeProvider
は、AddAuthentication
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 -
UseAuthorization
は、ユーザー コードによってUseAuthorization
がまだ呼び出されておらず、サービス プロバイダーでIAuthorizationHandlerProvider
が検出できる場合、次に追加されます。IAuthorizationHandlerProvider
は、AddAuthorization
を使用するときに既定で追加され、サービスはIServiceProviderIsService
を使用して検出されます。 - ユーザーが構成したミドルウェアとエンドポイントは、
UseRouting
とUseEndpoints
の間に追加されます。
次のコードは、アプリに追加される自動ミドルウェアがどのようなものを生成するかを示しています。
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 => {});
場合によっては、既定のミドルウェア構成がアプリに対して正しくなく、変更が必要になることがあります。 たとえば、UseCors は UseAuthentication と UseAuthorization の前に呼び出される必要があります。
UseAuthentication
を呼び出す場合、アプリでは UseAuthorization
と UseCors
を呼び出す必要があります。
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
ルートの照合が発生する前にミドルウェアを実行する必要がある場合は、UseRouting を呼び出す必要があり、ミドルウェアは UseRouting
への呼び出しの前に配置する必要があります。 この場合、UseEndpoints は前述のように自動的に追加されるため、必要ありません。
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
ターミナル ミドルウェアを追加する場合:
- このミドルウェアは、
UseEndpoints
の後に追加される必要があります。 - ターミナル ミドルウェアが正しい場所に配置されるようにするため、アプリで
UseRouting
とUseEndpoints
を呼び出す必要があります。
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
ポートに応答します。
複数のポート
次のコードの場合、アプリは 3000
と 4000
ポートに応答します。
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"
Kestrel ファイルで appsettings.json
エンドポイントも構成されている場合、 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 | --環境 |
コンテンツ ルート | ASPNETCORE_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 |
静的ファイル | 静的ファイルとディレクトリ参照に対応するサポートを提供します。 | UseStaticFiles、UseFileServer |
WebSocket | WebSocket プロトコルを有効にします。 | UseWebSockets |
以下のセクションでは、要求処理、すなわちルーティング、パラメーター バインディング、応答について説明します。
ルーティング
構成された WebApplication
では、Map{Verb}
と MapMethods をサポートします。ここで {Verb}
は、Get
、Post
、Put
または 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 route 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 The user id is 3 and book id is 7
にから /users/3/books/7
が返されます。
ルート ハンドラーは、捕捉するパラメーターを宣言できます。 キャプチャするように宣言されたパラメーターを持つルートに対して要求が実行されると、パラメーターが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userId
と bookId
は両方とも 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 拡張メソッドは、共通のプレフィックスを持つエンドポイントのグループを整理するのに役立ちます。 これにより、繰り返しのコードを減らし、RequireAuthorizationを追加する WithMetadata や のようなメソッドを 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;
}
このシナリオでは、Location
結果の 201 Created
ヘッダーに相対アドレスを使用できます。
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
エンドポイントの最初のグループは、/public/todos
のプレフィックスが付いた要求にのみ一致し、認証なしでアクセスできます。 エンドポイントの 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 メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- 依存関係の挿入によって指定されるサービス
- 習慣
フォーム値からのバインドは、.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 |
ヘッダ |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの 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"));
HttpRequest と HttpResponse: 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();
- データを読み取るとき、
Stream
はHttpRequest.Body
と同じオブジェクトです。 - 要求本文は、既定ではバッファーされません。 読み取られた後の本文を巻き戻すことはできません。 ストリームを複数回読み取ることはできません。
- 基になるバッファーが破棄または再利用されるため、最小アクション ハンドラーの外部では
Stream
とPipeReader
は使用できません。
IFormFile と IFormFileCollection を使用したファイルのアップロード
次のコードでは、IFormFile と IFormFileCollection を使用して、ファイルをアップロードしています。
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 ヘッダーを使用した認証されたファイルのアップロード要求がサポートされています。
.NET 7 の ASP.NET Core には 偽造防止 の組み込みサポートがありません。
偽造防止は、.NET 8 以降の ASP.NET Core で使用できます 。 ただし、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
にバインドし、一致する Todo
値を持つ Id
項目を返します。
// 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
エンドポイントでは、上記の struct
を AsParameters 属性と共に使用します。
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);
struct
を AsParameters
と共に使うと、record
型を使うよりもパフォーマンスが向上します。
完全なサンプル コードは AspNetCore.Docs.Samples リポジトリにあります。
カスタム バインド
パラメーター バインドは、2 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
トライパース
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI Point: 12.3, 10.1
に対して /map?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 SortBy:xyz, SortDirection:Desc, CurrentPage:99
に対して /products?SortBy=xyz&SortDir=Desc&Page=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}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | 習慣 | 400 |
{ParameterType}.BindAsync がスローされる |
どちらでもよい | 習慣 | 500 |
JSON 本文を逆シリアル化できない | どちらでもよい | 体 | 400 |
コンテンツの型が正しくない (application/json でない) |
どちらでもよい | 体 | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- サービス:
[FromServices]
- パラメーター値:
[AsParameters]
- ルート値:
- 特殊な型
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- パラメーターの型に有効な静的
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な静的
TryParse
メソッドがある。- ルート テンプレートにパラメーター名が存在する場合。
app.Map("/todo/{id}", (int id) => {});
では、id
はルートからバインドされます。 - クエリ文字列からバインドされる。
- ルート テンプレートにパラメーター名が存在する場合。
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
ボディ バインドの 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 を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
応答
ルート ハンドラーは、次の型の戻り値をサポートしています。
-
IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます -
string
- これにはTask<string>
とValueTask<string>
が含まれます -
T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます
戻り値 | 動作 | コンテンツタイプ |
---|---|---|
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!" }));
TypedResults
を返すより、Results を返すことをお勧めします。 詳しくは、「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));
テキスト
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"));
組み込みの結果
共通の結果ヘルパーは、Results と TypedResults 静的クラスに含まれます。
TypedResults
を返すより、Results
を返すことをお勧めします。 詳しくは、「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
クラスを使います。 たとえば、応答状態コードを設定したり、別の 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 のクイック リファレンスを提供します。
- 経験豊富な開発者を対象としています。 概要については、「チュートリアル: ASP.NET Core で Minimal Web API を作成する」をご覧ください
Minimal API には次が含まれます。
- WebApplication および WebApplicationBuilder
- ルート ハンドラー
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
ポートに応答します。
複数のポート
次のコードの場合、アプリは 3000
と 4000
ポートに応答します。
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"
Kestrel ファイルで appsettings.json
エンドポイントも構成されている場合、 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 | --環境 |
コンテンツ ルート | ASPNETCORE_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 |
静的ファイル | 静的ファイルとディレクトリ参照に対応するサポートを提供します。 | UseStaticFiles、UseFileServer |
WebSocket | WebSocket プロトコルを有効にします。 | UseWebSockets |
要求処理
以下のセクションでは、ルーティング、パラメーター バインディング、応答について説明します。
ルーティング
構成された WebApplication
は Map{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 The user id is 3 and book id is 7
にから /users/3/books/7
が返されます。
ルート ハンドラーは、捕捉するパラメーターを宣言できます。 捕捉するように宣言されたパラメーターを持つルートに対して要求が実行されると、パラメーターが解析され、ハンドラーに渡されます。 これにより、タイプ セーフな方法で簡単に値を捕捉できるようになります。 上記のコードでは、userId
と bookId
は両方とも 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 メソッドとパラメーターの型に基づいて推測できます。
サポートされているバインディング ソース:
- ルート値
- クエリ文字列
- ヘッダー
- 本文 (JSON)
- 依存関係の挿入によって指定されるサービス
- 習慣
注
フォーム値からのバインドは、.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 |
ヘッダ |
service |
依存関係の挿入によって指定 |
HTTP メソッド GET
、HEAD
、OPTIONS
、DELETE
は、本文から暗黙的にバインドしません。 これらの 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"));
HttpRequest と HttpResponse: 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 つの方法でカスタマイズできます。
- ルート、クエリ、ヘッダーのバインディング ソースの場合、型の静的な
TryParse
メソッドを追加することにより、カスタムの型をバインドします。 - 型に対して
BindAsync
メソッドを実装することにより、バインディング プロセスを制御します。
トライパース
TryParse
には 2 つの API があります。
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
次のコードは、URI Point: 12.3, 10.1
に対して /map?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 SortBy:xyz, SortDirection:Desc, CurrentPage:99
に対して /products?SortBy=xyz&SortDir=Desc&Page=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}.TryParse は false を返します。 |
はい | ルート/クエリ/ヘッダー | 400 |
{ParameterType}.BindAsync は null を返します。 |
はい | 習慣 | 400 |
{ParameterType}.BindAsync がスローされる |
どちらでもよい | 習慣 | 500 |
JSON 本文を逆シリアル化できない | どちらでもよい | 体 | 400 |
コンテンツの型が正しくない (application/json でない) |
どちらでもよい | 体 | 415 |
バインディングの優先順位
パラメーターからバインディング ソースを決定するルールは次の通りです。
- パラメーターに対して定義されている明示的な属性 (From* 属性)、次の順序:
- ルート値:
[FromRoute]
- クエリ文字列:
[FromQuery]
- ヘッダー:
[FromHeader]
- 本文:
[FromBody]
- サービス:
[FromServices]
- ルート値:
- 特殊な型
- パラメーターの型に有効な
BindAsync
メソッドがある。 - パラメーターの型が文字列であるか、有効な
TryParse
メソッドがある。- ルート テンプレートにパラメーター名が存在する場合。
app.Map("/todo/{id}", (int id) => {});
では、id
はルートからバインドされます。 - クエリ文字列からバインドされる。
- ルート テンプレートにパラメーター名が存在する場合。
- パラメーターの型が依存関係の挿入によって提供されるサービスである場合、そのサービスがソースとして使用される。
- パラメーターが本文からのものである。
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 が返されます。
POST の場合{ "id": 1, "name": "Joe Smith" }
{ "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 を使用して要求本文にアクセスします。
- 要求本文をローカル ファイルにコピーします。
応答
ルート ハンドラーは、次の型の戻り値をサポートしています。
-
IResult
ベース - これにはTask<IResult>
とValueTask<IResult>
が含まれます -
string
- これにはTask<string>
とValueTask<string>
が含まれます -
T
(その他の型) - これにはTask<T>
とValueTask<T>
が含まれます
戻り値 | 動作 | コンテンツタイプ |
---|---|---|
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));
テキスト
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 | 結果.成功 |
テキスト応答の書き込み | text/plain (既定値)、構成可能 | 200 | 結果.Text |
応答をバイトとして書き込む | application/octet-stream (既定値)、構成可能 | 200 | 結果.バイト |
応答にバイトのストリームを書き込む | application/octet-stream (既定値)、構成可能 | 200 | Results.Stream |
content-disposition ヘッダーによるダウンロードのために、応答に対してファイルをストリーミングする | application/octet-stream (既定値)、構成可能 | 200 | 結果.ファイル |
状態コードを 404 に設定し、オプションの JSON 応答を指定する | 該当なし | 404 | 結果が見つかりません |
状態コードを 204 に設定する | 該当なし | 204 | 結果がありません |
状態コードを 422 に設定し、オプションの JSON 応答を指定する | 該当なし | 422 | 結果.処理できないエンティティ |
状態コードを 400 に設定し、オプションの JSON 応答を指定する | 該当なし | 400 | Results.不正なリクエスト |
状態コードを 409 に設定し、オプションの JSON 応答を指定する | 該当なし | 409 | 結果.衝突 |
問題の詳細の JSON オブジェクトを応答に書き込む | 該当なし | 500 (既定値)、構成可能 | 結果.問題 |
問題の詳細の JSON オブジェクトを検証エラーと共に応答に書き込む | 該当なし | 該当なし、構成可能 | 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();
承認
ルートは、承認ポリシーを使用して保護できます。 これらは、[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) を有効にする」を参照してください
関連項目
ASP.NET Core