Skip to content

Backend Libraries & Architecture — Laundry Management System

Document Information

Field Value
Project Laundry Management System
Version 1.0
Language English
Framework ASP.NET Core 10 + .NET 10
Document Type Backend Libraries & Developer Guide

Table of Contents

  1. Solution Structure
  2. Complete NuGet Package List
  3. Clean Architecture Layers
  4. Dependency Injection Conventions
  5. EF Core — Code-First Configuration
  6. CQRS with MediatR
  7. FluentValidation Setup
  8. Serilog + OpenTelemetry + Seq
  9. Health Checks Configuration
  10. Error Handling
  11. Code Conventions
  12. appsettings.json Template
  13. Program.cs Template

1. Solution Structure

backend/
├── Laundry.sln
├── Laundry.Api/                              # ASP.NET Core host
│   ├── Controllers/
│   │   ├── AuthController.cs
│   │   ├── InvoicesController.cs
│   │   ├── CustomersController.cs
│   │   ├── PaymentsController.cs
│   │   ├── CarpetController.cs
│   │   ├── TailoringController.cs
│   │   ├── InventoryController.cs
│   │   ├── ReportsController.cs
│   │   └── AdminController.cs
│   ├── Middleware/
│   │   ├── ClientTokenMiddleware.cs
│   │   ├── LicenseValidationMiddleware.cs
│   │   ├── GlobalExceptionMiddleware.cs
│   │   └── RequestLoggingMiddleware.cs
│   ├── Filters/
│   │   ├── PermissionFilter.cs
│   │   └── ValidateModelFilter.cs
│   ├── Program.cs
│   ├── appsettings.json
│   ├── appsettings.Development.json
│   └── Laundry.Api.csproj
├── Laundry.Application/                       # Use cases, CQRS, DTOs, validators
│   ├── Invoices/
│   │   ├── Commands/
│   │   │   ├── CreateInvoiceCommand.cs
│   │   │   ├── UpdateInvoiceStatusCommand.cs
│   │   │   └── ...
│   │   ├── Queries/
│   │   │   ├── GetInvoiceByIdQuery.cs
│   │   │   ├── SearchInvoicesQuery.cs
│   │   │   └── ...
│   │   ├── Dtos/
│   │   │   ├── InvoiceDto.cs
│   │   │   ├── InvoiceItemDto.cs
│   │   │   └── ...
│   │   ├── Validators/
│   │   │   └── CreateInvoiceValidator.cs
│   │   └── Handlers/
│   │       ├── CreateInvoiceHandler.cs
│   │       └── ...
│   ├── Payments/ ...
│   ├── Customers/ ...
│   ├── Carpet/ ...
│   ├── Tailoring/ ...
│   ├── Inventory/ ...
│   ├── Reports/ ...
│   ├── Sync/ ...
│   ├── Common/
│   │   ├── Interfaces/
│   │   ├── Behaviors/                          # MediatR pipeline behaviors
│   │   └── Mappings/
│   └── Laundry.Application.csproj
├── Laundry.Domain/                            # Entities, enums, value objects
│   ├── Entities/
│   │   ├── Invoice.cs
│   │   ├── Customer.cs
│   │   ├── Payment.cs
│   │   ├── JournalEntry.cs
│   │   └── ...
│   ├── Enums/
│   │   ├── OperationalStatus.cs
│   │   ├── FinancialStatus.cs
│   │   ├── PaymentMethod.cs
│   │   └── ...
│   ├── ValueObjects/
│   │   ├── Money.cs
│   │   ├── BranchId.cs
│   │   └── ...
│   ├── Events/
│   │   ├── InvoiceCreatedDomainEvent.cs
│   │   ├── PaymentReceivedDomainEvent.cs
│   │   └── ...
│   ├── Interfaces/
│   │   ├── IInvoiceRepository.cs
│   │   ├── IJournalEntryRepository.cs
│   │   ├── IUnitOfWork.cs
│   │   └── ...
│   └── Laundry.Domain.csproj
├── Laundry.Infrastructure/                    # EF Core, services, repositories
│   ├── Persistence/
│   │   ├── LaundryDbContext.cs
│   │   ├── Configurations/                    # EF Core IEntityTypeConfiguration
│   │   │   ├── InvoiceConfiguration.cs
│   │   │   └── ...
│   │   ├── Migrations/
│   │   └── Repositories/
│   ├── Accounting/
│   │   ├── AccountingRulesEngine.cs
│   │   └── JournalEntryGenerator.cs
│   ├── Licensing/
│   │   ├── LicenseValidator.cs
│   │   └── HardwareFingerprint.cs
│   ├── Sync/
│   │   ├── SyncExportService.cs
│   │   └── SyncImportService.cs
│   ├── Services/
│   │   └── DateTimeService.cs
│   └── Laundry.Infrastructure.csproj
└── Laundry.Tests/                             # xUnit tests
    ├── Unit/
    ├── Integration/
    └── Laundry.Tests.csproj

