How .NET 6 Minimal APIs has evolved ASP.NET Core

.NET 6 Minimal APIs has allowed .NET to evolve so we can reduce a large amount of boilerplate code.

To demonstrate this, we are going to take a small front-end Web API with a couple of endpoints. The front-end API has been built using .NET Core 2.1, and we want to upgrade it to .NET 6.

How much of the application will become ‘dead code’ that we can remove?

The scenario

A corporate company has a small blog where they post the latest company news for their employees.

Employees are invited to give their reaction by posting comments against each post.

The blog is powered using an ASP.NET Core Web API. It has been written using ASP.NET Core 2.1, and wishes to be upgraded to ASP.NET Core 6.

The API only has a couple of endpoints, which are:

Description Endpoint HTTP verb
Read all posts /api/post GET
Read post by slug /api/post/{slug} GET
Read comments by post ID /api/comment/{postId} GET
Create comment for post /api/comment POST

Given this scenario, this is a perfect candidate for using one of .NET 6’s new feature, Minimal APIs.

What are Minimal APIs?

Minimal APIs allows the ability to add simple API endpoints in an application’s configuration file. As a result, this means that we don’t have to go down the MVC route to create API endpoints.

Going down the MVC route has many benefits. However, it does have the drawback of having to create a lot of boilerplate code, which can be a little bit over the top, particularly if we only have a small number of endpoints.

And one of the benefits with Minimal APIs is the fact that it has support for dependency injection. So, we can pass in each of our services as parameters.

However, we need to be careful when using Minimal APIs to ensure that we aren’t using it too often. Minimal APIs are good for a couple of endpoints, but could get confusing if we have multiple endpoints in the same configuration file.

The Web API we wish to upgrade

As stated, it’s an ASP.NET Core Web API that we wish to upgrade from version 2.1 to 6.

We have a couple of entities that represent a post and a comment. The comment links up to the post through the PostId in a one-to-many relationship.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Post.cs
using System;
namespace RoundTheCode.FrontEndPostApi.Entites
{
    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Article {  get; set; }
        public string Slug {  get; set; }
        public DateTime Published {  get; set; }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Comment.cs
using System;
namespace RoundTheCode.FrontEndPostApi.Entites
{
    public class Comment
    {
        public int Id { get; set; }
        public int PostId {  get; set; }
        public string Message {  get; set; }   
        public DateTime Created {  get; set; } 
    }
}

Next, we have a CreateComment class. This represents the properties that will be requested through the API when we go to create a comment against a post. In this instance, it’s the post ID and the comment’s message.

1
2
3
4
5
6
7
8
9
10
// CreateComment.cs
namespace RoundTheCode.FrontEndPostApi.Models
{
    public class CreateComment
    {
        public int PostId {  get; set; }   
        public string Message { get; set; }
    }
}

Afterwards, we have the controllers set up for our Web API. These consist of a post controller and a comment controller.

The post controller has the ability to read all the posts, or to read a post by their slug.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// PostController.cs
using Microsoft.AspNetCore.Mvc;
using RoundTheCode.FrontEndPostApi.Entites;
using RoundTheCode.FrontEndPostApi.Extensions;
using RoundTheCode.FrontEndPostApi.Services;
using System.Collections.Generic;
namespace RoundTheCode.FrontEndPostApi.Controllers
{
    [ApiController]
    [Route("api/post")]
    public class PostController : ControllerBase
    {
        protected IPostService _postService;
        public PostController(IPostService postService)
        {
            _postService = postService.NotNull();
        }
        [HttpGet]
        public virtual IList<Post> ReadAll()
        {
            return _postService.ReadAll();
        }
        [HttpGet("{slug}")]
        public virtual ActionResult<Post> ReadBySlug(string slug)
        {
            var post = _postService.ReadBySlug(slug);
            if (post == null)
            {
                return NotFound();
            }
            return post;
        }
    }
}

With the comments controller, there is the ability to read all comments by a post, and to create a comment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// CommentController.cs
using Microsoft.AspNetCore.Mvc;
using RoundTheCode.FrontEndPostApi.Entites;
using RoundTheCode.FrontEndPostApi.Extensions;
using RoundTheCode.FrontEndPostApi.Models;
using RoundTheCode.FrontEndPostApi.Services;
using System.Collections.Generic;
namespace RoundTheCode.FrontEndPostApi.Controllers
{
    [ApiController]
    [Route("api/comment")]
    public class CommentController : ControllerBase
    {
        protected IPostService _postService;
        protected ICommentService _commentService;
        public CommentController(IPostService postService, ICommentService commentService)
        {
            _postService = postService.NotNull();
            _commentService = commentService.NotNull();
        }
        [HttpGet("{postId}")]
        public virtual ActionResult<IList<Comment>> ReadAllByPost(int postId)
        {
            var post = _postService.ReadById(postId);
            if (post == null)
            {
                return NotFound();
            }
            return Ok(_commentService.ReadAllByPost(postId));
        }
        [HttpPost]
        public virtual ActionResult<Comment> Create(CreateComment createComment)
        {
            var post = _postService.ReadById(createComment.PostId);
            if (post == null)
            {
                return NotFound();
            }
            return _commentService.Create(createComment);
        }
    }
}

Upgrading to .NET 6

The first thing we need to do is to upgrade the project to .NET 6. We can do that by going into the project’s .csproj file and changing the <TargetFramework> to net6.

In addition, we have Swagger documentation installed on the ASP.NET Core Web API, and we need to update that to the latest version. At present, this is version 6.2.3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- RoundTheCode.FrontEndPostApi.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
  </ItemGroup>
</Project>

Merging the Program and Startup classes

A change to .NET 6’s console template means that we don’t have to include the namespace, the Program class and the Main method.

That means we can remove these from our Program.cs file.

Next, we can take our current Startup.cs file and copy it into the Program.cs file.

From there, we need to make some amendments:

