Краткий справочник по минимальным API
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 9 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
Этот документ:
- Предоставляет краткий справочник по минимальным API.
- Предназначен для опытных разработчиков. Общие сведения см. в руководстве по созданию минимального API с помощью ASP.NET Core.
В набор минимальных 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 шаблон пустого веб-проекта.
Следующий код создает WebApplication (app
) без явного создания WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
инициализирует новый экземпляр класса WebApplication с предварительно настроенными значениями по умолчанию.
WebApplication
автоматически добавляет следующее ПО промежуточного слоя в Minimal API applications
зависимости от определенных условий:
-
UseDeveloperExceptionPage
сначала добавляется при выполненииHostingEnvironment
"Development"
действия . -
UseRouting
добавляется второй, если пользовательский код еще не вызвалUseRouting
и если настроены конечные точки, напримерapp.MapGet
. -
UseEndpoints
добавляется в конце конвейера ПО промежуточного слоя, если настроены какие-либо конечные точки. -
UseAuthentication
добавляется сразу после того,UseRouting
как пользовательский код еще не звонилUseAuthentication
и можетIAuthenticationSchemeProvider
быть обнаружен в поставщике услуг.IAuthenticationSchemeProvider
добавляется по умолчанию при использованииAddAuthentication
, а службы обнаруживаются с помощьюIServiceProviderIsService
. -
UseAuthorization
добавляется далее, если пользовательский код еще не вызвалUseAuthorization
и можетIAuthorizationHandlerProvider
быть обнаружен в поставщике услуг.IAuthorizationHandlerProvider
добавляется по умолчанию при использованииAddAuthorization
, а службы обнаруживаются с помощьюIServiceProviderIsService
. - Пользовательские ПО промежуточного слоя и конечные точки добавляются между
UseRouting
иUseEndpoints
.
Следующий код фактически представляет собой то, что добавляется в приложение автоматический по промежуточному слоям:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
В некоторых случаях конфигурация ПО промежуточного слоя по умолчанию не является правильной для приложения и требует изменения. Например, UseCors следует вызывать до UseAuthentication и UseAuthorization. Приложение должно вызываться UseAuthentication
и UseAuthorization
при UseCors
вызове:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
Если по промежуточному слоям следует запустить перед сопоставлением маршрутов, следует вызвать и UseRouting по промежуточному слоям следует поместить перед вызовом UseRouting
.
UseEndpoints Не требуется в этом случае, так как он автоматически добавляется, как описано ранее:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
При добавлении по промежуточного слоя терминала:
- По промежуточному слоям необходимо добавить после
UseEndpoints
. - Приложение должно вызываться
UseRouting
иUseEndpoints
таким образом, чтобы по промежуточному слоя терминала можно было разместить в правильном расположении.
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
ПО промежуточного слоя терминала — это ПО промежуточного слоя, которое выполняется, если конечная точка не обрабатывает запрос.
Использование портов
Если вы создаете веб-приложение с помощью Visual Studio или dotnet new
, автоматически создается файл Properties/launchSettings.json
с указанием портов, на которых отвечает это приложение. Запуск приложения из Visual Studio с параметрами портов, представленными в следующих примерах, возвращает диалоговое окно с сообщением об ошибке Unable to connect to web server 'AppName'
. Visual Studio возвращает ошибку, так как ожидается порт, указанный в Properties/launchSettings.json
, но приложение использует порт, указанный в app.Run("http://localhost:3000")
. Выполните в командной строке следующий пример кода для изменения портов.
В следующих разделах задается порт, на котором отвечает приложение.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
В приведенном выше коде приложение использует порт 3000
.
Несколько портов
В следующем коде приложение использует порты 3000
и 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Настройка порта из командной строки
Следующая команда настраивает для приложения работу с портом 7777
:
dotnet run --urls="https://localhost:7777"
Если в файле Kestrel настроена еще и конечная точка appsettings.json
, то используется файл с URL-адресом, указанным в appsettings.json
. Дополнительные сведения см. в разделе Конфигурация конечной точки Kestrel.
Получение порта из среды
Следующий код считывает значение порта из среды:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
Для настройки порта из среды лучше всего использовать переменную среды ASPNETCORE_URLS
, как показано в следующем разделе.
Настройка портов через переменную среды ASPNETCORE_URLS
Для настройки порта существует переменная среды ASPNETCORE_URLS
:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
поддерживает несколько URL-адресов:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Ожидание передачи данных на всех интерфейсах
В следующих примерах демонстрируется ожидание передачи данных на всех интерфейсах
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Ожидание передачи данных на всех интерфейсах с помощью ASPNETCORE_URLS
В предыдущих примерах можно использовать ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Прослушивание всех интерфейсов с помощью ASPNETCORE_HTTPS_PORTS
Приведенные выше примеры могут использовать ASPNETCORE_HTTPS_PORTS
и ASPNETCORE_HTTP_PORTS
.
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
Дополнительные сведения см. в разделе "Настройка конечных точек" для веб-сервера ASP.NET Core Kestrel
Выбор протокола HTTPS с сертификатом разработки
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Дополнительные сведения о сертификате разработки см. в разделе Доверие к сертификату разработки HTTPS в среде ASP.NET Core на ОС Windows и macOS.
Указание протокола 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();
В следующем коде показано, как получить доступ к ключам из контейнера DI с помощью атрибута [FromKeyedServices]
:
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.";
}
Дополнительные сведения о внедрении зависимостей см. в разделе ASP.NET Core.
WebApplicationBuilder
Пример кода в этом разделе использует WebApplicationBuilder.
Изменение корневой папки содержимого, имени приложения и среды
Следующий код задает корневую папку для содержимого, имя приложения и среду:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию.
Дополнительные сведения см. в статье Обзор основных понятий ASP.NET Core.
Изменение корневого каталога содержимого, имени приложения и среды с помощью переменных среды или командной строки
В следующей таблице представлены переменные среды и аргументы командной строки, которые позволяют изменить корневую папку содержимого, имя приложения и среду:
функция | Переменная среды | Аргумент командной строки |
---|---|---|
Имя приложения | ASPNETCORE_APPLICATIONNAME | --applicationName |
Имя среды | ASPNETCORE_ENVIRONMENT | --environment |
Корневой каталог содержимого | ASPNETCORE_CONTENTROOT | --contentRoot |
Все поставщики конфигурации
Следующий пример добавляет поставщик конфигурации INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Дополнительные сведения см. в разделе Поставщики конфигурации файлов в статье Конфигурация в ASP.NET Core.
Чтение конфигурации
По умолчанию WebApplicationBuilder считывает конфигурацию из нескольких источников, в том числе:
-
appSettings.json
иappSettings.{environment}.json
. - Переменные среды
- Командная строка
Полный список источников конфигурации см. в разделе "Конфигурация по умолчанию" в ASP.NET Core.
Следующий код считывает из конфигурации значение HelloKey
и отображает его в конечной точке /
. Если это значение конфигурации равно NULL, в message
сохраняется значение "Hello":
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Чтение данных из среды
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Добавление поставщиков ведения журнала
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Добавление служб
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Настройка IHostBuilder
К существующим методам расширения IHostBuilder можно обращаться через свойство Host.
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Настройка IWebHostBuilder
К существующим методам расширения IWebHostBuilder можно обращаться через свойство WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Изменение корневой папки веб-сайта
Корневая папка веб-сайта задается относительно корневой папки содержимого. По умолчанию это wwwroot
. В корневой папке веб-сайта ПО промежуточного слоя ищет статические файлы веб-сайта. Корневой веб-сайт можно изменить с помощью WebHostOptions
, командной строки или метода UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Пользовательский контейнер внедрения зависимостей (DI)
В следующем примере используется Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Добавление ПО промежуточного слоя
Любое существующее ПО промежуточного слоя для ASP.NET Core можно настроить в WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Дополнительные сведения см. в статье ПО промежуточного слоя ASP.NET Core.
Страница со сведениями об исключении для разработчика
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию. Страница исключений для разработчиков включена в предварительно настроенных параметрах по умолчанию. При выполнении следующего кода в среде разработки переход к адресу /
отображает страницу с удобным представлением информации об исключении.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ПО промежуточного слоя ASP.NET Core
В следующей таблице собраны некоторые примеры ПО промежуточного слоя, часто используемого с минимальными API.
ПО промежуточного слоя | Description | API |
---|---|---|
Аутентификация | Обеспечивает поддержку проверки подлинности. | UseAuthentication |
Авторизация | Обеспечивает поддержку авторизации. | UseAuthorization |
CORS | Настраивает общий доступ к ресурсам независимо от источника. | UseCors |
Обработчик исключений | Глобально обрабатывает исключения, создаваемые конвейером ПО промежуточного слоя. | UseExceptionHandler |
Forwarded Headers | Пересылает заголовки, переданные через прокси-сервер, в текущий запрос. | UseForwardedHeaders |
HTTPS Redirection | Перенаправляет все запросы с HTTP на HTTPS. | UseHttpsRedirection |
HTTP Strict Transport Security (HSTS) | ПО промежуточного слоя для повышения безопасности, которое добавляет специальный заголовок ответа. | UseHsts |
Ведение журнала запросов | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них. | UseHttpLogging |
Время ожидания запроса | Предоставляет поддержку настройки времени ожидания запроса, глобального значения по умолчанию и для каждой конечной точки. | UseRequestTimeouts |
Ведение журнала запросов W3C | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них в формате консорциума W3C. | UseW3CLogging |
Кэширование ответов | Обеспечивает поддержку для кэширования откликов. | UseResponseCaching |
Сжатие откликов | Обеспечивает поддержку для сжатия откликов. | UseResponseCompression |
Согласованность сеанса | Обеспечивает поддержку для управления пользовательскими сеансами. | UseSession |
Static Files | Обеспечивает поддержку для обработки статических файлов и просмотра каталогов. | UseStaticFiles, UseFileServer |
WebSockets | Обеспечивает поддержку протокола WebSockets. | UseWebSockets |
В следующих разделах рассматриваются обработка запросов: маршрутизация, привязка параметров и ответы.
Маршрутизация
Настроенный WebApplication
метод поддерживает Map{Verb}
и MapMethods где {Verb}
используется метод HTTP с регистром верблюда, например Get
, Post
Put
илиDelete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Аргументы Delegate , передаваемые этим методам, называются обработчиками маршрутов.
Обработчики маршрутов
Обработчики маршрутов — это методы, которые выполняются при обнаружении соответствия для маршрута. В роли обработчика маршрут может выступать лямбда-выражение, локальная функция, метод экземпляра или статический метод. Обработчики маршрутов могут быть синхронными или асинхронными.
Лямбда-выражение
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
Локальная функция
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Метод экземпляра
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Статический метод
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Конечная точка, определенная вне Program.cs
Минимальные API не должны находиться в Program.cs
.
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
См. также группы маршрутов далее в этой статье.
Именованные конечные точки и создание ссылок
Конечные точки можно указать имена для создания URL-адресов конечной точки. Использование именованной конечной точки позволяет избежать сложных путей кода в приложении:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
Приведенный выше код отображает The link to the hello route is /hello
из конечной точки /
.
ПРИМЕЧАНИЕ. Имена конечных точек чувствительны к регистру.
Имена конечных точек:
- Оно должно быть глобально уникальным.
- используются в качестве идентификатора операции OpenAPI, если включена поддержка OpenAPI. Дополнительные сведения см. в статье OpenAPI.
Параметры маршрута
Параметры маршрута могут быть захвачены в составе определения шаблона маршрута:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
Приведенный выше код возвращает The user id is 3 and book id is 7
из URI /users/3/books/7
.
Обработчик маршрута может объявлять параметры, которые нужно захватывать. Когда запрос выполняется в маршрут с параметрами, объявленными для записи, параметры анализируются и передаются обработчику. Это позволяет легко получать значения в строго типизированном виде. В приведенном выше коде userId
и bookId
имеют тип int
.
В приведенном выше коде создается исключение, если значение маршрута не может быть преобразовано в тип int
. Запрос GET по маршруту /users/hello/books/3
выдает следующее исключение:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Использование подстановочных знаков и перехват всех маршрутов
Следующая функция перехвата всех маршрутов возвращает значение Routing to hello
из конечной точки "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Ограничения маршрута
Ограничения маршрута ограничивают возможности сопоставления маршрута.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
В приведенной ниже таблице перечислены представленные выше примеры шаблонов маршрутов и их поведение.
Шаблон маршрута | Пример соответствующего URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Дополнительные сведения см. в разделе Справочник по ограничениям маршрутов в статье Маршрутизация в ASP.NET Core.
Группы маршрутов
Метод MapGroup расширения помогает упорядочивать группы конечных точек с общим префиксом. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata которые добавляют метаданные конечной точки.
Например, следующий код создает две аналогичные группы конечных точек:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
В этом сценарии можно использовать относительный адрес заголовка Location
201 Created
в результате:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Первая группа конечных точек будет соответствовать только запросам, префиксным и /public/todos
доступным без какой-либо проверки подлинности. Вторая группа конечных точек будет соответствовать только запросам, префиксным и /private/todos
требующим проверки подлинности.
Фабрика QueryPrivateTodos
фильтров конечных точек — это локальная функция, которая изменяет параметры обработчика TodoDb
маршрутов, чтобы разрешить доступ к частным данным и хранить данные о частных объектах.
Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с user
группой, может записывать {org}
параметры маршрута, {group}
определенные в префиксах внешней группы.
Префикс также может быть пустым. Это может быть полезно для добавления метаданных конечной точки или фильтров в группу конечных точек без изменения шаблона маршрута.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Добавление фильтров или метаданных в группу ведет себя так же, как и их отдельно к каждой конечной точке перед добавлением дополнительных фильтров или метаданных, которые могли быть добавлены во внутреннюю группу или определенную конечную точку.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
В приведенном выше примере внешний фильтр регистрирует входящий запрос до внутреннего фильтра, даже если он был добавлен вторым. Так как фильтры были применены к разным группам, порядок их добавления относительно друг друга не имеет значения. Фильтры заказов добавляются, если они применяются к той же группе или определенной конечной точке.
Запрос, который /outer/inner/
будет регистрировать следующее:
/outer group filter
/inner group filter
MapGet filter
Привязка параметра
Привязка параметров — это процесс преобразования данных запроса в строго типизированные параметры, выраженные обработчиками маршрутов. Источник привязки определяет, откуда будут привязаны параметры. Источники привязки могут быть явными или выведенными на основе метода HTTP и типа параметра.
Поддерживаемые источники привязки:
- значения маршрута;
- Строка запроса
- Верхний колонтитул
- текст (в формате JSON);
- Значения формы
- службы, предоставляемые путем внедрения зависимостей;
- Пользовательское
В следующем GET
обработчике маршрутов используются некоторые из этих источников привязки параметров:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
В следующей таблице показана связь между параметрами, используемыми в предыдущем примере, и связанными источниками привязки.
Параметр | Источник привязки |
---|---|
id |
Значение маршрута |
page |
строке запроса |
customHeader |
авторизации |
service |
Предоставляется путем внедрения зависимостей |
Методы HTTP GET
, HEAD
, OPTIONS
и DELETE
не выполняют неявную привязку из текста запроса. Чтобы выполнить привязку из тела (как JSON) для этих методов HTTP, следует явно выполнить привязку с [FromBody]
или прочитать из HttpRequest.
В следующем примере обработчик маршрута POST использует источник привязки тела (как JSON) для параметра person
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Все параметры в предыдущих примерах автоматически привязываются из данных запроса. Чтобы продемонстрировать удобство привязки параметров, в следующих обработчиках маршрутов показано, как считывать данные запроса непосредственно из запроса:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
Явная привязка параметров
Атрибуты можно использовать для явного объявления источника, из которого привязываются параметры.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
Параметр | Источник привязки |
---|---|
id |
Значение маршрута с именем id |
page |
Строка запроса с именем "p" |
service |
Предоставляется путем внедрения зависимостей |
contentType |
Заголовок с именем "Content-Type" |
Явная привязка из значений формы
Атрибут [FromForm]
привязывает значения формы:
app.MapPost("/todos", async ([FromForm] string name,
[FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
var todo = new Todo
{
Name = name,
Visibility = visibility
};
if (attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await attachment.CopyToAsync(stream);
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
Альтернативой является использование [AsParameters]
атрибута с пользовательским типом, который имеет свойства, аннотированные с [FromForm]
. Например, следующий код привязывается из значений NewTodoRequest
формы к свойствам структуры записи:
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
Дополнительные сведения см. в разделе об AsParameters далее в этой статье.
Полный пример кода находится в репозитории AspNetCore.Docs.Samples .
Безопасная привязка из IFormFile и IFormFileCollection
Сложная привязка формы поддерживается с помощью IFormFile и IFormFileCollection использования [FromForm]
:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
([FromForm] FileUploadForm fileUploadForm, HttpContext context,
IAntiforgery antiforgery) =>
{
await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
fileUploadForm.Name!, app.Environment.ContentRootPath);
return TypedResults.Ok($"Your file with the description:" +
$" {fileUploadForm.Description} has been uploaded successfully");
});
app.Run();
Параметры, привязанные к запросу, [FromForm]
включают маркер защиты от подделки. Маркер защиты проверяется при обработке запроса. Дополнительные сведения см. в разделе "Антифоргерия с минимальными API".
Дополнительные сведения см. в статье "Привязка формы" в минимальных API.
Полный пример кода находится в репозитории AspNetCore.Docs.Samples .
Привязка параметров с внедрением зависимостей
Привязка параметров для минимальных 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
необязательным, определите для него тип optional (необязательный) или укажите значение по умолчанию:
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 |
Приведенные выше значения nullable и default применяется ко всем источникам:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Приведенный выше код вызывает метод со значением NULL для параметра product, если текст запроса не отправлен.
ПРИМЕЧАНИЕ: если предоставлены недопустимые данные и параметр допускает значение NULL, обработчик маршрутов не выполняется.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI-адрес | result |
---|---|
/products?pageNumber=3 |
Возвращается: 3 |
/products |
Возвращается: 1 |
/products?pageNumber=two |
BadHttpRequestException : Не удалось выполнить привязку параметра "Nullable<int> pageNumber" для "two". |
/products/two |
Ошибка HTTP 404, нет соответствующего маршрута |
Дополнительные сведения см. в разделе Ошибки привязки.
Специальные типы
Следующие типы привязываются без явно заданных атрибутов:
HttpContext — контекст, содержащий все сведения о текущем HTTP-запросе или ответе:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest и HttpResponse — HTTP-запрос и ответ HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken — маркер отмены, связанный с текущим HTTP-запросом:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal — пользователь, связанный с запросом, привязанным из HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Привязка текста запроса в виде Stream
или PipeReader
Тело запроса может привязываться как Stream
или PipeReader
для эффективной поддержки сценариев, в которых пользователю необходимо обрабатывать данные и:
- Хранить данные в хранилище BLOB-объектов или поставить их в очередь у поставщика очередей.
- Обрабатывать хранимые данные с помощью рабочего процесса или облачной функции.
Например, данные могут быть помещены в очередь в Хранилище очередей Azure или храниться в Хранилище BLOB-объектов Azure.
Следующий код реализует фоновую очередь:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
Следующий код привязывает текст запроса к Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
Следующий код демонстрирует полный файл Program.cs
:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- При чтении данных
Stream
— это тот же объект, что иHttpRequest.Body
. - Текст запроса по умолчанию не буферизуется. После чтения текст не перематывается назад. Поток не может быть прочитан несколько раз.
-
Stream
иPipeReader
нельзя использовать за пределами обработчика минимального действия, так как базовые буферы будут удалены или использованы повторно.
Отправка файлов с помощью IFormFile и IFormFileCollection
Следующий код использует IFormFile и IFormFileCollection, чтобы отправить файл:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Поддерживаются запросы на отправку файлов с проверкой подлинности посредством заголовка авторизации, сертификата клиента или заголовка cookie.
Привязка к формам с помощью IFormCollection, IFormFile и IFormFileCollection
Привязка из параметров на основе форм с помощью IFormCollection, IFormFileи IFormFileCollection поддерживается. Метаданные OpenAPI выводятся для параметров формы для поддержки интеграции с пользовательским интерфейсом Swagger.
Следующий код отправляет файлы с помощью выводимой 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;
Привязка к коллекциям и сложным типам из форм
Привязка поддерживается для:
- Коллекции, например list and Dictionary
- Сложные типы, например
Todo
илиProject
В коде демонстрируется следующее.
- Минимальная конечная точка, которая привязывает многокомпонентные входные данные формы к сложному объекту.
- Как использовать службы антифоргерии для поддержки создания и проверки маркеров антифоргерии.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html><body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}"
type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" value="true" />
<input type="submit" />
<input name="isCompleted" type="hidden" value="false" />
</form>
</body></html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>>
([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
try
{
await antiforgery.ValidateRequestAsync(context);
return TypedResults.Ok(todo);
}
catch (AntiforgeryValidationException e)
{
return TypedResults.BadRequest("Invalid antiforgery token");
}
});
app.Run();
class Todo
{
public string Name { get; set; } = string.Empty;
public bool IsCompleted { get; set; } = false;
public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
В предыдущем коде:
- Целевой параметр должен быть аннотирован атрибутом
[FromForm]
, чтобы отсогласить от параметров, которые следует считывать из текста JSON. - Привязка из сложных типов или типов коллекций не поддерживается для минимальных API, скомпилированных с помощью генератора делегатов запросов.
- В разметке показаны дополнительные скрытые входные данные с именем
isCompleted
и значениемfalse
.isCompleted
Если флажок установлен при отправке формы, оба значенияtrue
иfalse
отправляются в виде значений. Если флажок снят, отправляется только скрытое входное значениеfalse
. Процесс привязки основных моделей ASP.NET считывает только первое значение при привязке кbool
значению, что приводит кtrue
флажкам флажков иfalse
снятию флажков.
Пример данных формы, отправленных в предыдущую конечную точку, выглядит следующим образом:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
Привязка массивов и строковых значений из заголовков и строк запроса
Следующий код демонстрирует привязку строк запроса к массиву примитивных типов, массивам строк и StringValues.
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
Привязка строк запроса или значений заголовков к массиву сложных типов поддерживается, если для типа реализовать TryParse
. Следующий код выполняет привязку к массиву строк и возвращает все элементы с указанными тегами.
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
В следующем коде показана модель и требуемая реализация TryParse
.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
В следующем коде выполняется привязка к массиву int
:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Чтобы протестировать предыдущий код, добавьте следующую конечную точку, чтобы заполнить базу данных элементами Todo
.
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Используйте средство, например HttpRepl
передать следующие данные в предыдущую конечную точку:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
В следующем коде выполняется привязка к ключу заголовка X-Todo-Id
и возвращаются элементы Todo
с соответствующими значениями Id
.
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Примечание.
При привязке string[]
из строки запроса отсутствие соответствующего значения строки запроса приведет к пустому массиву вместо значения NULL.
Привязка параметров для списков аргументов с помощью [AsParameters]
AsParametersAttribute обеспечивает простую привязку параметров к типам, а не сложную или рекурсивную привязку модели.
Рассмотрим следующий код:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
Рассмотрим следующую конечную точку GET
:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Для замены указанных выше выделенных параметров можно использовать следующую структуру (struct
):
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Оптимизированная конечная точка GET
использует приведенную выше структуру (struct
) с атрибутом AsParameters:
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
В приведенном ниже коде показаны дополнительные конечные точки в приложении:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Для рефакторинга списков параметров используются следующие классы:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
В следующем коде показаны оптимизированные конечные точки, использующие AsParameters
, предыдущую структуру (struct
) и классы:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Для замены предыдущих параметров можно использовать следующие типы record
:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Использование struct
с AsParameters
может обеспечить лучшую производительность, чем использование типа record
.
Полный пример кода приведен в репозитории AspNetCore.Docs.Samples.
Пользовательская привязка
Существует два способа настроить привязку параметров.
- Если в качестве источника привязки маршрутов используется маршрут, запрос или заголовок, привяжите пользовательские типы путем добавления статического метода
TryParse
для нужного типа. - Управление процессом привязки осуществляется путем реализации метода
BindAsync
для этого типа.
TryParse
TryParse
имеет два API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Следующий код отображает Point: 12.3, 10.1
для URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
имеет следующие API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Следующий код отображает SortBy:xyz, SortDirection:Desc, CurrentPage:99
для URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Ошибки привязки
Если привязка выполняется неудачно, платформа регистрирует сообщение отладки и возвращает клиенту коды состояния, которые могут быть разными в зависимости от условий сбоя.
Режим сбоя | Тип параметра, допускающий значение NULL | Источник привязки | Код состояния |
---|---|---|---|
{ParameterType}.TryParse возвращает false |
yes | Маршрут, запрос или заголовок | 400 |
{ParameterType}.BindAsync возвращает null |
yes | личный | 400 |
Выдает {ParameterType}.BindAsync |
Всё равно | личный | 500 |
Не удалось десериализовать текст JSON | Всё равно | текст | 400 |
Неправильный тип содержимого (не application/json ) |
Всё равно | текст | 415 |
Приоритет привязки
Правила для определения источника привязки на основе параметра:
- Явный атрибут, определенный для атрибутов параметра (From*) в следующем порядке:
- значения маршрута:
[FromRoute]
; - Строка запроса:
[FromQuery]
- заголовок:
[FromHeader]
; - Текст:
[FromBody]
- Форма:
[FromForm]
- служба:
[FromServices]
; - Значения параметров:
[AsParameters]
- значения маршрута:
- Специальные типы
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormCollection
(HttpContext.Request.Form
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- Тип параметра имеет допустимый статический
BindAsync
метод. - Тип параметра является строкой или имеет допустимый статический
TryParse
метод.- Если имя параметра существует в шаблоне маршрута, например,
app.Map("/todo/{id}", (int id) => {});
оно привязано к маршруту. - Привязывается из строки запроса.
- Если имя параметра существует в шаблоне маршрута, например,
- Если тип параметра является службой, предоставляемой путем внедрения зависимостей, то в качестве источника он использует эту службу.
- Параметр извлекается из текста запроса.
Настройка параметров десериализации JSON для привязки текста
Источник привязки тела используется System.Text.Json для десериализации. Изменить этот параметр по умолчанию невозможно, но можно настроить параметры сериализации и десериализации JSON.
Настройка параметров десериализации JSON глобально
Параметры, которые применяются глобально для приложения, можно настроить путем ConfigureHttpJsonOptionsвызова. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Так как пример кода настраивает сериализацию и десериализацию, он может считывать NameField
и включать NameField
в выходной КОД JSON.
Настройка параметров десериализации JSON для конечной точки
ReadFromJsonAsync имеет перегрузки, принимаюющие JsonSerializerOptions объект. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Так как приведенный выше код применяет настраиваемые параметры только к десериализации, выходные данные JSON исключаются NameField
.
Считывание текста запроса
Считайте текст запроса напрямую, используя параметр HttpContext или HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
Предыдущий код:
- Обращается к тексту запроса с помощью HttpRequest.BodyReader.
- Копирует текст запроса в локальный файл.
Отклики
Обработчики маршрутов поддерживают следующие типы возвращаемых значений:
- на основе
IResult
, напримерTask<IResult>
иValueTask<IResult>
; -
string
, напримерTask<string>
иValueTask<string>
; -
T
(любой другой тип), напримерTask<T>
иValueTask<T>
.
Возвращаемое значение | Поведение | Тип контента |
---|---|---|
IResult |
Платформа вызывает IResult.ExecuteAsync | Определяется реализацией IResult |
string |
Платформа записывает строку непосредственно в ответ | text/plain |
T (любой другой тип) |
Платформа JSON-сериализует ответ | application/json |
Более подробное руководство по возврату возвращаемых значений обработчика маршрутов см. в статье "Создание ответов в приложениях API с минимальным количеством"
Примеры возвращаемых значений
Строковые возвращаемые значения
app.MapGet("/hello", () => "Hello World");
Возвращаемые значения в формате JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
Return TypedResults
Следующий код возвращает TypedResultsследующий код:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
TypedResults
Возврат предпочтительнее возвращатьResults. Дополнительные сведения см. в разделе TypedResults и Results.
Возвращаемые значения в формате IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
В следующем примере для настройки ответа используются встроенные типы результатов:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Пользовательский код состояния
app.MapGet("/405", () => Results.StatusCode(405));
Текст
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
Дополнительные примеры см. в статье "Создание ответов в минимальных приложениях API".
Перенаправление
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Файлы
app.MapGet("/download", () => Results.File("myfile.text"));
Встроенные результаты
Распространенные вспомогательные средства результатов существуют в и Results статических TypedResults классах.
TypedResults
Возврат предпочтительнее возвращатьResults
. Дополнительные сведения см. в разделе TypedResults и Results.
Изменение заголовков
Используйте объект HttpResponse
для изменения заголовков ответов:
app.MapGet("/", (HttpContext context) => {
// Set a custom header
context.Response.Headers["X-Custom-Header"] = "CustomValue";
// Set a known header
context.Response.Headers.CacheControl = $"public,max-age=3600";
return "Hello World";
});
Настройка результатов
Приложения могут управлять ответами через пользовательскую реализацию типа IResult. Следующий код является примером результата с типом HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Мы рекомендуем добавить в Microsoft.AspNetCore.Http.IResultExtensions метод расширения, чтобы эти пользовательские результаты было проще искать.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Типизированные результаты
Интерфейс IResult может представлять значения, возвращаемые минимальными API, которые не используют неявную поддержку сериализации возвращаемого объекта в ответ HTTP. Статический класс 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();
Дополнительные сведения см. в статье Включение запросов CORS в ASP.NET Core.
ValidateScopes и ValidateOnBuild
ValidateScopes и ValidateOnBuild включены по умолчанию в среде разработки , но отключены в других средах.
Когда ValidateOnBuild
это true
так, контейнер DI проверяет конфигурацию службы во время сборки. Если конфигурация службы недопустима, сборка завершается ошибкой при запуске приложения, а не во время выполнения при запросе службы.
Когда ValidateScopes
это true
так, контейнер DI проверяет, не разрешена ли служба с областью действия из корневой области. Разрешение ограниченной службы из корневой области может привести к утечке памяти, так как служба хранится в памяти дольше, чем область запроса.
ValidateScopes
значение ValidateOnBuild
false по умолчанию в режимах, отличных от разработки, по соображениям производительности.
Следующий код по ValidateScopes
умолчанию включен в режиме разработки, но отключен в режиме выпуска:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
// Intentionally getting service provider from app, not from the request
// This causes an exception from attempting to resolve a scoped service
// outside of a scope.
// Throws System.InvalidOperationException:
// 'Cannot resolve scoped service 'MyScopedService' from root provider.'
var service = app.Services.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved");
});
app.Run();
public class MyScopedService { }
Следующий код по ValidateOnBuild
умолчанию включен в режиме разработки, но отключен в режиме выпуска:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();
// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
var service = context.RequestServices.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved correctly!");
});
app.Run();
public class MyScopedService { }
public class AnotherService
{
public AnotherService(BrokenService brokenService) { }
}
public class BrokenService { }
Следующий код отключает ValidateScopes
и ValidateOnBuild
в Development
:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
// Doesn't detect the validation problems because ValidateScopes is false.
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = false;
options.ValidateOnBuild = false;
});
}
См. также
- Краткий справочник по минимальным API
- Создание документов OpenAPI
- Создание ответов в минимальных приложениях API
- Фильтры в минимальных приложениях API
- Обработка ошибок в минимальных API
- Проверка подлинности и авторизация в минимальных API
- Тестирование минимальных приложений API
- Маршрутизация с коротким каналом
- Identity Конечные точки API
- Поддержка контейнера внедрения зависимостей к ключу службы
- Взгляд за кулисами минимальных конечных точек API
- Организация api-интерфейсов ASP.NET Core
- Обсуждение проверки Fluent на сайте GitHub
Этот документ:
- Предоставляет краткий справочник по минимальным API.
- Предназначен для опытных разработчиков. Общие сведения см . в руководстве по созданию минимального API с помощью ASP.NET Core
В набор минимальных 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 шаблон пустого веб-проекта.
Следующий код создает WebApplication (app
) без явного создания WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
инициализирует новый экземпляр класса WebApplication с предварительно настроенными значениями по умолчанию.
WebApplication
автоматически добавляет следующее ПО промежуточного слоя в Minimal API applications
зависимости от определенных условий:
-
UseDeveloperExceptionPage
сначала добавляется при выполненииHostingEnvironment
"Development"
действия . -
UseRouting
добавляется второй, если пользовательский код еще не вызвалUseRouting
и если настроены конечные точки, напримерapp.MapGet
. -
UseEndpoints
добавляется в конце конвейера ПО промежуточного слоя, если настроены какие-либо конечные точки. -
UseAuthentication
добавляется сразу после того,UseRouting
как пользовательский код еще не звонилUseAuthentication
и можетIAuthenticationSchemeProvider
быть обнаружен в поставщике услуг.IAuthenticationSchemeProvider
добавляется по умолчанию при использованииAddAuthentication
, а службы обнаруживаются с помощьюIServiceProviderIsService
. -
UseAuthorization
добавляется далее, если пользовательский код еще не вызвалUseAuthorization
и можетIAuthorizationHandlerProvider
быть обнаружен в поставщике услуг.IAuthorizationHandlerProvider
добавляется по умолчанию при использованииAddAuthorization
, а службы обнаруживаются с помощьюIServiceProviderIsService
. - Пользовательские ПО промежуточного слоя и конечные точки добавляются между
UseRouting
иUseEndpoints
.
Следующий код фактически представляет собой то, что добавляется в приложение автоматический по промежуточному слоям:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
В некоторых случаях конфигурация ПО промежуточного слоя по умолчанию не является правильной для приложения и требует изменения. Например, UseCors следует вызывать до UseAuthentication и UseAuthorization. Приложение должно вызываться UseAuthentication
и UseAuthorization
при UseCors
вызове:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
Если по промежуточному слоям следует запустить перед сопоставлением маршрутов, следует вызвать и UseRouting по промежуточному слоям следует поместить перед вызовом UseRouting
.
UseEndpoints Не требуется в этом случае, так как он автоматически добавляется, как описано ранее:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
При добавлении по промежуточного слоя терминала:
- По промежуточному слоям необходимо добавить после
UseEndpoints
. - Приложение должно вызываться
UseRouting
иUseEndpoints
таким образом, чтобы по промежуточному слоя терминала можно было разместить в правильном расположении.
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
ПО промежуточного слоя терминала — это ПО промежуточного слоя, которое выполняется, если конечная точка не обрабатывает запрос.
Использование портов
Если вы создаете веб-приложение с помощью Visual Studio или dotnet new
, автоматически создается файл Properties/launchSettings.json
с указанием портов, на которых отвечает это приложение. Запуск приложения из Visual Studio с параметрами портов, представленными в следующих примерах, возвращает диалоговое окно с сообщением об ошибке Unable to connect to web server 'AppName'
. Visual Studio возвращает ошибку, так как ожидается порт, указанный в Properties/launchSettings.json
, но приложение использует порт, указанный в app.Run("http://localhost:3000")
. Выполните в командной строке следующий пример кода для изменения портов.
В следующих разделах задается порт, на котором отвечает приложение.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
В приведенном выше коде приложение использует порт 3000
.
Несколько портов
В следующем коде приложение использует порты 3000
и 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Настройка порта из командной строки
Следующая команда настраивает для приложения работу с портом 7777
:
dotnet run --urls="https://localhost:7777"
Если в файле Kestrel настроена еще и конечная точка appsettings.json
, то используется файл с URL-адресом, указанным в appsettings.json
. Дополнительные сведения см. в разделе Конфигурация конечной точки 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();
Дополнительные сведения о сертификате разработки см. в разделе Доверие к сертификату разработки HTTPS в среде ASP.NET Core на ОС Windows и macOS.
Указание протокола HTTPS с пользовательским сертификатом
В следующих разделах показано, как указать пользовательский сертификат с помощью appsettings.json
файла и конфигурации.
Настройка пользовательского сертификата в appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Настройка пользовательского сертификата в конфигурации
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Использование API сертификатов
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Настройка
Следующий код считывает данные из системы конфигурации:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
Дополнительные сведения см. в статье Конфигурация в ASP.NET Core.
Ведение журнала
Следующий код записывает сообщение в журнал при запуске приложения:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
Дополнительные сведения см. в статье Ведение журнала в .NET Core и ASP.NET Core.
Доступ к контейнеру внедрения зависимостей (DI)
В следующем коде показано, как получить службы из контейнера DI во время запуска приложения.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
Дополнительные сведения см. в статье Внедрение зависимостей в ASP.NET Core.
WebApplicationBuilder
Пример кода в этом разделе использует WebApplicationBuilder.
Изменение корневой папки содержимого, имени приложения и среды
Следующий код задает корневую папку для содержимого, имя приложения и среду:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию.
Дополнительные сведения см. в статье Обзор основных понятий ASP.NET Core.
Изменение корневой папки содержимого, имени приложения и среды через переменные среды или командную строку
В следующей таблице представлены переменные среды и аргументы командной строки, которые позволяют изменить корневую папку содержимого, имя приложения и среду:
функция | Переменная среды | Аргумент командной строки |
---|---|---|
Имя приложения | ASPNETCORE_APPLICATIONNAME | --applicationName |
Имя среды | ASPNETCORE_ENVIRONMENT | --environment |
Корневой каталог содержимого | ASPNETCORE_CONTENTROOT | --contentRoot |
Все поставщики конфигурации
Следующий пример добавляет поставщик конфигурации INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Дополнительные сведения см. в разделе Поставщики конфигурации файлов в статье Конфигурация в ASP.NET Core.
Чтение конфигурации
По умолчанию WebApplicationBuilder считывает конфигурацию из нескольких источников, в том числе:
-
appSettings.json
иappSettings.{environment}.json
. - Переменные среды
- Командная строка
Следующий код считывает из конфигурации значение HelloKey
и отображает его в конечной точке /
. Если это значение конфигурации равно NULL, в message
сохраняется значение "Hello":
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Полный список считываемых источников конфигурации представлен в разделе Конфигурация по умолчанию в статье Конфигурация в ASP.NET Core.
Добавление поставщиков ведения журнала
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Добавление служб
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Настройка IHostBuilder
К существующим методам расширения IHostBuilder можно обращаться через свойство Host.
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Настройка IWebHostBuilder
К существующим методам расширения IWebHostBuilder можно обращаться через свойство WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Изменение корневой папки веб-сайта
Корневая папка веб-сайта задается относительно корневой папки содержимого. По умолчанию это wwwroot
. В корневой папке веб-сайта ПО промежуточного слоя ищет статические файлы веб-сайта. Корневой веб-сайт можно изменить с помощью WebHostOptions
, командной строки или метода UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Пользовательский контейнер внедрения зависимостей (DI)
В следующем примере используется Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Добавление ПО промежуточного слоя
Любое существующее ПО промежуточного слоя для ASP.NET Core можно настроить в WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Дополнительные сведения см. в статье ПО промежуточного слоя ASP.NET Core.
Страница со сведениями об исключении для разработчика
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию. Страница исключений для разработчиков включена в предварительно настроенных параметрах по умолчанию. При выполнении следующего кода в среде разработки переход к адресу /
отображает страницу с удобным представлением информации об исключении.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ПО промежуточного слоя ASP.NET Core
В следующей таблице собраны некоторые примеры ПО промежуточного слоя, часто используемого с минимальными API.
ПО промежуточного слоя | Description | API |
---|---|---|
Аутентификация | Обеспечивает поддержку проверки подлинности. | UseAuthentication |
Авторизация | Обеспечивает поддержку авторизации. | UseAuthorization |
CORS | Настраивает общий доступ к ресурсам независимо от источника. | UseCors |
Обработчик исключений | Глобально обрабатывает исключения, создаваемые конвейером ПО промежуточного слоя. | UseExceptionHandler |
Forwarded Headers | Пересылает заголовки, переданные через прокси-сервер, в текущий запрос. | UseForwardedHeaders |
HTTPS Redirection | Перенаправляет все запросы с HTTP на HTTPS. | UseHttpsRedirection |
HTTP Strict Transport Security (HSTS) | ПО промежуточного слоя для повышения безопасности, которое добавляет специальный заголовок ответа. | UseHsts |
Ведение журнала запросов | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них. | UseHttpLogging |
Время ожидания запроса | Предоставляет поддержку настройки времени ожидания запроса, глобального значения по умолчанию и для каждой конечной точки. | UseRequestTimeouts |
Ведение журнала запросов W3C | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них в формате консорциума W3C. | UseW3CLogging |
Кэширование ответов | Обеспечивает поддержку для кэширования откликов. | UseResponseCaching |
Сжатие откликов | Обеспечивает поддержку для сжатия откликов. | UseResponseCompression |
Согласованность сеанса | Обеспечивает поддержку для управления пользовательскими сеансами. | UseSession |
Static Files | Обеспечивает поддержку для обработки статических файлов и просмотра каталогов. | UseStaticFiles, UseFileServer |
WebSockets | Обеспечивает поддержку протокола WebSockets. | UseWebSockets |
В следующих разделах рассматриваются обработка запросов: маршрутизация, привязка параметров и ответы.
Маршрутизация
Настроенный WebApplication
метод поддерживает Map{Verb}
и MapMethods где {Verb}
используется метод HTTP с регистром верблюда, например Get
, Post
Put
илиDelete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Аргументы Delegate , передаваемые этим методам, называются обработчиками маршрутов.
Обработчики маршрутов
Обработчики маршрутов — это методы, которые выполняются при обнаружении соответствия для маршрута. В роли обработчика маршрут может выступать лямбда-выражение, локальная функция, метод экземпляра или статический метод. Обработчики маршрутов могут быть синхронными или асинхронными.
Лямбда-выражение
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
Локальная функция
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Метод экземпляра
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Статический метод
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Конечная точка, определенная вне Program.cs
Минимальные API не должны находиться в Program.cs
.
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
См. также группы маршрутов далее в этой статье.
Именованные конечные точки и создание ссылок
Конечные точки можно указать имена для создания URL-адресов конечной точки. Использование именованной конечной точки позволяет избежать сложных путей кода в приложении:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
Приведенный выше код отображает The link to the hello route is /hello
из конечной точки /
.
ПРИМЕЧАНИЕ. Имена конечных точек чувствительны к регистру.
Имена конечных точек:
- Оно должно быть глобально уникальным.
- используются в качестве идентификатора операции OpenAPI, если включена поддержка OpenAPI. Дополнительные сведения см. в статье OpenAPI.
Параметры маршрута
Параметры маршрута могут быть захвачены в составе определения шаблона маршрута:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
Приведенный выше код возвращает The user id is 3 and book id is 7
из URI /users/3/books/7
.
Обработчик маршрута может объявлять параметры, которые нужно захватывать. Когда запрос выполняется в маршрут с параметрами, объявленными для записи, параметры анализируются и передаются обработчику. Это позволяет легко получать значения в строго типизированном виде. В приведенном выше коде userId
и bookId
имеют тип int
.
В приведенном выше коде создается исключение, если значение маршрута не может быть преобразовано в тип int
. Запрос GET по маршруту /users/hello/books/3
выдает следующее исключение:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Использование подстановочных знаков и перехват всех маршрутов
Следующая функция перехвата всех маршрутов возвращает значение Routing to hello
из конечной точки "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Ограничения маршрута
Ограничения маршрута ограничивают возможности сопоставления маршрута.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
В приведенной ниже таблице перечислены представленные выше примеры шаблонов маршрутов и их поведение.
Шаблон маршрута | Пример соответствующего URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Дополнительные сведения см. в разделе Справочник по ограничениям маршрутов в статье Маршрутизация в ASP.NET Core.
Группы маршрутов
Метод MapGroup расширения помогает упорядочивать группы конечных точек с общим префиксом. Это уменьшает повторяющийся код и позволяет настраивать целые группы конечных точек с одним вызовом методов, таких как RequireAuthorization и WithMetadata которые добавляют метаданные конечной точки.
Например, следующий код создает две аналогичные группы конечных точек:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
В этом сценарии можно использовать относительный адрес заголовка Location
201 Created
в результате:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
Первая группа конечных точек будет соответствовать только запросам, префиксным и /public/todos
доступным без какой-либо проверки подлинности. Вторая группа конечных точек будет соответствовать только запросам, префиксным и /private/todos
требующим проверки подлинности.
Фабрика QueryPrivateTodos
фильтров конечных точек — это локальная функция, которая изменяет параметры обработчика TodoDb
маршрутов, чтобы разрешить доступ к частным данным и хранить данные о частных объектах.
Группы маршрутов также поддерживают вложенные группы и сложные шаблоны префикса с параметрами маршрута и ограничениями. В следующем примере обработчик маршрутов, сопоставленный с user
группой, может записывать {org}
параметры маршрута, {group}
определенные в префиксах внешней группы.
Префикс также может быть пустым. Это может быть полезно для добавления метаданных конечной точки или фильтров в группу конечных точек без изменения шаблона маршрута.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
Добавление фильтров или метаданных в группу ведет себя так же, как и их отдельно к каждой конечной точке перед добавлением дополнительных фильтров или метаданных, которые могли быть добавлены во внутреннюю группу или определенную конечную точку.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
В приведенном выше примере внешний фильтр регистрирует входящий запрос до внутреннего фильтра, даже если он был добавлен вторым. Так как фильтры были применены к разным группам, порядок их добавления относительно друг друга не имеет значения. Фильтры заказов добавляются, если они применяются к той же группе или определенной конечной точке.
Запрос, который /outer/inner/
будет регистрировать следующее:
/outer group filter
/inner group filter
MapGet filter
Привязка параметра
Привязка параметров — это процесс преобразования данных запроса в строго типизированные параметры, выраженные обработчиками маршрутов. Источник привязки определяет, откуда будут привязаны параметры. Источники привязки могут быть явными или выведенными на основе метода HTTP и типа параметра.
Поддерживаемые источники привязки:
- значения маршрута;
- Строка запроса
- Верхний колонтитул
- текст (в формате JSON);
- службы, предоставляемые путем внедрения зависимостей;
- Пользовательское
Привязка из значений формы изначально не поддерживается в .NET 6 и 7.
В следующем GET
обработчике маршрутов используются некоторые из этих источников привязки параметров:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
В следующей таблице показана связь между параметрами, используемыми в предыдущем примере, и связанными источниками привязки.
Параметр | Источник привязки |
---|---|
id |
Значение маршрута |
page |
строке запроса |
customHeader |
авторизации |
service |
Предоставляется путем внедрения зависимостей |
Методы HTTP GET
, HEAD
, OPTIONS
и DELETE
не выполняют неявную привязку из текста запроса. Чтобы выполнить привязку из тела (как JSON) для этих методов HTTP, следует явно выполнить привязку с [FromBody]
или прочитать из HttpRequest.
В следующем примере обработчик маршрута POST использует источник привязки тела (как JSON) для параметра person
:
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
необязательным, определите для него тип optional (необязательный) или укажите значение по умолчанию:
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 |
Приведенные выше значения nullable и default применяется ко всем источникам:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Приведенный выше код вызывает метод со значением NULL для параметра product, если текст запроса не отправлен.
ПРИМЕЧАНИЕ: если предоставлены недопустимые данные и параметр допускает значение NULL, обработчик маршрутов не выполняется.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI-адрес | result |
---|---|
/products?pageNumber=3 |
Возвращается: 3 |
/products |
Возвращается: 1 |
/products?pageNumber=two |
BadHttpRequestException : Не удалось выполнить привязку параметра "Nullable<int> pageNumber" для "two". |
/products/two |
Ошибка HTTP 404, нет соответствующего маршрута |
Дополнительные сведения см. в разделе Ошибки привязки.
Специальные типы
Следующие типы привязываются без явно заданных атрибутов:
HttpContext — контекст, содержащий все сведения о текущем HTTP-запросе или ответе:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest и HttpResponse — HTTP-запрос и ответ HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken — маркер отмены, связанный с текущим HTTP-запросом:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal — пользователь, связанный с запросом, привязанным из HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Привязка текста запроса в виде Stream
или PipeReader
Тело запроса может привязываться как Stream
или PipeReader
для эффективной поддержки сценариев, в которых пользователю необходимо обрабатывать данные и:
- Хранить данные в хранилище BLOB-объектов или поставить их в очередь у поставщика очередей.
- Обрабатывать хранимые данные с помощью рабочего процесса или облачной функции.
Например, данные могут быть помещены в очередь в Хранилище очередей Azure или храниться в Хранилище BLOB-объектов Azure.
Следующий код реализует фоновую очередь:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
Следующий код привязывает текст запроса к Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
Следующий код демонстрирует полный файл Program.cs
:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- При чтении данных
Stream
— это тот же объект, что иHttpRequest.Body
. - Текст запроса по умолчанию не буферизуется. После чтения текст не перематывается назад. Поток не может быть прочитан несколько раз.
-
Stream
иPipeReader
нельзя использовать за пределами обработчика минимального действия, так как базовые буферы будут удалены или использованы повторно.
Отправка файлов с помощью IFormFile и IFormFileCollection
Следующий код использует IFormFile и IFormFileCollection, чтобы отправить файл:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Поддерживаются запросы на отправку файлов с проверкой подлинности посредством заголовка авторизации, сертификата клиента или заголовка cookie.
Встроенная поддержка антифоргерии в ASP.NET Core 7.0 отсутствует.
Антифоргерия доступна в ASP.NET Core 8.0 и более поздних версий. Однако ее можно реализовать с помощью службы IAntiforgery
.
Привязка массивов и строковых значений из заголовков и строк запроса
Следующий код демонстрирует привязку строк запроса к массиву примитивных типов, массивам строк и StringValues.
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
Привязка строк запроса или значений заголовков к массиву сложных типов поддерживается, если для типа реализовать TryParse
. Следующий код выполняет привязку к массиву строк и возвращает все элементы с указанными тегами.
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
В следующем коде показана модель и требуемая реализация TryParse
.
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
В следующем коде выполняется привязка к массиву int
:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Чтобы протестировать предыдущий код, добавьте следующую конечную точку, чтобы заполнить базу данных элементами Todo
.
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Используйте средство тестирования API, например HttpRepl
передать следующие данные в предыдущую конечную точку:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
В следующем коде выполняется привязка к ключу заголовка X-Todo-Id
и возвращаются элементы Todo
с соответствующими значениями Id
.
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Примечание.
При привязке string[]
из строки запроса отсутствие соответствующего значения строки запроса приведет к пустому массиву вместо значения NULL.
Привязка параметров для списков аргументов с помощью [AsParameters]
AsParametersAttribute обеспечивает простую привязку параметров к типам, а не сложную или рекурсивную привязку модели.
Рассмотрим следующий код:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
Рассмотрим следующую конечную точку GET
:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Для замены указанных выше выделенных параметров можно использовать следующую структуру (struct
):
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
Оптимизированная конечная точка GET
использует приведенную выше структуру (struct
) с атрибутом AsParameters:
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
В приведенном ниже коде показаны дополнительные конечные точки в приложении:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Для рефакторинга списков параметров используются следующие классы:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
В следующем коде показаны оптимизированные конечные точки, использующие AsParameters
, предыдущую структуру (struct
) и классы:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Для замены предыдущих параметров можно использовать следующие типы record
:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
Использование struct
с AsParameters
может обеспечить лучшую производительность, чем использование типа record
.
Полный пример кода приведен в репозитории AspNetCore.Docs.Samples.
Пользовательская привязка
Существует два способа настроить привязку параметров.
- Если в качестве источника привязки маршрутов используется маршрут, запрос или заголовок, привяжите пользовательские типы путем добавления статического метода
TryParse
для нужного типа. - Управление процессом привязки осуществляется путем реализации метода
BindAsync
для этого типа.
TryParse
TryParse
имеет два API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Следующий код отображает Point: 12.3, 10.1
для URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
имеет следующие API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Следующий код отображает SortBy:xyz, SortDirection:Desc, CurrentPage:99
для URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Ошибки привязки
Если привязка выполняется неудачно, платформа регистрирует сообщение отладки и возвращает клиенту коды состояния, которые могут быть разными в зависимости от условий сбоя.
Режим сбоя | Тип параметра, допускающий значение NULL | Источник привязки | Код состояния |
---|---|---|---|
{ParameterType}.TryParse возвращает false |
yes | Маршрут, запрос или заголовок | 400 |
{ParameterType}.BindAsync возвращает null |
yes | личный | 400 |
Выдает {ParameterType}.BindAsync |
Не имеет значения | личный | 500 |
Не удалось десериализовать текст JSON | Не имеет значения | текст | 400 |
Неправильный тип содержимого (не application/json ) |
Не имеет значения | текст | 415 |
Приоритет привязки
Правила для определения источника привязки на основе параметра:
- Явный атрибут, определенный для атрибутов параметра (From*) в следующем порядке:
- значения маршрута:
[FromRoute]
; - Строка запроса:
[FromQuery]
- заголовок:
[FromHeader]
; - Текст:
[FromBody]
- служба:
[FromServices]
; - Значения параметров:
[AsParameters]
- значения маршрута:
- Специальные типы
HttpContext
-
HttpRequest
(HttpContext.Request
) -
HttpResponse
(HttpContext.Response
) -
ClaimsPrincipal
(HttpContext.User
) -
CancellationToken
(HttpContext.RequestAborted
) -
IFormFileCollection
(HttpContext.Request.Form.Files
) -
IFormFile
(HttpContext.Request.Form.Files[paramName]
) -
Stream
(HttpContext.Request.Body
) -
PipeReader
(HttpContext.Request.BodyReader
)
- Тип параметра имеет допустимый статический
BindAsync
метод. - Тип параметра является строкой или имеет допустимый статический
TryParse
метод.- Если имя параметра существует в шаблоне маршрута. В
app.Map("/todo/{id}", (int id) => {});
,id
привязана к маршруту. - Привязывается из строки запроса.
- Если имя параметра существует в шаблоне маршрута. В
- Если тип параметра является службой, предоставляемой путем внедрения зависимостей, то в качестве источника он использует эту службу.
- Параметр извлекается из текста запроса.
Настройка параметров десериализации JSON для привязки текста
Источник привязки тела используется System.Text.Json для десериализации. Изменить этот параметр по умолчанию невозможно, но можно настроить параметры сериализации и десериализации JSON.
Настройка параметров десериализации JSON глобально
Параметры, которые применяются глобально для приложения, можно настроить путем ConfigureHttpJsonOptionsвызова. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Так как пример кода настраивает сериализацию и десериализацию, он может считывать NameField
и включать NameField
в выходной КОД JSON.
Настройка параметров десериализации JSON для конечной точки
ReadFromJsonAsync имеет перегрузки, принимаюющие JsonSerializerOptions объект. В следующем примере содержатся общедоступные поля и форматы выходных данных JSON.
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Так как приведенный выше код применяет настраиваемые параметры только к десериализации, выходные данные JSON исключаются NameField
.
Считывание текста запроса
Считайте текст запроса напрямую, используя параметр HttpContext или HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
Предыдущий код:
- Обращается к тексту запроса с помощью HttpRequest.BodyReader.
- Копирует текст запроса в локальный файл.
Отклики
Обработчики маршрутов поддерживают следующие типы возвращаемых значений:
- на основе
IResult
, напримерTask<IResult>
иValueTask<IResult>
; -
string
, напримерTask<string>
иValueTask<string>
; -
T
(любой другой тип), напримерTask<T>
иValueTask<T>
.
Возвращаемое значение | Поведение | Тип контента |
---|---|---|
IResult |
Платформа вызывает IResult.ExecuteAsync | Определяется реализацией IResult |
string |
Платформа записывает строку непосредственно в ответ | text/plain |
T (любой другой тип) |
Платформа JSON-сериализует ответ | application/json |
Более подробное руководство по возврату возвращаемых значений обработчика маршрутов см. в статье "Создание ответов в приложениях API с минимальным количеством"
Примеры возвращаемых значений
Строковые возвращаемые значения
app.MapGet("/hello", () => "Hello World");
Возвращаемые значения в формате JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
Return TypedResults
Следующий код возвращает TypedResultsследующий код:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
TypedResults
Возврат предпочтительнее возвращатьResults. Дополнительные сведения см. в разделе TypedResults и Results.
Возвращаемые значения в формате IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
В следующем примере для настройки ответа используются встроенные типы результатов:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Пользовательский код состояния
app.MapGet("/405", () => Results.StatusCode(405));
Текст
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
Дополнительные примеры см. в статье "Создание ответов в минимальных приложениях API".
Перенаправление
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Файлы
app.MapGet("/download", () => Results.File("myfile.text"));
Встроенные результаты
Распространенные вспомогательные средства результатов существуют в и Results статических TypedResults классах.
TypedResults
Возврат предпочтительнее возвращатьResults
. Дополнительные сведения см. в разделе TypedResults и Results.
Настройка результатов
Приложения могут управлять ответами через пользовательскую реализацию типа IResult. Следующий код является примером результата с типом HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Мы рекомендуем добавить в Microsoft.AspNetCore.Http.IResultExtensions метод расширения, чтобы эти пользовательские результаты было проще искать.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Типизированные результаты
Интерфейс IResult может представлять значения, возвращаемые минимальными API, которые не используют неявную поддержку сериализации возвращаемого объекта в ответ HTTP. Статический класс 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();
Дополнительные сведения см. в статье Включение запросов CORS в ASP.NET Core.
См. также
Этот документ:
- Предоставляет краткий справочник по минимальным API.
- Предназначен для опытных разработчиков. Общие сведения см . в руководстве по созданию минимального API с помощью ASP.NET Core
В набор минимальных API входят следующие элементы:
- WebApplication и WebApplicationBuilder.
- Обработчики маршрутов
WebApplication
Шаблон ASP.NET Core создает следующий код:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Приведенный выше код можно создать, набрав dotnet new web
в командной строке или выбрав в Visual Studio шаблон пустого веб-проекта.
Следующий код создает WebApplication (app
) без явного создания WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
инициализирует новый экземпляр класса WebApplication с предварительно настроенными значениями по умолчанию.
Использование портов
Если вы создаете веб-приложение с помощью Visual Studio или dotnet new
, автоматически создается файл Properties/launchSettings.json
с указанием портов, на которых отвечает это приложение. Запуск приложения из Visual Studio с параметрами портов, представленными в следующих примерах, возвращает диалоговое окно с сообщением об ошибке Unable to connect to web server 'AppName'
. Выполните в командной строке следующий пример кода для изменения портов.
В следующих разделах задается порт, на котором отвечает приложение.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
В приведенном выше коде приложение использует порт 3000
.
Несколько портов
В следующем коде приложение использует порты 3000
и 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Настройка порта из командной строки
Следующая команда настраивает для приложения работу с портом 7777
:
dotnet run --urls="https://localhost:7777"
Если в файле Kestrel настроена еще и конечная точка appsettings.json
, то используется файл с URL-адресом, указанным в appsettings.json
. Дополнительные сведения см. в разделе Конфигурация конечной точки 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();
Дополнительные сведения о сертификате разработки см. в разделе Доверие к сертификату разработки HTTPS в среде ASP.NET Core на ОС Windows и macOS.
Указание протокола HTTPS с пользовательским сертификатом
В следующих разделах показано, как указать пользовательский сертификат с помощью appsettings.json
файла и конфигурации.
Настройка пользовательского сертификата в appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Настройка пользовательского сертификата в конфигурации
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Использование API сертификатов
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Чтение данных из среды
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
Дополнительные сведения об использовании среды см. в статье Использование нескольких сред в ASP.NET Core.
Настройка
Следующий код считывает данные из системы конфигурации:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
Дополнительные сведения см. в статье Конфигурация в ASP.NET Core.
Ведение журнала
Следующий код записывает сообщение в журнал при запуске приложения:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
Дополнительные сведения см. в статье Ведение журнала в .NET Core и ASP.NET Core.
Доступ к контейнеру внедрения зависимостей (DI)
В следующем коде показано, как получить службы из контейнера DI во время запуска приложения.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
Дополнительные сведения см. в статье Внедрение зависимостей в ASP.NET Core.
WebApplicationBuilder
Пример кода в этом разделе использует WebApplicationBuilder.
Изменение корневой папки содержимого, имени приложения и среды
Следующий код задает корневую папку для содержимого, имя приложения и среду:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию.
Дополнительные сведения см. в статье Обзор основных понятий ASP.NET Core.
Изменение корневой папки содержимого, имени приложения и среды через переменные среды или командную строку
В следующей таблице представлены переменные среды и аргументы командной строки, которые позволяют изменить корневую папку содержимого, имя приложения и среду:
функция | Переменная среды | Аргумент командной строки |
---|---|---|
Имя приложения | ASPNETCORE_APPLICATIONNAME | --applicationName |
Имя среды | ASPNETCORE_ENVIRONMENT | --environment |
Корневой каталог содержимого | ASPNETCORE_CONTENTROOT | --contentRoot |
Все поставщики конфигурации
Следующий пример добавляет поставщик конфигурации INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Дополнительные сведения см. в разделе Поставщики конфигурации файлов в статье Конфигурация в ASP.NET Core.
Чтение конфигурации
По умолчанию WebApplicationBuilder считывает конфигурацию из нескольких источников, в том числе:
-
appSettings.json
иappSettings.{environment}.json
. - Переменные среды
- Командная строка
Полный список считываемых источников конфигурации представлен в разделе Конфигурация по умолчанию в статье Конфигурация в ASP.NET Core.
Следующий код считывает из конфигурации значение HelloKey
и отображает его в конечной точке /
. Если это значение конфигурации равно NULL, в message
сохраняется значение "Hello":
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Чтение данных из среды
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Добавление поставщиков ведения журнала
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Добавление служб
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Настройка IHostBuilder
К существующим методам расширения IHostBuilder можно обращаться через свойство Host.
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Настройка IWebHostBuilder
К существующим методам расширения IWebHostBuilder можно обращаться через свойство WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Изменение корневой папки веб-сайта
Корневая папка веб-сайта задается относительно корневой папки содержимого. По умолчанию это wwwroot
. В корневой папке веб-сайта ПО промежуточного слоя ищет статические файлы веб-сайта. Корневой веб-сайт можно изменить с помощью WebHostOptions
, командной строки или метода UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Пользовательский контейнер внедрения зависимостей (DI)
В следующем примере используется Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Добавление ПО промежуточного слоя
Любое существующее ПО промежуточного слоя для ASP.NET Core можно настроить в WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Дополнительные сведения см. в статье ПО промежуточного слоя ASP.NET Core.
Страница со сведениями об исключении для разработчика
WebApplication.CreateBuilder инициализирует новый экземпляр класса WebApplicationBuilder с предварительно настроенными значениями по умолчанию. Страница исключений для разработчиков включена в предварительно настроенных параметрах по умолчанию. При выполнении следующего кода в среде разработки переход к адресу /
отображает страницу с удобным представлением информации об исключении.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ПО промежуточного слоя ASP.NET Core
В следующей таблице собраны некоторые примеры ПО промежуточного слоя, часто используемого с минимальными API.
ПО промежуточного слоя | Description | API |
---|---|---|
Аутентификация | Обеспечивает поддержку проверки подлинности. | UseAuthentication |
Авторизация | Обеспечивает поддержку авторизации. | UseAuthorization |
CORS | Настраивает общий доступ к ресурсам независимо от источника. | UseCors |
Обработчик исключений | Глобально обрабатывает исключения, создаваемые конвейером ПО промежуточного слоя. | UseExceptionHandler |
Forwarded Headers | Пересылает заголовки, переданные через прокси-сервер, в текущий запрос. | UseForwardedHeaders |
HTTPS Redirection | Перенаправляет все запросы с HTTP на HTTPS. | UseHttpsRedirection |
HTTP Strict Transport Security (HSTS) | ПО промежуточного слоя для повышения безопасности, которое добавляет специальный заголовок ответа. | UseHsts |
Ведение журнала запросов | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них. | UseHttpLogging |
Ведение журнала запросов W3C | Обеспечивает поддержку ведения журнала для HTTP-запросов и ответов на них в формате консорциума W3C. | UseW3CLogging |
Кэширование ответов | Обеспечивает поддержку для кэширования откликов. | UseResponseCaching |
Сжатие откликов | Обеспечивает поддержку для сжатия откликов. | UseResponseCompression |
Согласованность сеанса | Обеспечивает поддержку для управления пользовательскими сеансами. | UseSession |
Static Files | Обеспечивает поддержку для обработки статических файлов и просмотра каталогов. | UseStaticFiles, UseFileServer |
WebSockets | Обеспечивает поддержку протокола 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();
Обработчики маршрутов
Обработчики маршрутов — это методы, которые выполняются при обнаружении соответствия для маршрута. Обработчики маршрутов могут быть функцией любой фигуры, включая синхронную или асинхронную. В роли обработчика маршрут может выступать лямбда-выражение, локальная функция, метод экземпляра или статический метод.
Лямбда-выражение
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();
Приведенный выше код возвращает The user id is 3 and book id is 7
из URI /users/3/books/7
.
Обработчик маршрута может объявлять параметры, которые нужно захватывать. Когда выполняется запрос по маршруту, для которого объявлены захватываемые параметры, эти параметры анализируются и передаются в обработчик. Это позволяет легко получать значения в строго типизированном виде. В приведенном выше коде userId
и bookId
имеют тип int
.
В приведенном выше коде создается исключение, если значение маршрута не может быть преобразовано в тип int
. Запрос GET по маршруту /users/hello/books/3
выдает следующее исключение:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Использование подстановочных знаков и перехват всех маршрутов
Следующая функция перехвата всех маршрутов возвращает значение Routing to hello
из конечной точки "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Ограничения маршрута
Ограничения маршрута ограничивают возможности сопоставления маршрута.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
В приведенной ниже таблице перечислены представленные выше примеры шаблонов маршрутов и их поведение.
Шаблон маршрута | Пример соответствующего URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Дополнительные сведения см. в разделе Справочник по ограничениям маршрутов в статье Маршрутизация в ASP.NET Core.
Привязка параметров
Привязка параметров — это процесс преобразования данных запроса в строго типизированные параметры, выраженные обработчиками маршрутов. Источник привязки определяет, откуда будут привязаны параметры. Источники привязки могут быть явными или выведенными на основе метода HTTP и типа параметра.
Поддерживаемые источники привязки:
- значения маршрута;
- Строка запроса
- Верхний колонтитул
- текст (в формате JSON);
- службы, предоставляемые путем внедрения зависимостей;
- Пользовательское
Примечание.
Привязка из значений форм не имеет встроенной поддержки в .NET.
В следующем примере обработчик маршрута GET использует некоторые из этих источников привязки параметров:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
В следующей таблице показана связь между параметрами, используемыми в предыдущем примере, и связанными источниками привязки.
Параметр | Источник привязки |
---|---|
id |
Значение маршрута |
page |
строке запроса |
customHeader |
авторизации |
service |
Предоставляется путем внедрения зависимостей |
Методы HTTP GET
, HEAD
, OPTIONS
и DELETE
не выполняют неявную привязку из текста запроса. Чтобы выполнить привязку из тела (как JSON) для этих методов HTTP, следует явно выполнить привязку с [FromBody]
или прочитать из HttpRequest.
В следующем примере обработчик маршрута POST использует источник привязки тела (как JSON) для параметра person
:
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.
Привязка параметров с помощью внедрения зависимостей
Привязка параметров для минимальных 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
необязательным, определите для него тип optional (необязательный) или укажите значение по умолчанию:
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 |
Приведенные выше значения nullable и default применяется ко всем источникам:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
Приведенный выше код вызывает метод со значением NULL для параметра product, если текст запроса не отправлен.
ПРИМЕЧАНИЕ: если предоставлены недопустимые данные и параметр допускает значение NULL, обработчик маршрутов не выполняется.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI-адрес | result |
---|---|
/products?pageNumber=3 |
Возвращается: 3 |
/products |
Возвращается: 1 |
/products?pageNumber=two |
BadHttpRequestException : Не удалось выполнить привязку параметра "Nullable<int> pageNumber" для "two". |
/products/two |
Ошибка HTTP 404, нет соответствующего маршрута |
Дополнительные сведения см. в разделе Ошибки привязки.
Специальные типы
Следующие типы привязываются без явно заданных атрибутов:
HttpContext — контекст, содержащий все сведения о текущем HTTP-запросе или ответе:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest и HttpResponse — HTTP-запрос и ответ HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken — маркер отмены, связанный с текущим HTTP-запросом:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal — пользователь, связанный с запросом, привязанным из HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Пользовательская привязка
Существует два способа настроить привязку параметров.
- Если в качестве источника привязки маршрутов используется маршрут, запрос или заголовок, привяжите пользовательские типы путем добавления статического метода
TryParse
для нужного типа. - Управление процессом привязки осуществляется путем реализации метода
BindAsync
для этого типа.
TryParse
TryParse
имеет два API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
Следующий код отображает Point: 12.3, 10.1
для URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
имеет следующие API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
Следующий код отображает SortBy:xyz, SortDirection:Desc, CurrentPage:99
для URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Ошибки привязки
Если привязка выполняется неудачно, платформа регистрирует сообщение отладки и возвращает клиенту коды состояния, которые могут быть разными в зависимости от условий сбоя.
Режим сбоя | Тип параметра, допускающий значение NULL | Источник привязки | Код состояния |
---|---|---|---|
{ParameterType}.TryParse возвращает false |
yes | Маршрут, запрос или заголовок | 400 |
{ParameterType}.BindAsync возвращает null |
yes | личный | 400 |
Выдает {ParameterType}.BindAsync |
Не имеет значения | личный | 500 |
Не удалось десериализовать текст JSON | Не имеет значения | текст | 400 |
Неправильный тип содержимого (не application/json ) |
Не имеет значения | текст | 415 |
Приоритет привязки
Правила для определения источника привязки на основе параметра:
- Явный атрибут, определенный для атрибутов параметра (From*) в следующем порядке:
- значения маршрута:
[FromRoute]
; - Строка запроса:
[FromQuery]
- заголовок:
[FromHeader]
; - Текст:
[FromBody]
- служба:
[FromServices]
;
- значения маршрута:
- Специальные типы
- Тип параметра имеет допустимый метод
BindAsync
. - Тип параметра имеет строковое значение или допустимый метод
TryParse
.- Если имя параметра существует в шаблоне маршрута. В
app.Map("/todo/{id}", (int id) => {});
,id
привязана к маршруту. - Привязывается из строки запроса.
- Если имя параметра существует в шаблоне маршрута. В
- Если тип параметра является службой, предоставляемой путем внедрения зависимостей, то в качестве источника он использует эту службу.
- Параметр извлекается из текста запроса.
Настройка привязки JSON
Текст запроса в качестве источника привязки использует System.Text.Json для десериализации. Изменить это значение по умолчанию нельзя, но привязку можно настроить с помощью других методов, описанных выше. Чтобы настроить параметры сериализатора JSON, используйте код следующего вида.
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/products", (Product product) => product);
app.Run();
class Product
{
// These are public fields, not properties.
public int Id;
public string? Name;
}
Предыдущий код:
- Настраивает входные и выходные параметры JSON по умолчанию.
- Возвращает следующий код JSON
При публикации{ "id": 1, "name": "Joe Smith" }
{ "Id": 1, "Name": "Joe Smith" }
Считывание текста запроса
Считайте текст запроса напрямую, используя параметр HttpContext или HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
Предыдущий код:
- Обращается к тексту запроса с помощью HttpRequest.BodyReader.
- Копирует текст запроса в локальный файл.
Отклики
Обработчики маршрутов поддерживают следующие типы возвращаемых значений:
- на основе
IResult
, напримерTask<IResult>
иValueTask<IResult>
; -
string
, напримерTask<string>
иValueTask<string>
; -
T
(любой другой тип), напримерTask<T>
иValueTask<T>
.
Возвращаемое значение | Поведение | Тип контента |
---|---|---|
IResult |
Платформа вызывает IResult.ExecuteAsync | Определяется реализацией IResult |
string |
Платформа записывает строку непосредственно в ответ | text/plain |
T (любой другой тип) |
Платформа будет выполнять сериализацию ответа в формат JSON | application/json |
Примеры возвращаемых значений
Строковые возвращаемые значения
app.MapGet("/hello", () => "Hello World");
Возвращаемые значения в формате JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
Возвращаемые значения в формате IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
В следующем примере для настройки ответа используются встроенные типы результатов:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Пользовательский код состояния
app.MapGet("/405", () => Results.StatusCode(405));
Текст
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Перенаправление
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Файлы
app.MapGet("/download", () => Results.File("myfile.text"));
Встроенные результаты
В статическом классе Microsoft.AspNetCore.Http.Results
есть несколько распространенных вспомогательных методов результата.
Description | Тип ответа | Код состояния | API |
---|---|---|---|
Записывает ответ JSON с дополнительными параметрами | application/json | 200 | Results.Json |
Записывает ответ в формате JSON | application/json | 200 | Results.Ok |
Записывает текстовый ответ | по умолчанию text/plain, можно настроить | 200 | Results.Text |
Записывает ответ в формате байтов | По умолчанию application/octet-stream, можно настроить | 200 | Results.Bytes |
Записывает в ответ поток байтов | По умолчанию application/octet-stream, можно настроить | 200 | Results.Stream |
Выполняет потоковую передачу файла для скачивания с заголовком content-disposition | По умолчанию application/octet-stream, можно настроить | 200 | Results.File |
Задает код состояния 404 с необязательным ответом JSON | Н/П | 404 | Results.NotFound |
Задает код состояния 204 | Н/П | 204 | Results.NoContent |
Задает код состояния 422 с необязательным ответом JSON | Н/П | 422 | Results.UnprocessableEntity |
Задает код состояния 400 с необязательным ответом JSON | Н/П | 400 | Results.BadRequest |
Задает код состояния 409 с необязательным ответом JSON | Н/П | 409 | Results.Conflict |
Записывает в ответ объект в формате JSON со сведениями о проблеме | Н/П | По умолчанию 500, можно настроить | Results.Problem |
Записывает в ответ объект в формате JSON со сведениями о проблеме и ошибками проверки | Н/П | Н/Д, можно настроить | Results.ValidationProblem |
Настройка результатов
Приложения могут управлять ответами через пользовательскую реализацию типа IResult. Следующий код является примером результата с типом HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Мы рекомендуем добавить в Microsoft.AspNetCore.Http.IResultExtensions метод расширения, чтобы эти пользовательские результаты было проще искать.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Авторизация
Для защиты маршрутов можно применять политики авторизации. Они могут быть объявлены через атрибут [Authorize]
или с помощью метода RequireAuthorization.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
Приведенный выше код можно создать с использованием RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
В следующем примере используется авторизация на основе политик.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
Разрешение доступа к конечной точке пользователям, не прошедшим проверку подлинности
[AllowAnonymous]
разрешает доступ к конечным точкам пользователям, не прошедшим проверку подлинности:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Для маршрутов можно включить CORS с помощью политик CORS. CORS можно объявить через атрибут [EnableCors]
или с помощью метода RequireCors. В следующих примерах включается CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
Дополнительные сведения см. в статье Включение запросов CORS в ASP.NET Core.
См. также
ASP.NET Core