Last active
October 9, 2026 12:15
-
Star
(747)
You must be signed in to star a gist -
Fork
(79)
You must be signed in to fork a gist
-
-
Save davidfowl/ff1addd02d239d2d26f4648a06158727 to your computer and use it in GitHub Desktop.
Minimal APIs at a glance
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Minimal APIs at a glance | |
| [This document](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0) now exists on the official ASP.NET core docs page. | |
| - **Application** | |
| - [WebApplication](#webapplication) | |
| - [WebApplicationBuilder](#webapplicationbuilder) | |
| - **Request Handling** | |
| - [Routing](#routing) | |
| - [Parameter Binding](#parameter-binding) | |
| - [Responses](#responses) | |
| - [Authorization](#authorization) | |
| - [CORS](#cors) | |
| - [OpenAPI](#openapi) | |
| - **Advanced** | |
| - [Building libraries for ASP.NET Core](#building-libraries-for-aspnet-core) | |
| - [Differences with ASP.NET Core MVC](#differences-with-aspnet-core-mvc) | |
| ## WebApplication | |
| ### Creating an application | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| This listens to port http://localhost:5000 and https://localhost:5001 by default. | |
| ### Changing the port | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run("http://localhost:3000"); | |
| ``` | |
| ### Multiple ports | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Urls.Add("http://localhost:3000"); | |
| app.Urls.Add("http://localhost:4000"); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| ### Reading the port from environment | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| var port = Environment.GetEnvironmentVariable("PORT") ?? "3000"; | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run($"http://localhost:{port}"); | |
| ``` | |
| ### Changing the ports via environment variables | |
| You can set the `ASPNETCORE_URLS` environment variable to change the address: | |
| ``` | |
| ASPNETCORE_URLS=http://localhost:3000 | |
| ``` | |
| This supports multiple urls: | |
| ``` | |
| ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000 | |
| ``` | |
| ### Listening on all interfaces | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Urls.Add("http://*:3000"); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Urls.Add("http://+:3000"); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Urls.Add("http://0.0.0.0:3000"); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| This syntax also works in the environment variables: | |
| ``` | |
| ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005 | |
| ``` | |
| ### HTTPS with development certificate | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Urls.Add("https://localhost:3000"); | |
| app.MapGet("/", () => "Hello World"); | |
| app.Run(); | |
| ``` | |
| ### HTTPS with custom certificate | |
| ***Using appsettings.json*** | |
| ```json | |
| { | |
| "Kestrel" :{ | |
| "Certificates": { | |
| "Default": { | |
| "Path": "cert.pem", | |
| "KeyPath": "key.pem" | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| **Configuration via code** | |
| ```csharp | |
| 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(); | |
| ``` | |
| **Using the certificate APIs** | |
| ```csharp | |
| 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(); | |
| ``` | |
| ### Reading the environment | |
| ```csharp | |
| 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(); | |
| ``` | |
| ### Reading configuration | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| Console.WriteLine($"The configuration value is {app.Configuration["key"]}"); | |
| app.Run(); | |
| ``` | |
| ### Logging | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.Logger.LogInformation("The application started"); | |
| app.Run(); | |
| ``` | |
| ## WebApplicationBuilder | |
| ### Changing the content root, application name and environment | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(new WebApplicationOptions | |
| { | |
| ApplicationName = typeof(Program).Assembly.FullName, | |
| ContentRootPath = Directory.GetCurrentDirectory(), | |
| EnvironmentName = Environments.Staging | |
| }); | |
| Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}"); | |
| Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}"); | |
| Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}"); | |
| var app = builder.Build(); | |
| ``` | |
| ### Changing the content root, application name and environment | |
| These may also be specified via the following environment variables: | |
| - `ASPNETCORE_ENVIRONMENT` | |
| - `ASPNETCORE_CONTENTROOT` | |
| - `ASPNETCORE_APPLICATIONNAME` | |
| OR via command line arguments: | |
| - `--applicationName` | |
| - `--environment` | |
| - `--contentRoot` | |
| ### Adding configuration providers | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Configuration.AddIniFile("appsettings.ini"); | |
| var app = builder.Build(); | |
| ``` | |
| ### Reading configuration | |
| By default the `WebApplicationBuilder` reads configuration from: | |
| - `appSettings.json` | |
| - `appSettings.{environment}.json` | |
| - environment variables | |
| - The command line | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Reads the ConnectionStrings section of configuration and looks for a sub key called Todos | |
| var connectionString = builder.Configuration.GetConnectionString("Todos"); | |
| Console.WriteLine($"My connection string is {connectionString}"); | |
| var app = builder.Build(); | |
| ``` | |
| ### Reading the environment | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| if (builder.Environment.IsDevelopment()) | |
| { | |
| Console.WriteLine($"Running in development"); | |
| } | |
| var app = builder.Build(); | |
| ``` | |
| ### Adding logging providers | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Configure JSON logging to the console | |
| builder.Logging.AddJsonConsole(); | |
| var app = builder.Build(); | |
| ``` | |
| ### Adding services | |
| ```csharp | |
| 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(); | |
| ``` | |
| ### Customizing the IHostBuilder | |
| Existing extension methods on `IHostBuilder` can be accessed using the `Host` property. | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Wait 30 seconds for graceful shutdown | |
| builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30)); | |
| var app = builder.Build(); | |
| ``` | |
| ### Customizing the IWebHostBuilder | |
| Existing extension methods on `IWebHostBuilder` can be accessed using the `WebHost` property. | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Change the HTTP server implemenation to be HTTP.sys based | |
| builder.WebHost.UseHttpSys(); | |
| var app = builder.Build(); | |
| ``` | |
| ### Changing the web root | |
| By default, the web root is relative to the content root in the `wwwroot` folder. This is where the static files | |
| middleware expects to find static files. You can change this by using the `UseWebRoot` method on the `WebHost` property: | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Look for static files in webroot | |
| builder.WebHost.UseWebRoot("webroot"); | |
| var app = builder.Build(); | |
| ``` | |
| ### Custom dependency injection container | |
| This example uses [Autofac](https://autofac.readthedocs.io/en/latest/integration/aspnetcore.html) | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); | |
| // Register your own things directly with Autofac here. Don't | |
| // call builder.Populate(), that happens in AutofacServiceProviderFactory | |
| // for you. | |
| builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule())); | |
| var app = builder.Build(); | |
| ``` | |
| ### Adding Middleware | |
| Any existing ASP.NET Core middleware can be configured on the WebApplication: | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| // Setup the file server to serve static files | |
| app.UseFileServer(); | |
| app.Run(); | |
| ``` | |
| ### Developer Exception Page | |
| The WebApplication has a the developer exception enabled by default when the environment is development: | |
| ```csharp | |
| var app = WebApplication.Create(args); | |
| app.MapGet("/", () => throw new InvalidOperationException("Oops, the '/' route has thrown an exception.")); | |
| app.Run(); | |
| ``` | |
| Navigating to `/` will render a friendly page that shows the exception. | |
| ### ASP.NET Core Middleware | |
| |Middleware |Description|API| | |
| |--|--|--| | |
| |Authentication|Provides authentication support. | `app.UseAuthentication()` | |
| |Authorization|Provides authorization support. | `app.UseAuthorization()` | |
| |CORS|Configures Cross-Origin Resource Sharing. | `app.UseCors()` | |
| |Exception Handler| Globally handles exceptions thrown by the middleware pipeline. | `app.UseExceptionHandler()` | |
| |Forwarded Headers| Forwards proxied headers onto the current request. | `app.UseForwardedHeaders()` | |
| |HTTPS Redirection|Redirect all HTTP requests to HTTPS. | `app.UseHttpsRedirection()` | |
| |HTTP Strict Transport Security (HSTS)|Security enhancement middleware that adds a special response header. | `app.UseHsts()` | |
| |Request Logging|Provides support for logging HTTP requests and responses. | `app.UseHttpLogging()` | |
| |W3C Request Logging|Provides support for logging HTTP requests and responses in the [W3C format](https://www.w3.org/TR/WD-logfile.html). | `app.UseW3CLogging()` | |
| |Response Caching|Provides support for caching responses. | `app.UseResponseCaching()` | |
| |Response Compression|Provides support for compressing responses. | `app.UseResponseCompression()` | |
| |Session|Provides support for managing user sessions. | `app.UseSession()` | |
| |Static Files|Provides support for serving static files and directory browsing. | `app.UseStaticFiles()`, `app.UseFileServer()` | |
| |WebSockets|Enables the WebSockets protocol. | `app.UseWebSockets()` | |
| ## Routing | |
| You can route to handlers using the various Map* methods on `WebApplication`. There's a `Map{HTTPMethod}` method to allow handling different HTTP methods: | |
| ```csharp | |
| app.MapGet("/", () => "This is a GET"); | |
| app.MapPost("/", () => "This is a POST"); | |
| app.MapPut("/", () => "This is a PUT"); | |
| app.MapDelete("/", () => "This is a DELETE"); | |
| ``` | |
| Other HTTP methods: | |
| ```csharp | |
| app.MapMethods("/options-or-head", new [] { "OPTIONS", "HEAD" }, () => "This is an options or head request "); | |
| ``` | |
| ### Route Handlers | |
| Route handlers are methods that execute when the a route matches. Route handlers can be a function or any shape (including synchronous or asynchronous). It can be a lambda expression, a local function, | |
| an instance method or a static method. | |
| **Lambda expression** | |
| ```csharp | |
| app.MapGet("/", () => "This is an inline lambda"); | |
| var handler = () => "This is a lambda variable"; | |
| app.MapGet("/", handler); | |
| ``` | |
| **Local function** | |
| ```csharp | |
| string LocalFunction() => "This is local function" | |
| app.MapGet("/", LocalFunction); | |
| ``` | |
| **Instance method** | |
| ```csharp | |
| var handler = new HelloHandler(); | |
| app.MapGet("/", handler.Hello); | |
| class HelloHandler | |
| { | |
| public string Hello() | |
| { | |
| return "Hello World"; | |
| } | |
| } | |
| ``` | |
| **Static method** | |
| ```csharp | |
| app.MapGet("/", HelloHandler.Hello); | |
| class HelloHandler | |
| { | |
| public static string Hello() | |
| { | |
| return "Hello World"; | |
| } | |
| } | |
| ``` | |
| This provides the appropriate flexiblity when deciding how to organize your route handlers. | |
| ### Naming routes and link generation | |
| Routes can be given names in order to generate URLs to them. This avoids having to hard code paths in your application. | |
| ```csharp | |
| app.MapGet("/hello", () => "Hello there") | |
| .WithName("hi"); | |
| app.MapGet("/", (LinkGenerator linker) => $"The link to the hello route is {linker.GetPathByName("hi", values: null)}"); | |
| ``` | |
| Route names are inferred from method names if specified: | |
| ```csharp | |
| string Hi() => "Hello there"; | |
| app.MapGet("/hello", Hi); | |
| app.MapGet("/", (LinkGenerator linker) => $"The link to the hello route is {linker.GetPathByName("Hi", values: null)}"); | |
| ``` | |
| **NOTE: Route names are case sensitive!** | |
| These names must be globally unique and are also used as the OpenAPI operation id when OpenAPI support is enabled (see the OpenAPI/Swagger section for more details). | |
| ### Route Parameters | |
| You can capture route parameters as part of the route pattern definition: | |
| ```csharp | |
| app.MapGet("/users/{userId}/books/{bookId}", (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}"); | |
| ``` | |
| The route handler can declare the parameters that it wants to capture, and when a request is made to this route, the parameters will be parsed and passed to the handler. This makes it easy to capture the values in a type safe way (above, you can be sure that the `userId` and `bookId` are both `int`). | |
| If the route values are not valid `int`s then an exception will be thrown. The following request will result in the exception below: | |
| ``` | |
| GET /users/hello/books/3 | |
| ``` | |
| ``` | |
| Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to bind parameter "int userId" from "hello". | |
| at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.ParameterBindingFailed(HttpContext httpContext, String parameterTypeName, String parameterName, String sourceValue, Boolean shouldThrow) | |
| at lambda_method1(Closure , Object , HttpContext ) | |
| at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass37_0.<Create>b__0(HttpContext httpContext) | |
| ``` | |
| ### Wildcards/Catch all routes | |
| ```csharp | |
| app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}"); | |
| ``` | |
| ### Route constraints | |
| Route constraints are influence the matching behavior of a route. | |
| ```csharp | |
| 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}"); | |
| ``` | |
| |Path|Match| | |
| |--|--| | |
| |`/todos/1`|`/todos/{id:int}`| | |
| |`/todos/something`|`/todos/{text}`| | |
| |`/posts/mypost`| `/posts/{slug:regex(^[a-z0-9_-]+$)}` | | |
| |`/posts/%`| No match | | |
| ### Contraints | |
| | constraint | Example | Example Matches | Notes | | |
| | ---------- | ------- | --------------- | ----- | | |
| | `int` | `{id:int}` | `123456789`, `-123456789` | Matches any integer | | |
| | `bool` | `{active:bool}` | `true`, `FALSE` | Matches `true` or `false`. Case-insensitive | | |
| | `datetime` | `{dob:datetime}` | `2016-12-31`, `2016-12-31 7:32pm` | Matches a valid `DateTime` value in the invariant culture. See preceding warning. | | |
| | `decimal` | `{price:decimal}` | `49.99`, `-1,000.01` | Matches a valid `decimal` value in the invariant culture. See preceding warning.| | |
| | `double` | `{weight:double}` | `1.234`, `-1,001.01e8` | Matches a valid `double` value in the invariant culture. See preceding warning.| | |
| | `float` | `{weight:float}` | `1.234`, `-1,001.01e8` | Matches a valid `float` value in the invariant culture. See preceding warning.| | |
| | `guid` | `{id:guid}` | `CD2C1638-1638-72D5-1638-DEADBEEF1638` | Matches a valid `Guid` value | | |
| | `long` | `{ticks:long}` | `123456789`, `-123456789` | Matches a valid `long` value | | |
| | `minlength(value)` | `{username:minlength(4)}` | `Rick` | String must be at least 4 characters | | |
| | `maxlength(value)` | `{filename:maxlength(8)}` | `MyFile` | String must be no more than 8 characters | | |
| | `length(length)` | `{filename:length(12)}` | `somefile.txt` | String must be exactly 12 characters long | | |
| | `length(min,max)` | `{filename:length(8,16)}` | `somefile.txt` | String must be at least 8 and no more than 16 characters long | | |
| | `min(value)` | `{age:min(18)}` | `19` | Integer value must be at least 18 | | |
| | `max(value)` | `{age:max(120)}` | `91` | Integer value must be no more than 120 | | |
| | `range(min,max)` | `{age:range(18,120)}` | `91` | Integer value must be at least 18 but no more than 120 | | |
| | `alpha` | `{name:alpha}` | `Rick` | String must consist of one or more alphabetical characters, `a`-`z` and case-insensitive. | | |
| | `regex(expression)` | `{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}` | `123-45-6789` | String must match the regular expression. See tips about defining a regular expression. | | |
| | `required` | `{name:required}` | `Rick` | Used to enforce that a non-parameter value is present during URL generation | | |
| ## Parameter Binding | |
| Parameter binding is the process of turning 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 explict or inferred based 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 forms are not natively supported in this release.** | |
| ### GET, HEAD, OPTIONS, DELETE | |
| The HTTP methods GET, HEAD, OPTIONS, DELETE will never bind from body. All other binding sources are supported. | |
| **NOTE: If you need to support the case where you have a GET with a body, you can directly read it from the HttpRequest.** | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Added as service | |
| builder.AddSingleton<Service>(); | |
| var app = builder.Build(); | |
| app.MapGet("/{id}", (int id, int page, Service service) => { }); | |
| class Service { } | |
| ``` | |
| |Parameter| Binding Source| | |
| |--|--| | |
| |id|route value| | |
| |page|query string| | |
| |service|provided by dependency injection| | |
| ### Other verbs (POST, PUT, PATCH, etc) | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| // Added as service | |
| builder.AddSingleton<Service>(); | |
| app.MapPost("/", (Person person, Service service) => { }); | |
| class Service { } | |
| record Person(string Name, int Age); | |
| ``` | |
| |Parameter| Binding Source| | |
| |--|--| | |
| |person|body (as JSON)| | |
| |service|provided by dependency injection| | |
| ### Explicit Parameter Binding | |
| Attributes can be used to explicitly declare where parameters should be bound from. | |
| ```csharp | |
| using Microsoft.AspNetCore.Mvc; | |
| app.MapGet("/{id}", ([FromRoute] int id, | |
| [FromQuery(Name = "p")] int page, | |
| [FromServices] Service service, | |
| [FromHeader(Name = "Content-Type")] string contentType) => { }); | |
| ``` | |
| |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"`| | |
| Binding from form values is not supported at this time. | |
| ### Optional parameters | |
| Parameters declared in route handlers will be treated as required. This means if a request matches the route, the route handler will only execute if all required paramters are provided in the request. Failure to do so will result in an error. | |
| ```csharp | |
| app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}"); | |
| ``` | |
| Since the `pageNumber` paramter is required, this route handler won't execute if the query string `pageNumber` isn't provided. To make it optional define the type as nullable. | |
| ```csharp | |
| app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}"); | |
| ``` | |
| This also works with methods that have a default value: | |
| ```csharp | |
| string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}"; | |
| app.MapGet("/products", ListProducts); | |
| ``` | |
| The above will default to 1 if the pageNumber isn't specified in the query string. | |
| This logic applies to all sources. | |
| ```csharp | |
| app.MapPost("/products", (Product? product) => () => { }); | |
| ``` | |
| The above will call the method with a null product if no request body was sent. | |
| **NOTE: If invalid data is provided and the parameter is nullable, the route handler will not be executed.** | |
| ```csharp | |
| app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}"); | |
| ``` | |
| The following request will result in a 400 (see the **Binding Failures** section below for more details) | |
| ``` | |
| GET /products?pageNumber=two | |
| ``` | |
| ### Special types | |
| There are some special types that the framework supports binding without any explicit attributes: | |
| - `HttpContext` - The context which holds all the information about the current http request/response. | |
| - `HttpRequest` - The http request | |
| - `HttpResponse` - The http reponse | |
| - `CancellationToken` - The cancellation token associated with the current http request. | |
| - `ClaimsPrincipal` - The user associated with the request (`HttpContext.User`). | |
| **HttpContext** | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World")); | |
| app.Run(); | |
| ``` | |
| **HttpRequest/HttpResponse** | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}")); | |
| app.Run(); | |
| ``` | |
| **CancellationToken** | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| app.MapGet("/", async (CancellationToken cancellationToken) => | |
| { | |
| await MakeLongRunningRequestAsync(cancellationToken) | |
| }); | |
| app.Run(); | |
| ``` | |
| **ClaimsPrincipal** | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name); | |
| app.Run(); | |
| ``` | |
| ### Custom Binding | |
| There are 2 ways to customize parameter binding: | |
| 1. For route, query, and header binding sources, you may bind custom types by adding a static `TryParse` method to your type. | |
| 2. You can completely take over the binding process by implementing a `BindAsync` method on your type. | |
| **TryParse** | |
| The TryParse method must be of the form(s): | |
| ```csharp | |
| public static bool TryParse(string value, T out result); | |
| public static bool TryParse(string value, IFormatProvider provider, T out result); | |
| ``` | |
| **Example** | |
| ```csharp | |
| app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}"); | |
| 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; | |
| } | |
| } | |
| ``` | |
| A request to `/map?point=(12.3,10.1)` returns: | |
| ``` | |
| Point: 12.3,10.1 | |
| ``` | |
| **BindAsync** | |
| The `BindAsync` method must be of the form: | |
| ```csharp | |
| public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter); | |
| ``` | |
| **Example** | |
| ```csharp | |
| app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}"); | |
| 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 will log a debug message and it will return 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 read 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 are as follows: | |
| 1. Explicit attribute defined on parameter (From* attributes) in the following order: | |
| 1. Route values (`FromRoute`) | |
| 1. Query string (`FromQuery`) | |
| 1. Header (`FromHeader`) | |
| 1. Body (`FromBody`) | |
| 1. Service (`FromServices`) | |
| 1. Special types | |
| 1. `HttpContext` | |
| 1. `HttpRequest` | |
| 1. `HttpResponse` | |
| 1. `ClaimsPrincipal` | |
| 1. `CancellationToken` | |
| 1. Parameter type has a valid `BindAsync` method. | |
| 1. Parameter type is a string or has a valid `TryParse` method. | |
| 1. If the parameter name exists in the route template e.g. `app.Map("/todo/{id}", (int id) => {});`, then it will be bound from the route. | |
| 1. It will be bound from the query string. | |
| 1. If the parameter type is a service provided by dependency injection, it will use that as the source. | |
| 1. The parameter is from the body. | |
| ### Customizing JSON binding | |
| The body binding source uses System.Text.Json for de-serialization. It is *NOT* possible to change this default but you can customize | |
| the binding using other techniques described in above sections. To customize JSON serializer options, you can use | |
| the following: | |
| ```csharp | |
| 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 instead of properties | |
| public int Id; | |
| public string Name; | |
| } | |
| ``` | |
| This configures both the input and output default JSON options. | |
| ## Responses | |
| Route handlers support 2 types of return values: | |
| 1. `IResult` based - This includes `Task<IResult>` and `ValueTask<IResult>` | |
| 1. `string` - This includes `Task<string>` and `ValueTask<string>` | |
| 1. `T` (Any other type) - This includes `Task<T>` and `ValueTask<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: string return values** | |
| ```csharp | |
| app.MapGet("/hello", () => "Hello World"); | |
| ``` | |
| **Example: JSON return values** | |
| ```csharp | |
| app.MapGet("/hello", () => new { Message = "Hello World" }); | |
| ``` | |
| **Example: IResult return values** | |
| ```csharp | |
| app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" })); | |
| ``` | |
| The following example uses the built-in result types to customize the response: | |
| ```csharp | |
| app.MapGet("/todos/{id}", (int id, TodoDb db) => | |
| db.Todos.Find(id) is Todo todo | |
| ? Results.Ok(todo) | |
| : Results.NotFound() | |
| ); | |
| ``` | |
| ### JSON | |
| ```csharp | |
| app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" })); | |
| ``` | |
| ### Custom Status Code | |
| ```csharp | |
| app.MapGet("/405", () => Results.StatusCode(405)); | |
| ``` | |
| ### Text | |
| ```csharp | |
| app.MapGet("/text", () => Results.Text("This is some text")); | |
| ``` | |
| ### Stream | |
| ```csharp | |
| var proxyClient = new HttpClient(); | |
| app.MapGet("/pokemon", async () => | |
| { | |
| var stream = await proxyClient.GetStreamAsync("http://myurl/pokedex.json") | |
| // Proxy the response as JSON | |
| return Results.Stream(stream, "application/json"); | |
| }); | |
| ``` | |
| ### Redirect | |
| ```csharp | |
| app.MapGet("/old-path", () => Results.Redirect("/new-path")); | |
| ``` | |
| ### File | |
| ```csharp | |
| app.MapGet("/download", () => Results.File("foo.text")); | |
| ``` | |
| ### Built-in results | |
| Common result helpers exist on 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 | |
| Users can take control of responses by implementing a custom `IResult` type. Here's an example of an HTML result type: | |
| ```csharp | |
| namespace Microsoft.AspNetCore.Http; | |
| static class ResultsExtensions | |
| { | |
| public static IResult Html(this IResultExtensions resultExtensions, string html) | |
| { | |
| ArgumentNullException.ThrowIfNull(resultExtensions, nameof(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. | |
| ```csharp | |
| 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>")); | |
| ``` | |
| ## Authorization | |
| Routes can be protected using authorization policies. These can be declared via the `Authorize` attribute or by using the `RequireAuthorization` method. | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", b => b.RequireClaim("admin", "true")); | |
| var app = builder.Build(); | |
| app.UseAuthorization(); | |
| ``` | |
| ```csharp | |
| app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization"); | |
| ``` | |
| OR | |
| ```csharp | |
| app.MapGet("/auth", () => "This endpoint requires authorization") | |
| .RequireAuthorization(); | |
| ``` | |
| Authorization policies can be configured as well: | |
| ```csharp | |
| app.MapGet("/admin", [Authorize("AdminsOnly")] () => "This endpoint is for admins only"); | |
| ``` | |
| OR | |
| ```csharp | |
| app.MapGet("/admin", () => "This endpoint is for admins only") | |
| .RequireAuthorization("AdminsOnly"); | |
| ``` | |
| ### Allowing unauthenticated users to an endpoint | |
| It's also possible to allow any unauthenticated users to endpoints by using the `AllowAnonymous` attribute or | |
| the `AllowAnonymous` method. | |
| ```csharp | |
| app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for admins only"); | |
| ``` | |
| ```csharp | |
| app.MapGet("/login", () => "This endpoint is for admins only") | |
| .AllowAnonymous(); | |
| ``` | |
| ## CORS | |
| Routes can be CORS enabled using CORS policies. These can be declared via the `EnableCors` attribute or by using the | |
| `RequireCors` method. | |
| ```csharp | |
| var builder = WebApplication.CreateBuilder(args); | |
| builder.Services.AddCors(options => options.AddPolicy("AnyOrigin", o => o.AllowAnyOrigin())); | |
| var app = builder.Build(); | |
| app.UseCors(); | |
| ``` | |
| ```csharp | |
| app.MapGet("/cors", [EnableCors("AnyOrigin")] () => "This endpoint allows cross origin requests!"); | |
| ``` | |
| OR | |
| ```csharp | |
| app.MapGet("/cors", () => "This endpoint allows cross origin requests!") | |
| .RequireCors("AnyOrigin"); | |
| ``` | |
| ## OpenAPI | |
| It's possible to describe the OpenAPI specification for route handlers using [Swashbuckle](https://www.nuget.org/packages/Swashbuckle.AspNetCore/). | |
| Below is an example of a typical ASP.NET Core application with OpenAPI suppport: | |
| ```csharp | |
| 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")); | |
| } | |
| ``` | |
| ### Describing response types | |
| ```csharp | |
| app.MapGet("/api/products", (int id, ProductDb db) => db.Products.Find(id) is Product product ? Results.Ok(product) : Results.NotFound()) | |
| .Produces<Product>(200) | |
| .Produces(404); | |
| ``` | |
| ### Exclude Open API description | |
| ```csharp | |
| app.MapGet("/skipme", () => { }) | |
| .ExcludeFromDescription(); | |
| ``` | |
| ### Add operation ids to Open API | |
| ```csharp | |
| app.MapGet("/api/products", (ProductDb db) => db.Products.ToListAsync()) | |
| .WithName("GetProducts"); | |
| ``` | |
| ### Add tags to the Open API description (used for grouping) | |
| ```csharp | |
| app.MapGet("/api/products", (ProductDb db) => db.Products.ToListAsync()) | |
| .WithTags("ProductsGroup"); | |
| ``` | |
| ### Describe request body | |
| ```csharp | |
| app.MapGet("/upload", async (HttpRequest req) => | |
| { | |
| if (!req.HasFormContentType) | |
| { | |
| return Results.BadRequest(); | |
| } | |
| var form = await req.ReadFormAsync(); | |
| var file = form.Files["file"]; | |
| if (file is null) | |
| { | |
| return Results.BadRequest(); | |
| } | |
| var uploads = Path.Combine(uploadsPath, file.FileName); | |
| await using var fileStream = File.OpenWrite(uploads); | |
| await using var uploadStream = file.OpenReadStream(); | |
| await uploadStream.CopyToAsync(fileStream); | |
| return Results.NoContent(); | |
| }) | |
| .Accepts<IFormFile>("multipart/form-data"); | |
| ``` | |
| ## Building libraries for ASP.NET Core | |
| The existing .NET ecosystem has built extensibility around `IServiceCollection`, `IHostBuilder` and `IWebHostBuilder`. These properties are available on the `WebApplicationBuilder` as `Services`, `Host` and `WebHost`. | |
| The `WebApplication` implements both `Microsoft.AspNetCore.Builder.IApplicationBuilder` and `Microsoft.AspNetCore.Routing.IEndpointRouteBuilder`. | |
| We expect library authors to continue targeting `IHostBuilder`, `IWebHostBuilder`, `IApplicationBuilder` and `IEndpointRouteBuilder` when building ASP.NET Core specific components. This will ensure that your middleware, route handler, or other extensibility points continue to work across different hosting models. | |
| ## Differences with ASP.NET Core MVC | |
| - No support for filters. i.e. `IAsyncAuthorizationFilter`, `IAsyncActionFilter`, `IAsyncExceptionFilter`, `IAsyncResultFilter`, `IAsyncResourceFilter` | |
| - No support for model binding. i.e. `IModelBinderProvider`, `IModelBinder`. Support can be added with a custom binding shim. | |
| - No support for binding from forms. This includes binding `IFormFile` (this will be added in the future). | |
| - No built-in support for validation. i.e. `IModelValidator` | |
| - No support for application parts or the application model. There's no way to apply or build your own conventions. | |
| - No built-in view rendering support. We recommend using Razor Pages for rendering views. | |
| - No support for [JsonPatch](https://www.nuget.org/packages/Microsoft.AspNetCore.JsonPatch/) | |
| - No support for [OData](https://www.nuget.org/packages/Microsoft.AspNetCore.OData/) | |
| - No support for [ApiVersioning](https://www.nuget.org/packages/Microsoft.AspNetCore.Mvc.Versioning/), see [this issue](https://github.com/dotnet/aspnet-api-versioning/issues/751) for more details. |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
What is BindProperties?