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.
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)
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); });
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.
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
As always we can install this package in a few different ways.
Carter
and install itdotnet add package carter
in command prompt in your project folder.Install-Package Carter
in Nuget package manager console to install.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.
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.
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.
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.
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.
In this post, we’ll see how to test gRPC Server applications using different clients. And… Read More
In this post, we'll create a new gRPC project in ASP.NET Core and see what's… Read More
In this blog post, we’ll see how to run dotnet core projects without opening visual… Read More
Programmatically evaluating policies is useful when we want to provide access or hide some data… Read More
We saw how we could set up policy-based authorization in our previous article. In this… Read More
What is policy-based authorization and how to set up policy-based authorization with handlers and policies… Read More
This website uses cookies.