Skip to content

Backend Stack

Vulcan's backend is built with .NET 10 LTS, using ASP.NET Core Minimal APIs and Entity Framework Core.

Core Technologies

TechnologyVersionPurpose
.NET10.0 LTSModern, high-performance framework for building APIs and services
ASP.NET Core10.0Web framework for building RESTful APIs with robust middleware
Entity Framework Core10.0Modern ORM for .NET with LINQ support, JSON complex types, and migrations

Validation & Documentation

TechnologyVersionPurpose
FluentValidation11.xStrongly-typed validation rules for .NET applications
Swagger/OpenAPI7.xAPI documentation and interactive testing interface

Database

TechnologyVersionPurpose
PostgreSQL16.xRobust, open-source relational database with advanced features
Npgsql10.x.NET data provider for PostgreSQL

CQRS & Logging

TechnologyVersionPurpose
Mediator3.0.1Source generator-based CQRS dispatcher
Serilog3.xStructured logging framework

Testing

TechnologyVersionPurpose
xUnit2.xUnit testing framework for .NET applications
FluentAssertions7.xFluent assertion library for tests

Architecture Patterns

Vertical Slice Architecture

Each feature is self-contained with all CRUD operations colocated:

Features/
└── Leads/
    ├── GetAllLeads/
    │   ├── GetAllLeadsQuery.cs
    │   ├── GetAllLeadsHandler.cs
    │   └── GetAllLeadsResponse.cs
    ├── GetLead/
    ├── CreateLead/
    ├── UpdateLead/
    └── DeleteLead/

CQRS Pattern

Commands (writes) and Queries (reads) are separated:

csharp
// Query - read operation
public record GetLeadQuery(Guid Id) : IRequest<LeadResponse?>;

// Command - write operation
public record CreateLeadCommand(
    string Name,
    string Email
) : IRequest<CreateLeadResponse>;

Mediator Pattern

Using Mediator 3.0.1 for dispatching commands and queries:

csharp
// Handler
public class GetLeadHandler : IRequestHandler<GetLeadQuery, LeadResponse?>
{
    private readonly AppDbContext _context;

    public GetLeadHandler(AppDbContext context)
    {
        _context = context;
    }

    public async ValueTask<LeadResponse?> Handle(
        GetLeadQuery request,
        CancellationToken cancellationToken)
    {
        var lead = await _context.Leads
            .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);

        return lead?.ToResponse();
    }
}

Minimal API Endpoints

csharp
public static class LeadEndpoints
{
    public static void MapLeadEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/leads")
            .WithTags("Leads")
            .RequireAuthorization();

        group.MapGet("/", async (IMediator mediator) =>
            await mediator.Send(new GetAllLeadsQuery()));

        group.MapGet("/{id:guid}", async (Guid id, IMediator mediator) =>
            await mediator.Send(new GetLeadQuery(id)));

        group.MapPost("/", async (CreateLeadCommand cmd, IMediator mediator) =>
            await mediator.Send(cmd));
    }
}

Database Conventions

TypeConventionExample
Tablessnake_case pluralleads, work_packages
Columnssnake_casefirst_name, created_at
Primary Keysidid
Foreign Keys[entity]_idcustomer_id
Indexesix_[table]_[columns]ix_leads_email

Build Commands

bash
dotnet run                           # Start service
dotnet build                         # Build
dotnet test                          # Run tests
dotnet ef migrations add <Name>      # Create migration
dotnet ef database update            # Apply migrations

Key Conventions

Important

  • Use ValueTask<T> not Task<T> for Mediator handlers
  • All endpoints require authorization unless [AllowAnonymous]
  • One database per capability - never share databases
  • Validators are colocated with Commands in feature folders

Built with VitePress | v1.1.0