2. Complete NuGet Package List

Laundry.Api

Package Version Purpose
Microsoft.AspNetCore.Authentication.JwtBearer 10.* JWT authentication
Asp.Versioning.Mvc 8.* API versioning
Swashbuckle.AspNetCore 7.* Swagger / OpenAPI
Serilog.AspNetCore 9.* Structured logging
Serilog.Sinks.Seq 9.* Log to Seq
Serilog.Sinks.PostgreSQL 5.* Log to database
OpenTelemetry.Extensions.Hosting 1.* OpenTelemetry core
OpenTelemetry.Instrumentation.AspNetCore 1.* HTTP request tracing
AspNetCore.HealthChecks.UI 8.* Health dashboard
AspNetCore.HealthChecks.UI.Client 8.* Health UI client
AspNetCore.HealthChecks.Npgsql 8.* PostgreSQL health check
AspNetCore.HealthChecks.Uris 8.* Traefik/Seq health check

Laundry.Application

Package Version Purpose
MediatR 12.* CQRS — commands + queries
FluentValidation.AspNetCore 11.* Input validation
AutoMapper 13.* DTO ↔ Entity mapping
Microsoft.Extensions.Logging.Abstractions 10.* Logging abstractions

Laundry.Domain

Package Version Purpose
MediatR.Contracts 2.* Domain event contracts

Laundry.Infrastructure

Package Version Purpose
Npgsql.EntityFrameworkCore.PostgreSQL 10.* PostgreSQL EF Core provider
Microsoft.EntityFrameworkCore.Sqlite 10.* SQLite for offline branches
Microsoft.EntityFrameworkCore.Design 10.* EF Core CLI tools
Dapper 2.* Raw SQL for reports/ledger
Quartz.Extensions.Hosting 3.* Background job scheduling
BCrypt.Net-Next 4.* Password hashing
OpenTelemetry.Instrumentation.EntityFrameworkCore 1.* EF query tracing

Laundry.Tests

Package Version Purpose
xunit 2.* Test framework
Moq 4.* Mocking
FluentAssertions 7.* Assertion library
Testcontainers.PostgreSql 4.* Integration test DB
Microsoft.AspNetCore.Mvc.Testing 10.* Web API integration tests
coverlet.collector 6.* Code coverage

3. Clean Architecture Layers

