A Minimal API with .NET 6 using C#

kenslearningcurve

Kenji Elzerman

Posted on November 24, 2022

A Minimal API with .NET 6 using C#

Writing APIs isn't really new. We are doing it for some time now. But with the new .NET and other updates, we have the .NET 6 minimal API. Creating an API with less code! In this article, I will walk you through the steps you need to take to create a minimal API using C# and .NET 6.

Introduction

In this article, I am going to show and explain the steps for creating and running a .NET 6 minimal API. But before we dive into the minimal API with .NET 6, let's look at how we create APIs. Especially the controllers, actions, and some configurations.

When you create and Web API with .NET 5, it will scaffold a folder with the name 'Controllers'. Within this folder are classes that contain actions. The combination of a controller and action makes the endpoint for the API. The action is a method that can do logical work, or call another service class that handles the logic. It looks something like this:

Image description

The Solution Explorer shows the folder ‘Controllers’, which has the file WeatherForecastController.cs. That file contains a class with the same name and actions. It also contains configurations for the API and route.

There is also a Startup.cs file, which contains the configurations for the project. For example, dependency injection, Entity Framework, Swagger, and much more.

This is how we have been doing it for a pretty long time. Now, Microsoft has introduced the Minimal API.

Minimal API Explained

A minimal API basically removes the Startup.cs and places that code in program.cs. Controllers, with the actions, are removed and placed in the program.cs as mappings. It allows us to keep the coding for the API at a bare minimum.

It uses Lambda expressions for each API call. You can configure a route and a request type. It looks something like this:

app.MapGet("/movies", () => new List<string>() { "Shrek", "The Matrix", "Inception"});
Enter fullscreen mode Exit fullscreen mode

The app is defined in the startup.cs, which is the WebApplication.MapGet is a method that ‘maps’ the route to the lambda expression. In the above example, I want to map the route https://localhost:12345/movies and make it return a list of movie titles.

Besides MapGet there are also MapPost, MapDelete, and MapPut. These represent the attribute HTTPGet, HTTPPost, HTTPDelete, and HTTPPut we used on the actions in the .NET 5 APIs.

That’s the basic idea. Let’s jump into an example!

Create a Minimal API

Since we now know the basic story behind a minimal API, let’s create one and see how it works. In the following chapters, I will show you the most common tasks developers need to make. A simple mapping, a mapping with async, and the use of dependency injections.

Let’s create a new project with Visual Studio 2022 and select the ASP.NET Core Web API template. Give it a good project name. I will call mine “MinimalApiExample” and the solution name is the same.

The additional information screen needs a bit of attention.

Image description

Every project template in Visual Studio 2022 has its own additional information. Some options are the same, like the framework. Most of the options you see here are pretty common, but if you look closely, you will see two new checkboxes:

  • Use controllers (uncheck to use minimal APIs)
  • Do not use top-level statements

For this article, I don’t care about those top-level statements. But I do care about the first one. This is checked by default and it will scaffold the controllers and actions, as we are used to in the ‘old’ way. If you uncheck, Visual Studio will not create those controllers and that’s exactly what we want. Let’s uncheck it and press the **Create **button.

When it is all done with loading and creating the basic files, you might notice the project’s structure. We are missing the folder ‘Controllers’, which is just what we want.
If you open the program.cs, you might notice some differences as well. There is no app.MapControllers() and all of a sudden, there is an app.MapGet(…). Welcome to minimal API!

Mapping the Endpoints

With minimal APIs, you don’t need to create new controllers with actions. It’s all done with lambda expressions. By mapping your endpoints, you can tell the API what addresses the users/clients can enter. Let’s take a look at the example in the program.cs:

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");
Enter fullscreen mode Exit fullscreen mode

