Back to: ASP.NET Core Web API Tutorials
Minimal API in ASP.NET Core
In this article, I will discuss How to Implement Minimal API in ASP.NET Core Application. Please read our previous article discussing Unit Testing in ASP.NET Core Web API. At the end of this article, you will understand the following pointers:
- What is the Minimal API in ASP.NET Core?
- Why Minimal APIs in ASP.NET Core?
- Why do we need to use Minimal APIs over the Traditional Approach in ASP.NET Core?
- How Do We Create a Minimal API in ASP.NET Core?
- Dependency Injection in ASP.NET Core Minimal APIs
- Request Validation in Minimal APIs
- Limitations of Minimal API in ASP.NET Core
- What are the differences between Minimal API and Controller-Based API?
What is the Minimal API in ASP.NET Core?
Minimal APIs in ASP.NET Core provide a lightweight approach to building HTTP APIs with minimal dependencies. They reduce the complexity of setting up new applications by minimizing the need for controllers and actions. Minimal APIs allow developers to define endpoints directly within the Program.cs file using a functional approach. They allow developers to quickly define routes and handle requests using a minimal amount of code. So, they were introduced to simplify the process of creating small, focused APIs without the overhead of controllers and traditional routing.
Why Minimal APIs in ASP.NET Core?
The primary advantages of using Minimal APIs in ASP.NET Core are simplicity and performance. Minimal APIs are ideal for microservices, small applications, or applications that primarily need to serve API endpoints without requiring the full features of ASP.NET Core MVC. The following are the Key Features of Minimal APIs:
- Simplified Routing and Endpoint Configuration: Endpoints are defined directly in the Program.cs file, reducing the need for separate controllers.
- Reduced Code: By minimizing the amount of code necessary to set up an API, development becomes faster and more straightforward.
- Support for Dependency Injection and Middleware: Despite their simplicity, Minimal APIs fully support dependency injection and can use middleware components.
Why do we need to use Minimal APIs over the Traditional Approach in ASP.NET Core?
We need to choose Minimal APIs over the traditional approach in ASP.NET Core because of the following reasons:
- Simplicity: Less code to write and maintain compared to MVC.
- Performance: Reduced overhead can lead to faster performance.
- Rapid Development: Quicker to set up and start developing.
- Flexibility: Ideal for microservices and small applications.
How to Create a Minimal API in ASP.NET Core?
Let’s create a Minimal API in ASP.NET Core that performs CRUD (Create, Read, Update, Delete) operations on an Employee model using hardcoded data.
Create a New ASP.NET Core Project
So, open Visual Studio and click on the Create a new project option, as shown in the image below.
Once you click on the Create a new project option, the following Create a new project window will open. Here, you need to select the “ASP.NET Core Web API” template, which uses C# as the programming language, and then click the Next button, as shown in the image below.
Once you click the Next button, the Configure your new Project window will open. Here, you need to specify the Project name (MinimalAPIDemo) and the location where you want to create the project. Finally, you need to click on the Next button, as shown in the image below.
Once you click on the Next button, the Additional Information window will open. Here, you need to select the Target .NET version and the authentication Types. Whether you want to configure HTTPS and enable Docker, etc., we will use .NET 8, so select .NET 8 as the Framework. We will not use any authentication now, so select the authentication type None. Then, apart from configuring HTTPS and Enabling OpenAPI support, please unselect the rest of the check.
Note: To develop Minimal API, please do not select the Use Controllers check box.
Once you click on the Create button, Visual Studio will create the ASP.NET Core Web API project with the following file and folder structure.
How to Implement Minimal API in ASP.NET Core:
In ASP.NET Core Minimal APIs, the MapGet, MapPost, MapPut, and MapDelete methods are used to define HTTP endpoints directly in the Program.cs file. Each method corresponds to an HTTP verb and serves to configure routing, handle requests, and send responses for different types of operations typical in RESTful APIs. Let us have a look at these methods:
- MapGet: Handles HTTP GET requests. This method is used to retrieve data. It’s typically used for reading operations where no modification of the data occurs. For instance, fetching a list of items or retrieving a single item by its identifier.
- MapPost: Handles HTTP POST requests. This method is used for creating new resources. POST requests typically receive data in the body of the request (like new item details), which is then used to create a new record or resource in the database or in-memory structure.
- MapPut: Handles HTTP PUT requests. This method is used to update an existing resource. PUT requests are idempotent, meaning multiple identical requests should have the same effect as a single one. They typically require an identifier to find the resource and a payload that describes the updated data.
- MapDelete: Handles HTTP DELETE requests. This method is used to delete resources. A DELETE request typically requires an identifier to find the resource that should be removed.
Note: Each of these methods (MapGet, MapPost, MapPut, MapDelete) corresponds to a specific HTTP method (GET, POST, PUT, DELETE). Handlers defined with these methods typically perform operations such as retrieving data, creating new resources, updating existing resources, or deleting resources based on the HTTP method and the data sent in the request.
Create the Employee Model
Right-click on the project and select Add > New Folder. Name it Models. Right-click on the Models folder, create a new class file named Employee.cs, and then copy and paste the following code.
namespace MinimalAPIDemo.Models { public class Employee { public int Id { get; set; } public string Name { get; set; } public string Position { get; set; } public decimal Salary { get; set; } } }
Modify Program.cs Class:
Next, we need to modify the program class as follows: We have hard-coded employee data, and then we perform the CRUD operations using the map method. The following code is self-explained, so please read the comment lines for a better understanding.
// Importing necessary namespaces and classes using MinimalAPIDemo.Models; // Creating a builder for the application var builder = WebApplication.CreateBuilder(args); // Add services to the DI container // Add API explorer for endpoint documentation builder.Services.AddEndpointsApiExplorer(); // Add Swagger for API documentation builder.Services.AddSwaggerGen(); // Build the application var app = builder.Build(); // Configure the HTTP request pipeline for the development environment if (app.Environment.IsDevelopment()) { // Use Swagger middleware to generate Swagger Documentation app.UseSwagger(); // Use Swagger UI middleware to interact with the Swagger documentation app.UseSwaggerUI(); } // Create an in-memory list to store Employee data var employeeList = new List<Employee> { new Employee { Id = 1, Name = "John Doe", Position = "Software Engineer", Salary = 60000 }, new Employee { Id = 2, Name = "Jane Smith", Position = "Project Manager", Salary = 80000 } }; // CRUD operations for Employee model // Endpoint to retrieve all employees // Map a GET request to /employees to return the employee list app.MapGet("/employees", () => employeeList); // Endpoint to retrieve a single employee by their ID app.MapGet("/employees/{id}", (int id) => { // Find the employee with the specified ID var employee = employeeList.FirstOrDefault(e => e.Id == id); // Return 200 OK if found, otherwise 404 Not Found return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // Endpoint to create a new employee app.MapPost("/employees", (Employee newEmployee) => { // Determine the next ID for the new employee newEmployee.Id = employeeList.Count > 0 ? employeeList.Max(emp => emp.Id) + 1 : 1; // Add the new employee to the list employeeList.Add(newEmployee); // Return a 201 Created response with the new employee return Results.Created($"/employees/{newEmployee.Id}", newEmployee); }); // Endpoint to update an existing employee app.MapPut("/employees/{id}", (int id, Employee updatedEmployee) => { // Find the employee by ID var employee = employeeList.FirstOrDefault(emp => emp.Id == id); if (employee is null) return Results.NotFound(); // If not found, return 404 // Update employee details employee.Name = updatedEmployee.Name; employee.Position = updatedEmployee.Position; employee.Salary = updatedEmployee.Salary; // Return the updated employee return Results.Ok(employee); }); // Endpoint to delete an employee app.MapDelete("/employees/{id}", (int id) => { // Find the employee by ID var employee = employeeList.FirstOrDefault(emp => emp.Id == id); if (employee is null) return Results.NotFound(); // If not found, return 404 // Remove the employee from the list employeeList.Remove(employee); // Return a 204 No Content response return Results.NoContent(); }); // Run the application app.Run();
Objectives of Endpoints
Let us understand the objective of each endpoint:
GET /employees:
This endpoint handles GET requests to the path /employees and returns a list of all employees stored in employeeList. It’s useful for clients who need to retrieve information about all employees at once. This endpoint is straightforward and does not involve any parameters, directly returning the entire list.
GET /employees/{id}:
This endpoint handles GET requests to /employees/{id}, where {id} is a dynamic parameter representing an employee’s ID. It searches the employeeList for an employee with the given ID and returns that employee if found. If no employee matches the ID, it returns a 404 Not Found response. This is useful for retrieving detailed information about a specific employee.
POST /employees:
This endpoint handles POST requests to /employees and is intended for creating a new employee. It receives an employee object in the request body, assigns a unique ID to the new employee, adds them to employeeList, and returns a 201 Created response along with the URI of the new employee and the employee object itself. This endpoint is essential for expanding the list of employees dynamically.
PUT /employees/{id}:
This endpoint handles PUT requests to /employees/{id} and updates an existing employee’s data. It finds an employee by ID and, if found, updates their details with the information provided in the request body. If the employee is not found, it returns a 404 Not Found. This is essential for maintaining up-to-date information on employees on the list.
DELETE /employees/{id}:
This endpoint handles DELETE requests to /employees/{id} and removes an employee from employeeList based on their ID. If the employee is found and removed, it returns a 204 No Content response, indicating successful deletion without returning any data. This endpoint is essential for removing employees who are no longer part of the organization or the dataset.
Now, run the application and access the above endpoints using client tools such as Swagger, Fiddler, and Postman. It should work as expected.
Dependency Injection in ASP.NET Core Minimal APIs:
Dependency injection in Minimal APIs is handled similarly to traditional ASP.NET Core applications. Services are registered in the builder.Services collection and injected into endpoint handlers.
Now, let us refactor our example and use dependency injection for handling Employee related logic. For this, we will create a service class that encapsulates the employee management functionality. We will then register this service with the dependency injection container in ASP.NET Core and modify the endpoints to use this service.
Create the Employee Service Interface and Implementation
First, create an interface named IEmployeeService.cs within the Models folder, and then copy and paste the following code:
namespace MinimalAPIDemo.Models { public interface IEmployeeService { List<Employee> GetAllEmployees(); Employee? GetEmployeeById(int id); Employee AddEmployee(Employee employee); Employee? UpdateEmployee(int id, Employee updatedEmployee); bool DeleteEmployee(int id); } }
Next, create a class file named EmployeeService.cs within the Models folder and copy and paste the following code. The EmployeeService class implements the IEmployeeService interface and provides implementations to methods.
namespace MinimalAPIDemo.Models { public class EmployeeService : IEmployeeService { private readonly List<Employee> _employeeList; public EmployeeService() { // Initialize the in-memory list with some sample data _employeeList = new List<Employee> { new Employee { Id = 1, Name = "John Doe", Position = "Software Engineer", Salary = 60000 }, new Employee { Id = 2, Name = "Jane Smith", Position = "Project Manager", Salary = 80000 } }; } public List<Employee> GetAllEmployees() { return _employeeList; } public Employee? GetEmployeeById(int id) { return _employeeList.FirstOrDefault(e => e.Id == id); } public Employee AddEmployee(Employee newEmployee) { newEmployee.Id = _employeeList.Count > 0 ? _employeeList.Max(emp => emp.Id) + 1 : 1; _employeeList.Add(newEmployee); return newEmployee; } public Employee? UpdateEmployee(int id, Employee updatedEmployee) { var employee = _employeeList.FirstOrDefault(emp => emp.Id == id); if (employee == null) return null; employee.Name = updatedEmployee.Name; employee.Position = updatedEmployee.Position; employee.Salary = updatedEmployee.Salary; return employee; } public bool DeleteEmployee(int id) { var employee = _employeeList.FirstOrDefault(emp => emp.Id == id); if (employee == null) return false; _employeeList.Remove(employee); return true; } } }
Register the Service in the Dependency Injection Container
Please add the following statement to the Program.cs class file to register the EmployeeService:
builder.Services.AddSingleton<IEmployeeService, EmployeeService>();
Modify the Program class:
Next, modify the Program class to use the EmployeeService. The EmployeeService is injected into the endpoints.
using MinimalAPIDemo.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the DI container // Add API explorer for endpoint documentation builder.Services.AddEndpointsApiExplorer(); // Add Swagger for API documentation builder.Services.AddSwaggerGen(); // Register EmployeeService in the DI container builder.Services.AddSingleton<IEmployeeService, EmployeeService>(); // Build the application var app = builder.Build(); // Configure the HTTP request pipeline for the development environment if (app.Environment.IsDevelopment()) { // Use Swagger middleware to generate Swagger Documentation app.UseSwagger(); // Use Swagger UI middleware to interact with the Swagger documentation app.UseSwaggerUI(); } // CRUD operations for Employee model // The EmployeeService is injected into the endpoints // Endpoint to retrieve all employees app.MapGet("/employees", (IEmployeeService employeeService) => employeeService.GetAllEmployees()); // Endpoint to retrieve a single employee by their ID app.MapGet("/employees/{id}", (int id, IEmployeeService employeeService) => { var employee = employeeService.GetEmployeeById(id); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // Endpoint to create a new employee app.MapPost("/employees", (Employee newEmployee, IEmployeeService employeeService) => { var createdEmployee = employeeService.AddEmployee(newEmployee); return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); }); // Endpoint to update an existing employee app.MapPut("/employees/{id}", (int id, Employee updatedEmployee, IEmployeeService employeeService) => { var employee = employeeService.UpdateEmployee(id, updatedEmployee); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // Endpoint to delete an employee app.MapDelete("/employees/{id}", (int id, IEmployeeService employeeService) => { var result = employeeService.DeleteEmployee(id); return result ? Results.NoContent() : Results.NotFound(); }); // Run the application app.Run();
Now, run the application and test the endpoints. It should work as expected.
Request Validation in Minimal APIs:
Request validation in Minimal APIs can be done using Data Annotation Attributes like [Required], [Range], etc., or you can integrate libraries like FluentValidation. Let us see how to implement validation using data annotation. So, modify the Employee class as follows. Here, you can see we have decorated the model properties with different data annotation attributes:
namespace MinimalAPIDemo.Models { public class Employee { public int Id { get; set; } [Required(ErrorMessage = "Name is required")] [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")] public string Name { get; set; } [Required(ErrorMessage = "Position is required")] [StringLength(50, ErrorMessage = "Position can't be longer than 50 characters")] public string Position { get; set; } [Range(30000, 200000, ErrorMessage = "Salary must be between 30000 and 200000")] public decimal Salary { get; set; } } }
The service itself does not need to be heavily modified for data annotations, as the validation will be handled at the API endpoint level.
Creating ValidationHelper:
Next, create a class file named ValidationHelper.cs within the Models folder and then copy and paste the following code. This class contains one generic static method, which is going to validate the model object of any type, which contains properties decorated with Data Annotation Attributes.
using System.ComponentModel.DataAnnotations; namespace MinimalAPIDemo.Models { public static class ValidationHelper { // TryValidate method performs validation on a generic model object. // 'T' represents any type which means this method can be used with any model. public static bool TryValidate<T>(T model, out List<ValidationResult> validationResults) { // Create a ValidationContext for the model, which contains information about the model's type // and any additional metadata var validationContext = new ValidationContext(model, null, null); // Initialize the list to store validation results (errors, if any) validationResults = new List<ValidationResult>(); // Perform the validation using the Validator class, which uses reflection to find // and validate the properties of the model based on data annotations. // This method returns true if the model passes all validation rules; otherwise, false. // The 'true' parameter specifies that all properties should be validated. return Validator.TryValidateObject(model, validationContext, validationResults, true); } } }
The TryValidate method provides a reusable, generic way to validate any model object using Data Annotations. It performs validation on the provided model and returns a list of validation results, indicating any validation errors.
Update Endpoints to Handle Request Validation
Modify the Program.cs endpoints to check for model validation errors. This involves using the ValidationHelper class TryValidate method to check the validity of the model after it’s bound from the request. So, modify the Program class as follows:
using MinimalAPIDemo.Models; // Create a builder for the web application var builder = WebApplication.CreateBuilder(args); // Add services to the DI container // Add API explorer for endpoint documentation builder.Services.AddEndpointsApiExplorer(); // Add Swagger for API documentation builder.Services.AddSwaggerGen(); // Register EmployeeService in the DI container builder.Services.AddSingleton<IEmployeeService, EmployeeService>(); // Build the application var app = builder.Build(); // Configure the HTTP request pipeline for the development environment if (app.Environment.IsDevelopment()) { // Use Swagger middleware to generate Swagger Documentation app.UseSwagger(); // Use Swagger UI middleware to interact with the Swagger documentation app.UseSwaggerUI(); } // CRUD operations for Employee model // The EmployeeService is injected into the endpoints // Endpoint to retrieve all employees app.MapGet("/employees", (IEmployeeService employeeService) => employeeService.GetAllEmployees()); // Endpoint to retrieve a single employee by their ID app.MapGet("/employees/{id}", (int id, IEmployeeService employeeService) => { var employee = employeeService.GetEmployeeById(id); return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // Endpoint to create a new employee with validation app.MapPost("/employees", (Employee newEmployee, IEmployeeService employeeService) => { // Validate the new employee using ValidationHelper if (!ValidationHelper.TryValidate(newEmployee, out var validationResults)) { // Return 400 Bad Request if validation fails return Results.BadRequest(validationResults); } // Add the new employee using the EmployeeService var createdEmployee = employeeService.AddEmployee(newEmployee); // Return 201 Created with the new employee's data return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); }); // Endpoint to update an existing employee with validation app.MapPut("/employees/{id}", (int id, Employee updatedEmployee, IEmployeeService employeeService) => { // Validate the updated employee using ValidationHelper if (!ValidationHelper.TryValidate(updatedEmployee, out var validationResults)) { return Results.BadRequest(validationResults); // Return 400 Bad Request if validation fails } // Update the employee using the EmployeeService var employee = employeeService.UpdateEmployee(id, updatedEmployee); // Return 200 OK if found and updated, otherwise 404 Not Found return employee is not null ? Results.Ok(employee) : Results.NotFound(); }); // Endpoint to delete an employee app.MapDelete("/employees/{id}", (int id, IEmployeeService employeeService) => { var result = employeeService.DeleteEmployee(id); return result ? Results.NoContent() : Results.NotFound(); }); // Run the application app.Run();
With the above changes in place, run the application and test the Post and Put endpoints where we have implemented validation. It should work as expected.
How do you handle JSON Serialization and Deserialization in Minimal APIs?
ASP.NET Core’s built-in JSON formatter automatically handles JSON serialization and deserialization in Minimal APIs. Input parameters are automatically bound from JSON requests, and return values are serialized to JSON.
Limitations of Minimal API in ASP.NET Core.
The following are some of the Limitations of Minimal API in ASP.NET Core:
- Limited Support for Complex Applications: They might not be suitable for applications with complex business logic requiring detailed validation, complex routing, or extensive middleware use.
- Features: Lacks some advanced features provided by MVC controllers (e.g., attribute routing, filters).
- Less Convention: More manual configuration and setup are required for features that are automatically handled in MVC.
- Organization: This can lead to less organized code if not structured properly.
What are the differences between Minimal API and Controller-Based API?
The following are the differences between Minimal API and Controller-Based API
Minimal API:
- Defines routes and handlers directly in the Program.cs.
- Less code and configuration.
- Suitable for simple and small applications.
Controller-Based API:
- Uses controllers to group related action methods.
- Supports attribute routing, model binding, validation, and filters.
- Better suited for larger applications requiring advanced features.
In the next article, I will discuss How to Implement Error Handling and Logging in ASP.NET Core Minimal API with Examples. In this article, I explain How to Implement Minimal API in ASP.NET Core Application with Examples. I hope you enjoy this article, Minimal API in ASP.NET Core.