In the previous post, we saw what are Minimal APIs and how we can create them in ASP.NET Core 6. In this post, we’ll see how we can refactor our Minimal APIs using Carter framework.
We had UserRepository in our previous post, I’m using the same repository for this example as well so that I don’t have to re-create UserRepository again.
Along with UserRepository (which has basic CRUD operations) let’s have a products repository as well.
Adding products repository
Let’s create a product repository.
public class Product { public int ProductId { get; set; } public string ProductName { get; set; } public double Price { get; set; } public DateTime CreatedOn { get; set; } public bool IsValid() { return ProductId != 0 && !string.IsNullOrWhiteSpace(ProductName) && Price >= 0; } } public interface IProductsRepository { bool CreateProduct(Product product); Product GetProductById(int id); bool UpdateProduct(int id, Product product); bool DeleteProduct(int id); } public class ProductsRepository : IProductsRepository { private List<Product> products; private readonly ILogger<IProductsRepository> _logger; public ProductsRepository(ILogger<IProductsRepository> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); GenerateSeedData(); } public bool CreateProduct(Product product) { var productNameExistsAlready = products.Any(p => p.ProductName == product.ProductName); if (productNameExistsAlready) { _logger.LogError("Product exists already!"); return false; } products.Add(product); return true; } public Product GetProductById(int id) { return products.FirstOrDefault(a => a.ProductId == id); } public bool UpdateProduct(int id, Product product) { var p = products.FirstOrDefault(a => a.ProductId == id); p.ProductName = product.ProductName; p.CreatedOn = DateTime.Now; p.Price = product.Price; return true; } public bool DeleteProduct(int id) { var product = products.FirstOrDefault(p => p.ProductId == id); if (product == null) { return false; } products.Remove(product); return true; } private void GenerateSeedData() { products = new Faker<Product>() .RuleFor(p => p.ProductId, p => p.IndexFaker) .RuleFor(p => p.ProductName, p => p.Random.Words(2)) .RuleFor(p => p.Price, p => Convert.ToDouble(p.Finance.Amount())) .RuleFor(p => p.CreatedOn, p => p.Date.Recent()) .Generate(50); } }
The products repository has 4 basic CRUD (Create, Read, Update, Delete) operations and a product model class. And we’ve generated our seed data for our products repo with the help of Bogus for C# (a fake data generator)
Registering API’s in Program.cs
Now, let’s register this product’s repository as a singleton in program.cs
.
builder.Services.AddSingleton<IProductsRepository, ProductsRepository>();
After registering the Products repository, let’s also define the endpoints/API’s for Products.
// PRODUCTS operations app.MapPost("/product/create", (Product product, IProductsRepository productsRepo) => { if (!product.IsValid()) return Results.BadRequest(); productsRepo.CreateProduct(product); return Results.StatusCode(201); }); app.MapGet("/product/{id}", (int id, IProductsRepository repo) => { if (id <= 0) return Results.BadRequest(); return Results.Ok(repo.GetProductById(id)); }); app.MapPut("/product/update/{id}", (int id, Product product, IProductsRepository productRepo) => { if (!product.IsValid()) return Results.BadRequest(); productRepo.UpdateProduct(id, product); return Results.Ok(); }); app.MapDelete("/product/delete/{id}", (int id, IProductsRepository productsRepo) => { return productsRepo.DeleteProduct(id); });
Program.cs hell
Let’s see what we have in our Program.cs
class. Here is the complete file (with UserRepository operations included).
using MinimalAPIs.Repositories; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton<IUserRepository, UserRepository>(); builder.Services.AddSingleton<IProductsRepository, ProductsRepository>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); #region API's // USER operations app.MapGet("/user/{id}", async (int id, IUserRepository userRepository, CancellationToken cancellationToken) => { var user = await userRepository.GetUserAsync(id, cancellationToken); if (user is null) return Results.BadRequest(); return Results.Ok(user); }).WithName("GetUsers"); app.MapPut("/user/update/{id}", (int id, User user, IUserRepository userRepository) => { return userRepository.UpdateUser(id, user); }).WithName("UpdateUsers"); app.MapDelete("/user/{id}", (int id, IUserRepository userRepository) => { userRepository.DeleteUser(id); return Results.Ok(); }).WithName("DeleteUser"); // PRODUCTS operations app.MapPost("/product/create", (Product product, IProductsRepository productsRepo) => { if (!product.IsValid()) return Results.BadRequest(); productsRepo.CreateProduct(product); return Results.StatusCode(201); }); app.MapGet("/product/{id}", (int id, IProductsRepository repo) => { if (id <= 0) return Results.BadRequest(); return Results.Ok(repo.GetProductById(id)); }); app.MapPut("/product/update/{id}", (int id, Product product, IProductsRepository productRepo) => { if (!product.IsValid()) return Results.BadRequest(); productRepo.UpdateProduct(id, product); return Results.Ok(); }); app.MapDelete("/product/delete/{id}", (int id, IProductsRepository productsRepo) => { return productsRepo.DeleteProduct(id); }); #endregion app.Run();
With just products and user operations, the program.cs
file already looks absurd. As the project goes on, we may end up searching for endpoints in Program.cs file.
So, let’s refactor this mess of endpoints with the Carter framework.
What is Carter framework?
Carter is framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing code to be more explicit and most importantly more enjoyable.
From https://github.com/CarterCommunity/Carter
Installing Carter
As always we can install this package in a few different ways.
- Head over to Nuget package manager in visual studio and search for
Carter
and install it - Run
dotnet add package carter
in command prompt in your project folder. - Run
Install-Package Carter
in Nuget package manager console to install.
Setup carter for Minimal API’s
After installing the carter framework, we have to modify our Program.cs
file to look like this.
var builder = WebApplication.CreateBuilder(args); // all our dependancies and other services goes here builder.Services.AddCarter(); var app = builder.Build(); // any pipeline configuration goes here app.MapCarter(); app.Run();
All we have to do is add Carter to our services and to our pipeline and we should be good.
We used to write app.MapGet(...)
just before the app.Run()
statement. With carter, there is no need to write it here. We can have them in separate modules per entity.
Creating Carter Modules
Before running the app, we’ve to create classes that implement ICarterModule
interface. In the framework, they call these classes Modules. So, let’s create two modules. One for user and the other for products.
public class UserModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("/user/{id}", async (int id, IUserRepository userRepository, CancellationToken cancellationToken) => { var user = await userRepository.GetUserAsync(id, cancellationToken); if (user is null) return Results.BadRequest(); return Results.Ok(user); }).WithName("GetUsers"); app.MapPut("/user/update/{id}", (int id, User user, IUserRepository userRepository) => { return userRepository.UpdateUser(id, user); }).WithName("UpdateUsers"); app.MapDelete("/user/{id}", (int id, IUserRepository userRepository) => { userRepository.DeleteUser(id); return Results.Ok(); }).WithName("DeleteUser"); } } public class ProductsModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapPost("/product/create", (Product product, IProductsRepository productsRepo) => { if (!product.IsValid()) return Results.BadRequest(); productsRepo.CreateProduct(product); return Results.StatusCode(201); }); app.MapGet("/product/{id}", (int id, IProductsRepository repo) => { if (id <= 0) return Results.BadRequest(); return Results.Ok(repo.GetProductById(id)); }); app.MapPut("/product/update/{id}", (int id, Product product, IProductsRepository productRepo) => { if (!product.IsValid()) return Results.BadRequest(); productRepo.UpdateProduct(id, product); return Results.Ok(); }); app.MapDelete("/product/delete/{id}", (int id, IProductsRepository productsRepo) => { return productsRepo.DeleteProduct(id); }); } }
We don’t have to register these modules in Program.cs
anymore because the classes that implement ICarterModule
interface are already registered as Singletons through AddCarter()
method. It’s automatic.
Let’s run and see if this works.
More refactoring
The AddRoutes
method in both UserModule
and ProductsModule
doesn’t look nice as the new endpoint implementations are within MapGet/MapPost/MapPut/MapDelete
endpoints.
Our endpoints are really simple but if they are getting more complex we can move those implementations to be private within the module.
Let’s move the implementations to private methods in the products module.
public class ProductsModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapPost("/product/create", CreateProduct); app.MapGet("/product/{id}", GetProductById); app.MapPut("/product/update/{id}", UpdateProduct); app.MapDelete("/product/delete/{id}", DeleteProduct); } private IResult CreateProduct(Product product, IProductsRepository productsRepo) { if (!product.IsValid()) return Results.BadRequest(); productsRepo.CreateProduct(product); return Results.StatusCode(201); } private IResult GetProductById(int id, IProductsRepository repo) { if (id <= 0) return Results.BadRequest(); return Results.Ok(repo.GetProductById(id)); } private IResult UpdateProduct(int id, Product product, IProductsRepository productRepo) { if (!product.IsValid()) return Results.BadRequest(); productRepo.UpdateProduct(id, product); return Results.Ok(); } private bool DeleteProduct(int id, IProductsRepository repo) { return repo.DeleteProduct(id); } }
Notice all the endpoints return IResult
except DeleteProduct
method which returns a boolean. You don’t have to wrap the result of the endpoint within the Results object every time. It’s up to you to return whatever you want from an endpoint.
Now our endpoints in the AddRoutes
method looks clean after we moved our endpoint implementations to private methods within ProductsModule
class.
We can do the same thing for UserModule
as well. I don’t want to bloat this article with that. I’ll do it in the GitHub repository.
That’s it! We’ve refactored our minimal APIs with the Carter framework by moving them into separate modules.
Conclusion
Carter is an open source lightweight framework that separates registering endpoints from Program.cs
file and makes our endpoints look clean.
The fluent validation dependency for Carter is also an excellent addition for writing validations. If you don’t know it, consider taking a peek at it. This library will help write more readable validations.
Carter framework also has a response negotiator as minimal APIs do not support content negotiation by default. As this is a little off-topic for this blog I’m not going into it.
References
- Refactored Minimal APIs code with Carter framework (Github repo)
- What are Minimal APIs
- Carter Framework
- Bogus for C#
- Fluent Validation
Karthik is a passionate Full Stack developer working primarily on .NET Core, microservices, distributed systems, VUE and JavaScript. He also loves NBA basketball so you might find some NBA examples in his posts and he owns this blog.
Pingback: Dew Drop – August 9, 2022 (#3740) – Morning Dew by Alvin Ashcraft
Pingback: The Morning Brew - Chris Alcock » The Morning Brew #3527