基本 API 快速參考
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
此文件:
- 提供基本 API 的快速參考。
- 適合有經驗的開發人員。 如需簡介,請參閱教學課程:使用 ASP.NET Core 建立基本 API。
基本 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 範本來建立。
下列程式碼會在未明確建立 WebApplicationBuilder 的情況下建立 WebApplication (app
):
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
會使用預先設定的預設值,初始化 WebApplication 類別的新執行個體。
WebApplication
會根據特定條件,在 Minimal API applications
中自動新增下列中介軟體:
- 當
HostingEnvironment
是"Development"
,會先新增UseDeveloperExceptionPage
。 - 如果使用者程式碼尚未呼叫
UseRouting
,且已設定端點 (例如app.MapGet
),則會接著新增UseRouting
。 - 如果已設定任何端點,則會在中介軟體管線的結尾新增
UseEndpoints
。 - 如果使用者程式碼尚未呼叫
UseAuthentication
,且如果可以在服務提供者中偵測到IAuthenticationSchemeProvider
,則會在UseRouting
之後立即新增UseAuthentication
。 使用AddAuthentication
時,且使用IServiceProviderIsService
偵測到服務,預設會新增IAuthenticationSchemeProvider
。 - 如果使用者程式碼尚未呼叫
UseAuthorization
,且如果可以在服務提供者中偵測到IAuthorizationHandlerProvider
,則會接著新增UseAuthorization
。 使用AddAuthorization
時,且使用IServiceProviderIsService
偵測到服務,預設會新增IAuthorizationHandlerProvider
。 - 使用者設定的中介軟體和端點會在
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 => {});
在某些情況下,應用程式的預設中介軟體設定不正確,而且需要修改。 例如,應該在 UseAuthentication 和 UseAuthorization 之前呼叫 UseCors。 如果呼叫 UseCors
,則應用程式需要呼叫 UseAuthentication
和 UseAuthorization
:
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'
。 Visual Studio 傳回錯誤,因為它預期的是在 Properties/launchSettings.json
中指定的連接埠,但應用程式正在使用 app.Run("http://localhost:3000")
所指定的連接埠。 從命令列執行下列連接埠變更範例。
下列各節會設定應用程式回應的連接埠。
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"
如果 appsettings.json
檔案中也已設定 Kestrel 端點,則會使用 appsettings.json
檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定
從環境讀取連接埠
下列程式碼會從環境讀取連接埠:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS
環境變數,如下一節所示。
透過 ASPNETCORE_URLS 環境變數設定連接埠
ASPNETCORE_URLS
環境變數可用來設定連接埠:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支援多個 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
在所有介面上接聽
下列範例示範在所有介面上接聽
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 在所有介面上接聽
上述範例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
使用 ASPNETCORE_HTTPS_PORTS 在所有介面上接聽
上述範例可以使用 ASPNETCORE_HTTPS_PORTS
和 ASPNETCORE_HTTP_PORTS
。
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
如需詳細資訊,請參閱設定 ASP.NET Core Kestrel Web 伺服器的端點
使用開發憑證指定 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
如需開發憑證的詳細資訊,請參閱信任 Windows 和 macOS 上的 ASP.NET Core HTTPS 開發憑證。
使用自訂憑證指定 HTTPS
下列各節說明如何使用 appsettings.json
檔案和透過設定指定自訂憑證。
使用 appsettings.json
指定自訂憑證
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
透過設定指定自訂憑證
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用憑證 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
讀取環境
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
如需使用環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境
組態
下列程式碼會從設定系統讀取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
如需詳細資訊,請參閱 ASP.NET Core 中的設定
記錄
下列程式碼會將訊息寫入至應用程式啟動時的記錄檔:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
如需詳細資訊,請參閱 .NET Core 與 ASP.NET Core 中的記錄
存取相依性插入 (DI) 容器
下列程式碼示範如何在應用程式啟動期間從 DI 容器取得服務:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
下列程式碼示範如何使用 [FromKeyedServices]
屬性從 DI 容器存取金鑰:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
如需 DI 的詳細資訊,請參閱在 ASP.NET Core 中插入相依性。
WebApplicationBuilder
本節包含使用 WebApplicationBuilder 的範例程式碼。
變更內容根目錄、應用程式名稱和環境
下列程式碼會設定內容根目錄、應用程式名稱和環境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 會初始化具有預先設定之預設值的之 WebApplicationBuilder 類別的新執行個體。
如需詳細資訊,請參閱 ASP.NET Core 基礎知識概觀
使用環境變數或命令列來變更內容根目錄、應用程式名稱和環境
下表顯示用來變更內容根目錄、應用程式名稱和環境的環境變數和命令列引數:
功能 | 環境變數 | 命令列引數 |
---|---|---|
應用程式名稱 | ASPNETCORE_APPLICATIONNAME | --applicationName |
環境名稱 | ASPNETCORE_ENVIRONMENT | --environment |
內容根目錄 | ASPNETCORE_CONTENTROOT | --contentRoot |
新增組態提供者
下列範例會新增 INI 設定提供者:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
如需詳細資訊,請參閱 ASP.NET Core 中的設定中的檔案設定提供者。
讀取設定
依預設,WebApplicationBuilder 會從多個來源讀取設定,包括:
appSettings.json
和appSettings.{environment}.json
- 環境變數
- 命令列
如需讀取的設定來源完整清單,請參閱 ASP.NET Core 中的設定的預設設定。
下列程式碼會從設定讀取 HelloKey
,並在 /
端點顯示值。 如果設定值為 null,則會將 "Hello" 指派給 message
:
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
您可以使用 Host 屬性來存取 IHostBuilder 上的現有擴充方法:
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
您可以使用 WebApplicationBuilder.WebHost 屬性來存取 IWebHostBuilder 上的擴充方法。
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();
新增中介軟體
您可以在 WebApplication
上設定任何現有的 ASP.NET Core 中介軟體:
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 中介軟體
下表列出一些經常搭配基本 API 使用的中介軟體。
中介軟體 | 描述 | API |
---|---|---|
驗證 | 提供驗證支援。 | UseAuthentication |
授權 | 提供授權支援。 | UseAuthorization |
CORS | 設定跨原始來源資源共用。 | UseCors |
例外處理常式 | 全域處理中介軟體管線擲回的例外狀況。 | UseExceptionHandler |
轉送標頭 | 將設為 Proxy 的標頭轉送到目前要求。 | UseForwardedHeaders |
HTTPS 重新導向 | 將所有 HTTP 要求重新導向至 HTTPS。 | UseHttpsRedirection |
HTTP 嚴格的傳輸安全性 (HSTS) | 增強安全性的中介軟體,可新增特殊的回應標頭。 | UseHsts |
要求記錄 | 提供記錄 HTTP 要求和回應的支援。 | UseHttpLogging |
要求逾時 | 提供設定要求逾時、全域預設和每個端點的支援。 | UseRequestTimeouts |
W3C 要求記錄 | 提供以 W3C 格式記錄 HTTP 要求和回應的支援。 | UseW3CLogging |
回應快取 | 提供快取回應的支援。 | UseResponseCaching |
回應壓縮 | 提供壓縮回應的支援。 | UseResponseCompression |
工作階段 | 提供管理使用者工作階段的支援。 | UseSession |
靜態檔案 | 支援靜態檔案的提供和目錄瀏覽。 | UseStaticFiles, UseFileServer |
WebSocket | 啟用 WebSockets 通訊協定。 | UseWebSockets |
下列各節涵蓋要求處理:路由、參數繫結和回應。
路由
已設定的 WebApplication
支援 Map{Verb}
和 MapMethods,其中的 {Verb}
是駝峰式大小寫的 HTTP 方法,例如 Get
、Post
、Put
或 Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
傳遞給這些方法的 Delegate 引數稱為「路由處理常式」。
路由處理常式
路由處理常式是路由相符時所執行的方法。 路由處理常式可以是 Lambda 運算式、本機函式、執行個體方法或靜態方法。 路由處理常式可以是同步或非同步。
Lambda 運算式
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 支援時,會當做 OpenAPI 作業識別碼使用。 如需詳細資訊,請參閱 OpenAPI。
路由參數
路由參數可以擷取做為路由模式定義的一部分:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
上述程式碼會從 URI /users/3/books/7
傳回 The user id is 3 and book id is 7
。
路由處理常式可以宣告要擷取的參數。 當要求成為宣告要擷取參數的路由時,將剖析參數並傳遞至處理常式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userId
和 bookId
都是 int
。
在上述程式碼中,如果任一路由值無法轉換成 int
,則會擲回例外狀況。 GET 要求 /users/hello/books/3
會擲回下列例外狀況:
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,其可新增端點中繼資料) 來自訂整個端點群組。
例如,下列程式碼會建立兩個類似的端點群組:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
在此案例中,您可以在 201 Created
結果中使用 Location
標頭的相對位址:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
第一組端點只會比對以 /public/todos
為前置詞且不需要任何驗證即可存取的要求。 第二組端點只會比對以 /private/todos
為前置詞且需要驗證的要求。
QueryPrivateTodos
端點篩選處理站是本機函式,可修改路由處理常式的 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);
});
在上述範例中,即使已新增第二個要求,外部篩選條件也會在內部篩選條件之前記錄傳入要求。 由於篩選條件已套用至不同的群組,因此彼此相對的新增順序並不重要。 如果套用至相同的群組或特定端點,則篩選條件的新增順序就很重要。
對 /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
不會隱含從本文繫結。 若要從本文 (JSON 格式) 繫結這些 HTTP 方法,請明確繫結 [FromBody]
或從 HttpRequest 讀取。
下列範例 POST 路由處理常式會針對 person
參數使用本文 (JSON 格式) 的繫結來源:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
上述範例中的參數全都會自動從要求資料繫結。 為了示範參數繫結所提供的便利性,下列路由處理常式將示範如何直接從要求讀取要求資料:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
明確參數繫結
屬性可用來明確宣告參數繫結的來源位置。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
參數 | 繫結來源 |
---|---|
id |
具有名稱 id 的路由值 |
page |
具有名稱 "p" 的查詢字串 |
service |
由相依性插入提供 |
contentType |
具有名稱 "Content-Type" 的標頭 |
從表單值明確繫結
[FromForm]
屬性會繫結表單值:
app.MapPost("/todos", async ([FromForm] string name,
[FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
var todo = new Todo
{
Name = name,
Visibility = visibility
};
if (attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await attachment.CopyToAsync(stream);
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
替代方式是使用具有的自訂型別屬性以 [FromForm]
註解的 [AsParameters]
屬性。 例如,下列程式碼會從表單值繫結至 NewTodoRequest
記錄結構的屬性:
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
如需詳細資訊,請參閱本文稍後的 AsParameters 一節。
完整的範例程式碼位於 AspNetCore.Docs.Samples 存放庫中。
從 IFormFile 和 IFormFileCollection 保護繫結
您可以透過 [FromForm]
使用 IFormFile 和 IFormFileCollection 支援複雜的表單繫結:
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 存放庫中。
具有相依性插入的參數繫結
將型別設定為服務時,基本 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 | result |
---|---|
/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 | result |
---|---|
/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 產品呼叫方法。
注意:如果提供無效資料且參數可為 Null,則路由處理常式不會執行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/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 儲存體,或將資料加入佇列提供者的佇列。
- 使用背景工作處理序或雲端函式處理儲存的資料。
例如,資料可能會加入佇列至 Azure 佇列儲存體,或儲存在 Azure Blob 儲存體中。
下列程式碼會實作背景佇列:
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 從表單型參數繫結。 OpenAPI 中繼資料會推斷為表單參數,可支援與 Swagger UI 的整合。
下列程式碼會使用來自 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 攻擊的詳細資訊,請參閱使用基本 API 的防偽造功能
如需詳細資訊,請參閱基本 API 中的表單繫結;
繫結至表單中的集合和複雜型別
繫結支援下列項目:
下列程式碼顯示:
- 將多部分表單輸入繫結至複雜物件的基本端點。
- 如何使用防偽服務來支援反偽權杖的產生和驗證。
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));
}
在上述程式碼中:
- 目標參數必須以
[FromForm]
屬性標註,以釐清應從 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
,並傳回具有相符 Id
值的 Todo
項目:
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
注意
從查詢字串繫結 string[]
時,沒有任何相符的查詢字串值會導致空陣列,而不是 Null 值。
具有 [AsParameters] 引數清單的參數繫結
AsParametersAttribute 可實現型別的簡單參數繫結,而不是複雜或遞迴模型繫結。
請考慮下列程式碼:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
考慮下列 GET
端點:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
下列 struct
可用來取代上述強調顯示的參數:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
重構的 GET
端點會使用上述 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 存放庫中的完整範例程式碼。
自訂繫結
自訂參數繫結的方式有兩個:
- 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態
TryParse
方法來繫結自訂型別。 - 藉由在型別上實作
BindAsync
方法來控制繫結程序。
TryParse
TryParse
有兩個 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下列程式碼會以 URI /map?Point=12.3,10.1
顯示 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
有下列 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下列程式碼會以 URI /products?SortBy=xyz&SortDir=Desc&Page=99
顯示 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
繫結失敗
繫結失敗時,架構會記錄偵錯訊息,並根據失敗模式將各種狀態碼傳回至用戶端。
失敗模式 | 可為 Null 的參數型別 | 繫結來源 | 狀態碼 |
---|---|---|---|
{ParameterType}.TryParse 傳回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 傳回 null |
是 | custom | 400 |
{ParameterType}.BindAsync 擲回 |
不重要 | custom | 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
// }
由於範例程式碼會同時設定序列化和還原序列化,因此其可以讀取 NameField
並將 NameField
包含在輸出的 JSON 中。
設定端點的 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 |
如需路由處理常式傳回值的更深入指南,請參閱在基本 API 應用程式中建立回應
範例傳回值
字串傳回值
app.MapGet("/hello", () => "Hello World");
JSON 傳回值
app.MapGet("/hello", () => new { Message = "Hello World" });
傳回 TypedResults
下列程式碼會傳回 TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
相較於傳回 Results,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
IResult 傳回值
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
下列範例會使用內建的結果型別來自訂回應:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自訂狀態碼
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
資料流
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
如需更多範例,請參閱在基本 API 應用程式中建立回應。
重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
檔案
app.MapGet("/download", () => Results.File("myfile.text"));
內建結果
常見的結果協助程式存在於 Results 和 TypedResults 靜態類別中。 相較於傳回 Results
,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
自訂結果
應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 HTML 結果型別的範例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
具型別的結果
IResult 介面可以呈現從最小的 API 傳回值,其未利用會將傳回的物件序列化為 HTTP 回應的 JSON 的隱含支援。 靜態 Results 類別是用來建立代表不同回應型別的不同 IResult
物件。 例如,設定回應狀態碼或重新導向至另一個 URL。
實作 IResult
的型別是公用的,可在測試時允許型別判斷。 例如:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
您可以在靜態 TypedResults 類別上查看對應方法的傳回型別,以尋找要轉換成的正確公用 IResult
型別。
如需更多範例,請參閱在基本 API 應用程式中建立回應。
篩選
如需詳細資訊,請參閱最小 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 { }
下列程式碼會在 Development
中停用 ValidateScopes
和 ValidateOnBuild
:
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;
});
}
另請參閱
此文件:
- 提供基本 API 的快速參考。
- 適合有經驗的開發人員。 如需簡介,請參閱教學課程:使用 ASP.NET Core 建立基本 API
基本 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 範本來建立。
下列程式碼會在未明確建立 WebApplicationBuilder 的情況下建立 WebApplication (app
):
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
會使用預先設定的預設值,初始化 WebApplication 類別的新執行個體。
WebApplication
會根據特定條件,在 Minimal API applications
中自動新增下列中介軟體:
- 當
HostingEnvironment
是"Development"
,會先新增UseDeveloperExceptionPage
。 - 如果使用者程式碼尚未呼叫
UseRouting
,且已設定端點 (例如app.MapGet
),則會接著新增UseRouting
。 - 如果已設定任何端點,則會在中介軟體管線的結尾新增
UseEndpoints
。 - 如果使用者程式碼尚未呼叫
UseAuthentication
,且如果可以在服務提供者中偵測到IAuthenticationSchemeProvider
,則會在UseRouting
之後立即新增UseAuthentication
。 使用AddAuthentication
時,且使用IServiceProviderIsService
偵測到服務,預設會新增IAuthenticationSchemeProvider
。 - 如果使用者程式碼尚未呼叫
UseAuthorization
,且如果可以在服務提供者中偵測到IAuthorizationHandlerProvider
,則會接著新增UseAuthorization
。 使用AddAuthorization
時,且使用IServiceProviderIsService
偵測到服務,預設會新增IAuthorizationHandlerProvider
。 - 使用者設定的中介軟體和端點會在
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 => {});
在某些情況下,應用程式的預設中介軟體設定不正確,而且需要修改。 例如,應該在 UseAuthentication 和 UseAuthorization 之前呼叫 UseCors。 如果呼叫 UseCors
,則應用程式需要呼叫 UseAuthentication
和 UseAuthorization
:
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'
。 Visual Studio 傳回錯誤,因為它預期的是在 Properties/launchSettings.json
中指定的連接埠,但應用程式正在使用 app.Run("http://localhost:3000")
所指定的連接埠。 從命令列執行下列連接埠變更範例。
下列各節會設定應用程式回應的連接埠。
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"
如果 appsettings.json
檔案中也已設定 Kestrel 端點,則會使用 appsettings.json
檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定
從環境讀取連接埠
下列程式碼會從環境讀取連接埠:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS
環境變數,如下一節所示。
透過 ASPNETCORE_URLS 環境變數設定連接埠
ASPNETCORE_URLS
環境變數可用來設定連接埠:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支援多個 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
如需使用環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境
在所有介面上接聽
下列範例示範在所有介面上接聽
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 在所有介面上接聽
上述範例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
使用開發憑證指定 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
如需開發憑證的詳細資訊,請參閱信任 Windows 和 macOS 上的 ASP.NET Core HTTPS 開發憑證。
使用自訂憑證指定 HTTPS
下列各節說明如何使用 appsettings.json
檔案和透過設定指定自訂憑證。
使用 appsettings.json
指定自訂憑證
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
透過設定指定自訂憑證
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用憑證 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
組態
下列程式碼會從設定系統讀取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
如需詳細資訊,請參閱 ASP.NET Core 中的設定
記錄
下列程式碼會將訊息寫入至應用程式啟動時的記錄檔:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
如需詳細資訊,請參閱 .NET Core 與 ASP.NET Core 中的記錄
存取相依性插入 (DI) 容器
下列程式碼示範如何在應用程式啟動期間從 DI 容器取得服務:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
如需詳細資訊,請參閱在 ASP.NET Core 中插入相依性。
WebApplicationBuilder
本節包含使用 WebApplicationBuilder 的範例程式碼。
變更內容根目錄、應用程式名稱和環境
下列程式碼會設定內容根目錄、應用程式名稱和環境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 會初始化具有預先設定之預設值的之 WebApplicationBuilder 類別的新執行個體。
如需詳細資訊,請參閱 ASP.NET Core 基礎知識概觀
使用環境變數或命令列來變更內容根目錄、應用程式名稱和環境
下表顯示用來變更內容根目錄、應用程式名稱和環境的環境變數和命令列引數:
功能 | 環境變數 | 命令列引數 |
---|---|---|
應用程式名稱 | ASPNETCORE_APPLICATIONNAME | --applicationName |
環境名稱 | ASPNETCORE_ENVIRONMENT | --environment |
內容根目錄 | ASPNETCORE_CONTENTROOT | --contentRoot |
新增組態提供者
下列範例會新增 INI 設定提供者:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
如需詳細資訊,請參閱 ASP.NET Core 中的設定中的檔案設定提供者。
讀取設定
依預設,WebApplicationBuilder 會從多個來源讀取設定,包括:
appSettings.json
和appSettings.{environment}.json
- 環境變數
- 命令列
下列程式碼會從設定讀取 HelloKey
,並在 /
端點顯示值。 如果設定值為 null,則會將 "Hello" 指派給 message
:
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
您可以使用 Host 屬性來存取 IHostBuilder 上的現有擴充方法:
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
您可以使用 WebApplicationBuilder.WebHost 屬性來存取 IWebHostBuilder 上的擴充方法。
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();
新增中介軟體
您可以在 WebApplication
上設定任何現有的 ASP.NET Core 中介軟體:
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 中介軟體
下表列出一些經常搭配基本 API 使用的中介軟體。
中介軟體 | 描述 | API |
---|---|---|
驗證 | 提供驗證支援。 | UseAuthentication |
授權 | 提供授權支援。 | UseAuthorization |
CORS | 設定跨原始來源資源共用。 | UseCors |
例外處理常式 | 全域處理中介軟體管線擲回的例外狀況。 | UseExceptionHandler |
轉送標頭 | 將設為 Proxy 的標頭轉送到目前要求。 | UseForwardedHeaders |
HTTPS 重新導向 | 將所有 HTTP 要求重新導向至 HTTPS。 | UseHttpsRedirection |
HTTP 嚴格的傳輸安全性 (HSTS) | 增強安全性的中介軟體,可新增特殊的回應標頭。 | UseHsts |
要求記錄 | 提供記錄 HTTP 要求和回應的支援。 | UseHttpLogging |
要求逾時 | 提供設定要求逾時、全域預設和每個端點的支援。 | UseRequestTimeouts |
W3C 要求記錄 | 提供以 W3C 格式記錄 HTTP 要求和回應的支援。 | UseW3CLogging |
回應快取 | 提供快取回應的支援。 | UseResponseCaching |
回應壓縮 | 提供壓縮回應的支援。 | UseResponseCompression |
工作階段 | 提供管理使用者工作階段的支援。 | UseSession |
靜態檔案 | 支援靜態檔案的提供和目錄瀏覽。 | UseStaticFiles, UseFileServer |
WebSocket | 啟用 WebSockets 通訊協定。 | UseWebSockets |
下列各節涵蓋要求處理:路由、參數繫結和回應。
路由
已設定的 WebApplication
支援 Map{Verb}
和 MapMethods,其中的 {Verb}
是駝峰式大小寫的 HTTP 方法,例如 Get
、Post
、Put
或 Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
傳遞給這些方法的 Delegate 引數稱為「路由處理常式」。
路由處理常式
路由處理常式是路由相符時所執行的方法。 路由處理常式可以是 Lambda 運算式、本機函式、執行個體方法或靜態方法。 路由處理常式可以是同步或非同步。
Lambda 運算式
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 支援時,會當做 OpenAPI 作業識別碼使用。 如需詳細資訊,請參閱 OpenAPI。
路由參數
路由參數可以擷取做為路由模式定義的一部分:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
上述程式碼會從 URI /users/3/books/7
傳回 The user id is 3 and book id is 7
。
路由處理常式可以宣告要擷取的參數。 當要求成為宣告要擷取參數的路由時,將剖析參數並傳遞至處理常式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userId
和 bookId
都是 int
。
在上述程式碼中,如果任一路由值無法轉換成 int
,則會擲回例外狀況。 GET 要求 /users/hello/books/3
會擲回下列例外狀況:
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,其可新增端點中繼資料) 來自訂整個端點群組。
例如,下列程式碼會建立兩個類似的端點群組:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
在此案例中,您可以在 201 Created
結果中使用 Location
標頭的相對位址:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
第一組端點只會比對以 /public/todos
為前置詞且不需要任何驗證即可存取的要求。 第二組端點只會比對以 /private/todos
為前置詞且需要驗證的要求。
QueryPrivateTodos
端點篩選處理站是本機函式,可修改路由處理常式的 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);
});
在上述範例中,即使已新增第二個要求,外部篩選條件也會在內部篩選條件之前記錄傳入要求。 由於篩選條件已套用至不同的群組,因此彼此相對的新增順序並不重要。 如果套用至相同的群組或特定端點,則篩選條件的新增順序就很重要。
對 /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
不會隱含從本文繫結。 若要從本文 (JSON 格式) 繫結這些 HTTP 方法,請明確繫結 [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 中原生不支援從表單值繫結。
具有相依性插入的參數繫結
將型別設定為服務時,基本 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 | result |
---|---|
/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 | result |
---|---|
/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 產品呼叫方法。
注意:如果提供無效資料且參數可為 Null,則路由處理常式不會執行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/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 儲存體,或將資料加入佇列提供者的佇列。
- 使用背景工作處理序或雲端函式處理儲存的資料。
例如,資料可能會加入佇列至 Azure 佇列儲存體,或儲存在 Azure Blob 儲存體中。
下列程式碼會實作背景佇列:
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 標頭的已驗證檔案上傳要求。
ASP.NET Core 7.0 中沒有防偽造功能的內建支援。 ASP.NET Core 8.0 和更新版本中提供防偽造功能。 不過,可以使用 IAntiforgery
服務來實作此功能。
從標頭和查詢字串繫結陣列和字串值
下列程式碼會示範將查詢字串繫結至簡單型別、字串陣列和 StringValues 的陣列:
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
型別已實作 TryParse
時,支援將查詢字串或標頭值繫結至複雜型別的陣列。 下列程式碼會繫結至字串陣列,並傳回具有指定標記的所有項目:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
下列程式碼會顯示模型和必要的 TryParse
實作:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
下列程式碼會繫結至 int
陣列:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
若要測試上述程式碼,請新增下列端點以將 Todo
項目填入資料庫:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
使用 HttpRepl
之類的 API 測試工具,將下列資料傳遞至先前的端點:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
下列程式碼會繫結至標頭索引鍵 X-Todo-Id
,並傳回具有相符 Id
值的 Todo
項目:
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
注意
從查詢字串繫結 string[]
時,沒有任何相符的查詢字串值會導致空陣列,而不是 Null 值。
具有 [AsParameters] 引數清單的參數繫結
AsParametersAttribute 可實現型別的簡單參數繫結,而不是複雜或遞迴模型繫結。
請考慮下列程式碼:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
考慮下列 GET
端點:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
下列 struct
可用來取代上述強調顯示的參數:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
重構的 GET
端點會使用上述 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 存放庫中的完整範例程式碼。
自訂繫結
自訂參數繫結的方式有兩個:
- 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態
TryParse
方法來繫結自訂型別。 - 藉由在型別上實作
BindAsync
方法來控制繫結程序。
TryParse
TryParse
有兩個 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下列程式碼會以 URI /map?Point=12.3,10.1
顯示 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
有下列 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下列程式碼會以 URI /products?SortBy=xyz&SortDir=Desc&Page=99
顯示 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
繫結失敗
繫結失敗時,架構會記錄偵錯訊息,並根據失敗模式將各種狀態碼傳回至用戶端。
失敗模式 | 可為 Null 的參數型別 | 繫結來源 | 狀態碼 |
---|---|---|---|
{ParameterType}.TryParse 傳回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 傳回 null |
是 | custom | 400 |
{ParameterType}.BindAsync 擲回 |
沒有關係 | custom | 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
// }
由於範例程式碼會同時設定序列化和還原序列化,因此其可以讀取 NameField
並將 NameField
包含在輸出的 JSON 中。
設定端點的 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 |
如需路由處理常式傳回值的更深入指南,請參閱在基本 API 應用程式中建立回應
範例傳回值
字串傳回值
app.MapGet("/hello", () => "Hello World");
JSON 傳回值
app.MapGet("/hello", () => new { Message = "Hello World" });
傳回 TypedResults
下列程式碼會傳回 TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
相較於傳回 Results,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
IResult 傳回值
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
下列範例會使用內建的結果型別來自訂回應:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自訂狀態碼
app.MapGet("/405", () => Results.StatusCode(405));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
資料流
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
如需更多範例,請參閱在基本 API 應用程式中建立回應。
重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
檔案
app.MapGet("/download", () => Results.File("myfile.text"));
內建結果
常見的結果協助程式存在於 Results 和 TypedResults 靜態類別中。 相較於傳回 Results
,偏好傳回 TypedResults
。 如需詳細資訊,請參閱 TypedResults 與 Results。
自訂結果
應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 HTML 結果型別的範例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
具型別的結果
IResult 介面可以呈現從最小的 API 傳回值,其未利用會將傳回的物件序列化為 HTTP 回應的 JSON 的隱含支援。 靜態 Results 類別是用來建立代表不同回應型別的不同 IResult
物件。 例如,設定回應狀態碼或重新導向至另一個 URL。
實作 IResult
的型別是公用的,可在測試時允許型別判斷。 例如:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
您可以在靜態 TypedResults 類別上查看對應方法的傳回型別,以尋找要轉換成的正確公用 IResult
型別。
如需更多範例,請參閱在基本 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)
另請參閱
此文件:
- 提供基本 API 的快速參考。
- 適合有經驗的開發人員。 如需簡介,請參閱教學課程:使用 ASP.NET Core 建立基本 API
基本 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 範本來建立。
下列程式碼會在未明確建立 WebApplicationBuilder 的情況下建立 WebApplication (app
):
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"
如果 appsettings.json
檔案中也已設定 Kestrel 端點,則會使用 appsettings.json
檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定
從環境讀取連接埠
下列程式碼會從環境讀取連接埠:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS
環境變數,如下一節所示。
透過 ASPNETCORE_URLS 環境變數設定連接埠
ASPNETCORE_URLS
環境變數可用來設定連接埠:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支援多個 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
在所有介面上接聽
下列範例示範在所有介面上接聽
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 在所有介面上接聽
上述範例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
使用開發憑證指定 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
如需開發憑證的詳細資訊,請參閱信任 Windows 和 macOS 上的 ASP.NET Core HTTPS 開發憑證。
使用自訂憑證指定 HTTPS
下列各節說明如何使用 appsettings.json
檔案和透過設定指定自訂憑證。
使用 appsettings.json
指定自訂憑證
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
透過設定指定自訂憑證
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用憑證 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
讀取環境
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
如需使用環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境
組態
下列程式碼會從設定系統讀取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
如需詳細資訊,請參閱 ASP.NET Core 中的設定
記錄
下列程式碼會將訊息寫入至應用程式啟動時的記錄檔:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
如需詳細資訊,請參閱 .NET Core 與 ASP.NET Core 中的記錄
存取相依性插入 (DI) 容器
下列程式碼示範如何在應用程式啟動期間從 DI 容器取得服務:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
如需詳細資訊,請參閱在 ASP.NET Core 中插入相依性。
WebApplicationBuilder
本節包含使用 WebApplicationBuilder 的範例程式碼。
變更內容根目錄、應用程式名稱和環境
下列程式碼會設定內容根目錄、應用程式名稱和環境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 會初始化具有預先設定之預設值的之 WebApplicationBuilder 類別的新執行個體。
如需詳細資訊,請參閱 ASP.NET Core 基礎知識概觀
使用環境變數或命令列來變更內容根目錄、應用程式名稱和環境
下表顯示用來變更內容根目錄、應用程式名稱和環境的環境變數和命令列引數:
功能 | 環境變數 | 命令列引數 |
---|---|---|
應用程式名稱 | ASPNETCORE_APPLICATIONNAME | --applicationName |
環境名稱 | ASPNETCORE_ENVIRONMENT | --environment |
內容根目錄 | ASPNETCORE_CONTENTROOT | --contentRoot |
新增組態提供者
下列範例會新增 INI 設定提供者:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
如需詳細資訊,請參閱 ASP.NET Core 中的設定中的檔案設定提供者。
讀取設定
依預設,WebApplicationBuilder 會從多個來源讀取設定,包括:
appSettings.json
和appSettings.{environment}.json
- 環境變數
- 命令列
如需讀取的設定來源完整清單,請參閱 ASP.NET Core 中的設定的預設設定
下列程式碼會從設定讀取 HelloKey
,並在 /
端點顯示值。 如果設定值為 null,則會將 "Hello" 指派給 message
:
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
您可以使用 Host 屬性來存取 IHostBuilder 上的現有擴充方法:
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
您可以使用 WebApplicationBuilder.WebHost 屬性來存取 IWebHostBuilder 上的擴充方法。
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();
新增中介軟體
您可以在 WebApplication
上設定任何現有的 ASP.NET Core 中介軟體:
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 中介軟體
下表列出一些經常搭配基本 API 使用的中介軟體。
中介軟體 | 描述 | API |
---|---|---|
驗證 | 提供驗證支援。 | UseAuthentication |
授權 | 提供授權支援。 | UseAuthorization |
CORS | 設定跨原始來源資源共用。 | UseCors |
例外處理常式 | 全域處理中介軟體管線擲回的例外狀況。 | UseExceptionHandler |
轉送標頭 | 將設為 Proxy 的標頭轉送到目前要求。 | UseForwardedHeaders |
HTTPS 重新導向 | 將所有 HTTP 要求重新導向至 HTTPS。 | UseHttpsRedirection |
HTTP 嚴格的傳輸安全性 (HSTS) | 增強安全性的中介軟體,可新增特殊的回應標頭。 | UseHsts |
要求記錄 | 提供記錄 HTTP 要求和回應的支援。 | UseHttpLogging |
W3C 要求記錄 | 提供以 W3C 格式記錄 HTTP 要求和回應的支援。 | UseW3CLogging |
回應快取 | 提供快取回應的支援。 | UseResponseCaching |
回應壓縮 | 提供壓縮回應的支援。 | UseResponseCompression |
工作階段 | 提供管理使用者工作階段的支援。 | UseSession |
靜態檔案 | 支援靜態檔案的提供和目錄瀏覽。 | UseStaticFiles, UseFileServer |
WebSocket | 啟用 WebSockets 通訊協定。 | 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();
路由處理常式
路由處理常式是路由相符時所執行的方法。 路由處理常式可以是任何形式的函式,包括同步或非同步。 路由處理常式可以是 Lambda 運算式、本機函式、執行個體方法或靜態方法。
Lambda 運算式
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 支援時,會當做 OpenAPI 作業識別碼使用。 如需詳細資訊,請參閱 OpenAPI。
路由參數
路由參數可以擷取做為路由模式定義的一部分:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
上述程式碼會從 URI /users/3/books/7
傳回 The user id is 3 and book id is 7
。
路由處理常式可以宣告要擷取的參數。 當要求成為宣告要擷取參數的路由時,參數會進行剖析並傳遞至處理常式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userId
和 bookId
都是 int
。
在上述程式碼中,如果任一路由值無法轉換成 int
,則會擲回例外狀況。 GET 要求 /users/hello/books/3
會擲回下列例外狀況:
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
不會隱含從本文繫結。 若要從本文 (JSON 格式) 繫結這些 HTTP 方法,請明確繫結 [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 的參數繫結
將型別設定為服務時,基本 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 | result |
---|---|
/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 | result |
---|---|
/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 產品呼叫方法。
注意:如果提供無效資料且參數可為 Null,則路由處理常式不會執行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/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);
自訂繫結
自訂參數繫結的方式有兩個:
- 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態
TryParse
方法來繫結自訂型別。 - 藉由在型別上實作
BindAsync
方法來控制繫結程序。
TryParse
TryParse
有兩個 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下列程式碼會以 URI /map?Point=12.3,10.1
顯示 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
有下列 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下列程式碼會以 URI /products?SortBy=xyz&SortDir=Desc&Page=99
顯示 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
繫結失敗
繫結失敗時,架構會記錄偵錯訊息,並根據失敗模式將各種狀態碼傳回至用戶端。
失敗模式 | 可為 Null 的參數型別 | 繫結來源 | 狀態碼 |
---|---|---|---|
{ParameterType}.TryParse 傳回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 傳回 null |
是 | custom | 400 |
{ParameterType}.BindAsync 擲回 |
沒有關係 | custom | 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
張貼時{ "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));
Text
app.MapGet("/text", () => Results.Text("This is some text"));
資料流
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
檔案
app.MapGet("/download", () => Results.File("myfile.text"));
內建結果
常見的結果協助程式存在於 Microsoft.AspNetCore.Http.Results
靜態類別中。
描述 | 回應類型 | 狀態碼 | API |
---|---|---|---|
使用進階選項寫入 JSON 回應 | application/json | 200 | Results.Json |
寫入 JSON 回應 | application/json | 200 | Results.Ok |
撰寫文字回應 | text/plain (預設值),可設定 | 200 | Results.Text |
將回應寫入為位元組 | application/octet-stream (預設值),可設定 | 200 | Results.Bytes |
將位元組資料流寫入回應 | application/octet-stream (預設值),可設定 | 200 | Results.Stream |
使用 content-disposition 標頭將檔案串流至回應以下載 | application/octet-stream (預設值),可設定 | 200 | Results.File |
使用選用的 JSON 回應將狀態程式碼設定為 404 | N/A | 404 | Results.NotFound |
將狀態碼設定為 204 | N/A | 204 | Results.NoContent |
使用選用的 JSON 回應將狀態程式碼設定為 422 | N/A | 422 | Results.UnprocessableEntity |
使用選用的 JSON 回應將狀態程式碼設定為 400 | N/A | 400 | Results.BadRequest |
使用選用的 JSON 回應將狀態程式碼設定為 409 | N/A | 409 | Results.Conflict |
將問題回報詳細資料 JSON 物件寫入回應 | N/A | 500 (預設值),可設定 | Results.Problem |
將問題詳細資料 JSON 物件寫入回應,並提供驗證錯誤 | N/A | N/A,可設定 | Results.ValidationProblem |
自訂結果
應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 HTML 結果型別的範例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
授權
可以使用授權原則來保護路由。 這些可以透過 [Authorize]
屬性或使用 RequireAuthorization 方法宣告:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
上述程式碼可以使用 RequireAuthorization 撰寫:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
下列範例使用原則型授權:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
允許未經驗證的使用者存取端點
[AllowAnonymous]
會允許未經驗證的使用者存取端點:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
路由可以使用 CORS 原則來啟用 CORS。 CORS 可以透過 [EnableCors]
屬性或使用 RequireCors 方法宣告。 下列範例會啟用 CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
如需詳細資訊,請參閱在 ASP.NET Core 中啟用跨原始來源要求 (CORS)