Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Minimal APIs at a glance

Minimal APIs at a glance

WebApplication

Creating an application

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

var app = WebApplication.Create(args);

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

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

Multiple ports

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

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

var app = WebApplication.Create(args);

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

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

app.Run();
var app = WebApplication.Create(args);

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

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

app.Run();
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

var app = WebApplication.Create(args);

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

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

app.Run();

HTTPS with custom certificate

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

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

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

var app = WebApplication.Create(args);

Console.WriteLine($"The configuration value is {app.Configuration["key"]}");

app.Run();

Logging

var app = WebApplication.Create(args);

app.Logger.LogInformation("The application started");

app.Run();

WebApplicationBuilder

Changing the content root, application name and environment

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

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

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development");
}

var app = builder.Build();

Adding logging providers

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

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

Customizing the IWebHostBuilder

Existing extension methods on IWebHostBuilder can be accessed using the WebHost property.

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:

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

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:

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:

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

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:

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

app.MapGet("/", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

Local function

string LocalFunction() => "This is local function"

app.MapGet("/", LocalFunction);

Instance method

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

class HelloHandler
{
    public string Hello() 
    {
        return "Hello World";
    }
}

Static method

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.

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:

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:

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

app.MapGet("/posts/{rest*}", (string rest) => $"Routing to {rest}");

Route constraints

Route constraints are influence the matching behavior of a route.

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.

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)

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.

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.

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.

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

This also works with methods that have a default value:

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.

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.

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

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

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

Example

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:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);

Example

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)
    2. Query string (FromQuery)
    3. Header (FromHeader)
    4. Body (FromBody)
    5. Service (FromServices)
  2. Special types
    1. HttpContext
    2. HttpRequest
    3. HttpResponse
    4. ClaimsPrincipal
    5. CancellationToken
  3. Parameter type has a valid BindAsync method.
  4. 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.
    2. It will be bound from the query string.
  5. If the parameter type is a service provided by dependency injection, it will use that as the source.
  6. 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:

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>
  2. string - This includes Task<string> and ValueTask<string>
  3. 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

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

Example: JSON return values

app.MapGet("/hello", () => new { Message = "Hello World" });

Example: 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("/todos/{id}", (int id, TodoDb db) => 
    db.Todos.Find(id) is Todo todo 
        ? Results.Ok(todo)
        : Results.NotFound()
);

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

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

File

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:

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.

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.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", b => b.RequireClaim("admin", "true"));

var app = builder.Build();

app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization");

OR

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

Authorization policies can be configured as well:

app.MapGet("/admin", [Authorize("AdminsOnly")] () => "This endpoint is for admins only");

OR

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.

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for admins only");
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.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => options.AddPolicy("AnyOrigin", o => o.AllowAnyOrigin()));

var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors("AnyOrigin")] () => "This endpoint allows cross origin requests!");

OR

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.

Below is an example of a typical ASP.NET Core application with OpenAPI suppport:

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

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

app.MapGet("/skipme", () => { })
   .ExcludeFromDescription();

Add operation ids to Open API

app.MapGet("/api/products", (ProductDb db) => db.Products.ToListAsync())
   .WithName("GetProducts");

Add tags to the Open API description (used for grouping)

app.MapGet("/api/products", (ProductDb db) => db.Products.ToListAsync())
   .WithTags("ProductsGroup");

Describe request body

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
  • No support for OData
  • No support for ApiVersioning, see this issue for more details.
@YhorbyMatias

This comment has been minimized.

Copy link

@YhorbyMatias YhorbyMatias commented Sep 11, 2021

Looks awesome!

@martincostello

This comment has been minimized.

Copy link

@martincostello martincostello commented Sep 11, 2021

As of SDK version 6.0.100-rc.1.21460.8 I get 500 as the default with Results.Problem(), not 400. Is that a bug, or just a typo in your table above? Unit test for it has 500 as the default.

@dadhi

This comment has been minimized.

Copy link

@dadhi dadhi commented Sep 11, 2021

@davidfowl

Dependency injection with DryIoc

Add to .csproj file:

<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.0.0-preview-02" />

The Program.cs:

using DryIoc;
using DryIoc.Microsoft.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

