blog post

How To Be More Productive When Creating CRUD APIs in .NET

A Big Thanks To The Sponsors of This Blog Post

Master Key .Net Tools and Skills. Learn debugging, source code management, testing, cloud-native development, intelligent apps, and more with Tools and Skills by .NET 8 by bestseller Author Mark J Price.

Learn more

Master The Clean Architecture. This comprehensive course will teach you how to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 3,150+ students to accelerate your growth as a software architect.

Learn more

Master The Modular Monolith Architecture. This in-depth course will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. Join 1,050+ students to accelerate your growth as a software architect.

Learn more

I have been creating .NET webapps for more than 10 years. I have created a lot of complex applications and a lot of CRUD APIs. CRUD APIs by their nature are straightforward, but in every project you need to write the same boilerplate code to create, update, delete and read entities from the database.

A few years ago, I was looking for ready free solutions that will allow me to ship CRUD APIs faster. And I couldn't find the perfect solution. So I created my own.

In today's blog post, I will show you tools that allowed me to be more productive when creating CRUD APIs in .NET. I will show you examples of how to use these tools in an N-Tier (Layered) and Vertical Slice Architecture.

Getting Started With a Modern Set of Libraries

I created a set of libraries called Modern for fast and efficient development of CRUD APIs in .NET. Have a look at the documentation on the Github page.

It allows creating a production ready applications with just a set of models and configuration which can be further extended. Modern tools are flexible, easily changeable and extendable.

It includes the following components:

  • generic repositories for SQL and NoSQL databases
  • generic services with and without caching support
  • generic in memory services with in-memory filtering capabilities
  • in-memory and redis generic caches
  • generic set of CQRS queries and commands over repository (if you prefer CQRS over services)
  • generic controllers for all types of services
  • OData controllers for all types of services

Modern supports the following databases and frameworks:

  • EF Core
  • Dapper
  • MongoDB
  • LiteDB

Let's get started. Let's create a Web Api with CRUD operations for Tickets.

Step 1: Create a regular ASP.NET Core Web API project and add the following Nuget packages

bash
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Modern.Repositories.EFCore dotnet add package Modern.Services.DataStore dotnet add package Modern.Controllers.DataStore

Step 2: Create database Ticket entity

csharp
public class Ticket { public int Id { get; set; } public string Number { get; set; } public string Description { get; set; } public string Status { get; set; } public decimal Price { get; set; } public DateTime PurchasedAtUtc { get; set; } }

Step 3: Configure EF Core

csharp
public class EfCoreDbContext(DbContextOptions<EfCoreDbContext> options) : DbContext(options) { public DbSet<Ticket> Tickets { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDefaultSchema("tickets"); modelBuilder.Entity<Ticket>(entity => { entity.HasKey(x => x.Id); entity.HasIndex(x => x.Number); }); } } var postgresConnectionString = configuration.GetConnectionString("Postgres"); services.AddDbContext<EfCoreDbContext>(x => x .UseNpgsql(postgresConnectionString) .UseSnakeCaseNamingConvention() );

Step 4: Create public contract models for the Ticket entity

csharp
public record TicketDto { public required int Id { get; init; } public required string Number { get; init; } public required string Description { get; init; } public required string Status { get; init; } public required decimal Price { get; init; } } public record CreateTicketRequest { public required string Number { get; init; } public required string Description { get; init; } public required string Status { get; init; } public required decimal Price { get; init; } } public record UpdateTicketRequest { public required string Id { get; init; } public required string Number { get; init; } public required string Description { get; init; } public required string Status { get; init; } public required decimal Price { get; init; } }

Step 5: Register and map controllers in DI with Swagger support

csharp
builder.Services.AddControllers(); builder.Services .AddEndpointsApiExplorer() .AddSwaggerGen(); // ... var app = builder.Build(); app.MapControllers();

Step 6: Create database migration with EF Core Tools

Step 7: Register Modern tools in DI

csharp
builder.Services .AddModern() .AddRepositoriesEfCore(options => { options.AddRepository<EfCoreDbContext, Ticket, int>(); }) .AddServices(options => { options.AddService<TicketDto, Ticket, int>(); }) .AddControllers(options => { options.AddController<CreateTicketRequest, UpdateTicketRequest, TicketDto, Ticket, int>("api/tickets"); });

