A layered architecture organises code into projects that each have a single, bounded responsibility. The canonical modern .NET arrangement is four layers: Domain (the business model — pure C#, no infrastructure), Application (use cases, orchestration, and contracts), Infrastructure (data access, external services, IO), and Presentation (the ASP.NET Core API). Dependencies flow inward — outer layers know about inner layers, never the reverse. This structure is the direct ancestor of Clean Architecture and the exact layout used in the Capstone (Chapters 85–90).
Layer Responsibilities
// ── Domain Layer — the heart of the application ───────────────────────────
// BlogApp.Domain (class library, NO external dependencies)
// Contains:
// - Entities: Post, User, Comment (with domain logic and invariants)
// - Value Objects: Money, Slug, Email (immutable, equality by value)
// - Domain Events: PostPublishedEvent, UserRegisteredEvent
// - Domain Exceptions: BusinessRuleException, DomainValidationException
// - Enums: PostStatus, UserRole, ContentType
// Has ZERO project references — depends on nothing
public class Post // Domain entity
{
public int Id { get; private set; }
public string Title { get; private set; } = string.Empty;
public string Slug { get; private set; } = string.Empty;
public bool IsPublished { get; private set; }
private Post() { } // EF Core
public static Post Create(string title, string authorId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(title);
return new Post
{
Title = title.Trim(),
Slug = GenerateSlug(title),
};
}
public void Publish()
{
if (IsPublished) throw new BusinessRuleException("Post is already published.");
IsPublished = true;
}
private static string GenerateSlug(string title) =>
title.ToLowerInvariant().Trim().Replace(" ", "-");
}
Post.Publish() test needs no database, no HTTP context, no DI container — just a Post object and an assertion. The faster and simpler tests are, the more tests you write. This is the direct payoff of the domain isolation principle.Post.Publish() method that validates and sets state is far more maintainable than scattered publish logic across three different services.Layer Structure and Project References
// ── Application Layer — use cases and contracts ───────────────────────────
// BlogApp.Application (class library)
// References: BlogApp.Domain
// Contains:
// - Interfaces: IPostRepository, IEmailSender, IFileStorage
// - DTOs: PostDto, CreatePostRequest, PostSummaryDto
// - Validators: CreatePostRequestValidator (FluentValidation)
// - Services: PostService, UserService (implement application use cases)
// - Mappings: PostMappingProfile (AutoMapper or manual mappers)
public interface IPostRepository
{
Task<Post?> GetByIdAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<Post>> GetPublishedAsync(int page, int size, CancellationToken ct = default);
Task<Post> CreateAsync(Post post, CancellationToken ct = default);
Task<Post> UpdateAsync(Post post, CancellationToken ct = default);
}
// ── Infrastructure Layer — implementations ────────────────────────────────
// BlogApp.Infrastructure (class library)
// References: BlogApp.Application (for the interfaces to implement)
// Contains:
// - EfPostRepository : IPostRepository
// - SmtpEmailSender : IEmailSender
// - AppDbContext and EF Core configuration
// - External API clients
// ── Presentation (API) Layer — HTTP concerns ──────────────────────────────
// BlogApp.Api (webapi project)
// References: BlogApp.Application + BlogApp.Infrastructure
// Contains:
// - Controllers: PostsController, UsersController
// - Middleware: ExceptionHandlingMiddleware, AuthenticationMiddleware
// - Program.cs: DI registration, pipeline configuration
// - appsettings.json
Common Mistakes
Mistake 1 — Domain entities with no behaviour (Anemic Domain Model)
❌ Wrong — all properties public setters, no domain methods:
public class Post { public bool IsPublished { get; set; } } // anemic!
✅ Correct — encapsulate state transitions in domain methods with invariant enforcement.
Mistake 2 — Infrastructure depending on nothing (not implementing Application interfaces)
Infrastructure must reference Application to implement the interfaces defined there. The common mistake is defining repository interfaces in Infrastructure itself — this means Application must reference Infrastructure to use its own repositories, inverting the dependency direction.