This map is for an (HTTP)GET request. The endpoint is (https://localhost:12345)/weatherforecast. The return value of the GET method is a range of weather forecasts in JSON. Pretty simple, right?

Let’s remove all the example data, like the mapping, the variable summaries, and the internal record WeatherForecast(). Make sure you don’t accidentally remove the app.Run(), because it’s a bit sneaky between the example data.

Let’s create a new object, called Movie. Place this object in a new file or at the bottom of the program.cs. Then create a new variable that contains a list of movies. Note that the variable movies need to be declared and filled in before you can use them. I recommend placing it under the Swagger initiation. It can look like this:

List<movie> movies = new()
{
    new() { Id = 1, Rating = 5, Title = "Shrek" },
    new() { Id = 2, Rating = 1, Title = "Inception" },
    new() { Id = 3, Rating = 3, Title = "Jaws" },
    new() { Id = 4, Rating = 1, Title = "The Green Latern" },
    new() { Id = 5, Rating = 5, Title = "The Matrix" },
};

public class Movie
{
    public int Id { get; set; } 
    public string Title { get; set; }
    public int Rating { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Mapping GET

Mapping a GET request isn’t that hard to do. We just removed the example. Let’s create a new mapping that returns all the movies:

app.MapGet("/api/movies/", () =>
{
    return Results.Ok(movies);
});
Enter fullscreen mode Exit fullscreen mode

This will return the complete list of movies when the endpoint (https://localhost:1234)/api/movies are being called.

The MapGet indicates you want to create a GET endpoint. Using POST to this endpoint would not work.

If you want to get a specific movie by id, or some other parameter, you simply add it to the endpoint while using the MapGet.

app.MapGet("/api/movies/", () =>
{
    return Results.Ok(movies);
});

app.MapGet("/api/movies/{id:int}", (int id) =>
{
    return Results.Ok(movies.Single(x => x.Id == id));
});
Enter fullscreen mode Exit fullscreen mode

By adding {id:int} in the endpoint, you tell the API a number can be expected. Then, you add the variable declaration next and you can use it in the body.
You can also declare the id variable without the type, like this:

app.MapGet("/api/movies/{id:int}", (id) =>
{
    return Results.Ok(movies.Single(x => x.Id == id));
});
Enter fullscreen mode Exit fullscreen mode

But that doesn’t work, because then it’s assumed the first parameter is the HttpContext, not the parameter you are trying to use. Using the HttpContext is a great way to manage your own response to any request. But that is not what we are going for here.

Mapping POST

Another often-used request method is POST. It allows the client to send data to the API, where it can be consumed and handled. Creating a POST with minimal API isn’t that much different than a GET, except you need to reference an object that will catch the posted data from the client.

In the .NET 5 way, we are used to just declaring a type in the parameters of the action. It still works kinda the same. Take a look at the code below:

app.MapPost("/api/movies/", (Movie movie) =>
{
    movies.Add(movie);

    return Results.Ok(movies);
});
Enter fullscreen mode Exit fullscreen mode

Again, I use the app to map a new endpoint. In this case, I use MapPost, since I want to be able to post data to the API. Then, I add the Movie movie to the parameter list. This causes the API to map the incoming data from the body to the Movie object.

In the body, I add the new movie to the movies list. For demonstration purposes, I return the list of movies, including the newly added movie.

Mapping DELETE

The last mapping I want to show you is the DELETE request, or delete. The other request types are basically the same as GET, POST, or DEL.

Deletion needs a key, or something unique. Otherwise, you might be ending up deleting too much than you actually want. So we need a query parameter, like the id I showed in the MapGet-method.

app.MapDelete("/api/movies/{id:int}", (int id) =>
{
    movies.Remove(movies.Single(x => x.Id == id));

    return movies;
});
Enter fullscreen mode Exit fullscreen mode

It looks pretty much the same as the MapGet construction, except the MapDelete is used. Notice that I didn’t use the Results.Ok(…). It is not needed. Returning the movies like this will result in an HTTP 200 code, which is good.

If you test the API and want to delete something, you add the id of the item to delete in the URL. But if you send a GET request instead of a DELETE request, the API will return the movie that has that given id and not delete it.

Minimal API and Dependency Injection

You might want to use dependency injection in your API, which is a common practice. When using controllers, you inject the interfaces via the constructor. But we don’t have a constructor, we only have mappings with lambda expressions.

For this part, I created a small class with an interface. I also moved the list of movies to the new class:

public interface IMovies
{
    List<Movie> GetAll();
    Movie GetById(int id);
    void Delete(int id);
    void Insert(Movie movie);
}

public class Movies: IMovies
{
    private List<Movie> _movies = new()
    {
        new() { Id = 1, Rating = 5, Title = "Shrek" },
        new() { Id = 2, Rating = 1, Title = "Inception" },
        new() { Id = 3, Rating = 3, Title = "Jaws" },
        new() { Id = 4, Rating = 1, Title = "The Green Latern" },
        new() { Id = 5, Rating = 5, Title = "The Matrix" },
    };

    public void Delete(int id)
    {
        _movies.Remove(_movies.Single(x => x.Id == id));
    }

    public List<Movie> GetAll()
    {
        return _movies;
    }

    public Movie GetById(int id)
    {
        return _movies.Single(x => x.Id == id);
    }

    public void Insert(Movie movie)
    {
        _movies.Add(movie);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can configure the interface and the implementation class. In the program.cs, you can add your dependency injections by using the builder.Services. To add the IMovies and Movies, simply use the following line:

// Configuration for dependency injection
builder.Services.AddScoped<IMovies, Movies>();
Enter fullscreen mode Exit fullscreen mode

Place this line after builder.Services.AddSwaggerGen(). We have configured it, let’s use it! The MapGet is really easy:

app.MapGet("/api/movies/", (IMovies movies) =>
{
    return Results.Ok(movies.GetAll());
})
Enter fullscreen mode Exit fullscreen mode

Simply add the interface and a variable name to the parameter list of the mapping.

Okay, let’s take a look at the second get, the one with the id in the URL.

app.MapGet("/api/movies/{id:int}", (int id, IMovies movies) =>
{
    return Results.Ok(movies.GetById(id));
});
Enter fullscreen mode Exit fullscreen mode

Yup, that’s it! But… What happens if we switch the id and movies? Nothing! Well, something happens. It works just like the above example. .NET recognizes the types and the naming convention helps too. But if you want my advice: Keep a consistent order in your parameter list. I personally like to add the query parameters before the injections.

Let’s fix the last two mappings with DI:

app.MapPost("/api/movies/", (Movie movie, IMovies movies) =>
{
    movies.Insert(movie);

    return Results.Ok(movies.GetAll());
});

app.MapDelete("/api/movies/{id:int}", (int id, IMovies movies) =>
{
    movies.Delete(id);

    return movies.GetAll();
});
Enter fullscreen mode Exit fullscreen mode

Making it ASYNC

Most of our actions are made async to make the API handle multiple requests and make it work faster. The examples I showed you aren’t async. To make a mapping async isn’t that hard.

For this part, I made the methods in the class Movies async. It’s not the best example, but it’s about the API, not the logic in some basic example class.

public interface IMovies
{
    Task<List<Movie>> GetAll();
    Task<Movie> GetById(int id);
    Task Delete(int id);
    Task Insert(Movie movie);
}

public class Movies: IMovies
{
    private List<Movie> _movies = new()
    {
        new() { Id = 1, Rating = 5, Title = "Shrek" },
        new() { Id = 2, Rating = 1, Title = "Inception" },
        new() { Id = 3, Rating = 3, Title = "Jaws" },
        new() { Id = 4, Rating = 1, Title = "The Green Latern" },
        new() { Id = 5, Rating = 5, Title = "The Matrix" },
    };

    public async Task Delete(int id)
    {
        _movies.Remove(_movies.Single(x => x.Id == id));
    }

    public async Task<List<Movie>> GetAll()
    {
        return _movies;
    }

    public async Task<Movie> GetById(int id)
    {
        return _movies.Single(x => x.Id == id);
    }

    public async Task Insert(Movie movie)
    {
        _movies.Add(movie);
    }
}
Enter fullscreen mode Exit fullscreen mode

All we have to do now is make the mappings async. Which isn’t hard, just like in the previous chapters. Let’s start with MapGet again.

app.MapGet("/api/movies/", async (IMovies movies) =>
{
    return Results.Ok(await movies.GetAll());
});
Enter fullscreen mode Exit fullscreen mode

See? Not that hard. I marked the lambda expression as async and added await in front of the await movies.GetAll(), which is awaitable.

I could show you the other mappings too, but it’s all the same and I think you get the idea.

Conclusion

In the end my program.cs looks like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddScoped<IMovies, Movies>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/api/movies/", async (IMovies movies) =>
{
    return Results.Ok(await movies.GetAll());
});

app.MapGet("/api/movies/{id:int}", async (int id, IMovies movies) =>
{
    return Results.Ok(await movies.GetById(id));
});

app.MapPost("/api/movies/", async (Movie movie, IMovies movies) =>
{
    movies.Insert(movie);

    return Results.Ok(await movies.GetAll());
});

app.MapDelete("/api/movies/{id:int}", async (int id, IMovies movies) =>
{
    await movies.Delete(id);

    return await movies.GetAll();
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

I don’t have a folder with controllers that contain actions. It’s pretty flat, just the way we like it.

This is a great way if you have smaller APIs with not so many endpoints. It starts to get ugly when you have a lot of endpoints. You could get lost in all the mappings. The minimal API with .NET is better when you have a small project. If you have bigger projects you could switch to the good old controllers.

💖 💪 🙅 🚩
kenslearningcurve
Kenji Elzerman

Posted on November 24, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

A Minimal API with .NET 6 using C#
programming A Minimal API with .NET 6 using C#

November 24, 2022