In this article, we’ll look closely at the Carter library and how we can use it in our ASP.NET Core projects.
Let’s get started!
What Is the Carter Library
The Carter library is an open-source project providing an abstraction layer for ASP.NET Core applications. This layer serves as a way to manage our minimal APIs easily and efficiently. The package provides us with a set of interfaces and methods that help with endpoint management.
So, let’s examine this in action:
app.MapGet("/books", async (IBookService service) => { var books = await service.GetAllAsync(); return Results.Ok(books); }) .WithOpenApi(); app.MapGet("/books/{id:guid}", async (Guid id, IBookService service) => { var book = await service.GetByIdAsync(id); return Results.Ok(book); }) .WithOpenApi(); app.MapPost("/books", async (CreateBookRequest request, IBookService service) => { var book = await service.CreateAsync(request); return Results.Created($"/books/{book.Id}", book); }) .WithOpenApi(); // Other CRUD endpoints PUT, DELETE
Here, we have a minimal API with CRUD endpoints for a book management application. Furthermore, the endpoints are located in our Program
class and take up about 40 lines.
This is already a bit cumbersome to navigate and maintain. It will only get more complex as our application grows. Here is where Carter comes into play.
Next, we’ll see how it can make our lives easier.
How to Use Carter With ASP.NET Core Minimal APIs
Before we do anything with our code, we need to install the Carter library:
dotnet add package Carter
Once this is done, we can start working with it:
public class BookModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { } }
The library revolves around modules, so we start by creating the BookModule
and implementing the ICarterModule
interface. The interface comes with the AddRoutes()
method.
Let’s implement it:
public class BookModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { app.MapGet("/books", async (IBookService service) => { var books = await service.GetAllAsync(); return Results.Ok(books); }) .WithOpenApi(); app.MapGet("/books/{id:guid}", async (Guid id, IBookService service) => { var book = await service.GetByIdAsync(id); return Results.Ok(book); }) .WithOpenApi(); // Other CRUD endpoints POST, PUT, DELETE } }
The AddRoutes()
method takes one parameter of IEndpointRouteBuilder
type. Thanks to this, the only thing we have to do with the module, is to move our endpoints from the Program
class to the AddRoutes()
method.
Next, we must register our module:
builder.Services.AddCarter();
In our Program
class, we use the AddCarter()
extension method on our service collection. This will scan our project for all implementations of the ICarterModule
interface and register them with a Singleton lifetime.
Finally, we map our endpoints:
app.MapCarter();
We call the MapCarter()
method on our WebApplication
instance. By doing this, we map our endpoints and make them visible to any users of our API.
Carter and Model Validation in ASP.NET Core
With Carter, we also get a FluentValidation integration, which is installed alongside the Carter NuGet package. So, let’s utilize it:
public class CreateBookRequestValidator : AbstractValidator<CreateBookRequest> { public CreateBookRequestValidator() { RuleFor(x => x.Title).NotNull().NotEmpty(); RuleFor(x => x.Author).NotNull().NotEmpty(); RuleFor(x => x.ISBN).NotNull().NotEmpty(); } }
We start by creating the CreateBookRequestValidator
class. Next, we implement the AbstractValidator<T>
abstract class, where T
is our CreateBookRequest
entity. After this, we write some validation rules – in our case, we want the Title
, Author
, and ISBN
property not to be null
or empty.
Let’s incorporate the validation into our CREATE
endpoint:
app.MapPost("/books", async ( HttpContext context, CreateBookRequest request, IBookService service) => { var result = context.Request.Validate(request); if (!result.IsValid) { context.Response.StatusCode = (int)HttpStatusCode.UnprocessableEntity; await context.Response.Negotiate(result.GetFormattedErrors()); return Results.UnprocessableEntity(); } var book = await service.CreateAsync(request); return Results.Created($"/books/{book.Id}", book); }) .WithOpenApi();
In our BookModule
class, we make some updates to our /books
endpoint. We start by adding the HttpContext
of the request as a parameter.
Then, we use the Validate()
method, which is an extension method on the HttpRequest
class that we get from Carter, passing the CreateBookRequest
parameter. The method uses an internal implementation of the IValidatorLocator
interface to try to resolve an appropriate validator. Consequently, this removes the need for us to register our CreateBookRequestValidator
in the DI container. If there is no matching validator, we’ll get an InvalidOperationException
exception.
Finally, we use two other Carter methods – Negotiate()
and GetFormattedErrors()
. With the latter, we get a formatted output with the property name and error message for each error encountered by the validator. The Negotiate()
method executes content negotiation on the current HttpResponse
instance and tries to utilize an accepted media type if possible and if none is found – it will default to application/json.
Advanced Endpoint Configurations With Carter in ASP.NET Core
With Carter, we can go a step further with some advanced configuration:
public class BookModule : CarterModule { public BookModule() : base("/api") { WithTags("Books"); IncludeInOpenApi(); } public override void AddRoutes(IEndpointRouteBuilder app) { // CRUD endpoints } }
Here, we make our BookModule
class implement the CarterModule
abstract class and not the ICarterModule
interface. The first thing we do after that is to add the override
operator to the AddRoutes()
method.
Next, we create a constructor for the BookModule
class, call the CarterModule
‘s base one, and pass /api
to it. This will add a prefix of /api to all of our endpoints in this module. Inside the constructor, we use the WithTags()
method to group all endpoints under the Books tag.
Finally, we call the IncludeInOpenApi()
method. With it, we add OpenAPI annotation to all endpoints in the module and include all routes in the OpenAPI output. Therefore, we don’t need to call the WithOpenApi()
method on all of our endpoints.
The Carter library also provides us with various other methods for further customization of our modules. For example, if our application has authorization set up, we can activate it for all endpoints for a given module by calling the RequireAuthorization()
method. If we have a rate-limiting setup, we can either enlist the endpoints by using the RequireRateLimiting()
method and passing the name of the rate-limiting policy to it, or we can use the DisableRateLimiting()
method to disable rate-limiting.
Conclusion
In this article, we explored the Carter library and the streamlined approach to managing ASP.NET Core APIs it offers. By leveraging its modular structure, we can organize our endpoints efficiently into separate modules, enhancing code readability and maintainability.