基本 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 新增下列中間件:

下列程式碼實際上是將自動中介軟體新增至應用程式所產生的內容:

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 => {});

在某些情況下,應用程式的預設中介軟體設定不正確,而且需要修改。 例如,應該在 UseAuthenticationUseAuthorization 之前呼叫 UseCors。 如果呼叫 UseCors,則應用程式需要呼叫 UseAuthenticationUseAuthorization

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

如果中介軟體應在路由比對發生之前執行,則應該呼叫 UseRouting,而且中介軟體應該放在對 UseRouting 的呼叫之前。 UseEndpoints 在此案例中並非必要項目,因為它會如先前所述自動新增:

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

app.UseRouting();

// other middleware and endpoints

新增終端中介軟體時:

  • 中介軟體必須在 UseEndpoints 之後新增。
  • 應用程式必須呼叫 UseRoutingUseEndpoints,讓終端中介軟體可以放在正確的位置。
app.UseRouting();

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

app.UseEndpoints(e => {});

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

終端中介軟體是會在沒有可處理要求的端點時執行的中介軟體。

使用連接埠

使用 Visual Studio 或 dotnet new 建立 Web 應用程式時,會建立一個 Properties/launchSettings.json 檔案,其指定應用程式回應的連接埠。 在後續的連接埠設定範例中,從 Visual Studio 執行應用程式會傳回錯誤對話方塊 Unable to connect to web server 'AppName'。 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

多個連接埠

在下列程式碼中,應用程式會回應連接埠 30004000

var app = WebApplication.Create(args);

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

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

app.Run();

從命令列設定連接埠

下列命令會讓應用程式回應連接埠 7777

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

如果 appsettings.json 檔案中也已設定 Kestrel 端點,則會使用 appsettings.json 檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定

從環境讀取連接埠

下列程式碼會從環境讀取連接埠:

var app = WebApplication.Create(args);

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

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

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

從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS 環境變數,如下一節所示。

透過 ASPNETCORE_URLS 環境變數設定連接埠

ASPNETCORE_URLS 環境變數可用來設定連接埠:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS 支援多個 URL:

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

在所有介面上接聽

下列範例示範在所有介面上接聽

http://*:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

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

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

app.Run();

使用 ASPNETCORE_URLS 在所有介面上接聽

上述範例可以使用 ASPNETCORE_URLS

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

使用 ASPNETCORE_HTTPS_PORTS 在所有介面上接聽

