DEV Community

Cover image for How To Be More Productive When Creating CRUD APIs in .NET
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

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

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

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

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Create database Ticket entity

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; }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure EF Core

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()
);
Enter fullscreen mode Exit fullscreen mode

Step 4: Create public contract models for the Ticket entity

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; }
}
Enter fullscreen mode Exit fullscreen mode

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

builder.Services.AddControllers();

builder.Services
    .AddEndpointsApiExplorer()
    .AddSwaggerGen();

// ...

var app = builder.Build();
app.MapControllers();
Enter fullscreen mode Exit fullscreen mode

Step 6: Create database migration with EF Core Tools

Step 7: Register Modern tools in DI

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");
    });
Enter fullscreen mode Exit fullscreen mode

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:

options.AddService<Ticket, Ticket, int>();
options.AddController<CreateTicketRequest, UpdateTicketRequest, Ticket, Ticket, int>("api/tickets");
Enter fullscreen mode Exit fullscreen mode

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

dotnet add package Modern.Controllers.SourceGenerators
Enter fullscreen mode Exit fullscreen mode

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

[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; }
}
Enter fullscreen mode Exit fullscreen mode

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:

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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>.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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>();
    });
Enter fullscreen mode Exit fullscreen mode

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:

// Use IModernService<...> instead of ICustomTicketService
IModernService<TicketDto, Ticket, int> service

// Use IModernRepository<...> instead of ICustomTicketRepository
IModernRepository<Ticket, int> repository
Enter fullscreen mode Exit fullscreen mode

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:

dotnet add package Modern.Repositories.EFCore.SourceGenerators
dotnet add package Modern.Services.DataStore.SourceGenerators
Enter fullscreen mode Exit fullscreen mode

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

[ModernEfCoreRepository(typeof(EfCoreDbContext), typeof(Ticket), typeof(int))]
public class IServiceMarker;
Enter fullscreen mode Exit fullscreen mode

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

[ModernService(typeof(Ticket))]
public class TicketDto
{
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

Top comments (0)