Software Architect / Microsoft MVP (AI) and Technical Author

ASP.NET Core, C#, Minimal APIs

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.

JOIN MY EXCLUSIVE EMAIL LIST
Get the latest content and code from the blog posts!
I respect your privacy. No spam. Ever.

Leave a Reply