{tocify} $title={Table of Contents}
Tutorial: Create a minimal web API with ASP.NET Core
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 web API with ASP.NET Core.
For a tutorial on creating a web 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 /
|
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
Create
a Web 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.
• 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 web 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 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 a web 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 Web application builder and a application 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.
• Select
the Browse tab, and verify that Include prerelease is checked.
• 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.Ok(todo);
}
return Results.NotFound();
});
app.Run();
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:
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 also 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 DB context 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.
Test
posting data
The
following instructions post data to the app:
• Create
a new 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/{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 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 Not found 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.Ok(todo);
}
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.Ok(new TodoItemDTO(todo));
}
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.
Use JsonOptions
The
following code uses Json Options:
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 Json Serializer Options:
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.
Test minimal API
For
an example of testing a minimal API app, see this GitHub sample.
Nice
ReplyDelete