Clean Architecture Dependency Rules
🎯 Purpose
This is a CRITICAL GUARDRAIL that enforces Clean Architecture's fundamental dependency rule: dependencies flow inward only. Violations are BLOCKED to prevent architectural degradation.
⚡ When This Skill Activates
Automatically BLOCKS when:
- Attempting to add wrong project references
- Importing namespaces that violate dependency rules
- Detecting prohibited
usingstatements in Domain or Application layers
Triggered by:
- Keywords: "dependency", "reference", "architecture", "layer", "add project"
- File patterns: Domain//*.cs, Application//*.cs
- Content patterns:
using Explore.Infrastructure,using Microsoft.EntityFrameworkCorein Domain
🚨 The Dependency Rule
┌─────────────────────────────────────────────────────────────┐
│ ISLAMU EVENT ARCHITECTURE LAYERS │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. DOMAIN (Core) │ │
│ │ Explore.Domain │ │
│ │ ↑ NO DEPENDENCIES │ │
│ │ • Entities, Enums, Value Objects, Domain Events │ │
│ │ • Pure C# - No framework dependencies │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ References │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 2. APPLICATION (Use Cases) │ │
│ │ Explore.Application │ │
│ │ ↑ References: Domain ONLY │ │
│ │ • CQRS Commands/Queries, DTOs, Interfaces │ │
│ │ • MediatR, FluentValidation, AutoMapper │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ References │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 3. INFRASTRUCTURE (Implementation) │ │
│ │ Explore.Persistence + Explore.Infrastructure │ │
│ │ ↑ References: Application, Domain │ │
│ │ • DbContext, Repositories, External APIs │ │
│ │ • EF Core, PostgreSQL, Email, File Storage │ │
│ └─────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ References │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 4. PRESENTATION (Entry Points) │ │
│ │ Explore.API + Explore.Blazor │ │
│ │ ↑ References: ALL (Composition Root) │ │
│ │ • Controllers, Pages, Dependency Registration │ │
│ │ • ASP.NET Core, MudBlazor, SignalR │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
📚 Resources
| Resource | Description |
|---|---|
| dependency-rules.md | Complete dependency matrix and flow diagram |
| layer-responsibilities.md | What code belongs in each layer |
| violation-examples.md | Common violations and error messages |
| fix-patterns.md | How to fix violations using interfaces and DI |
✅ Valid Dependency Examples
// ✅ VALID: Application references Domain
namespace Explore.Application.Features.Events.Commands;
using Explore.Domain.Entities; // ✅ OK - App can reference Domain
using Explore.Domain.Enums; // ✅ OK
using MediatR; // ✅ OK - Framework dependency
// ✅ VALID: Infrastructure references Application and Domain
namespace Explore.Persistence.Repositories;
using Explore.Application.Interfaces; // ✅ OK - Implements interfaces
using Explore.Domain.Entities; // ✅ OK - Works with entities
using Microsoft.EntityFrameworkCore; // ✅ OK - Infrastructure can use EF Core
// ✅ VALID: API references all layers
namespace Explore.API.Controllers;
using Explore.Application.Features.Events.Commands; // ✅ OK
using Explore.Infrastructure.Services; // ✅ OK
using MediatR; // ✅ OK
❌ BLOCKED Violations
// ❌ BLOCKED: Domain referencing ANYTHING
namespace Explore.Domain.Entities;
using Microsoft.EntityFrameworkCore; // ❌ BLOCKED! Domain must be pure
using Explore.Application.DTOs; // ❌ BLOCKED! Dependency flows wrong way
// ❌ BLOCKED: Application referencing Infrastructure
namespace Explore.Application.Features.Events.Queries;
using Explore.Infrastructure.Persistence; // ❌ BLOCKED! Use interfaces instead
using Explore.API.Controllers; // ❌ BLOCKED! Wrong direction
// ❌ BLOCKED: Application referencing Presentation
namespace Explore.Application.Commands;
using Microsoft.AspNetCore.Mvc; // ❌ BLOCKED! Application must be framework-agnostic
🔧 Quick Fix: Use Dependency Inversion
Problem: Application needs database access (Infrastructure)
❌ Wrong - Direct dependency:
// In Explore.Application
using Explore.Infrastructure.Persistence; // ❌ BLOCKED
public class GetEventsHandler
{
private readonly ApplicationDbContext _context; // ❌ Concrete class
}
✅ Correct - Interface in Application, Implementation in Infrastructure:
// Step 1: Define interface in Application layer
// File: Explore.Application/Interfaces/IEventRepository.cs
namespace Explore.Application.Interfaces;
public interface IEventRepository
{
Task<List<Event>> GetAllAsync(CancellationToken cancellationToken);
}
// Step 2: Use interface in Application
// File: Explore.Application/Features/Events/Queries/GetEventListHandler.cs
namespace Explore.Application.Features.Events.Queries;
using Explore.Application.Interfaces; // ✅ OK - Same layer
public class GetEventListHandler : IRequestHandler<GetEventListQuery, List<EventDto>>
{
private readonly IEventRepository _repository; // ✅ Abstraction
public GetEventListHandler(IEventRepository repository)
{
_repository = repository;
}
public async Task<List<EventDto>> Handle(GetEventListQuery request, CancellationToken cancellationToken)
{
var events = await _repository.GetAllAsync(cancellationToken);
return events.Select(e => e.ToDto()).ToList();
}
}
// Step 3: Implement in Infrastructure layer
// File: Explore.Persistence/Repositories/EventRepository.cs
namespace Explore.Persistence.Repositories;
using Explore.Application.Interfaces; // ✅ OK - Implements interface
using Explore.Domain.Entities; // ✅ OK - Works with entities
using Microsoft.EntityFrameworkCore; // ✅ OK - Infrastructure can use EF Core
public class EventRepository : IEventRepository
{
private readonly ApplicationDbContext _context;
public async Task<List<Event>> GetAllAsync(CancellationToken cancellationToken)
{
return await _context.Events.ToListAsync(cancellationToken);
}
}
// Step 4: Register in API/Blazor (Composition Root)
// File: Explore.API/Program.cs or Explore.AppHost/Program.cs
builder.Services.AddScoped<IEventRepository, EventRepository>(); // ✅ DI binding
🎓 Why This Matters
Benefits of Clean Architecture:
- Testability: Domain and Application can be tested without database
- Flexibility: Swap PostgreSQL for SQL Server without changing business logic
- Maintainability: Business logic isolated from framework changes
- Team Scalability: Clear boundaries for parallel development
- Deployment Options: Domain can be reused across API, Blazor, CLI, etc.
Cost of Violations:
- Tight coupling makes testing difficult
- Framework upgrades break business logic
- Cannot reuse domain logic across projects
- Circular dependencies cause build failures
📖 Deep Dive
For comprehensive guidance:
- Dependency Matrix: dependency-rules.md
- Layer Responsibilities: layer-responsibilities.md
- Common Violations: violation-examples.md
- Fix Patterns: fix-patterns.md
🔑 CRITICAL: Validator Manual Instantiation Pattern
Rule: Validators Must Be Instantiated Manually, NOT DI Injected
BLOCKED Violation: Validators injected via DI in handler constructor
// ❌ BLOCKED: DI injection of validators
public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, BaseCommandResponse<Guid>>
{
private readonly IValidator<CreateEventDto> _validator; // ❌ BLOCKED!
public CreateEventCommandHandler(
IEventRepository eventRepository,
IMapper mapper,
IValidator<CreateEventDto> validator) // ❌ BLOCKED - DI injection
{
_validator = validator; // ❌ BLOCKED
}
public async Task<BaseCommandResponse<Guid>> Handle(...)
{
var validationResult = await _validator.ValidateAsync(request.EventDto); // ❌ BLOCKED
...
}
}
✅ Correct Pattern: Validator instantiated with dependencies passed to constructor
Real Example from Explore.Application/Features/Events/Handlers/Commands/CreateEventCommandHandler.cs:
// ✅ CORRECT: Manual instantiation with dependencies
namespace Explore.Application.Features.Events.Handlers.Commands;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Explore.Application.Contracts.Persistence;
using Explore.Application.DTOs.Event.Validators;
using Explore.Application.Features.Events.Requests.Commands;
using Explore.Application.Responses;
using Explore.Domain;
using MediatR;
public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, BaseCommandResponse<Guid>>
{
private readonly IEventRepository _eventRepository;
private readonly IAudienceAgeRepository _audienceAgeRepository;
private readonly IAudienceGenderRepository _audienceGenderRepository;
private readonly IEventTypeRepository _eventTypeRepository;
private readonly IActorRepository _actorRepository;
private readonly IStorageObjectRepository _storageObjectRepository;
private readonly IMapper _mapper;
public CreateEventCommandHandler(
IEventRepository eventRepository,
IAudienceAgeRepository audienceAgeRepository,
IAudienceGenderRepository audienceGenderRepository,
IEventTypeRepository eventTypeRepository,
IActorRepository actorRepository,
IStorageObjectRepository storageObjectRepository,
IMapper mapper)
{
_eventRepository = eventRepository;
_audienceAgeRepository = audienceAgeRepository;
_audienceGenderRepository = audienceGenderRepository;
_eventTypeRepository = eventTypeRepository;
_actorRepository = actorRepository;
_storageObjectRepository = storageObjectRepository;
_mapper = mapper;
}
public async Task<BaseCommandResponse<Guid>> Handle(CreateEventCommand request, CancellationToken cancellationToken)
{
var response = new BaseCommandResponse<Guid>();
// ✅ CORRECT: Validator instantiated manually with all dependencies
var validator = new CreateEventDtoValidator(
_audienceAgeRepository,
_audienceGenderRepository,
_eventTypeRepository,
_actorRepository,
_storageObjectRepository);
var validationResult = await validator.ValidateAsync(request.EventDto);
if (!validationResult.IsValid)
{
response.Success = false;
response.Message = "Event creation failed.";
response.Errors = validationResult.Errors.Select(e => e.ErrorMessage).ToList();
return response;
}
// Map DTO to Entity
var @event = _mapper.Map<Event>(request.EventDto);
@event.TotalViews = 0; // Set non-mapped properties
// Save through repository
@event = await _eventRepository.Create(@event);
response.Success = true;
response.Id = @event.Id;
response.Message = "Event created successfully.";
return response;
}
}
Why Manual Instantiation?
- Fine-grained dependency control: Each validator receives specific repositories it needs
- Prevents DI configuration issues: No need to register validators in DI container
- Simplifies testing: Easy to create test validators with mocked repositories
- Follows dbml-sync pattern: Consistent with 45+ entity implementations
Validator Constructor Pattern
Validators MUST accept repositories in constructor for FK validation.
Real Example from Explore.Application/DTOs/Event/Validators/CreateEventDtoValidator.cs:
namespace Explore.Application.DTOs.Event.Validators;
using FluentValidation;
using Explore.Application.Contracts.Persistence;
public class CreateEventDtoValidator : AbstractValidator<CreateEventDto>
{
private readonly IAudienceAgeRepository _audienceAgeRepository;
private readonly IAudienceGenderRepository _audienceGenderRepository;
private readonly IEventTypeRepository _eventTypeRepository;
private readonly IActorRepository _actorRepository;
private readonly IStorageObjectRepository _storageObjectRepository;
public CreateEventDtoValidator(
IAudienceAgeRepository audienceAgeRepository,
IAudienceGenderRepository audienceGenderRepository,
IEventTypeRepository eventTypeRepository,
IActorRepository actorRepository,
IStorageObjectRepository storageObjectRepository)
{
_audienceAgeRepository = audienceAgeRepository;
_audienceGenderRepository = audienceGenderRepository;
_eventTypeRepository = eventTypeRepository;
_actorRepository = actorRepository;
_storageObjectRepository = storageObjectRepository;
// Standard validation rules
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Title is required")
.MaximumLength(500);
// Foreign key validation with repository
RuleFor(x => x.AudienceAgeId)
.NotEmpty().WithMessage("Audience Age is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _audienceAgeRepository.Exists(id);
return exists;
})
.WithMessage("Audience Age not found");
RuleFor(x => x.EventTypeId)
.NotEmpty().WithMessage("Event Type is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _eventTypeRepository.Exists(id);
return exists;
})
.WithMessage("Event Type not found");
RuleFor(x => x.ActorId)
.NotEmpty().WithMessage("Actor is required")
.MustAsync(async (id, cancellation) =>
{
var exists = await _actorRepository.Exists(id);
return exists;
})
.WithMessage("Actor not found");
// Optional FK validation
RuleFor(x => x.FeaturedImageId)
.MustAsync(async (id, cancellation) =>
{
if (!id.HasValue) return true;
var exists = await _storageObjectRepository.Exists(id.Value);
return exists;
})
.WithMessage("Featured Image not found");
}
}
Enforcement Level: 🚨 BLOCK (Violations are prevented)
Override: Add @skip-architecture-check comment in file (use sparingly)