上述範例可以使用 ASPNETCORE_HTTPS_PORTSASPNETCORE_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.jsonappSettings.{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 方法,例如 GetPostPutDelete

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

路由處理常式可以宣告要擷取的參數。 對宣告為擷取之參數的路由提出要求時,參數會剖析並傳遞至處理程式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userIdbookId 都是 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 擴充方法可協助組織具有常見前置詞的端點群組。 其可減少重複的程式碼,並允許使用單一呼叫方法 (例如 RequireAuthorizationWithMetadata,其可新增端點中繼資料) 來自訂整個端點群組。

例如,下列程式碼會建立兩個類似的端點群組:

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 參數,以允許存取及儲存私人 Todo 資料。

路由群組也支援具有路由參數和條件約束的巢狀群組和複雜前置詞模式。 在下列範例中,對應至 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 方法 GETHEADOPTIONSDELETE 不會隱含從本文繫結。 若要從本文 (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] 使用 IFormFileIFormFileCollection 支援複雜的表單繫結:

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

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

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

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

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

app.Run();

[FromForm] 繫結至要求的參數包含防偽權杖。 處理要求時,防偽權杖會經過驗證。 如需詳細資訊,請參閱使用基本 API 進行防偽

如需詳細資訊,請參閱基本 API 中的表單繫結

完整的範例程式碼位於 AspNetCore.Docs.Samples 存放庫中。

具有相依性插入的參數繫結

將型別設定為服務時,基本 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"));
    
  • HttpRequestHttpResponse:HTTP 要求和 HTTP 回應:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken:與目前 HTTP 要求相關聯的取消權杖:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal:與要求相關聯的使用者,繫結自 HttpContext.User

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

將要求本文繫結為 StreamPipeReader

要求本文可以繫結為 StreamPipeReader,以有效率地支援使用者必須處理資料的案例,並:

  • 將資料儲存至 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();
  • 讀取資料時,StreamHttpRequest.Body 是相同的物件。
  • 預設不會緩衝處理要求本文。 讀取本文之後,將無法倒轉。 無法讀取資料流多次。
  • StreamPipeReader 無法在基本動作處理常式之外使用,因為基礎緩衝區將會受到處置或重複使用。

使用 IFormFile 和 IFormFileCollection 上傳檔案

下列程式碼會使用 IFormFileIFormFileCollection 來上傳檔案:

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

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

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

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

app.Run();

支援使用授權標頭用戶端憑證或 cookie 標頭的已驗證檔案上傳要求。

使用 IFormCollection、IFormFile 和 IFormFileCollection 繫結至表單

支援使用 IFormCollectionIFormFileIFormFileCollection 從表單型參數繫結。 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 中的表單繫結

繫結至表單中的集合和複雜型別

繫結支援下列項目:

  • 集合,例如清單字典
  • 複雜類型,例如 TodoProject

下列程式碼顯示:

  • 將多部分表單輸入繫結至複雜物件的基本端點。
  • 如何使用防偽造服務來支援反偽造權杖的產生和驗證。
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.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" />
               <input type="submit" />
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

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

app.Run();

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

在上述程式碼中:

  • 目標參數必須[FromForm] 屬性註解,以釐清應從 JSON 本文讀取的參數。
  • 對於使用要求委派產生器編譯的基本 API,支援從複雜或集合型別繫結。

從標頭和查詢字串繫結陣列和字串值

下列程式碼會示範將查詢字串繫結至簡單型別、字串陣列和 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 存放庫中的完整範例程式碼

自訂繫結

自訂參數繫結的方式有兩個:

  1. 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態 TryParse 方法來繫結自訂型別。
  2. 藉由在型別上實作 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

繫結優先順序

從參數判斷繫結來源的規則:

  1. 如下順序在參數上定義的明確屬性 (From* 屬性):
    1. 路由值:[FromRoute]
    2. 查詢字串:[FromQuery]
    3. 標題:[FromHeader]
    4. 本文:[FromBody]
    5. 表單:[FromForm]
    6. 服務:[FromServices]
    7. 參數值:[AsParameters]
  2. 特殊型別
    1. HttpContext
    2. HttpRequestHttpContext.Request
    3. HttpResponseHttpContext.Response
    4. ClaimsPrincipalHttpContext.User
    5. CancellationTokenHttpContext.RequestAborted
    6. IFormCollectionHttpContext.Request.Form
    7. IFormFileCollectionHttpContext.Request.Form.Files
    8. IFormFileHttpContext.Request.Form.Files[paramName]
    9. StreamHttpContext.Request.Body
    10. PipeReaderHttpContext.Request.BodyReader
  3. 參數型別具有有效的靜態 BindAsync 方法。
  4. 參數型別是字串或具有有效的靜態 TryParse 方法。
    1. 如果參數名稱存在於路由範本中 (例如 app.Map("/todo/{id}", (int id) => {});),則會從路由繫結。
    2. 從查詢字串繫結。
  5. 如果參數型別是相依性插入所提供的服務,它會使用該服務作為來源。
  6. 參數來自本文。

設定本文繫結的 JSON 還原序列化選項

本文繫結來源會使用 System.Text.Json 進行還原序列化。 無法變更此預設值,但可以設定 JSON 序列化和還原序列化選項。

全域設定 JSON 還原序列化選項

您可以叫用 ConfigureHttpJsonOptions 來設定要全域套用至應用程式的選項。 下列範例包含公用欄位和格式 JSON 輸出。

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

由於範例程式碼會同時設定序列化和還原序列化,因此其可以讀取 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

讀取要求本文

使用 HttpContextHttpRequest 參數直接讀取要求本文:

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();

上述 程式碼:

回覆

路由處理常式支援下列型別的傳回值:

  1. IResult 為基礎 - 這包括 Task<IResult>ValueTask<IResult>
  2. string - 這包括 Task<string>ValueTask<string>
  3. 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://consoto/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"));

內建結果

常見的結果協助程式存在於 ResultsTypedResults 靜態類別中。 相較於傳回 Results,偏好傳回 TypedResults。 如需詳細資訊,請參閱 TypedResults 與 Results

自訂結果

應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 HTML 結果型別的範例:

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

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

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

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

建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。

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

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

app.Run();

具型別的結果

IResult 介面可以呈現從基本 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)

ValidateScopes 和 ValidateOnBuild

ValidateScopesValidateOnBuild 預設 會在開發 環境中啟用,但在其他環境中停用。

當 是 trueValidateOnBuild,DI 容器會在建置時驗證服務組態。 如果服務組態無效,建置會在應用程式啟動時失敗,而不是在要求服務時於運行時間失敗。

當 是 trueValidateScopes,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 { }