var container = new Container(Rules.MicrosoftDependencyInjectionRules); // use MS.DI rules here to safely use the same container instance later

builder.Host.UseServiceProviderFactory(new DryIocServiceProviderFactory(container));

builder.Services.AddTransient<Foo>();
container.Register<Bar>(); // add directly with DryIoc

var app = builder.Build();

app.MapGet("/", (Foo foo) => $"Hello world with `{foo}`");
app.MapGet("/bar", (Foo foo, Bar bar) => $"Hello world with `{foo}` and `{bar}`");

app.Run();

public class Foo
{
    public Foo(Bar bar = null) {}
}

public class Bar {}
@Ruikuan

This comment has been minimized.

Copy link

@Ruikuan Ruikuan commented Sep 11, 2021

Can BindAsync be an extension method?

@egil

This comment has been minimized.

Copy link

@egil egil commented Sep 11, 2021

Thanks, pure awesomeness here. A few questions:

  1. What happens when TryParse returns false or BindAsync returns null? Is the route mapping skipped and another one is attempted, ultimately resulting in a 404, or does it result in a 400?

  2. If my return type from an endpoint is an IAsyncEnumerable<T>, does the response gets streamed to the caller or is the async enumerable iterated to completion before returning?

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

@martincostello

As of SDK version 6.0.100-rc.1.21460.8 I get 500 as the default with Results.Problem(), not 400. Is that a bug, or just a typo in your table above? Unit test for it has 500 as the default.

Seems it's an issue with my doc, I'll update it.

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

@dadhi Awesome!

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

@Ruikuan

Can BindAsync be an extension method?

No, it must be a static method on the type. This means you can't extend types directly but it lends itself to another pattern where you can (I will add this to the document). You use generics to accomplish the same thing:

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

@egil

What happens when TryParse returns false or BindAsync returns null? Is the route mapping skipped and another one is attempted, ultimately resulting in a 404, or does it result in a 400?

Route matching (and constraints) determine if there will be a 404 or not. If the route matching succeeds (the route pattern and constraints are valid) and TryParse false, or BindAsync returns null and your argument type isn't nullable, it'll return a 400.

var builder = WebApplication.Create(args);

app.MapGet("/", (Thing thing) => { });

app.Run();

public class Thing
{
    public static ValueTask<Thing?> BindAsync(HttpContext context, ParameterInfo parameterInfo)
    {
        return ValueTask.FromResult<Thing?>((Thing)null);
    }
}

The above always returns null to illustrate the point. If you return null from BindAsync and the target parameter isn't nullable, then it'll return a 400.

If my return type from an endpoint is an IAsyncEnumerable, does the response gets streamed to the caller or is the async enumerable iterated to completion before returning?

The results are streamed. We added support for IAsyncEnumerable<T> directly in the JSON de-serializer this release so that we get that for free.

@egil

This comment has been minimized.

Copy link

@egil egil commented Sep 11, 2021

Thanks @davidfowl.

The above always returns null to illustrate the point. If you return null from BindAsync and the target parameter isn't nullable, then it'll return a 400.

Is it possible to control the 400 payload in the case where BindAsync or TryParse fail, e.g. to return an error message?

The results are streamed. We added support for IAsyncEnumerable<T> directly in the JSON de-serializer this release so that we get that for free.

Cool, so I can just do Results.Ok(asyncEnumerable) or do I need to create my own IResult type that calls JsonSerializer.SerializeAsync and passes the async enumerable to it?

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

Is it possible to control the 400 payload in the case where BindAsync or TryParse fail, e.g. to return an error message?

No, there's no way to customize the responses here, there's an issue here dotnet/aspnetcore#35501. You'll need a custom middleware that can observe the 400 status code and translate that into something else. Right now, there's no clean way to get the error message though.

Cool, so I can just do Results.Ok(asyncEnumerable) or do I need to create my own IResult type that calls JsonSerializer.SerializeAsync and passes the async enumerable to it?

You can do Results.Ok(asyncEnumerable) or you can return it directly from the route handler.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    async IAsyncEnumerable<DateTime> Stream()
    {
        while (true)
        {
            yield return DateTime.Now;
            await Task.Delay(1000);
        }
    }

    return Stream();
});

