Minimal APIs(Nov. 2019) are architected to create HTTP APIs with minimal dependencies.
They are ideal for microservices and apps that want to include only the minimum:
- files,
- features
- dependencies in ASP.NET Core.
//Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
...
app.Run();
//Program.cs
var app = WebApplication.Create(args);
app.Run();
The web application is used to configure the HTTP pipeline (middleware), and routes.
//Program.cs
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
This listens to port http://localhost:5000 and https://localhost:5001 by default.
-
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
var app = builder.Build();
Specified via the following environment variables:
Via command line arguments:
//Program.cs
app.Urls.Add("https://localhost:3000");
//application.json
{
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
} }}
//via configuration
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
// Use the certificate APIs
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
Call WebApplication.CreateBuilder, which adds the following logging providers: Console,Debug, EventSource, EventLog: Windows only
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddEventLog();
builder.Logging.AddConsole();
//Configuration application.[environment].json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
//The following sample adds the INI configuration provider:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
//Read configuration with sources:
//appSettings.json, Environment variables, The command line
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
//Read the environment: IsDevelopment, IsStaging, IsProduction, IsEnvironment
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment()){
Console.WriteLine($"Running in development.");
}
//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>();
builder.Services.AddTransient()<ITodoRepository, TodoRepository>();
builder.Services.AddSingleton()<ITodoRepository, TodoRepository>();
var app = builder.Build();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<PodcastsCtx>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<PodcastsCtx>();
dbContext.Database.Migrate();
}
app.Run();
public class PodcastsCtx: DbContext
{
public PodcastsCtx(DbContextOptions<PodcastsCtx> options) : base(options){}
public DbSet<Show> Shows { get; set; }
}
Execute migrations on application start
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
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();
dotnet run --urls="https://localhost:7777"
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.Run($"http://*:{port}");
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
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();
A configured WebApplication supports Map{Verb} and MapMethods:
var app = WebApplication.Create(args);
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Route handlers are methods that execute when the route matches.
Route handlers can be:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
The preceding code displays "The link to the hello route is /hello" from the / endpoint.
string Hi() => "Hello there";
app.MapGet("/hello", 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.
Route names are case sensitive!
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
Route parameters can be captured as part of the route pattern definition.
Wildcard and catch all routes
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
The following catch all route returns Routing to hello from the `/posts/hello' endpoint
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
NOTE: Binding from forms is not natively supported in .NET 6.
Parameter binding is the process of converting request data into strongly typed parameters that are expressed by route handlers.
Supported binding sources:
Route values
Query string
Header
Body (as JSON)
Services provided by dependency injection
The HTTP methods GET, HEAD, OPTIONS, and DELETE don't implicitly bind from body. Bind explicitly with [FromBody]
NOTE: To support the case of GET with a body, directly read it from the HttpRequest.
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id, int page, Service service) => { });
class Service { }
id:route value, page: query string, service: Provided by dependency injection
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapPost("/", (Person person, Service service) => { });
class Service { }
record Person(string Name, int Age);
person: body (as JSON), service: Provided by dependency injection
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
1. Explicit attribute defined on parameter ([From*] attributes) in the following order:
2. If the parameter type is a service provided by dependency injection, it will use that as the source.
Route values ([FromRoute])
Query string ([FromQuery])
Header ([FromHeader])
Body ([FromBody])
Service ([FromServices])
3. The parameter is from the body.
The following types are bound without explicit attributes:
HttpContext : The context which holds all the information about the current HTTP request or response.
HttpRequest : The HTTP request
HttpResponse : The HTTP response
CancellationToken : The cancellation token associated with the current http request.
ClaimsPrincipal : The user associated with the request (HttpContext.User)
//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"));
//Builder
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
//Application
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Routes can be protected using authorization policies.
These can be declared via the [Authorize] attribute or by using the .RequireAuthorization method.
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization");
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("/admin", () => "This endpoint is for admins only")
.RequireAuthorization("AdminsOnly");
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for admins only");
app.MapGet("/login", () => "This endpoint is for admins only").AllowAnonymous();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
builder.Services.AddAuthorization(options =>{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
app.MapGet("/login", () => "This endpoint is for admins only").AllowAnonymous();
builder.Services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>
("BasicAuthentication", null);
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock, UserService userService ){}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// skip authentication if endpoint has [AllowAnonymous] attribute
var endpoint = Context.GetEndpoint();
if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
return AuthenticateResult.NoResult();
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");
// ......
return AuthenticateResult.Success();
}}
Routes can be CORS enabled using CORS policies.
CORS can be declared via the [EnableCors] attribute or by using the .RequireCors method.
const string MyAllowSpecificOrigins = "_allowedOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
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"));
}
An application can describe the OpenAPI specification for route handlers using Swashbuckle.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/swag", () => "Hello Swagger!");
app.MapGet("/skipme", () => "Skipping Swagger.")
.ExcludeFromDescription();
app.Run();
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
//config
builder.Services.AddSwaggerGen(c =>
{
c .AddSecurityDefinition("basic", new OpenApiSecurityScheme {
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "basic",
In = ParameterLocation.Header,
Description = "Basic Authorization header using the Bearer scheme."
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement { {
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "basic"
}
}, Array.Empty<string>() }
});
});
c .AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description="JWT authorization using bearer scheme",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},Array.Empty<string>()}});
app.UseExceptionHandler((errorApp) =>
{
errorApp.Run(async (context) =>
{
var exceptionHandlerFeature =
context.Features.Get<IExceptionHandlerFeature>();
if (exceptionHandlerFeature?.Error != null)
app.Logger.LogError(exceptionHandlerFeature?.Error,
"Global error logged again with some custom data!");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var errorMessage = new { Error = "Internal Server error! " };
await context.Response.WriteAsync(JsonSerializer.Serialize(errorMessage));
});
});
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//global error handler
app.UseExceptionHandler((errorApp) =>
{
// Handling code from prev slide ..
});
}
public class Todo
{
public int? Id { get; set; }
[Required]
public string? Title { get; set; }
}
if (!MiniValidator.TryValidate(todo, out var errors))
{
return Results.BadRequest(
errors.Keys.SelectMany(x =>
errors[x].Select(y => new { Property = x, Error = y }))
);
}
A minimalistic validation library built atop the existing features in .NET's System.ComponentModel.DataAnnotations namespace.
public class TodoValidator : AbstractValidator<Todo>
{
public TodoValidator() =>
RuleFor(x => x.Title).NotNull().WithMessage("Title required");
}
app.MapPut("/api/todos/{id}", IValidator<Todo> validator, ....) => {
//mapping and other code
var validationResult = validator.Validate(todo);
if (!validationResult.IsValid)
{
return Results.BadRequest(
validationResult.Errors
.Select(x =>
new { Property = x.PropertyName, Error = x.ErrorMessage }));
}});
Install-Package FluentValidation.AspNetCore -Version
A popular .NET library for building strongly-typed validation rules.
builder.Services.AddValidatorsFromAssemblyContaining<Todo>
(lifetime: ServiceLifetime.Scoped);
public class TodosApplication : WebApplicationFactory<Program>{
protected override IHost CreateHost(IHostBuilder builder){
builder.ConfigureServices(services => {
// services.RemoveAll(typeof(DbContextOptions<TodoDbContext>));
// services.AddDbContext<TodoDbContext>(options =>
// options.UseInMemoryDatabase("Testing", root));
});
return base.CreateHost(builder);
}
}
private readonly HttpClient _client;
public ApiTests()
{
var application = new TodosApplication();
_client = application.CreateClient();
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes("username:password")));
}
Install-Package Microsoft.NET.Test.Sdk
Install-Package Microsoft.AspNetCore.Mvc.Testing
//GET
[Fact]
public async Task Can_call_get_todos() {
var todos = await _client.GetFromJsonAsync<List<TodoDto>>("/api/todos");
Assert.True(todos is List<TodoDto>);
}
//POST & PUT
[Fact]
public async Task Can_post_todo() {
var response = await _client.PostAsJsonAsync("/api/todos",
new TodoDto(null, Title: "I want to do this thing tomorrow"));
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var todos = await _client.GetFromJsonAsync<List<Todo>>("/api/todos");
var todo = todos.Last();
Assert.Equal("I want to do this thing tomorrow", todo.Title);
Assert.False(todo.IsCompleted);
}
//Delete
[Fact]
public async Task Can_call_get_todos() {
var id = 23;
var response = await _client.DeleteAsync($"/api/todos/{id}");
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
}
@imhotepp
2022