下列程式代碼會在 ValidateScopesDevelopment停用 和 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 包含:

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 新增下列中間件:

下列程式碼實際上是將自動中介軟體新增至應用程式所產生的內容:

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 => {});

在某些情況下,應用程式的預設中介軟體設定不正確,而且需要修改。 例如,應該在 UseAuthenticationUseAuthorization 之前呼叫 UseCors。 如果呼叫 UseCors,則應用程式需要呼叫 UseAuthenticationUseAuthorization

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

如果中介軟體應在路由比對發生之前執行,則應該呼叫 UseRouting,而且中介軟體應該放在對 UseRouting 的呼叫之前。 UseEndpoints 在此案例中並非必要項目,因為它會如先前所述自動新增:

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

app.UseRouting();

// other middleware and endpoints

新增終端中介軟體時:

  • 中介軟體必須在 UseEndpoints 之後新增。
  • 應用程式必須呼叫 UseRoutingUseEndpoints,讓終端中介軟體可以放在正確的位置。
app.UseRouting();

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

app.UseEndpoints(e => {});

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

終端中介軟體是會在沒有可處理要求的端點時執行的中介軟體。

使用連接埠

使用 Visual Studio 或 dotnet new 建立 Web 應用程式時,會建立一個 Properties/launchSettings.json 檔案,其指定應用程式回應的連接埠。 在後續的連接埠設定範例中,從 Visual Studio 執行應用程式會傳回錯誤對話方塊 Unable to connect to web server 'AppName'。 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

多個連接埠

在下列程式碼中,應用程式會回應連接埠 30004000

var app = WebApplication.Create(args);

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

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

app.Run();

從命令列設定連接埠

下列命令會讓應用程式回應連接埠 7777

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

如果 appsettings.json 檔案中也已設定 Kestrel 端點,則會使用 appsettings.json 檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定

從環境讀取連接埠

下列程式碼會從環境讀取連接埠:

var app = WebApplication.Create(args);

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

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

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

從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS 環境變數,如下一節所示。

透過 ASPNETCORE_URLS 環境變數設定連接埠

ASPNETCORE_URLS 環境變數可用來設定連接埠:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS 支援多個 URL:

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

如需使用環境的詳細資訊,請參閱在 ASP.NET Core 中使用多個環境

在所有介面上接聽

下列範例示範在所有介面上接聽

http://*:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

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

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

app.Run();

使用 ASPNETCORE_URLS 在所有介面上接聽

上述範例可以使用 ASPNETCORE_URLS

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

使用開發憑證指定 HTTPS

var app = WebApplication.Create(args);

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

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

app.Run();

如需開發憑證的詳細資訊,請參閱信任 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.jsonappSettings.{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 方法,例如 GetPostPutDelete

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

路由處理常式可以宣告要擷取的參數。 對宣告為擷取之參數的路由提出要求時,參數會剖析並傳遞至處理程式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userIdbookId 都是 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 擴充方法可協助組織具有常見前置詞的端點群組。 其可減少重複的程式碼,並允許使用單一呼叫方法 (例如 RequireAuthorizationWithMetadata,其可新增端點中繼資料) 來自訂整個端點群組。

例如,下列程式碼會建立兩個類似的端點群組:

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 參數,以允許存取及儲存私人 Todo 資料。

路由群組也支援具有路由參數和條件約束的巢狀群組和複雜前置詞模式。 在下列範例中,對應至 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 方法 GETHEADOPTIONSDELETE 不會隱含從本文繫結。 若要從本文 (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"));
    
  • HttpRequestHttpResponse:HTTP 要求和 HTTP 回應:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken:與目前 HTTP 要求相關聯的取消權杖:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal:與要求相關聯的使用者,繫結自 HttpContext.User

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

將要求本文繫結為 StreamPipeReader

要求本文可以繫結為 StreamPipeReader,以有效率地支援使用者必須處理資料的案例,並:

  • 將資料儲存至 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();
  • 讀取資料時,StreamHttpRequest.Body 是相同的物件。
  • 預設不會緩衝處理要求本文。 讀取本文之後,將無法倒轉。 無法讀取資料流多次。
  • StreamPipeReader 無法在基本動作處理常式之外使用,因為基礎緩衝區將會受到處置或重複使用。

使用 IFormFile 和 IFormFileCollection 上傳檔案

下列程式碼會使用 IFormFileIFormFileCollection 來上傳檔案:

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

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

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

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

app.Run();

支援使用授權標頭用戶端憑證或 cookie 標頭的已驗證檔案上傳要求。

ASP.NET Core 7.0 中沒有防偽造功能的內建支援。 ASP.NET Core 8.0 和更新版本中提供防偽造功能。 不過,可以使用 IAntiforgery 服務來實作此功能。

從標頭和查詢字串繫結陣列和字串值

下列程式碼會示範將查詢字串繫結至簡單型別、字串陣列和 StringValues 的陣列:

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

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

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

型別已實作 TryParse 時,支援將查詢字串或標頭值繫結至複雜型別的陣列。 下列程式碼會繫結至字串陣列,並傳回具有指定標記的所有項目:

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

下列程式碼會顯示模型和必要的 TryParse 實作:

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

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

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

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

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

下列程式碼會繫結至 int 陣列:

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

若要測試上述程式碼,請新增下列端點以將 Todo 項目填入資料庫:

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

    return Results.Ok(todos);
});