app.Run();
@egil

This comment has been minimized.

Copy link

@egil egil commented Sep 11, 2021

You can do Results.Ok(asyncEnumerable) or you can return it directly from the route handler.

What are the rules for what can be returned directly from the route handler and what needs to be an IResult, and how is it handled? E.g. What gets converted to JSON and what gets just returned as a text (ToString'ed).

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

What are the rules for what can be returned directly from the route handler and what needs to be an IResult, and how is it handled? E.g. What gets converted to JSON and what gets just returned as a text (ToString'ed).

Good catch! I'm going to update the doc to add that section to responses.

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

OK @egil, I've updated the Responses section to have the information about this. Let me know if it's clear.

PS: Great questions.

@egil

This comment has been minimized.

Copy link

@egil egil commented Sep 11, 2021

Excellent David. This is answering a whole bunch of my questions!

@martincostello

This comment has been minimized.

Copy link

@martincostello martincostello commented Sep 11, 2021

The app.MapMethods() call example is missing the pattern parameter at the start.

app.MapMethods("/options-or-head", new [] { "OPTIONS", "HEAD" }, () => "This is an options or head request ");
@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

Thanks @martincostello, fixed!

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 11, 2021

@davidfowl How about a section on how users can change the default SerializerOptions with JsonOptions?

@martincostello

This comment has been minimized.

Copy link

@martincostello martincostello commented Sep 11, 2021

In the parameter binding example, the query and header attribute code samples need updating to explicitly assign the Name property:

using Microsoft.AspNetCore.Mvc;

app.MapGet("/{id}", ([FromRoute]int id, 
                     [FromQuery(Name = "p")]int page, 
                     [FromServices]Service service, 
                     [FromHeader(Name = "Content-Type")]string contentType) => { });
@MizardX

This comment has been minimized.

Copy link

@MizardX MizardX commented Sep 11, 2021

Isn't

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products", (int? pageNumber) => ListProducts);

supposed to be

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products", ListProducts);

?

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 11, 2021

typo:

The above will call the method will a null product if no request body was sent.

The above will call the method with a null product if no request body was sent.

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 11, 2021

void LocalFunction() => "This is local function"
string LocalFunction() => "This is local function";
@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

@davidfowl How about a section on how users can change the default SerializerOptions with JsonOptions?

https://gist.github.com/davidfowl/ff1addd02d239d2d26f4648a06158727#customizing-json-binding

@MizardX

Fixed.

@martincostello

Fixed.

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 11, 2021

Done

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 12, 2021

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 12, 2021

  • Differences with MVC section

    • No support for JsonPatch
  • Since most of the handlers are API rather than HTML, how about also include a CORS sample?

  • Maybe include UseAuthorization middleware in Authorization

@rdlucas2

This comment has been minimized.

Copy link

@rdlucas2 rdlucas2 commented Sep 12, 2021

"Built in contraints" is one of the headings in the routes section. Should be constraints.

Amazing work with all this, thanks!

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 12, 2021

@Kahbazi

  • Added CORS
  • Added the authorization middleware as well (I feel like a JWT sample would be good to add as well).

@rdlucas2

Done!

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 12, 2021

Custom Binding - Incorrect path :

A request to /point?point=(12.3,10.1) returns:
A request to /map/(12.3,10.1) returns:

Also is it too advanced for this doc to use Span in Point.TryParse?

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 12, 2021

Also is it too advanced for this doc to use Span in Point.TryParse?

Not too advanced but irrelevant.

@Kahbazi

This comment has been minimized.

Copy link

@Kahbazi Kahbazi commented Sep 12, 2021

Also is it too advanced for this doc to use Span in Point.TryParse?

Not too advanced but irrelevant.

I was thinking that maybe most users just copy-paste the implementation and expand it from there and if it is Span they have better code!

@Sebazzz

This comment has been minimized.

Copy link

@Sebazzz Sebazzz commented Sep 13, 2021

What is the difference with Endpoint routing?

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 13, 2021

@Sebazzz it’s built on endpoint routing.

@jasongerard

This comment has been minimized.

Copy link

@jasongerard jasongerard commented Sep 13, 2021

Would feel cleaner without the Map prefix IMO.

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 13, 2021

@jasongerard

Would feel cleaner without the Map prefix IMO.

There was lots of back and forth on that initially but there were 2 problems:

  • We have existing Map* overloads on IEndpointRouteBuilder, so it fits with that convention.
  • We have use Map* to denote routing APIs and Use* to denote middleware APIs. WebApplication exposes them both on the object and those prefixes help keep them separate (and groups them nicely in intellisense)
@jasongerard

This comment has been minimized.

Copy link

@jasongerard jasongerard commented Sep 13, 2021

@jasongerard

Would feel cleaner without the Map prefix IMO.

There was lots of back and forth on that initially but there were 2 problems:

@davidfowl Oh I'm sure there was. Can't make everyone happy. I haven't done .NET in about 5 years now but it's nice to see how things progress.

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 13, 2021

@davidfowl Oh I'm sure there was. Can't make everyone happy. I haven't done .NET in about 5 years now but it's nice to see how things progress.

@jasongerard Hopefully you'll try it again for a hobby project 😉

@GavinRay97

This comment has been minimized.

Copy link

@GavinRay97 GavinRay97 commented Sep 13, 2021

Thanks for making this guide! The minimal API's were the most exciting Web-based changes for me in recent .NET previews, and the significant speed boost they showed due to internal differences in router structure/behavior were icing on the cake.

You rock 🤘

@IEvangelist

This comment has been minimized.

Copy link

@IEvangelist IEvangelist commented Sep 14, 2021

Hey, @davidfowl - you pinged me about this and said in "if you feel like bug bashing". Here goes, a few questions, suggestions, and nits. Overall this is amazing, thanks for putting this together!

Reading the port from the environment

var app = WebApplication.Create(args);

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

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.Run($"http://localhost:{port}");

Nit: I'd suggest moving the assignment of the port closer to its usage.

HTTPS with custom certificate

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "site.crt";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "site.key";

var app = builder.Build();

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

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

app.Run();

I'm curious, do we want to show people that they can assign into the .Configuration["Some:Key"]? It kind of feels like an antipattern, is this a reasonable way to have the Kestrel certs configured?

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

Q: Is there a way to get the Exception that caused the error when specifying the "oops" endpoint with MapGet? I see it's aligned with the .UseExceptionHandler.

Developer Exception Page

The WebApplication has a the developer exception enabled by default when the environment is development:

var app = WebApplication.Create(args);

app.MapGet("/", () => throw new InvalidOperationException("Oops, the '/' route has thrown an exception."));

app.Run();

Add exception message, and remove { }, make it an expression instead.

Stream

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");
});