┌──────────────────────────────────────────────┐
│                  Laundry.Api                  │
│  Controllers, Middleware, Filters, Swagger    │
│  → References Application + Infrastructure   │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│             Laundry.Application               │
│  Commands, Queries, Handlers, DTOs, Validators│
│  → References Domain only                    │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│               Laundry.Domain                  │
│  Entities, Enums, ValueObjects, Interfaces    │
│  → References nothing (pure C#)              │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│          Laundry.Infrastructure               │
│  DbContext, Repositories, Accounting, Sync    │
│  → References Domain + Application           │
└──────────────────────────────────────────────┘

Rule: Dependencies flow inward. Domain knows nothing about Infrastructure. Application knows nothing about Infrastructure. API wires everything via DI.


4. Dependency Injection Conventions

// Program.cs — Registration pattern
builder.Services.AddScoped<IInvoiceRepository, InvoiceRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IAccountingRulesEngine, AccountingRulesEngine>();
builder.Services.AddScoped<ILicenseValidator, LicenseValidator>();

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationAssembly).Assembly));
builder.Services.AddValidatorsFromAssembly(typeof(ApplicationAssembly).Assembly);
builder.Services.AddAutoMapper(typeof(ApplicationAssembly).Assembly);

// Pipeline behaviors (executed in order)
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));

5. EF Core — Code-First Configuration

DbContext

public class LaundryDbContext : DbContext
{
    public DbSet<Invoice> Invoices => Set<Invoice>();
    public DbSet<InvoiceItem> InvoiceItems => Set<InvoiceItem>();
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Payment> Payments => Set<Payment>();
    public DbSet<JournalEntry> JournalEntries => Set<JournalEntry>();
    public DbSet<JournalEntryLine> JournalEntryLines => Set<JournalEntryLine>();
    // ... all entities

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfigurationsFromAssembly(typeof(LaundryDbContext).Assembly);

        // UUID generation
        foreach (var entity in builder.Model.GetEntityTypes())
        {
            if (entity.ClrType.GetProperty("Id")?.PropertyType == typeof(Guid))
            {
                entity.FindProperty("Id")!.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never;
            }
        }
    }
}

Entity Configuration Example

public class InvoiceConfiguration : IEntityTypeConfiguration<Invoice>
{
    public void Configure(EntityTypeBuilder<Invoice> builder)
    {
        builder.ToTable("invoices");
        builder.HasKey(x => x.Id);

        builder.Property(x => x.Id).ValueGeneratedNever(); // UUID from app layer
        builder.Property(x => x.InvoiceNumber).HasMaxLength(50).IsRequired();
        builder.HasIndex(x => x.InvoiceNumber).IsUnique();
        builder.Property(x => x.BranchId).IsRequired();
        builder.Property(x => x.OperationalStatus)
            .HasConversion<string>().HasMaxLength(20);
        builder.Property(x => x.FinancialStatus)
            .HasConversion<string>().HasMaxLength(20);
        builder.Property(x => x.GrandTotal).HasColumnType("numeric(18,4)");
        // Money columns use numeric(18,4) — never float

        builder.HasMany(x => x.Items)
            .WithOne(x => x.Invoice)
            .HasForeignKey(x => x.InvoiceId);
    }
}

Migrations

# Create migration
dotnet ef migrations add InitialCreate -p Laundry.Infrastructure -s Laundry.Api

# Apply migration
dotnet ef database update -p Laundry.Infrastructure -s Laundry.Api

6. CQRS with MediatR

Command Example

// Command
public record CreateInvoiceCommand(
    Guid CustomerId,
    Guid BranchId,
    List<InvoiceItemDto> Items,
    string? Notes
) : IRequest<Result<InvoiceDto>>;

// Handler
public class CreateInvoiceHandler : IRequestHandler<CreateInvoiceCommand, Result<InvoiceDto>>
{
    private readonly IInvoiceRepository _repo;
    private readonly IAccountingRulesEngine _engine;
    private readonly IUnitOfWork _uow;

    public async Task<Result<InvoiceDto>> Handle(CreateInvoiceCommand request, CancellationToken ct)
    {
        // 1. Validate
        // 2. Create domain entity
        var invoice = Invoice.Create(request.CustomerId, request.BranchId, request.Items);
        // 3. Persist
        await _repo.AddAsync(invoice, ct);
        // 4. Generate accounting entries
        var entries = _engine.Evaluate("InvoiceCreated", invoice);
        await _uow.AddJournalEntriesAsync(entries, ct);
        // 5. Commit
        await _uow.SaveChangesAsync(ct);
        // 6. Return DTO
        return Result<InvoiceDto>.Success(_mapper.Map<InvoiceDto>(invoice));
    }
}

