Skip to main content

Domain-Driven in .NET

About 3 min

Domain-Driven in .NET

Introduction

Domain-Driven Design (DDD) is an approach to building software where the business domain, not the technology, drives the design. DDD helps teams build maintainable, testable, and expressive systems by putting the focus on core business logic.

In DDD, the domain layer is the heart of the application. It's where we place the business rules, decisions, and invariants that define how the system is allowed to behave. Instead of scattering logic across controllers, services, use cases, or EF Core configurations, DDD encourages us to keep all business-critical behavior inside the domain model itself.

This means:

  • The domain objects protect their own invariants (e.g., a Lego set must always have a name and a positive number of pieces)
  • They expose behavior, not just data (e.g., Update, Delete, Create)
  • They prevent invalid states from ever being created (e.g., you cannot update a deleted Lego set)
  • They are independent of frameworks (no EF Core, no ASP.NET, no JSON attributes)

By structuring the model this way, the rest of the system becomes simpler. Controllers act as input/output mappers, application is responsible for the mapping (DTO - Entity), and the domain stays focused purely on rules and meaning.

LegoSet entity example

Below is a LegoSet aggregate root that follows these principles. It encapsulates:

  • Creation constraints
  • Update rules
  • Soft-delete behavior
  • Protection of invariants
  • Full control over its own state

This is a small example, but it represents the central DDD idea: put the business logic where it belongs: inside the domain.

namespace SweDemoBackend.Domain.Entities
{
  public class LegoSet
  {
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public int NumberOfPieces { get; private set; }
    public bool IsDeleted { get; private set; }

    private LegoSet(string name, int numberOfPieces)
    {
      if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Name required", nameof(name));
      if (numberOfPieces <= 0) throw new ArgumentOutOfRangeException(nameof(numberOfPieces));
      if(Id == Guid.Empty)
      {
        Id = Guid.NewGuid();
      }
      Name = name.Trim();
      NumberOfPieces = numberOfPieces;
    }

    public static LegoSet Create(string name, int numberOfPieces)
        => new(name, numberOfPieces);

    public void Update(string? newName, int? newPieceCount)
    {
      if (IsDeleted) throw new InvalidOperationException("Cannot update a deleted LegoSet.");

      if (newName is not null)
      {
        if (string.IsNullOrWhiteSpace(newName))
          throw new ArgumentException("Name required", nameof(newName));

        Name = newName.Trim();
      }

      if (newPieceCount is not null)
      {
        if (newPieceCount <= 0)
          throw new ArgumentOutOfRangeException(nameof(newPieceCount));

        NumberOfPieces = newPieceCount.Value;
      }
    }

    public void Delete()
    {
      if (IsDeleted) return;
      IsDeleted = true;
    }
  }
}

Code examples

Clean Architecture Light

As you can see below (the Controller code example) you can notice that we inject and use the repository directly in our controller. In our light implementation of Clean Architecture we'll only use the Application layer for the mapping between DTOs and Entities (and vice versa).

To summarize:

Controllers (API layer)

  • Injects repositories
  • Injects mapping

Application layer

  • Contains repository interfaces
  • Contains mapping profiles
  • Registers AutoMapper

Domain layer

  • Contains entities structured as discussed before

Contracts layer

  • Contains the Dtos

Infrastructure layer

  • Contains the implementation of the repositories
  • Contains all EF Core classes (DbContext, Migrations, ...)

Controller

using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using SweDemoBackend.Application.Interfaces.Repositories;
using SweDemoBackend.Contracts.Dtos;
using SweDemoBackend.Domain.Entities;

