Minimal APIs overview
This document:
- Provides an overview of minimal APIs.
- Is intended for experienced developers. For an introduction, see Tutorial: Create a minimal web API with ASP.NET Core
The minimal APIs consist of:
- New hosting APIs
- WebApplication and WebApplicationBuilder
- New routing APIs
WebApplication
The following code is generated by an ASP.NET Core template:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The preceding code can be created via dotnet new web on the command line or selecting the Empty Web template in Visual Studio.
The following code creates a WebApplication (app) without explicitly creating a WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create initializes a new instance of the WebApplication class with preconfigured defaults.
Working with ports
When a web app is created with Visual Studio or dotnet new, a Properties/launchSettings.json file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog Unable to connect to web server 'AppName'. Run the following port changing samples from the command line.
The following sections set the port the app responds to.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
In the preceding code, the app responds to port 3000.
Multiple ports
In the following code, the app responds to port 3000 and 4000.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Set the port from the command line
The following command makes the app respond to port 7777:
dotnet run --urls="https://localhost:7777"
If the Kestrel endpoint is also configured in the appsettings.json file, the appsettings.json file specified URL is used. For more information, see Kestrel endpoint configuration
Read the port from environment
The following code reads the port from the environment:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
The preferred way to set the port from the environment is to use the ASPNETCORE_URLS environment variable, which is shown in the following section.
Set the ports via the ASPNETCORE_URLS environment variable
The ASPNETCORE_URLS environment variable is available to set the port:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS supports multiple URLs:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Listen on all interfaces
The following samples demonstrate listening on all interfaces
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();
Listen on all interfaces using ASPNETCORE_URLS
The preceding samples can use ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Specify HTTPS with development certificate
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
For more information on the development certificate, see Trust the ASP.NET Core HTTPS development certificate on Windows and macOS.
Specify HTTPS using a custom certificate
The following sections show how to specify the custom certificate using the appsettings.json file and via configuration.
Specify the custom certificate with appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Specify the custom certificate via configuration
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();
Use the certificate APIs
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();
Read the environment
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();
For more information using the environment, see Use multiple environments in ASP.NET Core
Configuration
The following code reads from the configuration system:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
For more information, see Configuration in ASP.NET Core
Logging
The following code writes a message to the log on application startup:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
For more information, see Logging in .NET Core and ASP.NET Core
Access the Dependency Injection (DI) container
The following code shows how to get services from the DI container during application startup:
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();
For more information, see Dependency injection in ASP.NET Core.
WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.
Change the content root, application name, and environment
The following code sets the content root, application name, and environment:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
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 initializes a new instance of the WebApplicationBuilder class with preconfigured defaults.
For more information, see ASP.NET Core fundamentals overview
Change the content root, app name, and environment by environment variables or command line
The following table shows the environment variable and command-line argument used to change the content root, app name, and environment:
| feature | Environment variable | Command-line argument |
|---|---|---|
| Application name | ASPNETCORE_APPLICATIONNAME | --applicationName |
| Environment name | ASPNETCORE_ENVIRONMENT | --environment |
| Content root | ASPNETCORE_CONTENTROOT | --contentRoot |
Add configuration providers
The following sample adds the INI configuration provider:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
For detailed information, see File configuration providers in Configuration in ASP.NET Core.
Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources, including:
appSettings.jsonandappSettings.{environment}.json- Environment variables
- The command line
For a complete list of configuration sources read, see Default configuration in Configuration in ASP.NET Core
The following code reads HelloKey from configuration and displays the value at the / endpoint. If the configuration value is null, "Hello" is assigned to message:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Read the environment
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Add logging providers
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();
Add services
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();
Customize the IHostBuilder
Existing extension methods on IHostBuilder can be accessed using the Host property:
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();
Customize the IWebHostBuilder
Extension methods on IWebHostBuilder can be accessed using the WebApplicationBuilder.WebHost property.
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();
Change the web root
By default, the web root is relative to the content root in the wwwroot folder. Web root is where the static files middleware looks for static files. Web root can be changed with WebHostOptions, the command line, or with the UseWebRoot method:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Custom dependency injection (DI) container
The following example uses 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();
Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
For more information, see ASP.NET Core Middleware
Developer exception page
WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the development environment, navigating to / renders a friendly page that shows the exception.
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 Middleware
The following table lists some of the middleware frequently used with minimal APIs.
| Middleware | Description | API |
|---|---|---|
| Authentication | Provides authentication support. | UseAuthentication |
| Authorization | Provides authorization support. | UseAuthorization |
| CORS | Configures Cross-Origin Resource Sharing. | UseCors |
| Exception Handler | Globally handles exceptions thrown by the middleware pipeline. | UseExceptionHandler |
| Forwarded Headers | Forwards proxied headers onto the current request. | UseForwardedHeaders |
| HTTPS Redirection | Redirects all HTTP requests to HTTPS. | UseHttpsRedirection |
| HTTP Strict Transport Security (HSTS) | Security enhancement middleware that adds a special response header. | UseHsts |
| Request Logging | Provides support for logging HTTP requests and responses. | UseHttpLogging |
| W3C Request Logging | Provides support for logging HTTP requests and responses in the W3C format. | UseW3CLogging |
| Response Caching | Provides support for caching responses. | UseResponseCaching |
| Response Compression | Provides support for compressing responses. | UseResponseCompression |
| Session | Provides support for managing user sessions. | UseSession |
| Static Files | Provides support for serving static files and directory browsing. | UseStaticFiles, UseFileServer |
| WebSockets | Enables the WebSockets protocol. | UseWebSockets |
Request handling
The following sections cover routing, parameter binding, and responses.
Routing
A configured WebApplication supports Map{Verb} and 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();
Route Handlers
Route handlers are methods that execute when the route matches. Route handlers can be a function or any shape, including synchronous or asynchronous. Route handlers can be a lambda expression, a local function, an instance method or a static method.
Lambda expression
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();
Local function
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Instance method
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";
}
}
Static 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";
}
}
Name routes and link generation
Routes can be given names in order to generate URLs to the route. Using a named route avoids having to hard code paths in an app:
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 preceding code displays The link to the hello route is /hello from the / endpoint.
Route names are inferred from method names if specified:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string Hi() => "Hello there";
app.MapGet("/hello", Hi);
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("Hi", values: null)}");
app.Run();
REVIEW: {linker.GetPathByName("Hi", values: null)} is null in the preceding code.
NOTE: Route names are case sensitive.
Route names:
- Must be globally unique.
- Are used as the OpenAPI operation id when OpenAPI support is enabled. See the OpenAPI section for more details.
Route Parameters
Route parameters can be captured as part of the route pattern definition:
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 preceding code returns The user id is 3 and book id is 7 from the URI /users/3/books/7.
The route handler can declare the parameters to capture. When a request is made a route with parameters declared to capture, the parameters are parsed and passed to the handler. This makes it easy to capture the values in a type safe way. In the preceding code, userId and bookId are both int.
In the preceding code, if either route value cannot be converted to an int, an exception is thrown. The GET request /users/hello/books/3 throws the following exception:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Wildcard and catch all routes
The following catch all route returns Routing to hello from the `/posts/hello' endpoint:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Route constraints
Route constraints constrain the matching behavior of a route.
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();
The following table demonstrates the preceding route templates and their behavior:
| Route Template | Example Matching URI |
|---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
For more information, see Route constraint reference in Routing in ASP.NET Core.
Parameter Binding
Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers. A binding source determines where parameters are bound from. Binding sources can be explicit or inferred based on HTTP method and parameter type.
Supported binding sources:
- Route values
- Query string
- Header
- Body (as JSON)
- Services provided by dependency injection
- Custom
Note
Binding from form values is not natively supported in .NET.
The following example GET route handler uses some of these parameter binding sources:
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 { }
The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.
| Parameter | Binding Source |
|---|---|
id |
route value |
page |
query string |
customHeader |
header |
service |
Provided by dependency injection |
The HTTP methods GET, HEAD, OPTIONS, and DELETE don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, bind explicitly with [FromBody] or read from the HttpRequest.
The following example POST route handler uses a binding source of body (as JSON) for the person parameter:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following example route handlers show how to read request data directly from the request:
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>();
// ...
});
Explicit Parameter Binding
Attributes can be used to explicitly declare where parameters are bound from.
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);
| Parameter | Binding Source |
|---|---|
id |
route value with the name id |
page |
query string with the name "p" |
service |
Provided by dependency injection |
contentType |
header with the name "Content-Type" |
Note
Binding from form values is not natively supported in .NET.
Parameter binding with DI
Parameter binding for minimal APIs binds parameters through dependency injection when the type is configured as a service. It's not necessary to explicitly apply the [FromServices] attribute to a parameter. In the following code, both actions return the time:
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();
Optional parameters
Parameters declared in route handlers are treated as required:
- If a request matches the route, the route handler only runs if all required parameters are provided in the request.
- Failure to provide all required parameters results in an error.
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 returned |
/products |
BadHttpRequestException: Required parameter "int pageNumber" was not provided from query string. |
/products/1 |
HTTP 404 error, no matching route |
To make pageNumber optional, define the type as optional or provide a default value:
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 returned |
/products |
1 returned |
/products2 |
1 returned |
The preceding nullable and default value applies to all sources:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
The preceding code calls the method with a null product if no request body is sent.
NOTE: If invalid data is provided and the parameter is nullable, the route handler is not run.
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 returned |
/products |
1 returned |
/products/two |
BadHttpRequestException: Failed to bind parameter "Nullable<int> pageNumber" from "two". |
See the Binding Failures section for more information.
Special types
The following types are bound without explicit attributes:
HttpContext: The context which holds all the information about the current HTTP request or response:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));HttpRequest and HttpResponse: The HTTP request and HTTP response:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));CancellationToken: The cancellation token associated with the current HTTP request:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));ClaimsPrincipal: The user associated with the request, bound from HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Custom Binding
There are two ways to customize parameter binding:
- For route, query, and header binding sources, bind custom types by adding a static
TryParsemethod for the type. - Control the binding process by implementing a
BindAsyncmethod on a type.
TryParse
TryParse has two APIs:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
The following code displays Point: 12.3, 10.1 with the 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 has the following APIs:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
The following code displays SortBy:xyz, SortDirection:Desc, CurrentPage:99 with the 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
}
Binding failures
When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.
| Failure mode | Nullable Parameter Type | Binding Source | Status code |
|---|---|---|---|
{ParameterType}.TryParse returns false |
yes | route/query/header | 400 |
{ParameterType}.BindAsync returns null |
yes | custom | 400 |
{ParameterType}.BindAsync throws |
does not matter | custom | 500 |
| Failure to deserialize JSON body | does not matter | body | 400 |
Wrong content type (not application/json) |
does not matter | body | 415 |
Binding Precedence
The rules for determining a binding source from a parameter:
- Explicit attribute defined on parameter (From* attributes) in the following order:
- Route values:
[FromRoute] - Query string:
[FromQuery] - Header:
[FromHeader] - Body:
[FromBody] - Service:
[FromServices]
- Route values:
- Special types
HttpContextHttpRequestHttpResponseClaimsPrincipalCancellationToken
- Parameter type has a valid
BindAsyncmethod. - Parameter type is a string or has a valid
TryParsemethod.- If the parameter name exists in the route template e.g.
app.Map("/todo/{id}", (int id) => {});, then it's bound from the route. - Bound from the query string.
- If the parameter name exists in the route template e.g.
- If the parameter type is a service provided by dependency injection, it uses that service as the source.
- The parameter is from the body.
Customize JSON binding
The body binding source uses System.Text.Json for de-serialization. It is not possible to change this default, but the binding can be customized using other techniques described previously. To customize JSON serializer options, use code similar to the following:
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;
}
The preceding code:
- Configures both the input and output default JSON options.
- Returns the following JSON
When posting{ "id": 1, "name": "Joe Smith" }{ "Id": 1, "Name": "Joe Smith" }
Read the request body
Read the request body directly using a HttpContext or HttpRequest parameter:
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();
The preceding code:
- Accesses the request body using HttpRequest.BodyReader.
- Copies the request body to a local file.
Responses
Route handlers support the following types of return values:
IResultbased - This includesTask<IResult>andValueTask<IResult>string- This includesTask<string>andValueTask<string>T(Any other type) - This includesTask<T>andValueTask<T>
| Return value | Behavior | Content-Type |
|---|---|---|
IResult |
The framework calls IResult.ExecuteAsync | Decided by the IResult implementation |
string |
The framework writes the string directly to the response | text/plain |
T (Any other type) |
The framework will JSON serialize the response | application/json |
Example return values
string return values
app.MapGet("/hello", () => "Hello World");
JSON return values
app.MapGet("/hello", () => new { Message = "Hello World" });
IResult return values
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
The following example uses the built-in result types to customize the response:
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" }));
Custom Status Code
app.MapGet("/405", () => Results.StatusCode(405));
Text
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://consoto/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
File
app.MapGet("/download", () => Results.File("myfile.text"));
Built-in results
Common result helpers exist in the Microsoft.AspNetCore.Http.Results static class.
| Description | Response type | Status Code | API |
|---|---|---|---|
| Write a JSON response with advanced options | application/json | 200 | Results.Json |
| Write a JSON response | application/json | 200 | Results.Ok |
| Write a text response | text/plain (default), configurable | 200 | Results.Text |
| Write the response as bytes | application/octet-stream (default), configurable | 200 | Results.Bytes |
| Write a stream of bytes to the response | application/octet-stream (default), configurable | 200 | Results.Stream |
| Stream a file to the response for download with the content-disposition header | application/octet-stream (default), configurable | 200 | Results.File |
| Set the status code to 404, with an optional JSON response | N/A | 404 | Results.NotFound |
| Set the status code to 204 | N/A | 204 | Results.NoContent |
| Set the status code to 422, with an optional JSON response | N/A | 422 | Results.UnprocessableEntity |
| Set the status code to 400, with an optional JSON response | N/A | 400 | Results.BadRequest |
| Set the status code to 409, with an optional JSON response | N/A | 409 | Results.Conflict |
| Write a problem details JSON object to the response | N/A | 500 (default), configurable | Results.Problem |
| Write a problem details JSON object to the response with validation errors | N/A | N/A, configurable | Results.ValidationProblem |
Customizing results
Applications can control responses by implementing a custom IResult type. The following code is an example of an HTML result type:
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);
}
}
We recommend adding an extension method to Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more discoverable.
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();
Authorization
Routes can be protected using authorization policies. These can be declared via the [Authorize] attribute or by using the RequireAuthorization method:
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();
The preceding code can be written with RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
The following sample uses policy-based authorization:
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();
Allow unauthenticated users to access an endpoint
The [AllowAnonymous]
allows unauthenticated users to access endpoints:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Routes can be CORS enabled using CORS policies. CORS can be declared via the [EnableCors] attribute or by using the
RequireCors method. The following samples enable 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();
For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core
OpenAPI
An app can describe the OpenAPI specification for route handlers using Swashbuckle.
The following code is a typical ASP.NET Core app with OpenAPI support:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = builder.Environment.ApplicationName,
Version = "v1" });
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
$"{builder.Environment.ApplicationName} v1"));
}
app.MapGet("/swag", () => "Hello Swagger!");
app.Run();
Exclude Open API description
In the following sample, the /skipme endpoint is excluded from generating an OpenAPI description:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
Describe response types
The following example uses the built-in result types to customize the response:
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);
Add operation ids to Open API
app.MapGet("/todoitems2", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithName("GetToDoItems");
Add tags to the Open API description
The following code uses an OpenAPI grouping tag:
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithTags("TodoGroup");
This document:
- Provides an overview of minimal APIs.
- Is intended for experienced developers. For an introduction, see Tutorial: Create a minimal web API with ASP.NET Core
The minimal APIs consist of:
- New hosting APIs
- WebApplication and WebApplicationBuilder
- New routing APIs
WebApplication
The following code is generated by an ASP.NET Core template:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The preceding code can be created via dotnet new web on the command line or selecting the Empty Web template in Visual Studio.
The following code creates a WebApplication (app) without explicitly creating a WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create initializes a new instance of the WebApplication class with preconfigured defaults.
Working with ports
When a web app is created with Visual Studio or dotnet new, a Properties/launchSettings.json file is created that specifies the ports the app responds to. In the port setting samples that follow, running the app from Visual Studio returns an error dialog Unable to connect to web server 'AppName'. Run the following port changing samples from the command line.
The following sections set the port the app responds to.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
In the preceding code, the app responds to port 3000.
Multiple ports
In the following code, the app responds to port 3000 and 4000.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Set the port from the command line
The following command makes the app respond to port 7777:
dotnet run --urls="https://localhost:7777"
If the Kestrel endpoint is also configured in the appsettings.json file, the appsettings.json file specified URL is used. For more information, see Kestrel endpoint configuration
Read the port from environment
The following code reads the port from the environment:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
The preferred way to set the port from the environment is to use the ASPNETCORE_URLS environment variable, which is shown in the following section.
Set the ports via the ASPNETCORE_URLS environment variable
The ASPNETCORE_URLS environment variable is available to set the port:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS supports multiple URLs:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Listen on all interfaces
The following samples demonstrate listening on all interfaces
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();
Listen on all interfaces using ASPNETCORE_URLS
The preceding samples can use ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Specify HTTPS with development certificate
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
For more information on the development certificate, see Trust the ASP.NET Core HTTPS development certificate on Windows and macOS.
Specify HTTPS using a custom certificate
The following sections show how to specify the custom certificate using the appsettings.json file and via configuration.
Specify the custom certificate with appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Specify the custom certificate via configuration
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();
Use the certificate APIs
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();
Read the environment
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();
For more information using the environment, see Use multiple environments in ASP.NET Core
Configuration
The following code reads from the configuration system:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
For more information, see Configuration in ASP.NET Core
Logging
The following code writes a message to the log on application startup:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
For more information, see Logging in .NET Core and ASP.NET Core
Access the Dependency Injection (DI) container
The following code shows how to get services from the DI container during application startup:
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();
For more information, see Dependency injection in ASP.NET Core.
WebApplicationBuilder
This section contains sample code using WebApplicationBuilder.
Change the content root, application name, and environment
The following code sets the content root, application name, and environment:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
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 initializes a new instance of the WebApplicationBuilder class with preconfigured defaults.
For more information, see ASP.NET Core fundamentals overview
Change the content root, app name, and environment by environment variables or command line
The following table shows the environment variable and command-line argument used to change the content root, app name, and environment:
| feature | Environment variable | Command-line argument |
|---|---|---|
| Application name | ASPNETCORE_APPLICATIONNAME | --applicationName |
| Environment name | ASPNETCORE_ENVIRONMENT | --environment |
| Content root | ASPNETCORE_CONTENTROOT | --contentRoot |
Add configuration providers
The following sample adds the INI configuration provider:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
For detailed information, see File configuration providers in Configuration in ASP.NET Core.
Read configuration
By default the WebApplicationBuilder reads configuration from multiple sources, including:
appSettings.jsonandappSettings.{environment}.json- Environment variables
- The command line
For a complete list of configuration sources read, see Default configuration in Configuration in ASP.NET Core
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Read the environment
The following code reads HelloKey from configuration and displays the value at the / endpoint. If the configuration value is null, "Hello" is assigned to message:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Add logging providers
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();
Add services
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();
Customize the IHostBuilder
Existing extension methods on IHostBuilder can be accessed using the Host property:
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();
Customize the IWebHostBuilder
Extension methods on IWebHostBuilder can be accessed using the WebApplicationBuilder.WebHost property.
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();
Change the web root
By default, the web root is relative to the content root in the wwwroot folder. Web root is where the static files middleware looks for static files. Web root can be changed with WebHostOptions, the command line, or with the UseWebRoot method:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Custom dependency injection (DI) container
The following example uses 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();
Add Middleware
Any existing ASP.NET Core middleware can be configured on the WebApplication:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
For more information, see ASP.NET Core Middleware
Developer exception page
WebApplication.CreateBuilder initializes a new instance of the WebApplicationBuilder class with preconfigured defaults. The developer exception page is enabled in the preconfigured defaults. When the following code is run in the development environment, navigating to / renders a friendly page that shows the exception.
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 Middleware
The following table lists some of the middleware frequently used with minimal APIs.
| Middleware | Description | API |
|---|---|---|
| Authentication | Provides authentication support. | UseAuthentication |
| Authorization | Provides authorization support. | UseAuthorization |
| CORS | Configures Cross-Origin Resource Sharing. | UseCors |
| Exception Handler | Globally handles exceptions thrown by the middleware pipeline. | UseExceptionHandler |
| Forwarded Headers | Forwards proxied headers onto the current request. | UseForwardedHeaders |
| HTTPS Redirection | Redirects all HTTP requests to HTTPS. | UseHttpsRedirection |
| HTTP Strict Transport Security (HSTS) | Security enhancement middleware that adds a special response header. | UseHsts |
| Request Logging | Provides support for logging HTTP requests and responses. | UseHttpLogging |
| W3C Request Logging | Provides support for logging HTTP requests and responses in the W3C format. | UseW3CLogging |
| Response Caching | Provides support for caching responses. | UseResponseCaching |
| Response Compression | Provides support for compressing responses. | UseResponseCompression |
| Session | Provides support for managing user sessions. | UseSession |
| Static Files | Provides support for serving static files and directory browsing. | UseStaticFiles, UseFileServer |
| WebSockets | Enables the WebSockets protocol. | UseWebSockets |
Bind the request body as a Stream or PipeReader
The request body can bind as a Stream or PipeReader to efficiently support scenarios where the user has to process data and:
- Store the data to blob storage or enqueue the data to a queue provider.
- Process the stored data with a worker process or cloud function.
For example, the data might be enqueued to Azure Queue storage or stored in Azure Blob storage.
The following code implements a background queue:
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;
}
The following code binds the request body to a 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);
});
The following code shows the complete Program.cs file:
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();
- When reading data, the
Streamis the same object asHttpRequest.Body. - The request body isn’t buffered by default. After the body is read, it’s not rewindable. The stream can't be read multiple times.
- The
StreamandPipeReaderaren't usable outside of the minimal action handler as the underlying buffers will be disposed or reused.
Request handling
The following sections cover routing, parameter binding, and responses.
Routing
A configured WebApplication supports Map{Verb} and 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();
Route Handlers
Route handlers are methods that execute when the route matches. Route handlers can be a function or any shape, including synchronous or asynchronous. Route handlers can be a lambda expression, a local function, an instance method or a static method.
Lambda expression
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();
Local function
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Instance method
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";
}
}
Static 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";
}
}
Name routes and link generation
Routes can be given names in order to generate URLs to the route. Using a named route avoids having to hard code paths in an app:
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 preceding code displays The link to the hello route is /hello from the / endpoint.
Route names are inferred from method names if specified:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string Hi() => "Hello there";
app.MapGet("/hello", Hi);
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("Hi", values: null)}");
app.Run();
REVIEW: {linker.GetPathByName("Hi", values: null)} is null in the preceding code.
NOTE: Route names are case sensitive.
Route names:
- Must be globally unique.
- Are used as the OpenAPI operation id when OpenAPI support is enabled. See the OpenAPI section for more details.
Route Parameters
Route parameters can be captured as part of the route pattern definition:
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 preceding code returns The user id is 3 and book id is 7 from the URI /users/3/books/7.
The route handler can declare the parameters to capture. When a request is made a route with parameters declared to capture, the parameters are parsed and passed to the handler. This makes it easy to capture the values in a type safe way. In the preceding code, userId and bookId are both int.
In the preceding code, if either route value cannot be converted to an int, an exception is thrown. The GET request /users/hello/books/3 throws the following exception:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Wildcard and catch all routes
The following catch all route returns Routing to hello from the `/posts/hello' endpoint:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Route constraints
Route constraints constrain the matching behavior of a route.
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();
The following table demonstrates the preceding route templates and their behavior:
| Route Template | Example Matching URI |
|---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
For more information, see Route constraint reference in Routing in ASP.NET Core.
Parameter Binding
Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers. A binding source determines where parameters are bound from. Binding sources can be explicit or inferred based on HTTP method and parameter type.
Supported binding sources:
- Route values
- Query string
- Header
- Body (as JSON)
- Services provided by dependency injection
- Custom
Note
Binding from form values is not natively supported in .NET.
The following example GET route handler uses some of these parameter binding sources:
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 { }
The following table shows the relationship between the parameters used in the preceding example and the associated binding sources.
| Parameter | Binding Source |
|---|---|
id |
route value |
page |
query string |
customHeader |
header |
service |
Provided by dependency injection |
The HTTP methods GET, HEAD, OPTIONS, and DELETE don't implicitly bind from body. To bind from body (as JSON) for these HTTP methods, bind explicitly with [FromBody] or read from the HttpRequest.
The following example POST route handler uses a binding source of body (as JSON) for the person parameter:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
The parameters in the preceding examples are all bound from request data automatically. To demonstrate the convenience that parameter binding provides, the following example route handlers show how to read request data directly from the request:
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>();
// ...
});
Explicit Parameter Binding
Attributes can be used to explicitly declare where parameters are bound from.
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);
| Parameter | Binding Source |
|---|---|
id |
route value with the name id |
page |
query string with the name "p" |
service |
Provided by dependency injection |
contentType |
header with the name "Content-Type" |
Note
Binding from form values is not natively supported in .NET.
Parameter binding with DI
Parameter binding for minimal APIs binds parameters through dependency injection when the type is configured as a service. It's not necessary to explicitly apply the [FromServices] attribute to a parameter. In the following code, both actions return the time:
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();
Optional parameters
Parameters declared in route handlers are treated as required:
- If a request matches the route, the route handler only runs if all required parameters are provided in the request.
- Failure to provide all required parameters results in an error.
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 returned |
/products |
BadHttpRequestException: Required parameter "int pageNumber" was not provided from query string. |
/products/1 |
HTTP 404 error, no matching route |
To make pageNumber optional, define the type as optional or provide a default value:
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 returned |
/products |
1 returned |
/products2 |
1 returned |
The preceding nullable and default value applies to all sources:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
The preceding code calls the method with a null product if no request body is sent.
NOTE: If invalid data is provided and the parameter is nullable, the route handler is not run.
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 returned |
/products |
1 returned |
/products/two |
BadHttpRequestException: Failed to bind parameter "Nullable<int> pageNumber" from "two". |
See the Binding Failures section for more information.
Special types
The following types are bound without explicit attributes:
HttpContext: The context which holds all the information about the current HTTP request or response:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));HttpRequest and HttpResponse: The HTTP request and HTTP response:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));CancellationToken: The cancellation token associated with the current HTTP request:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));ClaimsPrincipal: The user associated with the request, bound from HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Bind arrays and string values from headers and query strings
The following code demonstrates binding query strings to an array of primitive types, string arrays, and 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]}");
Binding query strings or header values to an array of complex types is supported when the type has TryParse implemented. The following code binds to a string array and returns all the items with the specified tags:
// 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();
});
The following code shows the model and the required TryParse implementation:
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;
}
}
The following code binds to an int array:
// 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();
});
To test the preceding code, add the following endpoint to populate the database with Todo items:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Use a tool like Postman to pass the following data to the previous endpoint:
[
{
"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"
}
}
]
The following code binds to the header key X-Todo-Id and returns the Todo items with matching Id values:
// 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();
});
Custom Binding
There are two ways to customize parameter binding:
- For route, query, and header binding sources, bind custom types by adding a static
TryParsemethod for the type. - Control the binding process by implementing a
BindAsyncmethod on a type.
TryParse
TryParse has two APIs:
public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);
The following code displays Point: 12.3, 10.1 with the 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 has the following APIs:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
The following code displays SortBy:xyz, SortDirection:Desc, CurrentPage:99 with the 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
}
Binding failures
When binding fails, the framework logs a debug message and returns various status codes to the client depending on the failure mode.
| Failure mode | Nullable Parameter Type | Binding Source | Status code |
|---|---|---|---|
{ParameterType}.TryParse returns false |
yes | route/query/header | 400 |
{ParameterType}.BindAsync returns null |
yes | custom | 400 |
{ParameterType}.BindAsync throws |
does not matter | custom | 500 |
| Failure to deserialize JSON body | does not matter | body | 400 |
Wrong content type (not application/json) |
does not matter | body | 415 |
Binding Precedence
The rules for determining a binding source from a parameter:
- Explicit attribute defined on parameter (From* attributes) in the following order:
- Route values:
[FromRoute] - Query string:
[FromQuery] - Header:
[FromHeader] - Body:
[FromBody] - Service:
[FromServices]
- Route values:
- Special types
HttpContextHttpRequestHttpResponseClaimsPrincipalCancellationToken
- Parameter type has a valid
BindAsyncmethod. - Parameter type is a string or has a valid
TryParsemethod.- If the parameter name exists in the route template e.g.
app.Map("/todo/{id}", (int id) => {});, then it's bound from the route. - Bound from the query string.
- If the parameter name exists in the route template e.g.
- If the parameter type is a service provided by dependency injection, it uses that service as the source.
- The parameter is from the body.
Customize JSON binding
The body binding source uses System.Text.Json for de-serialization. It is not possible to change this default, but the binding can be customized using other techniques described previously. To customize JSON serializer options, use code similar to the following:
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;
}
The preceding code:
- Configures both the input and output default JSON options.
- Returns the following JSON
When posting{ "id": 1, "name": "Joe Smith" }{ "Id": 1, "Name": "Joe Smith" }
Read the request body
Read the request body directly using a HttpContext or HttpRequest parameter:
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();
The preceding code:
- Accesses the request body using HttpRequest.BodyReader.
- Copies the request body to a local file.
Responses
Route handlers support the following types of return values:
IResultbased - This includesTask<IResult>andValueTask<IResult>string- This includesTask<string>andValueTask<string>T(Any other type) - This includesTask<T>andValueTask<T>
| Return value | Behavior | Content-Type |
|---|---|---|
IResult |
The framework calls IResult.ExecuteAsync | Decided by the IResult implementation |
string |
The framework writes the string directly to the response | text/plain |
T (Any other type) |
The framework will JSON serialize the response | application/json |
Example return values
string return values
app.MapGet("/hello", () => "Hello World");
JSON return values
app.MapGet("/hello", () => new { Message = "Hello World" });
IResult return values
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
The following example uses the built-in result types to customize the response:
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" }));
Custom Status Code
app.MapGet("/405", () => Results.StatusCode(405));
Text
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://consoto/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Results.Stream overloads allow access to the underlying HTTP response stream without buffering. The following example uses ImageSharp to return a reduced size of the specified image:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/process-image/{strImage}", (string strImage, HttpContext http, CancellationToken token) =>
{
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(stream => ResizeImageAsync(strImage, stream, token), "image/jpeg");
});
async Task ResizeImageAsync(string strImage, Stream stream, CancellationToken token)
{
var strPath = $"wwwroot/img/{strImage}";
using var image = await Image.LoadAsync(strPath, token);
int width = image.Width / 2;
int height = image.Height / 2;
image.Mutate(x =>x.Resize(width, height));
await image.SaveAsync(stream, JpegFormat.Instance, cancellationToken: token);
}
The following example streams an image from Azure Blob storage:
app.MapGet("/stream-image/{containerName}/{blobName}",
async (string blobName, string containerName, CancellationToken token) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token), "image/jpeg");
});
The following example streams a video from an Azure Blob:
// GET /stream-video/videos/earth.mp4
app.MapGet("/stream-video/{containerName}/{blobName}",
async (HttpContext http, CancellationToken token, string blobName, string containerName) =>
{
var conStr = builder.Configuration["blogConStr"];
BlobContainerClient blobContainerClient = new BlobContainerClient(conStr, containerName);
BlobClient blobClient = blobContainerClient.GetBlobClient(blobName);
var properties = await blobClient.GetPropertiesAsync(cancellationToken: token);
DateTimeOffset lastModified = properties.Value.LastModified;
long length = properties.Value.ContentLength;
long etagHash = lastModified.ToFileTime() ^ length;
var entityTag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
http.Response.Headers.CacheControl = $"public,max-age={TimeSpan.FromHours(24).TotalSeconds}";
return Results.Stream(await blobClient.OpenReadAsync(cancellationToken: token),
contentType: "video/mp4",
lastModified: lastModified,
entityTag: entityTag,
enableRangeProcessing: true);
});
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
File
app.MapGet("/download", () => Results.File("myfile.text"));
Built-in results
Common result helpers exist in the Microsoft.AspNetCore.Http.Results static class.
| Description | Response type | Status Code | API |
|---|---|---|---|
| Write a JSON response with advanced options | application/json | 200 | Results.Json |
| Write a JSON response | application/json | 200 | Results.Ok |
| Write a text response | text/plain (default), configurable | 200 | Results.Text |
| Write the response as bytes | application/octet-stream (default), configurable | 200 | Results.Bytes |
| Write a stream of bytes to the response | application/octet-stream (default), configurable | 200 | Results.Stream |
| Stream a file to the response for download with the content-disposition header | application/octet-stream (default), configurable | 200 | Results.File |
| Set the status code to 404, with an optional JSON response | N/A | 404 | Results.NotFound |
| Set the status code to 204 | N/A | 204 | Results.NoContent |
| Set the status code to 422, with an optional JSON response | N/A | 422 | Results.UnprocessableEntity |
| Set the status code to 400, with an optional JSON response | N/A | 400 | Results.BadRequest |
| Set the status code to 409, with an optional JSON response | N/A | 409 | Results.Conflict |
| Write a problem details JSON object to the response | N/A | 500 (default), configurable | Results.Problem |
| Write a problem details JSON object to the response with validation errors | N/A | N/A, configurable | Results.ValidationProblem |
Customizing results
Applications can control responses by implementing a custom IResult type. The following code is an example of an HTML result type:
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);
}
}
We recommend adding an extension method to Microsoft.AspNetCore.Http.IResultExtensions to make these custom results more discoverable.
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();
Filters
See Filters in Minimal API applications
Authorization
Routes can be protected using authorization policies. These can be declared via the [Authorize] attribute or by using the RequireAuthorization method:
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();
The preceding code can be written with RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
The following sample uses policy-based authorization:
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();
Allow unauthenticated users to access an endpoint
The [AllowAnonymous]
allows unauthenticated users to access endpoints:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Routes can be CORS enabled using CORS policies. CORS can be declared via the [EnableCors] attribute or by using the
RequireCors method. The following samples enable 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();
For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core
Typed results
The IResult interface can represent values returned from minimal APIs that don’t utilize the implicit support for JSON serializing the returned object to the HTTP response. The static Results class is used to create varying IResult objects that represent different types of responses. For example, setting the response status code or redirecting to another URL.
The types implementing IResult are public, allowing for type assertions when testing. For example:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
OpenAPI
An app can describe the OpenAPI specification for route handlers using Swashbuckle. The OpenAPI specification defines a standard for documenting RESTful APIs so that end users and other services can examine the capabilities of an API service.
The following code is generated by the ASP.NET Core minimal web API template and uses OpenAPI:
using Microsoft.AspNetCore.OpenApi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
In the preceding highlighted code:
Microsoft.AspNetCore.OpenApiis explained in the next section.- AddEndpointsApiExplorer : Configures the app to use the API Explorer to discover and describe endpoints with default annotations.
WithOpenApioverrides matching, default annotations generated by the API Explorer with those produced from theMicrosoft.AspNetCore.OpenApipackage. UseSwaggeradds the Swagger middleware.UseSwaggerUIenables the Static File Middleware.- WithName: The IEndpointNameMetadata on the endpoint is used for link generation and is treated as the operation ID in the given endpoint's OpenAPI specification.
WithOpenApiis explained later in this article.
Microsoft.AspNetCore.OpenApi NuGet package
ASP.NET Core provides the Microsoft.AspNetCore.OpenApi package to interact with OpenAPI specifications for endpoints. The package acts as a link between the OpenAPI models that are defined in the Microsoft.AspNetCore.OpenApi package and the endpoints that are defined in Minimal APIs. The package provides an API that examines an endpoint's parameters, responses, and metadata to construct an OpenAPI annotation type that is used to describe an endpoint.
Microsoft.AspNetCore.OpenApi is added as a PackageReference to a project file:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.*-*" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>
When using Swashbuckle.AspNetCore with Microsoft.AspNetCore.OpenApi, Swashbuckle.AspNetCore 6.3.1 and later must be used.
WithOpenApi call on endpoints adds an OpenAPI annotation
Calling WithOpenApi on an endpoint without parameters adds an OpenAPI annotation to the endpoints metadata. This metadata can be:
- Consumed in third-party packages like Swashbuckle.AspNetCore.
- Displayed in the Swagger user interface or in YAML or JSON generated to define the file.
app.MapPost("/todoitems/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi();
Call WithOpenApi with parameters
The WithOpenApi method accepts a function that can be used to modify the OpenAPI annotation. For example, in the following code, a description is added to the first parameter of the endpoint:
app.MapPost("/todo2/{id}", async (int id, Todo todo, TodoDb db) =>
{
todo.Id = id;
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithOpenApi(generatedOperation =>
{
var parameter = generatedOperation.Parameters[0];
parameter.Description = "The ID associated with the created Todo";
return generatedOperation;
});
Exclude Open API description
In the following sample, the /skipme endpoint is excluded from generating an OpenAPI description:
using Microsoft.AspNetCore.OpenApi;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/swag", () => "Hello Swagger!")
.WithOpenApi();
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
Describe response types
The following example uses the built-in result types to customize the response:
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);
Add operation ids to Open API
app.MapGet("/todoitems2", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithName("GetToDoItems");
Add tags to the Open API description
The following code uses an OpenAPI grouping tag:
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync())
.WithTags("TodoGroup");
ASP.NET Core OpenAPI source code on GitHub
Povratne informacije
Pošalјite i prikažite povratne informacije za