Layered Architecture — Organising a Clean Solution

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(" ", "-");
}
Note: The Domain layer is deliberately isolated — no EF Core, no ASP.NET Core, no JSON libraries, no external services. This isolation has a practical benefit: the domain can be unit-tested with pure C# and no test infrastructure. A 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.
Tip: The Application layer should contain only orchestration — calling repositories, raising domain events, sending emails, returning DTOs. Business rules (invariants, policies, validations that reflect domain knowledge) belong in the Domain layer. Keeping business rules in the Domain means they apply consistently regardless of how the use case is triggered — HTTP request, CLI command, background job, or test. Business rules in Application can be bypassed by calling the repository directly.
Warning: Avoid creating an “Anemic Domain Model” — entities that are just data containers (only getters/setters, no behaviour) with all logic in Application services. When all logic is in Application services, it tends to be duplicated across services, difficult to find, and easy to bypass. Move invariants, state transition logic, and business rules into the entity methods where they belong. A 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.

🧠 Test Yourself

A developer puts IPostRepository in the Infrastructure project. What dependency problem does this create?