Let's break down what we are registering here:

  • EF Core DbContext, type of Ticket entity, and type of the primary key (int).
  • Service that uses Ticket entity and returns TicketDto.
  • Controller that uses CreateTicketRequest, UpdateTicketRequest and TicketDto to expose CRUD API endpoints

Let's run the application and see what happens.

Screenshot_1

Within a few minutes, we have created a fully functional CRUD API for Ticket entity that has the following endpoints:

  • Get entity by id
  • get all entities
  • create entity
  • update entity
  • partial entity update
  • delete entity
  • create many entities
  • update many entities
  • delete many entities

Source generation for WebApi models in Modern Libraries

You're not limited to only using DTO models. You can also use DBO models and expose them from the APIs, in case you need such an option:

csharp
options.AddService<Ticket, Ticket, int>(); options.AddController<CreateTicketRequest, UpdateTicketRequest, Ticket, Ticket, int>("api/tickets");

I have created a Nuget package with source generators for WebApi models:

bash
dotnet add package Modern.Controllers.SourceGenerators

You can add this attribute to the public contract model, and the source generator will create a CreateTicketRequest and UpdateTicketRequest models:

csharp
[WebApiEntityRequest(CreateRequestName = "CreateTicketRequest", UpdateRequestName = "UpdateTicketRequest")] public record TicketDto { [IgnoreCreateRequest] public required int Id { get; init; } public required string Number { get; init; } public required string Description { get; init; } public required string Status { get; init; } public required decimal Price { get; init; } }

Source generator uses all the properties for Create and Update requests. You can additionally specify what properties should be excluded from the requests by using the IgnoreCreateRequest or IgnoreUpdateRequest.

Customizing Repositories, Services and Controllers

At its core, Modern libraries follow the classic N-Tier (Layered) approach:

  • Repository
  • Service
  • Controller

Each layer can be overridden and further extended.

Modern Repository supports the following Query and Write operations.

Modern Repository has Where method that you can use to filter data by a given condition. However, you may need to create your own methods.

You can create your own repository interface that inherits from IModernRepository<TEntity, TId>. And create an implementation that inherits from ModernEfCoreRepository<TDbContext, TEntity, TId>.

Here is an example:

csharp
public interface ICustomTicketRepository: IModernRepository<Ticket, int> { Task<List<Ticket>> GetTicketsByDateAsync(DateTime date); } public class CustomTicketRepository : ModernEfCoreRepository<EfCoreDbContext, Ticket, int>, ICustomTicketRepository { public CustomTicketRepository( EfCoreDbContext dbContext, IOptions<EfCoreRepositoryConfiguration> configuration) : base(dbContext, configuration) { } public async Task<List<Ticket>> GetTicketsByDateAsync(DateTime date) { return await DbContext.Tickets .Where(x => x.PurchasedAtUtc >= date) .ToListAsync(); } }

In the same manner you can create your own service interface that inherits from IModernService<TEntityDto, TEntityDbo, TId>. And create an implementation that inherits from ModernService<TEntityDto, TEntityDbo, TId>.

csharp
public interface ICustomTicketService : IModernService<TicketDto, Ticket, int> { Task<List<Ticket>> GetTicketsByDateAsync(DateTime date); } public class CustomTicketService : ModernService<TicketDto, Ticket, int>, ICustomTicketService { private readonly ICustomTicketRepository _repository; public CustomTicketService( ICustomTicketRepository repository, ILogger<CustomTicketService> logger) : base(repository, logger) { _repository = repository; } public async Task<List<Ticket>> GetTicketsByDateAsync(DateTime date) { return await _repository.GetTicketsByDateAsync(date); } }

The same goes with Controllers, you can create a custom Controller that inherits from ModernController<TCreateRequest, TUpdateRequest, TEntityDto, TEntityDbo, TId>:

csharp
public record GetTicketsByDateRequest(DateTime Date); [ApiController] [Route("/api/custom-tickets")] public class CustomTicketController : ModernController<CreateTicketRequest, UpdateTicketRequest, TicketDto, Ticket, int> { private readonly ICustomTicketService _service; public CustomTicketController(ICustomTicketService service) : base(service) { _service = service; } [HttpGet("get-by-date")] public async Task<IActionResult> GetTicketsByDate( [Required, FromBody] GetTicketsByDateRequest request) { var entities = await _service.GetTicketsByDateAsync(request.Date).ConfigureAwait(false); return Ok(entities); } }

