Tutorial: Create a minimal API with ASP.NET Core
Note
This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.
By Rick Anderson and Tom Dykstra
Minimal APIs 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, and dependencies in ASP.NET Core.
This tutorial teaches the basics of building a minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help in choosing between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on creating an API project based on controllers that contains more features, see Create a web API.
Overview
This tutorial creates the following API:
API | Description | Request body | Response body |
---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
POST /todoitems |
Add a new item | To-do item | To-do item |
PUT /todoitems/{id} |
Update an existing item | To-do item | None |
DELETE /todoitems/{id} |
Delete an item | None | None |
Prerequisites
Visual Studio 2022 Preview with the ASP.NET and web development workload.
Create an API project
Start Visual Studio 2022 Preview and select Create a new project.
In the Create a new project dialog:
- Enter
Empty
in the Search for templates search box. - Select the ASP.NET Core Empty template and select Next.
- Enter
Name the project TodoApi and select Next.
In the Additional information dialog:
- Select .NET 8.0 (Long Term Support)
- Uncheck Do not use top-level statements
- Select Create
Examine the code
The Program.cs
file contains the following code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The preceding code:
- Creates a WebApplicationBuilder and a WebApplication with preconfigured defaults.
- Creates an HTTP GET endpoint
/
that returnsHello World!
:
Run the app
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches the Kestrel web server and opens a browser window.
Hello World!
is displayed in the browser. The Program.cs
file contains a minimal but complete app.
Add NuGet packages
NuGet packages must be added to support the database and diagnostics used in this tutorial.
- From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
- Select the Browse tab.
- Select Include prerelease.
- Enter Microsoft.EntityFrameworkCore.InMemory in the search box, and then select
Microsoft.EntityFrameworkCore.InMemory
. - Select the Project checkbox in the right pane and then select Install.
- Follow the preceding instructions to add the
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
package.
The model and database context classes
In the project folder, create a file named Todo.cs
with the following code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
The preceding code creates the model for this app. A model is a class that represents data that the app manages.
Create a file named TodoDb.cs
with the following code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
The preceding code defines the database context, which is the main class that coordinates Entity Framework functionality for a data model. This class derives from the Microsoft.EntityFrameworkCore.DbContext class.
Add the API code
Replace the contents of the Program.cs
file with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
The following highlighted code adds the database context to the dependency injection (DI) container and enables displaying database-related exceptions:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
The DI container provides access to the database context and other services.
This tutorial uses Endpoints Explorer and .http files to test the API.
Test posting data
The following code in Program.cs
creates an HTTP POST endpoint /todoitems
that adds data to the in-memory database:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Run the app. The browser displays a 404 error because there is no longer a /
endpoint.
Use the POST endpoint to add data to the app.
Select View > Other Windows > Endpoints Explorer.
Right-click the POST endpoint and select Generate request.
A new file is created in the project folder named
TodoApi.http
, with contents similar to the following example:@TodoApi_HostAddress = https://localhost:7031 Post {{TodoApi_HostAddress}}/todoitems ###
- The first line creates a variable that will be used for all of the endpoints.
- The next line defines a POST request.
- The triple hashtag (
###
) line is a request delimiter: what comes after it will be for a different request.
The POST request needs headers and a body. To define those parts of the request, add the following lines immediately after the POST request line:
Content-Type: application/json { "name":"walk dog", "isComplete":true }
The preceding code adds a Content-Type header and a JSON request body. The TodoApi.http file should now look like the following example, but with your port number:
@TodoApi_HostAddress = https://localhost:7057 Post {{TodoApi_HostAddress}}/todoitems Content-Type: application/json { "name":"walk dog", "isComplete":true } ###
Run the app.
Select the Send request link that is above the
POST
request line.The POST request is sent to the app and the response is displayed in the Response pane.
Examine the GET endpoints
The sample app implements several GET endpoints by calling MapGet
:
API | Description | Request body | Response body |
---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get all completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Test the GET endpoints
Test the app by calling the GET
endpoints from a browser or by using Endpoints Explorer. The following steps are for Endpoints Explorer.
In Endpoints Explorer, right-click the first GET endpoint, and select Generate request.
The following content is added to the
TodoApi.http
file:Get {{TodoApi_HostAddress}}/todoitems ###
Select the Send request link that is above the new
GET
request line.The GET request is sent to the app and the response is displayed in the Response pane.
The response body is similar to the following JSON:
[ { "id": 1, "name": "walk dog", "isComplete": false } ]
In Endpoints Explorer, right-click the third GET endpoint and select Generate request. The following content is added to the
TodoApi.http
file:GET {{TodoApi_HostAddress}}/todoitems/{id} ###
Replace
{id}
with1
.Select the Send request link that is above the new GET request line.
The GET request is sent to the app and the response is displayed in the Response pane.
The response body is similar to the following JSON:
{ "id": 1, "name": "walk dog", "isComplete": false }
This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, POST data to the app and try the GET request again.
Return values
ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
The return types can represent a wide range of HTTP status codes. For example, GET /todoitems/{id}
can return two different status values:
- If no item matches the requested ID, the method returns a 404 status NotFound error code.
- Otherwise, the method returns 200 with a JSON response body. Returning
item
results in an HTTP 200 response.
Examine the PUT endpoint
The sample app implements a single PUT endpoint using MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
This method is similar to the MapPost
method, except it uses HTTP PUT. A successful response returns 204 (No Content). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
Test the PUT endpoint
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Update the to-do item that has Id = 1 and set its name to "feed fish"
.
In Endpoints Explorer, right-click the PUT endpoint, and select Generate request.
The following content is added to the
TodoApi.http
file:Put {{TodoApi_HostAddress}}/todoitems/{id} ###
In the PUT request line, replace
{id}
with1
.Add the following lines immediately after the PUT request line:
Content-Type: application/json { "id": 1, "name": "feed fish", "isComplete": false }
The preceding code adds a Content-Type header and a JSON request body.
Select the Send request link that is above the new GET request line.
The PUT request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Examine and test the DELETE endpoint
The sample app implements a single DELETE endpoint using MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
In Endpoints Explorer, right-click the DELETE endpoint and select Generate request.
A DELETE request is added to
TodoApi.http
.Replace
{id}
in the DELETE request line with1
. The DELETE request should look like the following example:DELETE {{TodoApi_HostAddress}}/todoitems/1 ###
Select the Send request link for the DELETE request.
The DELETE request is sent to the app and the response is displayed in the Response pane. The response body is empty, and the status code is 204.
Use the MapGroup API
The sample app code repeats the todoitems
URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the MapGroup method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like RequireAuthorization and WithMetadata.
Replace the contents of Program.cs
with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
The preceding code has the following changes:
- Adds
var todoItems = app.MapGroup("/todoitems");
to set up the group using the URL prefix/todoitems
. - Changes all the
app.Map<HttpVerb>
methods totodoItems.Map<HttpVerb>
. - Removes the URL prefix
/todoitems
from theMap<HttpVerb>
method calls.
Test the endpoints to verify that they work the same.
Use the TypedResults API
Returning TypedResults rather than Results has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see TypedResults vs Results.
The Map<HttpVerb>
methods can call route handler methods instead of using lambdas. To see an example, update Program.cs with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
The Map<HttpVerb>
code now calls methods instead of lambdas:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
These methods return objects that implement IResult and are defined by TypedResults:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Unit tests can call these methods and test that they return the correct type. For example, if the method is GetAllTodos
:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler method. For example:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Prevent over-posting
Currently the sample app exposes the entire Todo
object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this article.
A DTO may be used to:
- Prevent over-posting.
- Hide properties that clients are not supposed to view.
- Omit some properties in order to reduce payload size.
- Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
To demonstrate the DTO approach, update the Todo
class to include a secret field:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
Verify you can post and get the secret field.
Create a file named TodoItemDTO.cs
with the following code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Update the code in Program.cs
to use this DTO model:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Verify you can post and get all fields except the secret field.
Troubleshooting with the completed sample
If you run into a problem you can't resolve, compare your code to the completed project. View or download completed project (how to download).
Next steps
- Configure JSON serialization options.
- Handle errors and exceptions: The developer exception page is enabled by default in the development environment for minimal API apps. For information about how to handle errors and exceptions, see Handle errors in ASP.NET Core APIs.
- For an example of testing a minimal API app, see this GitHub sample.
- OpenAPI support in minimal APIs.
- Quickstart: Publish to Azure.
- Organizing ASP.NET Core Minimal APIs
Learn more
Minimal APIs 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, and dependencies in ASP.NET Core.
This tutorial teaches the basics of building a minimal API with ASP.NET Core. Another approach to creating APIs in ASP.NET Core is to use controllers. For help in choosing between minimal APIs and controller-based APIs, see APIs overview. For a tutorial on creating an API project based on controllers that contains more features, see Create a web API.
Overview
This tutorial creates the following API:
API | Description | Request body | Response body |
---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
POST /todoitems |
Add a new item | To-do item | To-do item |
PUT /todoitems/{id} |
Update an existing item | To-do item | None |
DELETE /todoitems/{id} |
Delete an item | None | None |
Prerequisites
Visual Studio 2022 with the ASP.NET and web development workload.
Create an API project
Start Visual Studio 2022 and select Create a new project.
In the Create a new project dialog:
- Enter
Empty
in the Search for templates search box. - Select the ASP.NET Core Empty template and select Next.
- Enter
Name the project TodoApi and select Next.
In the Additional information dialog:
- Select .NET 7.0
- Uncheck Do not use top-level statements
- Select Create
Examine the code
The Program.cs
file contains the following code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The preceding code:
- Creates a WebApplicationBuilder and a WebApplication with preconfigured defaults.
- Creates an HTTP GET endpoint
/
that returnsHello World!
:
Run the app
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches the Kestrel web server and opens a browser window.
Hello World!
is displayed in the browser. The Program.cs
file contains a minimal but complete app.
Add NuGet packages
NuGet packages must be added to support the database and diagnostics used in this tutorial.
- From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
- Select the Browse tab.
- Enter Microsoft.EntityFrameworkCore.InMemory in the search box, and then select
Microsoft.EntityFrameworkCore.InMemory
. - Select the Project checkbox in the right pane and then select Install.
- Follow the preceding instructions to add the
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
package.
The model and database context classes
In the project folder, create a file named Todo.cs
with the following code:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
The preceding code creates the model for this app. A model is a class that represents data that the app manages.
Create a file named TodoDb.cs
with the following code:
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
The preceding code defines the database context, which is the main class that coordinates Entity Framework functionality for a data model. This class derives from the Microsoft.EntityFrameworkCore.DbContext class.
Add the API code
Replace the contents of the Program.cs
file with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
The following highlighted code adds the database context to the dependency injection (DI) container and enables displaying database-related exceptions:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
The DI container provides access to the database context and other services.
Install Postman to test the app
This tutorial uses Postman to test the API.
- Install Postman
- Start the web app.
- Start Postman.
- Select Workspaces > Create Workspace and then select Next.
- Name the workspace TodoApi and select Create.
- Select the settings gear icon > Settings (General tab) and disable SSL certificate verification.
Warning
Re-enable SSL certificate verification after testing the sample app.
Test posting data
The following code in Program.cs
creates an HTTP POST endpoint /todoitems
that adds data to the in-memory database:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Run the app. The browser displays a 404 error because there is no longer a /
endpoint.
Use the POST endpoint to add data to the app:
In Postman, create a new HTTP request by selecting New > HTTP.
Set the HTTP method to
POST
.Set the URI to
https://localhost:<port>/todoitems
. For example:https://localhost:5001/todoitems
Select the Body tab.
Select raw.
Set the type to JSON.
In the request body enter JSON for a to-do item:
{ "name":"walk dog", "isComplete":true }
Select Send.
Examine the GET endpoints
The sample app implements several GET endpoints by calling MapGet
:
API | Description | Request body | Response body |
---|---|---|---|
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get all completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Test the GET endpoints
Test the app by calling the endpoints from a browser or Postman. The following steps are for Postman.
- Create a new HTTP request.
- Set the HTTP method to GET.
- Set the request URI to
https://localhost:<port>/todoitems
. For example,https://localhost:5001/todoitems
. - Select Send.
The call to GET /todoitems
produces a response similar to the following:
[
{
"id": 1,
"name": "walk dog",
"isComplete": false
}
]
- Set the request URI to
https://localhost:<port>/todoitems/1
. For example,https://localhost:5001/todoitems/1
. - Select Send.
- The response is similar to the following:
{ "id": 1, "name": "walk dog", "isComplete": false }
This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, POST data to the app and try the GET request again.
Return values
ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
The return types can represent a wide range of HTTP status codes. For example, GET /todoitems/{id}
can return two different status values:
- If no item matches the requested ID, the method returns a 404 status NotFound error code.
- Otherwise, the method returns 200 with a JSON response body. Returning
item
results in an HTTP 200 response.
Examine the PUT endpoint
The sample app implements a single PUT endpoint using MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
This method is similar to the MapPost
method, except it uses HTTP PUT. A successful response returns 204 (No Content). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
Test the PUT endpoint
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Update the to-do item that has Id = 1 and set its name to "feed fish"
:
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
Examine and test the DELETE endpoint
The sample app implements a single DELETE endpoint using MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Use Postman to delete a to-do item:
- Set the method to
DELETE
. - Set the URI of the object to delete (for example
https://localhost:5001/todoitems/1
). - Select Send.
Use the MapGroup API
The sample app code repeats the todoitems
URL prefix each time it sets up an endpoint. APIs often have groups of endpoints with a common URL prefix, and the MapGroup method is available to help organize such groups. It reduces repetitive code and allows for customizing entire groups of endpoints with a single call to methods like RequireAuthorization and WithMetadata.
Replace the contents of Program.cs
with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", async (TodoDb db) =>
await db.Todos.ToListAsync());
todoItems.MapGet("/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
todoItems.MapGet("/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
todoItems.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
todoItems.MapPut("/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
todoItems.MapDelete("/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
The preceding code has the following changes:
- Adds
var todoItems = app.MapGroup("/todoitems");
to set up the group using the URL prefix/todoitems
. - Changes all the
app.Map<HttpVerb>
methods totodoItems.Map<HttpVerb>
. - Removes the URL prefix
/todoitems
from theMap<HttpVerb>
method calls.
Test the endpoints to verify that they work the same.
Use the TypedResults API
Returning TypedResults rather than Results has several advantages, including testability and automatically returning the response type metadata for OpenAPI to describe the endpoint. For more information, see TypedResults vs Results.
The Map<HttpVerb>
methods can call route handler methods instead of using lambdas. To see an example, update Program.cs with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
The Map<HttpVerb>
code now calls methods instead of lambdas:
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
These methods return objects that implement IResult and are defined by TypedResults:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Unit tests can call these methods and test that they return the correct type. For example, if the method is GetAllTodos
:
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
Unit test code can verify that an object of type Ok<Todo[]> is returned from the handler method. For example:
public async Task GetAllTodos_ReturnsOkOfTodosResult()
{
// Arrange
var db = CreateDbContext();
// Act
var result = await TodosApi.GetAllTodos(db);
// Assert: Check for the correct returned type
Assert.IsType<Ok<Todo[]>>(result);
}
Prevent over-posting
Currently the sample app exposes the entire Todo
object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this article.
A DTO may be used to:
- Prevent over-posting.
- Hide properties that clients are not supposed to view.
- Omit some properties in order to reduce payload size.
- Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
To demonstrate the DTO approach, update the Todo
class to include a secret field:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
Verify you can post and get the secret field.
Create a file named TodoItemDTO.cs
with the following code:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Update the code in Program.cs
to use this DTO model:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
RouteGroupBuilder todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
app.Run();
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db) {
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}
Verify you can post and get all fields except the secret field.
Troubleshooting with the completed sample
If you run into a problem you can't resolve, compare your code to the completed project. View or download completed project (how to download).
Next steps
Configure JSON serialization options
For information on how to configure JSON serialization in your Minimal API apps, see Configure JSON serialization options.
Handle errors and exceptions
The developer exception page is enabled by default in the development environment for minimal API apps. For information about how to handle errors and exceptions, see Handle errors in ASP.NET Core APIs.
Test minimal API apps
For an example of testing a minimal API app, see this GitHub sample.
Use OpenAPI (Swagger)
For information on how to use OpenAPI with minimal API apps, see OpenAPI support in minimal APIs.
Publish to Azure
For information on how to deploy to Azure, see Quickstart: Deploy an ASP.NET web app.
Learn more
For more information about minimal API apps, see Minimal APIs quick reference.
Minimal APIs 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, and dependencies in ASP.NET Core.
This tutorial teaches the basics of building a minimal API with ASP.NET Core. For a tutorial on creating an API project based on controllers that contains more features, see Create a web API. For a comparison, see Differences between minimal APIs and APIs with controllers in this document.
Overview
This tutorial creates the following API:
API | Description | Request body | Response body |
---|---|---|---|
GET / |
Browser test, "Hello World" | None | Hello World! |
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
POST /todoitems |
Add a new item | To-do item | To-do item |
PUT /todoitems/{id} |
Update an existing item | To-do item | None |
DELETE /todoitems/{id} |
Delete an item | None | None |
Prerequisites
- Visual Studio 2022 with the ASP.NET and web development workload.
- .NET 6.0 SDK
Create a API project
Start Visual Studio 2022 and select Create a new project.
In the Create a new project dialog:
- Enter
API
in the Search for templates search box. - Select the ASP.NET Core Web API template and select Next.
- Enter
Name the project TodoApi and select Next.
In the Additional information dialog:
- Select .NET 6.0 (Long-term support)
- Remove Use controllers (uncheck to use minimal APIs)
- Select Create
Examine the code
The Program.cs
file contains the following code:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
The project template creates a WeatherForecast
API with support for Swagger. Swagger is used to generate useful documentation and help pages for APIs.
The following highlighted code adds support for Swagger:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
Run the app
Press Ctrl+F5 to run without the debugger.
Visual Studio displays the following dialog:
Select Yes if you trust the IIS Express SSL certificate.
The following dialog is displayed:
Select Yes if you agree to trust the development certificate.
For information on trusting the Firefox browser, see Firefox SEC_ERROR_INADEQUATE_KEY_USAGE certificate error.
Visual Studio launches the Kestrel web server.
The Swagger page /swagger/index.html
is displayed. Select GET > Try it out> Execute
. The page displays:
- The Curl command to test the WeatherForecast API.
- The URL to test the WeatherForecast API.
- The response code, body, and headers.
- A drop down list box with media types and the example value and schema.
Copy and paste the Request URL in the browser: https://localhost:<port>/WeatherForecast
. JSON similar to the following is returned:
[
{
"date": "2021-10-19T14:12:50.3079024-10:00",
"temperatureC": 13,
"summary": "Bracing",
"temperatureF": 55
},
{
"date": "2021-10-20T14:12:50.3080559-10:00",
"temperatureC": -8,
"summary": "Bracing",
"temperatureF": 18
},
{
"date": "2021-10-21T14:12:50.3080601-10:00",
"temperatureC": 12,
"summary": "Hot",
"temperatureF": 53
},
{
"date": "2021-10-22T14:12:50.3080603-10:00",
"temperatureC": 10,
"summary": "Sweltering",
"temperatureF": 49
},
{
"date": "2021-10-23T14:12:50.3080604-10:00",
"temperatureC": 36,
"summary": "Warm",
"temperatureF": 96
}
]
Update the generated code
This tutorial focuses on creating an API, so we'll delete the Swagger code and the WeatherForecast
code. Replace the contents of the Program.cs
file with the following:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The following highlighted code creates a WebApplicationBuilder and a WebApplication with preconfigured defaults:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
The following code creates an HTTP GET endpoint /
which returns Hello World!
:
app.MapGet("/", () => "Hello World!");
app.Run();
runs the app.
Remove the two "launchUrl": "swagger",
lines from the Properties/launchSettings.json
file. When the launchUrl
isn't specified, the web browser requests the /
endpoint.
Run the app. Hello World!
is displayed. The updated Program.cs
file contains a minimal but complete app.
Add NuGet packages
NuGet packages must be added to support the database and diagnostics used in this tutorial.
- From the Tools menu, select NuGet Package Manager > Manage NuGet Packages for Solution.
- Enter Microsoft.EntityFrameworkCore.InMemory in the search box, and then select
Microsoft.EntityFrameworkCore.InMemory
. - Select the Project checkbox in the right pane and then select Install.
- Follow the preceding instructions to add the
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
package.
Add the API code
Replace the contents of the Program.cs
file with the following code:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
The model and database context classes
The sample app contains the following model:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
A model is a class that represents data that the app manages. The model for this app is the Todo
class.
The sample app contains the following database context class:
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
The database context is the main class that coordinates Entity Framework functionality for a data model. This class is created by deriving from the Microsoft.EntityFrameworkCore.DbContext class.
The following highlighted code adds the database context to the dependency injection (DI) container and enables displaying database-related exceptions:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
The DI container provides access to the database context and other services.
The following code creates an HTTP POST endpoint /todoitems
to add data to the in-memory database:
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
Install Postman to test the app
This tutorial uses Postman to test the API.
- Install Postman
- Start the web app.
- Start Postman.
- Disable SSL certificate verification
- From File > Settings (General tab), disable SSL certificate verification.
Warning
Re-enable SSL certificate verification after testing the controller.
- From File > Settings (General tab), disable SSL certificate verification.
Test posting data
The following instructions post data to the app:
Create a new HTTP request.
Set the HTTP method to
POST
.Set the URI to
https://localhost:<port>/todoitems
. For example:https://localhost:5001/todoitems
Select the Body tab.
Select raw.
Set the type to JSON.
In the request body enter JSON for a to-do item:
{ "name":"walk dog", "isComplete":true }
Select Send.
Examine the GET endpoints
The sample app implements several GET endpoints using calls to MapGet
:
API | Description | Request body | Response body |
---|---|---|---|
GET / |
Browser test, "Hello World" | None | Hello World! |
GET /todoitems |
Get all to-do items | None | Array of to-do items |
GET /todoitems/complete |
Get all completed to-do items | None | Array of to-do items |
GET /todoitems/{id} |
Get an item by ID | None | To-do item |
app.MapGet("/", () => "Hello World!");
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
Test the GET endpoints
Test the app by calling the two endpoints from a browser or Postman. For example:
GET https://localhost:5001/todoitems
GET https://localhost:5001/todoitems/1
The call to GET /todoitems
produces a response similar to the following:
[
{
"id": 1,
"name": "Item1",
"isComplete": false
}
]
Test the GET endpoints with Postman
- Create a new HTTP request.
- Set the HTTP method to GET.
- Set the request URI to
https://localhost:<port>/todoitems
. For example,https://localhost:5001/todoitems
. - Select Send.
This app uses an in-memory database. If the app is restarted, the GET request doesn't return any data. If no data is returned, first POST data to the app.
Return values
ASP.NET Core automatically serializes the object to JSON and writes the JSON into the body of the response message. The response code for this return type is 200 OK, assuming there are no unhandled exceptions. Unhandled exceptions are translated into 5xx errors.
The return types can represent a wide range of HTTP status codes. For example, GET /todoitems/{id}
can return two different status values:
- If no item matches the requested ID, the method returns a 404 status NotFound error code.
- Otherwise, the method returns 200 with a JSON response body. Returning
item
results in an HTTP 200 response.
Examine the PUT endpoint
The sample app implements a single PUT endpoint using MapPut
:
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
This method is similar to the MapPost
method, except it uses HTTP PUT. A successful response returns 204 (No Content). According to the HTTP specification, a PUT request requires the client to send the entire updated entity, not just the changes. To support partial updates, use HTTP PATCH.
Test the PUT endpoint
This sample uses an in-memory database that must be initialized each time the app is started. There must be an item in the database before you make a PUT call. Call GET to ensure there's an item in the database before making a PUT call.
Update the to-do item that has Id = 1 and set its name to "feed fish"
:
{
"id": 1,
"name": "feed fish",
"isComplete": false
}
Examine the DELETE endpoint
The sample app implements a single DELETE endpoint using MapDelete
:
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
Use Postman to delete a to-do item:
- Set the method to
DELETE
. - Set the URI of the object to delete (for example
https://localhost:5001/todoitems/1
). - Select Send.
Prevent over-posting
Currently the sample app exposes the entire Todo
object. Production apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. DTO is used in this article.
A DTO may be used to:
- Prevent over-posting.
- Hide properties that clients are not supposed to view.
- Omit some properties in order to reduce payload size.
- Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
To demonstrate the DTO approach, update the Todo
class to include a secret field:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
Verify you can post and get the secret field.
Create a DTO model:
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
Update the code to use TodoItemDTO
:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
app.MapPost("/todoitems", async (TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int id, TodoItemDTO todoItemDTO, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}
Verify you can't post or get the secret field.
Differences between minimal APIs and APIs with controllers
- No support for filters: For example, no support for IAsyncAuthorizationFilter, IAsyncActionFilter, IAsyncExceptionFilter, IAsyncResultFilter, and 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. We plan to add support for
IFormFile
in the future.
- No support for binding from forms. This includes binding IFormFile. We plan to add support for
- 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.
Use JsonOptions
The following code uses JsonOptions:
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.MapGet("/", () => new Todo { Name = "Walk dog", IsComplete = false });
app.Run();
class Todo
{
// These are public fields instead of properties.
public string? Name;
public bool IsComplete;
}
The following code uses JsonSerializerOptions:
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
app.MapGet("/", () => Results.Json(new Todo {
Name = "Walk dog", IsComplete = false }, options));
app.Run();
class Todo
{
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
The preceding code uses web defaults, which converts property names to camel case.
Troubleshooting with the completed sample
If you run into a problem you can't resolve, compare your code to the completed project. View or download completed project (how to download).
Test minimal API
For an example of testing a minimal API app, see this GitHub sample.
Publish to Azure
For information on deploying to Azure, see Quickstart: Deploy an ASP.NET web app.
Additional resources
ASP.NET Core
Feedback
Submit and view feedback for