Query Example (Dapper for Reports)

// Query
public record GetCustomerBalanceQuery(Guid CustomerId) : IRequest<Result<decimal>>;

// Handler — uses Dapper, not EF Core
public class GetCustomerBalanceHandler : IRequestHandler<GetCustomerBalanceQuery, Result<decimal>>
{
    private readonly IDbConnection _db;

    public async Task<Result<decimal>> Handle(GetCustomerBalanceQuery request, CancellationToken ct)
    {
        var balance = await _db.QuerySingleOrDefaultAsync<decimal>(@"
            SELECT COALESCE(SUM(debit), 0) - COALESCE(SUM(credit), 0)
            FROM journal_entry_lines
            WHERE customer_id = @CustomerId
              AND account_code LIKE '120%'", new { request.CustomerId });
        return Result<decimal>.Success(balance);
    }
}

7. FluentValidation Setup

public class CreateInvoiceValidator : AbstractValidator<CreateInvoiceCommand>
{
    public CreateInvoiceValidator()
    {
        RuleFor(x => x.CustomerId).NotEmpty();
        RuleFor(x => x.BranchId).NotEmpty();
        RuleFor(x => x.Items).NotEmpty().Must(x => x.Count > 0)
            .WithMessage("Invoice must contain at least one item.");
        RuleForEach(x => x.Items).SetValidator(new InvoiceItemValidator());
    }
}

// MediatR pipeline behavior — auto-validates before handler
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);
            var results = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, ct)));
            var failures = results.SelectMany(r => r.Errors).Where(f => f != null).ToList();
            if (failures.Count != 0) throw new ValidationException(failures);
        }
        return await next();
    }
}

8. Serilog + OpenTelemetry + Seq

// Program.cs
builder.Host.UseSerilog((ctx, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.FromLogContext()
    .Enrich.WithMachineName()
    .WriteTo.Console()
    .WriteTo.Seq(ctx.Configuration["SeqServerUrl"] ?? "http://localhost:5341"));

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("Laundry.Api"))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://seq:5341/ingest/otlp/v1/traces")))
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddPrometheusExporter());

9. Health Checks Configuration

builder.Services.AddHealthChecks()
    .AddNpgsql(builder.Configuration.GetConnectionString("Default")!)
    .AddUrlGroup(new Uri("http://traefik:8080/ping"), "traefik")
    .AddUrlGroup(new Uri("http://seq:5341/health"), "seq");

builder.Services.AddHealthChecksUI(s => s
    .AddHealthCheckEndpoint("Laundry API", "/health"))
    .AddInMemoryStorage();

// Map endpoints
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.MapHealthChecksUI(o => o.UIPath = "/health-ui");
app.MapPrometheusScrapingEndpoint("/metrics");

10. Error Handling

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ValidationException ex)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new
            {
                error = "VALIDATION_ERROR",
                details = ex.Errors.Select(e => new { field = e.PropertyName, message = e.ErrorMessage })
            });
        }
        catch (NotFoundException ex)
        {
            context.Response.StatusCode = 404;
            await context.Response.WriteAsJsonAsync(new { error = "NOT_FOUND", message = ex.Message });
        }
        catch (LicenseExpiredException ex)
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsJsonAsync(new { error = "LICENSE_EXPIRED", message = ex.Message, daysRemaining = ex.DaysRemaining });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new { error = "INTERNAL_ERROR", message = "An unexpected error occurred." });
        }
    }
}

11. Code Conventions