I'm curious if we should show the HttpClient object coming in from DI via the IHttpClientFactory?

Route constraints

Nit: The route itself isn't constrained, the parameters are. I think this should be called Route parameter constraints.

Q: do we still have the ability to create custom route parameter constraints? If so, how - did that change? Common example feature gap enum constraints.

Explicit Parameter Binding

Attributes can be used to explicitly declare where parameters should be bound from.

using Microsoft.AspNetCore.Mvc;

app.MapGet("/{id}", ([FromRoute] int id, 
    [FromQuery(Name = "p")] int page, 
    [FromServices] Service service, 
    [FromHeader(Name = "Content-Type")] string contentType) => { });

Nit: formatting with explicit, attributes closing ] were touching parameter type def also, adjusted alignment of attributes.

@guardrex

This comment has been minimized.

Copy link

@guardrex guardrex commented Sep 17, 2021

@davidfowl ... WRT a GetServices affair ... for example, to seed a dB in demonstration sample app ... my example starting point is the following code ...

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        var sp = host.Services.GetService<IServiceScopeFactory>()
            .CreateScope()
            .ServiceProvider;
        var options = sp.GetRequiredService<DbContextOptions<ContactContext>>();
        await DatabaseUtility.EnsureDbCreatedAndSeedWithCountOfAsync(options, 500);

        await host.RunAsync();
    }
        
    ...
}

How about the following right after var app = builder.Build(); call? ...

