.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 ourProgram
class by callingWebApplication. CreateBuilder(args);
- Move everything from our
ConfigureServices
method in ourStartup
class so it’s called from ourServices
property in ourbuilder
instance. - Build our
builder
instance, and store it in anapp
instance. - Move everything from our
Configure
method in ourStartup
class so it’s called from ourapp
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
andCommentController
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.