Skip to content

Refactoring Minimal APIs with Carter

refactoring minimal apis

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.

  1. Head over to Nuget package manager in visual studio and search for Carter and install it
  2. Run dotnet add package carter in command prompt in your project folder.
  3. 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

Leave a Reply

Your email address will not be published.