namespace swe_demo_api.Controllers
{
  [Route("api/[controller]")]
  [ApiController]
  public class LegoSetController : ControllerBase
  {
    [HttpGet]
    //[Authorize]
    public async Task<ActionResult<List<LegoSetResponseDto>>> GetAllLegoSets(
      [FromServices] ILegoSetRepository repo,
      [FromServices] IMapper mapper,
      CancellationToken ct
    )
    {
      //string userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;
      var entities = await repo.GetLegoSetsAsync(ct);
      return Ok(mapper.Map<List<LegoSetResponseDto>>(entities));
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<LegoSetResponseDto>> GetById(Guid id,
      [FromServices] ILegoSetRepository repo,
      [FromServices] IMapper mapper,
      CancellationToken ct)
    {
      var entity = await repo.GetByIdAsync(id, ct);

      if (entity != null)
      {
        return Ok(mapper.Map<LegoSetResponseDto>(entity));
      }
      else
      {
        return NotFound();
      }
    }

    [HttpPost]
    public async Task<ActionResult<LegoSetResponseDto>> Create(
      [FromBody] CreateLegoSetDto request,
      [FromServices] ILegoSetRepository repo,
      [FromServices] IMapper mapper,
      CancellationToken ct
    )
    {
      var set = LegoSet.Create(request.Name, request.NumberOfPieces);
      await repo.AddAsync(set, ct);
      await repo.SaveChangesAsync(ct);

      return Ok(mapper.Map<LegoSetResponseDto>(set));
    }

    [HttpPut("{id:guid}")]
    public async Task<IActionResult> Update(
            Guid id,
            [FromBody] UpdateLegoSetDto request,
            [FromServices] ILegoSetRepository repo,
            CancellationToken ct)
    {
      var legoSet = await repo.GetByIdAsync(id, ct);
      if (legoSet is null) return NotFound();

      legoSet.Update(request.Name, request.NumberOfPieces);

      await repo.SaveChangesAsync(ct);


      return NoContent();
    }

    [HttpDelete("{id:guid}")]
    public async Task<IActionResult> Delete(Guid id,
      [FromServices] ILegoSetRepository repo,
      CancellationToken ct)
    {
      var legoSet = await repo.GetByIdAsync(id, ct);
      if (legoSet is null) return NotFound();

      await repo.DeleteAsync(legoSet, ct);
      await repo.SaveChangesAsync(ct);

      return NoContent();
    }
  }
}

Contracts

namespace SweDemoBackend.Contracts.Dtos
{
  public class CreateLegoSetDto
  {
    public string Name { get; set; }
    public int NumberOfPieces { get; set; }
  }

  public class UpdateLegoSetDto
  {
    public string? Name { get; set; }
    public int? NumberOfPieces { get; set; }
  }

  public class LegoSetResponseDto
  {
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int NumberOfPieces { get; set; }
  }
}

Application

using AutoMapper;
using SweDemoBackend.Contracts.Dtos;
using SweDemoBackend.Domain.Entities;

namespace SweDemoBackend.Application.MappingProfiles
{
  public class LegoSetProfile : Profile
  {
    public LegoSetProfile()
    {
      CreateMap<LegoSet, LegoSetResponseDto>();
    }
  }
}
using SweDemoBackend.Domain.Entities;

namespace SweDemoBackend.Application.Interfaces.Repositories
{
  public interface ILegoSetRepository
  {
    Task<IEnumerable<LegoSet>> GetLegoSetsAsync(CancellationToken ct = default);
    Task<LegoSet?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task AddAsync(LegoSet legoSet, CancellationToken ct);
    Task DeleteAsync(LegoSet legoSet, CancellationToken cancellationToken = default);
    Task SaveChangesAsync(CancellationToken ct = default);
  }
}

Infrastructure

using Microsoft.EntityFrameworkCore;
using SweDemoBackend.Application.Interfaces.Repositories;
using SweDemoBackend.Domain.Entities;

namespace SweDemoBackend.Infrastructure.Repositories
{
  public class LegoSetRepository : ILegoSetRepository
  {
    private readonly BricksForKidsDbContext _db;

    public LegoSetRepository(BricksForKidsDbContext db)
    {
      _db = db;
    }

    public async Task<IEnumerable<LegoSet>> GetLegoSetsAsync(CancellationToken ct = default)
    {
      return await _db.LegoSets.ToListAsync();
    }

    public async Task AddAsync(LegoSet legoSet, CancellationToken ct)
    {
      await _db.LegoSets.AddAsync(legoSet, ct);
    }

    public async Task<LegoSet?> GetByIdAsync(Guid id, CancellationToken ct = default)
    {
      return await _db
                .LegoSets
                .FirstOrDefaultAsync(x => x.Id == id && !x.IsDeleted, ct);
    }

    public Task DeleteAsync(LegoSet legoSet, CancellationToken cancellationToken = default)
    {
      legoSet.Delete();
      return Task.CompletedTask;
    }

    public async Task SaveChangesAsync(CancellationToken ct = default)
    {
      await _db.SaveChangesAsync(ct);
    }
  }
}