  • Create a new builder instance in our Program class by calling WebApplication. CreateBuilder(args);
  • Move everything from our ConfigureServices method in our Startup class so it’s called from our Services property in our builder instance.
  • Build our builder instance, and store it in an app instance.
  • Move everything from our Configure method in our Startup class so it’s called from our app instance.
  • Call app.Run(); to run the application.

The code will look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RoundTheCode.FrontEndPostApi.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
builder.Services.AddSingleton<IPostService, PostService>();
builder.Services.AddSingleton<ICommentService, CommentService>();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (builder.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseHsts();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    c.RoutePrefix = string.Empty;
});
app.UseHttpsRedirection();
app.Run();

Moving from MVC to Minimal APIs

Next, we want to move our API endpoints from our controllers into Minimal APIs.

For this, we are going to use the MapGet and MapPost methods in our app instance. The app instance also has MapPut and MapDelete if we wanted to create update and delete endpoints, but in this instance, we are not going to use them.

In addition, Minimal APIs has support for dependency injection, so we can pass these in as parameters into the method.

Using our existing API endpoints, this is how it will look when we start to use Minimal APIs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Program.cs
...
app.MapGet("api/comment/{postId}", (IPostService postService, ICommentService commentService, int postId) =>
{
    var post = postService.ReadById(postId);
    if (post == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(commentService.ReadAllByPost(postId));
});
app.MapPost("api/comment", (IPostService postService, ICommentService commentService, CreateComment createComment) =>
{
    var post = postService.ReadById(createComment.PostId);
    if (post == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(commentService.Create(createComment));
});
// Posts API
app.MapGet("api/post", (IPostService postService) =>
{
    return postService.ReadAll();
});
app.MapGet("api/post/{slug}", (IPostService postService, string slug) =>
{
    var post = postService.ReadBySlug(slug);
    if (post == null)
    {
        return Results.NotFound();
    }
    return Results.Ok(post);
});
app.Run();

Tidying up the Program class

Finally, we need to tidy up the Program class. We can remove the AddMvc method as our endpoints are now using Minimal APIs.

1
2
3
4
// Program.cs
// This line can be removed.
builder.Services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

However, we will now find that there are no API endpoints in our Swagger documentation when we do this.

In-order to resolve this, we need to add the following line just before the AddSwaggerGen method:

1
2
3
4
5
6
7
8
// Program.cs
...
builder.Services.AddEndpointsApiExplorer(); // Need to add this in for it to appear in Swagger
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "Post API", Version = "v1" });
});
...

Removing dead code

That’s all our changes needed in our Program class. We can now go ahead and delete the following files:

  • Delete the Startup class.
  • Delete both the PostController and CommentController class.

That now leaves us with our entities, our models, our services and the Program class. That’s it!

Thoughts on Minimal APIs

The clue is in the title. Minimal APIs is a good lightweight for a small number of endpoints with simple functionality.

Is it ideal for every API? Not in our opinion. Adding API endpoints in the Program class will make it a very large file. We could extend it out to other files, but then we might as well use the MVC route and get the extra functionality.

And the limited functionality options available means that more complicated endpoints will be harder to convert.

But it does offer a new way of creating API endpoints and reduces a lot of boiler code.

Posted in Hosting Article.