使用類似 HttpRepl 的 API 測試工具,將下列資料傳遞至上一個端點:

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

下列程式碼會繫結至標頭索引鍵 X-Todo-Id,並傳回具有相符 Id 值的 Todo 項目:

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

注意

從查詢字串繫結 string[] 時,沒有任何相符的查詢字串值會導致空陣列,而不是 Null 值。

具有 [AsParameters] 引數清單的參數繫結

AsParametersAttribute 可實現型別的簡單參數繫結,而不是複雜或遞迴模型繫結。

請考慮下列程式碼:

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

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

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

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

考慮下列 GET 端點:

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

下列 struct 可用來取代上述強調顯示的參數:

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

重構的 GET 端點會使用上述 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 存放庫中的完整範例程式碼

自訂繫結

自訂參數繫結的方式有兩個:

  1. 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態 TryParse 方法來繫結自訂型別。
  2. 藉由在型別上實作 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

繫結優先順序

從參數判斷繫結來源的規則:

  1. 如下順序在參數上定義的明確屬性 (From* 屬性):
    1. 路由值:[FromRoute]
    2. 查詢字串:[FromQuery]
    3. 標題:[FromHeader]
    4. 本文:[FromBody]
    5. 服務:[FromServices]
    6. 參數值:[AsParameters]
  2. 特殊型別
    1. HttpContext
    2. HttpRequestHttpContext.Request
    3. HttpResponseHttpContext.Response
    4. ClaimsPrincipalHttpContext.User
    5. CancellationTokenHttpContext.RequestAborted
    6. IFormFileCollectionHttpContext.Request.Form.Files
    7. IFormFileHttpContext.Request.Form.Files[paramName]
    8. StreamHttpContext.Request.Body
    9. PipeReaderHttpContext.Request.BodyReader
  3. 參數型別具有有效的靜態 BindAsync 方法。
  4. 參數型別是字串或具有有效的靜態 TryParse 方法。
    1. 如果參數名稱存在於路由範本中 (例如 app.Map("/todo/{id}", (int id) => {});),則會從路由繫結。
    2. 從查詢字串繫結。
  5. 如果參數型別是相依性插入所提供的服務,它會使用該服務作為來源。
  6. 參數來自本文。

設定本文繫結的 JSON 還原序列化選項

本文繫結來源會使用 System.Text.Json 進行還原序列化。 無法變更此預設值,但可以設定 JSON 序列化和還原序列化選項。

全域設定 JSON 還原序列化選項

您可以叫用 ConfigureHttpJsonOptions 來設定要全域套用至應用程式的選項。 下列範例包含公用欄位和格式 JSON 輸出。

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

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

app.Run();

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

由於範例程式碼會同時設定序列化和還原序列化,因此其可以讀取 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

讀取要求本文

使用 HttpContextHttpRequest 參數直接讀取要求本文:

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();

上述 程式碼:

回覆

路由處理常式支援下列型別的傳回值:

  1. IResult 為基礎 - 這包括 Task<IResult>ValueTask<IResult>
  2. string - 這包括 Task<string>ValueTask<string>
  3. 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://consoto/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"));

內建結果

常見的結果協助程式存在於 ResultsTypedResults 靜態類別中。 相較於傳回 Results,偏好傳回 TypedResults。 如需詳細資訊,請參閱 TypedResults 與 Results

自訂結果

應用程式可以藉由實作自訂 IResult 型別來控制回應。 下列程式碼是 HTML 結果型別的範例:

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

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

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

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

建議您新增擴充方法至 Microsoft.AspNetCore.Http.IResultExtensions,讓這些自訂結果更容易探索。

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

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

app.Run();

具型別的結果

IResult 介面可以呈現從基本 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)