Convention Rule
Naming PascalCase for public (types, methods, properties). camelCase for parameters, locals. _camelCase for private fields. I prefix for interfaces.
Files One class/enum/interface per file. File name matches type name.
Async All I/O operations are async. Suffix methods with Async. Use CancellationToken.
Nullable Enable nullable reference types in all projects (<Nullable>enable</Nullable>).
Records Use record for DTOs and value objects. Use record class for entities (EF Core compatible).
Errors Use Result<T> pattern for expected failures. Throw exceptions only for unexpected errors.
Money Use decimal for all monetary values. Map to numeric(18,4) in PostgreSQL. Never use float/double.
UUIDs All primary keys are Guid. Generated via Guid.NewGuid() in the application layer. Never auto-generated by DB.
Transactions Every command handler wraps in a DB transaction via the TransactionBehavior MediatR pipeline behavior.

12. appsettings.json Template

{
  "ConnectionStrings": {
    "Default": "Host=postgres;Port=5432;Database=laundry;Username=laundry;Password=${DB_PASSWORD}"
  },
  "BranchMode": "offline_sync",
  "BranchId": "550e8400-e29b-41d4-a716-446655440001",
  "BranchCode": "CAIRO-01",
  "Domain": "laundry.local",
  "SeqServerUrl": "http://seq:5341",
  "LicensePath": "/license/license.dat",
  "ClientTokenSecret": "",
  "Jwt": {
    "Key": "",
    "Issuer": "laundry-api",
    "ExpiryMinutes": 480
  },
  "Serilog": {
    "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning" } },
    "WriteTo": [
      { "Name": "Console" },
      { "Name": "Seq", "Args": { "serverUrl": "http://seq:5341" } }
    ]
  },
  "HealthChecksUI": {
    "HealthChecks": [ { "Name": "Laundry API", "Uri": "/health" } ],
    "EvaluationTimeInSeconds": 30
  }
}

13. Program.cs Template

var builder = WebApplication.CreateBuilder(args);

// Serilog
builder.Host.UseSerilog((ctx, cfg) =>
    cfg.ReadFrom.Configuration(ctx.Configuration)
       .Enrich.FromLogContext()
       .WriteTo.Console()
       .WriteTo.Seq(ctx.Configuration["SeqServerUrl"]!));

// Services
builder.Services.AddControllers();
builder.Services.AddApiVersioning();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Auth
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

// EF Core
builder.Services.AddDbContext<LaundryDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

// Dapper
builder.Services.AddScoped<IDbConnection>(_ =>
    new NpgsqlConnection(builder.Configuration.GetConnectionString("Default")));

// MediatR + FluentValidation + AutoMapper
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<ApplicationAssembly>());
builder.Services.AddValidatorsFromAssemblyContaining<ApplicationAssembly>();
builder.Services.AddAutoMapper(typeof(ApplicationAssembly));

// Health Checks + OpenTelemetry
builder.Services.AddHealthChecks()
    .AddNpgsql(builder.Configuration.GetConnectionString("Default")!);
builder.Services.AddHealthChecksUI(s => s.AddHealthCheckEndpoint("API", "/health"))
    .AddInMemoryStorage();

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t.AddAspNetCoreInstrumentation().AddOtlpExporter())
    .WithMetrics(m => m.AddAspNetCoreInstrumentation().AddPrometheusExporter());

// Application services
builder.Services.AddScoped<IInvoiceRepository, InvoiceRepository>();
builder.Services.AddScoped<IAccountingRulesEngine, AccountingRulesEngine>();
// ... all repositories and services

var app = builder.Build();

// Middleware pipeline
app.UseSerilogRequestLogging();
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseMiddleware<LicenseValidationMiddleware>();
app.UseMiddleware<ClientTokenMiddleware>();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseStaticFiles();        // Serve Angular wwwroot
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.MapHealthChecksUI(o => o.UIPath = "/health-ui");
app.MapPrometheusScrapingEndpoint("/metrics");
app.MapFallbackToFile("index.html"); // Angular SPA fallback

// Auto-run migrations on startup
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<LaundryDbContext>();
    await db.Database.MigrateAsync();
}

app.Run();

Revision History

Date Version Author Changes
2026-05-10 1.0 Backend Lead Initial backend libraries & architecture guide