An Intro to Minimal APIs with ASP.NET Core and .NET 8.0
I recently had a few issues working with Minimal APIs and .NET 8.0.
In typical fashion, I created an empty project, adding in the various parts until I found the problem.
This post shows you how to create a bare bones CRUD Minimal API project. The functionality is simple:
- Add a person to a list
- Search for a person in the list by name
- Update a person by id
- Delete a person from the list
Out of the box .NET 8.0 http status codes are returned when an operation is successful or fails.
There are no 3rd party dependencies.
For simplicity, data is stored in a List<T>.
The solution is, well, very minimal.
~
What are Minimal APIs
Minimal APIs take a simplified approach to building web APIs in using .NET.
They were introduced in ASP.NET Core 6.0, and let you create lightweight HTTP services with less boilerplate code compared to traditional approaches.
With minimal APIs, you can define endpoints and their corresponding request-handling logic using a succinct syntax. This is often in a single Program.cs file.
Benefits of Minimal APIs
Instead of setting up controllers and routes as in traditional ASP.NET Core applications, minimal APIs use lambda expressions or delegate-based syntax to define endpoints directly within the application’s entry point. This results in more concise code and faster development cycles, especially for simple APIs or microservices.
Why Use Minimal APIs
Minimal APIs are suitable for scenarios where you need to quickly create small, focused APIs without the overhead of a full MVC framework.
For more complex scenarios however, traditional MVC controllers and routing might still be preferable due to their extensibility and organization capabilities.
~
Using Minimal APIs, an Example
In this example you will see how to create a simple CRUD Minimal API. It will let you create, search, update, and delete a person.
One thing I like about Minimal APIs is absolutely ZERO ceremony or any additional classes in the solution just to spin up the API.
All API code resides within a single file Program.cs.
PersonDTO and SearchDTO
We need a DTO class to represent a person and another to represent search criteria. You can see these here:
PersonDto
This represents a person and is used to during API operations that involve: creating and updating.
public class PersonDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTime? Dob { get; set; }
public PersonDto(PersonModel personModel)
{
Id = personModel.Id;
Name = personModel.Name;
Dob = personModel.Dob;
}
[JsonConstructor]
public PersonDto(string? name, DateTime? dob)
{
this.Name = name;
this.Dob = dob;
}
}
Two constructors are included. One to convert a DTO to Model.
Another with the JsonConstructor attribute.
Read more about the System.Text.Json namespace here.
This attribute lets the minimal api serializer which constructor to use.
Failure to set this attribute will result in the following error:
“System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MinimalPlayground.Request.PersonDto'. Path: $ | LineNumber: 0 | BytePositionInLine: 1. ---> System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'MinimalPlayground.Request.PersonDto'.”
SearchDto
This supports searching for a person and used by a get operation:
public class SearchDto
{
public string? Name { get; set; }
public DateTime? Dob { get; set; }
}
~
Create a Person
The following code shows how we create a person:
// create a person
app.MapPost("/api/person", async (PersonDto request) =>
{
var personModel = new PersonModel
{
Id = Guid.NewGuid(),
Name = request.Name,
Dob = request.Dob
};
_persons.Add(personModel);
return Results.Created($"/person/{personModel.Id}", new PersonDto(personModel));
});
Search for a Person
The following code shows how we search for a person:
// get a person by criteria
app.MapGet("/api/person", async ([AsParameters] SearchDto searchDto) =>
{
var person = _persons.Where(p => p.Name.Contains(searchDto.Name)).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
return Results.Ok(new PersonDto(person));
});
You see in the above we have an additional attribute AsParameters. This lets use a custom type with properties for databinding values in the request (rather than specify each parameter in the minimal api endpoint.
Update a Person
The following code shows how we update a person:
// update a person
app.MapPut("/api/person/{id}", async (Guid id, PersonDto request) =>
{
var person = _persons.Where(p => p.Id == id).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
person.Name = request.Name;
person.Dob = request.Dob;
return Results.Ok(new PersonDto(person));
});
Delete a Person
The following code shows how we create a person:
// delete a person
app.MapDelete("/api/person/{id}", async (Guid id) =>
{
var person = _persons.Where(p => p.Id == id).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
_persons.Remove(person);
return Results.NoContent();
});
~
Entire Code Listing
The entire code listing is detailed is included for reference:
public class Program
{
private static List<PersonModel> _persons = new List<PersonModel>();
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// create a person
app.MapPost("/api/person", async (PersonDto request) =>
{
var personModel = new PersonModel
{
Id = Guid.NewGuid(),
Name = request.Name,
Dob = request.Dob
};
_persons.Add(personModel);
return Results.Created($"/person/{personModel.Id}", new PersonDto(personModel));
});
// get a person by criteria
app.MapGet("/api/person", async ([AsParameters] SearchDto searchDto) =>
{
var person = _persons.Where(p => p.Name.Contains(searchDto.Name)).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
return Results.Ok(new PersonDto(person));
});
// update a person
app.MapPut("/api/person/{id}", async (Guid id, PersonDto request) =>
{
var person = _persons.Where(p => p.Id == id).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
person.Name = request.Name;
person.Dob = request.Dob;
return Results.Ok(new PersonDto(person));
});
// delete a person
app.MapDelete("/api/person/{id}", async (Guid id) =>
{
var person = _persons.Where(p => p.Id == id).FirstOrDefault();
if (person == null)
{
return Results.NotFound();
}
_persons.Remove(person);
return Results.NoContent();
});
app.Run();
}
}
~
Testing
You can see how each of the code performs in Postman:
Adding a Person
Creating a person by posting JSON in the request body. A http status code of 201 is returned:

Searching for a Person
Searching for a person by name. The data is returned and the http status code of 200 is set:

Updating a Person
Updating a person record by supplying the person-id in the url and setting the content we want to update in the body as JSON in the request. An http status code of 200 is returned:

Deleting a Person
Deleting a person record by supplying the person-id in the url. An http status code of 204 is returned:

That’s it!
~
Further Thoughts
I like the conciseness and clarity of Minimal APIs in .NET. In the real-world I would encapsulate all business logic in a service class that belonged to a class library project.
There is a trade-off though in that you must hand code a lot of the things you’d get out of the box with traditional Web API controllers.
At the time of writing, the following is still not available in minimal APIs:
- No built-in support for model binding (IModelBinderProvider, IModelBinder). Support can be added with a custom binding shim.
- No built-in support for validation (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
I can see me implementing a payments micro-service using minimal APIs.
This may service a web application and handle the Stripe payment integrations. Again, there is a trade off in that you can end up with multiple microservices to maintain.
Often, Azure App Service and a regular web API with multiple endpoints is a reasonable solution.
~
Where to Learn More
You can learn more about Minimal APIs and .NET here.