Skip to content

Chapter 2: Designing the Backend API (ASP.NET Core)

Excellent! With a solid database schema in place, we will now build the Backend API layer, applying the DDD and CQRS patterns we've discussed.


Architecture Overview

We will follow the Clean Architecture (or Onion Architecture) principles, dividing the project into clear layers: Domain, Application, Infrastructure, and WebAPI.

Let's focus on a specific business flow: Commenting on a post.


1. The "Write" Side (Command Side): Creating a New Comment

This is the processing flow when a user submits a comment.

a. Aggregate Design

A critical question arises: Should a Comment be part of the Post Aggregate?

  • The answer is no. A post can have thousands of comments. Loading the entire Post Aggregate just to add one Comment is extremely inefficient.
  • Therefore, a Comment will be its own Aggregate Root.

Domain/Aggregates/Comment.cs (Rich Domain Model)

csharp
public class Comment
{
    public Guid Id { get; private set; }
    public Guid PostId { get; private set; }
    public Guid AuthorId { get; private set; }
    public string Content { get; private set; }
    public DateTime CreatedAt { get; private set; }

    // Use a private constructor to control object creation
    private Comment() {}

    // Use a factory method to encapsulate initialization and validation logic
    public static Comment Create(Guid postId, Guid authorId, string content)
    {
        if (string.IsNullOrWhiteSpace(content))
        {
            throw new ArgumentException("Comment content cannot be empty.");
        }

        return new Comment
        {
            Id = Guid.NewGuid(), // A UUIDv7 would be generated here
            PostId = postId,
            AuthorId = authorId,
            Content = content,
            CreatedAt = DateTime.UtcNow
        };
    }
}

b. Command and Handler Design

We will use the Mediator pattern (the MediatR library is very popular) to decouple commands.

Application/Comments/Commands/CreateComment.cs

csharp
// The Command is just a DTO containing the necessary data
public record CreateCommentCommand(Guid PostId, Guid AuthorId, string Content) : IRequest<Guid>;

// The Handler contains the orchestration logic
public class CreateCommentCommandHandler : IRequestHandler<CreateCommentCommand, Guid>
{
    private readonly ICommentRepository _commentRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateCommentCommandHandler(ICommentRepository commentRepository, IUnitOfWork unitOfWork)
    {
        _commentRepository = commentRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Guid> Handle(CreateCommentCommand request, CancellationToken cancellationToken)
    {
        // 1. Use the Aggregate's factory method to create the object
        var comment = Comment.Create(request.PostId, request.AuthorId, request.Content);

        // 2. Use the Repository to add it
        await _commentRepository.AddAsync(comment);

        // 3. Save the transaction
        await _unitOfWork.SaveChangesAsync(cancellationToken);

        return comment.Id;
    }
}

2. The "Read" Side (Query Side): Fetching a List of Comments

This is the processing flow to display comments under a post.

a. Query and DTO Design

We will completely bypass the Aggregate and Repository. The Query will directly query the database for maximum performance.

Application/Comments/Queries/GetComments.cs

csharp
// A flat DTO, containing only the data to be displayed
public record CommentDto(Guid Id, string AuthorUsername, string Content, DateTime CreatedAt);

// Query
public record GetCommentsQuery(Guid PostId) : IRequest<List<CommentDto>>;

// Query Handler
public class GetCommentsQueryHandler : IRequestHandler<GetCommentsQuery, List<CommentDto>>
{
    private readonly AppDbContext _context; // Directly using DbContext

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

    public async Task<List<CommentDto>> Handle(GetCommentsQuery request, CancellationToken cancellationToken)
    {
        // Using AsNoTracking and Select for the highest read performance
        return await _context.Comments
            .Where(c => c.PostId == request.PostId)
            .OrderBy(c => c.CreatedAt)
            .Select(c => new CommentDto(
                c.Id,
                c.Author.Username, // EF Core will automatically create JOIN
                c.Content,
                c.CreatedAt
            ))
            .AsNoTracking()
            .ToListAsync(cancellationToken);
    }
}

Performance Analysis

  • Write Flow (Command): Very tight and safe. Every new comment must go through the factory method Comment.Create(), ensuring data integrity. The logic is entirely encapsulated in the Domain.
  • Read Flow (Query): Very fast and efficient. It doesn't incur the overhead of creating complex domain objects, just fetching exactly what the UI needs.

This is the power of CQRS combined with DDD. You have a backend that is both secure and maintainable on the write side, and super fast on the read side.

The next step is to design the Frontend to interact with these APIs. Are you ready?