另請參閱

此文件:

基本 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 類別的新執行個體。

使用連接埠

使用 Visual Studio 或 dotnet new 建立 Web 應用程式時,會建立一個 Properties/launchSettings.json 檔案,其指定應用程式回應的連接埠。 在後續的連接埠設定範例中,從 Visual Studio 執行應用程式會傳回錯誤對話方塊 Unable to connect to web server 'AppName'。 從命令列執行下列連接埠變更範例。

下列各節會設定應用程式回應的連接埠。

var app = WebApplication.Create(args);

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

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

在上述程式碼中,應用程式會回應連接埠 3000

多個連接埠

在下列程式碼中,應用程式會回應連接埠 30004000

var app = WebApplication.Create(args);

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

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

app.Run();

從命令列設定連接埠

下列命令會讓應用程式回應連接埠 7777

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

如果 appsettings.json 檔案中也已設定 Kestrel 端點,則會使用 appsettings.json 檔案指定的 URL。 如需詳細資訊,請參閱 Kestrel 端點設定

從環境讀取連接埠

下列程式碼會從環境讀取連接埠:

var app = WebApplication.Create(args);

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

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

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

從環境設定連接埠的偏好方式是使用 ASPNETCORE_URLS 環境變數,如下一節所示。

透過 ASPNETCORE_URLS 環境變數設定連接埠

ASPNETCORE_URLS 環境變數可用來設定連接埠:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS 支援多個 URL:

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

在所有介面上接聽

下列範例示範在所有介面上接聽

http://*:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

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

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

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

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

app.Run();

使用 ASPNETCORE_URLS 在所有介面上接聽

上述範例可以使用 ASPNETCORE_URLS

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

使用開發憑證指定 HTTPS

var app = WebApplication.Create(args);

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

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

app.Run();

如需開發憑證的詳細資訊,請參閱信任 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.jsonappSettings.{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

路由處理常式可以宣告要擷取的參數。 當要求成為宣告要擷取參數的路由時,參數會進行剖析並傳遞至處理常式。 這可讓您輕鬆地以類型安全的方式擷取值。 在上述程式碼中,userIdbookId 都是 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 方法 GETHEADOPTIONSDELETE 不會隱含從本文繫結。 若要從本文 (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"));
    
  • HttpRequestHttpResponse:HTTP 要求和 HTTP 回應:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken:與目前 HTTP 要求相關聯的取消權杖:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal:與要求相關聯的使用者,繫結自 HttpContext.User

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

自訂繫結

自訂參數繫結的方式有兩個:

  1. 針對路由、查詢和標頭繫結來源,可藉由新增型別的靜態 TryParse 方法來繫結自訂型別。
  2. 藉由在型別上實作 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

繫結優先順序

從參數判斷繫結來源的規則:

  1. 如下順序在參數上定義的明確屬性 (From* 屬性):
    1. 路由值:[FromRoute]
    2. 查詢字串:[FromQuery]
    3. 標題:[FromHeader]
    4. 本文:[FromBody]
    5. 服務:[FromServices]
  2. 特殊型別
    1. HttpContext
    2. HttpRequestHttpContext.Request
    3. HttpResponseHttpContext.Response
    4. ClaimsPrincipalHttpContext.User
    5. CancellationTokenHttpContext.RequestAborted
  3. 參數型別具有有效的 BindAsync 方法。
  4. 參數型別是字串或具有有效的 TryParse 方法。
    1. 如果參數名稱存在於路由範本中 (例如 app.Map("/todo/{id}", (int id) => {});),則會從路由繫結。
    2. 從查詢字串繫結。
  5. 如果參數型別是相依性插入所提供的服務,它會使用該服務作為來源。
  6. 參數來自本文。

自訂 JSON 繫結

本文繫結來源會使用 System.Text.Json 來取消序列化。 您無法變更此預設值,但可以使用先前所述的其他技術來自訂繫結。 若要自訂 JSON 序列化程式選項,請使用類似下列的程式碼:

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

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

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

上述 程式碼:

  • 設定輸入和輸出預設 JSON 選項。
  • 傳回下列 JSON
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    張貼時
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

讀取要求本文

使用 HttpContextHttpRequest 參數直接讀取要求本文:

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();

上述 程式碼:

回覆

路由處理常式支援下列型別的傳回值:

  1. IResult 為基礎 - 這包括 Task<IResult>ValueTask<IResult>
  2. string - 這包括 Task<string>ValueTask<string>
  3. 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://consoto/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)

另請參閱

基本 API 中的 OpenAPI 支援