var serviceProvider = builder.Services.BuildServiceProvider();
using var scope = serviceProvider.CreateAsyncScope();
var sp = scope.ServiceProvider.GetService<IServiceScopeFactory>()
    .CreateScope()
    .ServiceProvider;
var options = sp.GetRequiredService<DbContextOptions<ContactContext>>();
await DatabaseUtility.EnsureDbCreatedAndSeedWithCountOfAsync(options, 500);

There's one complaint on build for that (the var sp = line) ...

Dereference of a possibly null reference.

BTW -- I can't run this 6.0 sample app yet to confirm that ☝️ works. This sample app is a WIP with lots of other 6.0 errors for me to resolve before the whole app will actually run. UPDATE: Ok ... it runs 🎉 ... it seeds . Unless u have an objection to that ☝️, it seems like that will be good for our EFCore-Blazor Server sample.

Note that this is ONLY for a sample app, only to seed a dB for demonstration purposes, and isn't shown/discussed by the topic. We're explicitly warning devs off of this approach for production apps in a code remark. This code merely exists to make the sample app run without a lot of dev ceremony to seed the dB. I just need to know if this GetService approach is the right way to go.

@IEvangelist

This comment has been minimized.

Copy link

@IEvangelist IEvangelist commented Sep 17, 2021

Hey @davidfowl - I'm writing a Minimal API version of an ASP.NET Core SignalR app and I was thinking it would be great to see examples for that here too (and the other place, you know which one I'm talking about).

Something similar to the following?

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(
  options => options.AddDefaultPolicy(
      builder => builder.AllowAnyMethod()
          .AllowAnyHeader()
          .AllowCredentials()
          .SetIsOriginAllowed(origin => true)));

builder.Services.AddSignalR(options => options.EnableDetailedErrors = true)
    .AddMessagePackProtocol();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseCors();
app.MapHub<ChatHub>("/chat");

app.Run();

public class ChatHub : Hub
{
    public Task Send(string name, string message) =>
        Clients.All.SendAsync("broadcastMessage", name, message);
}
@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 17, 2021

@guardrex

How about the following right after var app = builder.Build(); call? ...

Yes but not what you wrote, it would look like this:

await using var scope = app.Services.GetService<IServiceScopeFactory>().CreateAsyncScope();
var options = scope.ServiceProvider.GetRequiredService<DbContextOptions<ContactContext>>();
await DatabaseUtility.EnsureDbCreatedAndSeedWithCountOfAsync(options, 500);
@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 17, 2021

@IEvangelist

It belongs somewhere, but not as part of this doc.

@guardrex

This comment has been minimized.

Copy link

@guardrex guardrex commented Sep 17, 2021

Thx @davidfowl ... and then I should ignore the warning? ...

Program.cs(46,25): warning CS8604: Possible null reference argument for parameter 'serviceScopeFactory' in 'AsyncServiceScope ServiceProviderServiceExtensions.CreateAsyncScope(IServiceScopeFactory serviceScopeFactory)'.

@IEvangelist

This comment has been minimized.

Copy link

@IEvangelist IEvangelist commented Sep 17, 2021

@IEvangelist

It belongs somewhere, but not as part of this doc.

@davidfowl Sweet! Thanks

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 17, 2021

Program.cs(46,25): warning CS8604: Possible null reference argument for parameter 'serviceScopeFactory' in 'AsyncServiceScope ServiceProviderServiceExtensions.CreateAsyncScope(IServiceScopeFactory serviceScopeFactory)'.

Nullable reference types. It should actually be:

await using var scope = app.Services.GetRequiredService<IServiceScopeFactory>().CreateAsyncScope();
var options = scope.ServiceProvider.GetRequiredService<DbContextOptions<ContactContext>>();
await DatabaseUtility.EnsureDbCreatedAndSeedWithCountOfAsync(options, 500);
@WeihanLi

This comment has been minimized.

Copy link

@WeihanLi WeihanLi commented Sep 17, 2021

How could we get ModelState in minimal API?

@davidfowl

This comment has been minimized.

Copy link
Owner Author

@davidfowl davidfowl commented Sep 17, 2021

There is no model state. See this section.

@WeihanLi

This comment has been minimized.

Copy link

@WeihanLi WeihanLi commented Sep 17, 2021

Get it, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment