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
PostAggregate just to add oneCommentis extremely inefficient. - Therefore, a
Commentwill be its own Aggregate Root.
Domain/Aggregates/Comment.cs (Rich Domain Model)
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
// 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
// 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 methodComment.Create(), ensuring data integrity. The logic is entirely encapsulated in theDomain. - 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?