After creating your own implementations, you need to register them as Concrete implementations:

csharp
builder.Services .AddModern() .AddRepositoriesEfCore(options => { options.AddRepository<EfCoreDbContext, Ticket, int>(); options.AddConcreteRepository<ICustomTicketRepository, CustomTicketRepository>(); }) .AddServices(options => { options.AddService<TicketDto, Ticket, int>(); options.AddConcreteService<ICustomTicketService, CustomTicketService>(); }) .AddControllers(options => { options.AddController<CreateTicketRequest, UpdateTicketRequest, TicketDto, Ticket, int>("api/tickets"); options.AddController<CustomTicketController>(); });

In all previous examples, we were using our own customized implementations of a Repository and Service. What if you don't need to create your own version of Repository or Service and use just a base version from the package?

The first option is to use the base interface of IModernService or IModernRepository from the Modern package:

csharp
// Use IModernService<...> instead of ICustomTicketService IModernService<TicketDto, Ticket, int> service // Use IModernRepository<...> instead of ICustomTicketRepository IModernRepository<Ticket, int> repository

If you dislike such long types, you can create your own interface and implementation that inherit from the base types and make them empty. But this can be tedious.

If this is the case, you can use the following packages with source generators:

bash
dotnet add package Modern.Repositories.EFCore.SourceGenerators dotnet add package Modern.Services.DataStore.SourceGenerators

You can need to use a marker empty class and specify the ModernEfCoreRepository attribute:

csharp
[ModernEfCoreRepository(typeof(EfCoreDbContext), typeof(Ticket), typeof(int))] public class IServiceMarker;

To autogenerate service, you need to add a ModernService attribute to the Dto class:

csharp
[ModernService(typeof(Ticket))] public class TicketDto { }

NOTE: source generator only supports classes for Dto at the moment.

As a result, source generators will create the following classes:

Screenshot_2

Using Modern Libraries in Vertical Slice Architecture

If you prefer using Vertical Slice Architecture or even combination of Vertical Slices and Clean Architecture, you can find Modern libraries very helpful for you.

You can create a minimal API Endpoint for your CreateTicketEndpoint Vertical Slice:

csharp
public class CreateTicketEndpoint : IEndpoint { public void MapEndpoint(WebApplication app) { app.MapPost("/api/tickets/v2", Handle); } private static async Task<IResult> Handle( [FromBody] CreateTicketRequestV2 request, IValidator<CreateTicketRequestV2> validator, ITicketRepository ticketRepository, CancellationToken cancellationToken) { var validationResult = await validator.ValidateAsync(request, cancellationToken); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } var entity = request.MapToEntity(); entity = await ticketRepository.CreateAsync(entity, cancellationToken); var response = entity.MapToResponse(); return Results.Ok(response); } }

Here I directly use ITicketRepository that can eliminate writing some boilerplate code.

Additional Features Supported By Modern Libraries

Modern supports repositories for the following databases and frameworks:

  • EF Core: EF Core DbContext, DbContextFactory and UnitOfWork
  • Dapper (doesn't support dynamic filtering)
  • MongoDB
  • LiteDB

Modern supports the following services:

  • Services that perform CRUD operations over entities in the database
  • Services that add a caching layer (InMemory or Redis) to save items and retrieve items by id
  • Services that use a full in-memory cache that has all items cached

If you prefer using MediatR, Modern supports the following:

  • Queries and Commands that perform CRUD operations over entities in the database
  • Queries and Commands that add a caching layer (InMemory or Redis) to save items and retrieve items by id

Modern supports the following controllers:

  • Controllers that perform CRUD operations over entities in the database (use regular or cached service)
  • Controllers that use a full in-memory service

Summary

Modern libraries provide various components that help you write less boilerplate code for your CRUD APIs. I definitely recommend checking this library on the GitHub and the documentation on the GitHub Wiki.

If you like the library - make sure to give a star on the GitHub.

Hope you find this blog post useful. Happy coding!

You can download source code for this blog post for free

Improve Your .NET and Architecture Skills

Join my community of 1800+ developers and architects.

Each week you will get 2 practical tips with best practises and architecture advice.