Minimal APIs in .NET 6 — A Complete Guide(Beginners + Advanced)
With Real-world Example
.NET 6 was released last week and it’s being branded as “The Fastest .NET” yet by Microsoft. It comes with a lot of exciting new features, language and performance improvements. It’s the first LTS release since .NET Core 3.1 and will be supported for three years.
In this article, we will cover the following topics:
- What are the Minimal APIs?
- Creating a real-world example (with SqlServer DB and EFCore)
- How to add OpenAPI Specifications using Swagger
- How to Secure Minimal APIs using JWT Authentication
What are the Minimal APIs?
.NET6 has made it extremely simple to write REST APIs with minimal dependencies. At first glance, it seems that Minimal APIs are Microsoft's answer to NodeJS (with ExpressJS) HTTP server that provides minimal API.
Consider the following code:
var express = require("express");var app = express();
app.listen(3000, () => {console.log("Server running on port 3000");});app.get("/platforms", (req, res, next) => {res.json(["Windows", "Mac", "Linux", "Unix"]);});
This is a pretty standard API using ExpressJS that provides a GET endpoint to access all available platforms using a NodeJS server.
Now, Let’s see how the code will look like using Minimal APIs in .NET 6
var builder = WebApplication.CreateBuilder(args);var app = builder.Build();app.MapGet(“/platforms”, () => “Windows,Mac,Linux,Unix”);app.Run();
That’s it! It’s minimalism at its best — no more Startup.cs, API Controllers, Extra dependencies, etc. You just need these 4 lines of code to produce the following output:
Without further adieu, let’s get started:
Prerequisites:
- Download .NET 6 SDK and ASP.NET Core Runtime (with hosting bundle for IIS)
- Visual Studio 2022 (Unfortunately .NET 6 is not supported with Visual Studio 2019)
1. Create a new Project
Create a new project using Visual Studio 2022 and select the “ASP.NET Core Empty” project template.
Go with the defaults (make sure that the framework is set to .NET 6.0)
You can see Program.cs file in your ASP.NET Core project, containing the hello world API.
You might be wondering what happened to standard using statements?
This is another great new feature of .NET 6 — “Implicit Global Usings” that automatically generates invisible using statements and declares them globally so you don’t have to deal with the clutter of declaring namespaces repeatedly in every single file.
Edit your .Csproj file and you will notice that there’s a new entry of ImplicitUsing which is set to true.
You may check obj/Debug/net6.0 folder to see the hidden auto-generated file — [ProjectName].GlobalUsings.g.cs. You can define a separate class to keep all your using statements in one place.
If you don’t want to use this feature you can disable the flag in your .csproj file.
Note: If you want to run your API on a specific port, you can specify it within the Run method.
app.Run(“http://localhost:3000");
You can even run your API on multiple ports.
app.MapGet(“/”, () => “Hello World!”);app.Urls.Add(“http://localhost:3000");app.Urls.Add(“http://localhost:4000");app.Run();
In the above examples, app.MapGet method is using an inline lambda expression. In many cases, you would want to use a static or instance method instead.
var builder = WebApplication.CreateBuilder(args);var app = builder.Build();app.MapGet(“/”, MyHandler.Hello);app.Run();class MyHandler{public static string Hello(){ return “Hello World!”; }}
What about POST, PUT, Delete, etc?
Web application builder has separate methods for those requests.
- app.MapPost()
- app.MapPut()
- app.MapDelete()
Minimal APIs in .NET 6 — A Real World Example
In the following example, we will use the Azure SQL DB to connect with our APIs and it will provide the following CRUD functionalities:
- Get All Books
- Add New Book
- Search book by Keyword
- Update book Titles by ID
In addition to that, we will also learn how to add OpenAPISpeficiation — SwaggerUI to create documentation and also some advanced concepts like adding Authentication and Authorization using JWT Tokens.
- Let’s create a simple DB and define tables to hold books and author information.
Note: You may use this script to create the above tables and populate them with some sample data.
2. Create a new Project
Install following packages:
Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets -Version 1.2.1Install-Package Azure.Identityinstall-package Microsoft.EntityFrameworkCore
Now we need to add some POCO classes. Instead of adding it in the program.cs let’s create a new folder ‘Models’ to place all the model classes in our project, such as Book.cs
Now we need to initialize DBContext as below:
builder.Services.AddDbContext<BooksDB>(options =>{options.UseSqlServer(Environment.GetEnvironmentVariable(“AzureConnectionString”)); });
.NET6 has simplified a lot of tedious tasks by introducing WebApplicationBuilder which gives access to the new Configuration builder, Service Collection, Environment, Host (IHostBuilder), WebHost (IWebHostBuild), and provides a straightforward way to inject dependencies to the DI containers.
Minimal APIs Example# 1: Fetch all the books from the database
Let’s write our first endpoint to fetch all the available books from the database.
app.MapGet(“/books”, async (BooksDB db) =>await db.Books.ToListAsync());
In the above method, BooksDB is injected from services so we can use it inside our method to perform various DB operations.
We can now invoke the endpoint and see the response as a JSON array.
Before we add more endpoints, let’s see how Minimal APIs support open API specifications.
Minimal APIs in .NET6 supports generating swagger documents using Swashbuckle.
Here are the steps:
- Install the swashbuckler package for asp.net core
install-package Swashbuckle.AspNetCore
2. Inject the swagger services in your WebApplication builder.
builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();
3. Use Swagger in your application by adding the middleware to render the Swagger UI.
app.UseSwagger();app.UseSwaggerUI();
Build and run the application and you will be able to see the swagger UI on /swagger/index.html
Now, it’s time to document the API by annotating the API methods. We can do this using the old-fashioned way using the Attributes. However, with .NET6, you can use extension methods that come with minimal APIs.
app.MapGet(“/books”, async (BooksDB db) =>await db.Books.ToListAsync()).Produces<List<Book>>(StatusCodes.Status200OK).WithName(“GetAllBooks”).WithTags(“Getters”);
- Produces → defines the return type and expected status.
- WithName →uniquely identifies the endpoint.
- WithTags → group all the relevant endpoints.
If you want to exclude any method from the swagger description, you can do so by adding ExcludeFromDescription() extension method as shown below:
app.MapGet(“/”, () => “Hello! This is .NET 6 Minimal API Demo on Azure App Service”)
.ExcludeFromDescription();
Now we are done with Swagger stuff so let’s add the remaining methods to our API.
Minimal APIs Example#2: Add a new record to the database
To add a new entry to the database we will use the MapPost method along with explicit parameters binding.
// Add new book to Sql Server DBapp.MapPost(“/books”,async ([FromBody] Book addbook,[FromServices] BooksDB db, HttpResponse response) =>{db.Books.Add(addbook);await db.SaveChangesAsync();response.StatusCode = 200;response.Headers.Location = $”books/{addbook.BookID}”;}).Accepts<Book>(“application/json”).Produces<Book>(StatusCodes.Status201Created).WithName(“AddNewBook”).WithTags(“Setters”);
Here we used two Attributes to explicitly declare where parameters are bound from.
- For instance, [FromBody] attribute is used to specify that that JSON object will be passed as a parameter to our ModelBinder.
- [FromServices] is used to specify that this parameter DB will be injected from the services DI container.
As this is a POST method, we have to annotate this using an extension method — Accepts to specify the request body and content type.
Minimal APIs Example#3: Update an existing book title using ID
To update an existing record, we will use the MapPut method as below:
app.MapPut("/books",[AllowAnonymous] async (int bookID,string bookTitle, [FromServices] BooksDB db, HttpResponse response) =>{var mybook = db.Books.SingleOrDefault(s => s.BookID == bookID);if (mybook == null) return Results.NotFound();mybook.Title = bookTitle;await db.SaveChangesAsync();return Results.Created("/books",mybook);}).Produces<Book>(StatusCodes.Status201Created).Produces(StatusCodes.Status404NotFound).WithName("UpdateBook").WithTags("Setters");
You can see that instead of using JSON object, we used two parameters bookID and Title (to be updated). In the method implementation, we are simply fetching the record using bookID and if it is found, the book title is updated and returned to the response with 201 Created status.
We are using multiple extension methods to indicate that this could also respond with the 404 Status if the record wasn’t found.
Let’s try it out and execute the request by providing the required values and you will see that request is successfully completed.
Minimal APIs Example#4: Fetch a single record using ID
app.MapGet("/books/{id}", async (BooksDB db, int id) =>await db.Books.SingleOrDefaultAsync(s => s.BookID == id) is Book mybook ? Results.Ok(mybook) : Results.NotFound()).Produces<Book>(StatusCodes.Status200OK).WithName("GetBookbyID").WithTags("Getters");
In the above example, we are simply returning the JSON object if the record is found, otherwise, 404 will be returned.
Minimal APIs Example#5: Perform a search for a given keyword
app.MapGet(“/books/search/{query}”,(string query, BooksDB db) =>{var _selectedBooks = db.Books.Where(x => x.Title.ToLower().Contains(query.ToLower())).ToList();return _selectedBooks.Count>0? Results.Ok(_selectedBooks): Results.NotFound(Array.Empty<Book>());}).Produces<List<Book>>(StatusCodes.Status200OK).WithName(“Search”).WithTags(“Getters”);
This example returns the list of books if the title is matched with the given keyword.
Minimal APIs Example#6: Get Paginated Result set
app.MapGet(“/books_by_page”, async (int pageNumber,int pageSize, BooksDB db) =>await db.Books.Skip((pageNumber — 1) * pageSize).Take(pageSize).ToListAsync()//await db.Books.ToListAsync()).Produces<List<Book>>(StatusCodes.Status200OK).WithName(“GetBooksByPage”).WithTags(“Getters”);
This method takes two parameters — pageNumber and pageSize and returns the paginated results from the database.
e.g ‘/books_by_page?pageNumber=2&pageSize=5’ produces the following response.
In the next section, we will see how we can add authentication and authorization using JWT in minimal APIs.
Minimal APIs — Adding Authentication and Authorization using JWT
Make sure you have the following packages installed.
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Install-Package Microsoft.IdentityModel.Tokens
Now let’s create the following classes
public record UserDto(string UserName, string Password);public record UserModel{[Required]public string UserName { get; set; }[Required]public string Password { get; set; }}public interface IUserRepositoryService{UserDto GetUser(UserModel userModel);}public class UserRepositoryService : IUserRepositoryService{private List<UserDto> _users => new(){new(“admin”, “abc123”),};public UserDto GetUser(UserModel userModel){return _users.FirstOrDefault(x => string.Equals(x.UserName, userModel.UserName) && string.Equals(x.Password, userModel.Password));}}
In the above code, we have created UserDTO and model classes to mock our in-memory user store and a repository method to verify the credentials.
Next, we create classes for JWT token implementation and generation.
public interface ITokenService{string BuildToken(string key, string issuer,string audience, UserDto user);}public class TokenService : ITokenService{private TimeSpan ExpiryDuration = new TimeSpan(0, 30, 0);public string BuildToken(string key, string issuer,string audience, UserDto user){var claims = new[]{new Claim(ClaimTypes.Name, user.UserName),new Claim(ClaimTypes.NameIdentifier,Guid.NewGuid().ToString())};var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);var tokenDescriptor = new JwtSecurityToken(issuer, audience, claims,expires: DateTime.Now.Add(ExpiryDuration), signingCredentials: credentials);return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);}}
You can add those classes in a separate folder for clarity.
It’s time to inject our dependencies in WebApplicationBuilder Services
builder.Services.AddSingleton<TokenService>(new TokenService());builder.Services.AddSingleton<IUserRepositoryService>(new UserRepositoryService());
Now we need to configure Authentication and Authorization services.
builder.Services.AddAuthorization();builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt =>{opt.TokenValidationParameters = new(){ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ValidateIssuerSigningKey = true,ValidIssuer = builder.Configuration[“Jwt:Issuer”],ValidAudience = builder.Configuration[“Jwt:Audience”],IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration[“Jwt:Key”]))};});
Note: Make sure your appsettings.json contains the following key/value pairs
“Jwt”: {“Key”: “your-secret-key”,“Issuer”: "”,“Audience”: “"}
Finally, we can simply add the middleware for authentication and authorization
app.UseAuthentication();app.UseAuthorization();
Now we need to add API methods such as /login to verify the user’s credentials and issue the JWT token.
app.MapPost("/login", [AllowAnonymous] async ([FromBodyAttribute]UserModel userModel, TokenService tokenService, IUserRepositoryService userRepositoryService, HttpResponse response) => {var userDto = userRepositoryService.GetUser(userModel);
if (userDto == null)
{response.StatusCode = 401;return;}var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"], userDto);await response.WriteAsJsonAsync(new { token = token });return;}).Produces(StatusCodes.Status200OK).WithName("Login").WithTags("Accounts");
In this method, we are simply taking the JSON object of type UserModel that contains username and password, other parameters are being passed [FromServices] implicitly — It verifies the given credentials against in-memory store and issues the JWT token after successful verification. We also use the [AllowAnonymous] attribute to make sure this endpoint is accessible without a bearer token.
Let’s create a protected resource to make sure if it’s working as intended
app.MapGet(“/AuthorizedResource”, (Func<string>)([Authorize] () => “Action Succeeded”)).Produces(StatusCodes.Status200OK).WithName(“Authorized”).WithTags(“Accounts”).RequireAuthorization();
RequireAuthorization() extension method indicates that this method can’t be invoked without passing a JWT bearer token in the request header.
As we are using Swagger UI to execute and test our endpoints so we need to add a little tweak so we can store our JWT token in swagger and then continue executing the protected endpoints without having to deal with requests headers.
builder.Services.AddSwaggerGen(c =>{var securityScheme = new OpenApiSecurityScheme{Name = “JWT Authentication”,Description = “Enter JWT Bearer token **_only_**”,In = ParameterLocation.Header,Type = SecuritySchemeType.Http,Scheme = “bearer”, // must be lower caseBearerFormat = “JWT”,Reference = new OpenApiReference{Id = JwtBearerDefaults.AuthenticationScheme,Type = ReferenceType.SecurityScheme}};c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);c.AddSecurityRequirement(new OpenApiSecurityRequirement{{securityScheme, new string[] { }}});});
The above code will add the Authorize button into the swagger UI that can store JWT bearer token for subsequent requests.
Execute the login endpoint and provide user credentials (admin/abc123) — Copy and paste the token in the Auth window (as shown below) then click on Authorize.
You are now logged in and will be able to execute the authorized endpoints (Bearer token will be passed automatically by Swagger UI)
Conclusion
In this article, we have learned about minimal APIs in .NET 6 and also explored how to perform various CRUD and other database operations, generate Swagger-based Documentation and implement JWT based authentication and authorization.
You can download the complete source code from this repo and also see it in action.