Prompt file imported from ThiagoGuislotti/copilot-instructions (
.github/prompts/refactor-to-clean-architecture.prompt.md). Fill in{{targetPath}},{{currentArchitecture}},{{hasTests}},{{targetLayer}},{{EntityName}}before use. Copyright stays with the author.
Refactor to Clean Architecture
Analyze existing code and refactor to Clean Architecture following SOLID principles.
Instructions
Refactor code based on:
Input Variables
{{targetPath}}- File or directory path{{currentArchitecture}}- Current pattern{{hasTests}}- Test coverage status{{targetLayer}}- Clean Architecture layer
Clean Architecture Layers
1. Domain Layer (Core)
Purpose: Business entities, value objects, domain logic
Dependencies: None (Pure business logic)
Location: src/ProjectName.Domain/
Domain/
Entities/ # Business entities with behavior
ValueObjects/ # Immutable value types
Enums/ # Domain enumerations
Interfaces/ # Repository and service abstractions
Exceptions/ # Domain-specific exceptions
Events/ # Domain events
Refactoring Steps:
- Identify business entities and extract to
Entities/ - Extract value objects (immutable types) to
ValueObjects/ - Move domain logic from services to entity methods
- Define repository interfaces (no implementations)
- Create domain-specific exceptions
- Remove all infrastructure dependencies
Example Entity:
namespace ProjectName.Domain.Entities;
/// <summary>
/// Represents a {{EntityName}} in the business domain
/// </summary>
public sealed class {{EntityName}}
{
public int Id { get; private set; }
public string Name { get; private set; }
public DateTime CreatedAt { get; private set; }
private {{EntityName}}() { } // EF Core constructor
public static {{EntityName}} Create(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("{{EntityName}} name cannot be empty");
return new {{EntityName}}
{
Name = name,
CreatedAt = DateTime.UtcNow
};
}
public void UpdateName(string newName)
{
if (string.IsNullOrWhiteSpace(newName))
throw new DomainException("{{EntityName}} name cannot be empty");
Name = newName;
}
}
2. Application Layer (Use Cases)
Purpose: Application logic, CQRS commands/queries, DTOs
Dependencies: Domain layer only
Location: src/ProjectName.Application/
Application/
Commands/ # CQRS commands with handlers
Queries/ # CQRS queries with handlers
Dtos/ # Data transfer objects
Requests/ # Input DTOs
Responses/ # Output DTOs
Mapping/ # Entity ↔ DTO mappers
Validation/ # Input validation (FluentValidation)
Interfaces/ # Application service abstractions
Exceptions/ # Application-specific exceptions
Refactoring Steps:
- Extract API controllers logic to command/query handlers
- Create DTOs for all API inputs/outputs
- Implement request validators with FluentValidation
- Add MediatR for CQRS pattern
- Create mappers for Entity ↔ DTO conversions
- Remove infrastructure dependencies from handlers
Example Command + Handler:
// Command
public sealed record Create{{EntityName}}Command(Create{{EntityName}}Request Request)
: IRequest<Result<{{EntityName}}Response>>;
// Handler
public sealed class Create{{EntityName}}Handler
: IRequestHandler<Create{{EntityName}}Command, Result<{{EntityName}}Response>>
{
private readonly I{{EntityName}}Repository _repository;
public Create{{EntityName}}Handler(I{{EntityName}}Repository repository)
{
_repository = repository;
}
public async Task<Result<{{EntityName}}Response>> Handle(
Create{{EntityName}}Command command,
CancellationToken cancellationToken)
{
// Business logic using domain entities
var entity = {{EntityName}}.Create(command.Request.Name);
await _repository.AddAsync(entity, cancellationToken);
return Result<{{EntityName}}Response>.Success(new {{EntityName}}Response
{
Id = entity.Id,
Name = entity.Name
});
}
}
3. Infrastructure Layer (Implementation)
Purpose: External concerns (DB, files, APIs, caching)
Dependencies: Domain, Application layers
Location: src/ProjectName.Infrastructure/
Infrastructure/
Persistence/ # EF Core DbContext, configurations
Repositories/ # Repository implementations
Services/ # External service integrations
Caching/ # Caching implementations
Messaging/ # Message queue implementations
Storage/ # File storage implementations
Refactoring Steps:
- Move EF Core DbContext to
Persistence/ - Move repository implementations to
Repositories/ - Extract external API clients to
Services/ - Configure entity mappings with Fluent API
- Implement repository interfaces from Domain
- Remove direct DB access from Application/API layers
Example Repository:
namespace ProjectName.Infrastructure.Repositories;
public sealed class {{EntityName}}Repository : I{{EntityName}}Repository
{
private readonly ApplicationDbContext _context;
public {{EntityName}}Repository(ApplicationDbContext context)
{
_context = context;
}
public async Task<{{EntityName}}?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
return await _context.{{EntityName}}s
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
}
public async Task AddAsync({{EntityName}} entity, CancellationToken cancellationToken)
{
await _context.{{EntityName}}s.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}
4. API/Presentation Layer (Entry Point)
Purpose: HTTP endpoints, authentication, Swagger
Dependencies: Application layer (uses commands/queries via MediatR)
Location: src/ProjectName.Api/
Api/
Controllers/ # Thin controllers (delegate to MediatR)
Middleware/ # Exception handling, logging
Filters/ # Action filters, authorization
Configuration/ # Startup configuration
Properties/ # Launch settings
Refactoring Steps:
- Thin down controllers - only routing and delegation
- Move business logic to Application handlers
- Add middleware for exception handling
- Configure dependency injection in Program.cs
- Add Swagger/OpenAPI documentation
- Remove direct repository/DbContext access
Example Controller:
[ApiController]
[Route("api/[controller]")]
public sealed class {{EntityName}}Controller : ControllerBase
{
private readonly IMediator _mediator;
public {{EntityName}}Controller(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
[ProducesResponseType(typeof({{EntityName}}Response), StatusCodes.Status201Created)]
public async Task<ActionResult<{{EntityName}}Response>> Create(
[FromBody] Create{{EntityName}}Request request,
CancellationToken cancellationToken)
{
var command = new Create{{EntityName}}Command(request);
var result = await _mediator.Send(command, cancellationToken);
return result.IsSuccess
? CreatedAtAction(nameof(GetById), new { id = result.Value.Id }, result.Value)
: BadRequest(result.Error);
}
}
Refactoring Process
Phase 1: Analysis
- Identify current architecture patterns
- Map dependencies between components
- Locate business logic scattered across layers
- Document data flow and external dependencies
- Assess test coverage and quality
Phase 2: Domain Extraction
- Create Domain project
- Extract business entities with behavior
- Move value objects and enums
- Define repository interfaces
- Remove infrastructure dependencies
- Add domain exceptions
Phase 3: Application Layer
- Create Application project
- Add MediatR for CQRS
- Create commands/queries for use cases
- Extract handlers from controllers/services
- Create DTOs for API contracts
- Add FluentValidation for requests
- Implement mappers (Entity ↔ DTO)
Phase 4: Infrastructure Separation
- Create Infrastructure project
- Move EF Core DbContext
- Implement repositories
- Configure entity mappings
- Extract external service clients
- Add caching/messaging implementations
Phase 5: API Simplification
- Thin down controllers
- Remove business logic
- Delegate to MediatR handlers
- Add exception middleware
- Configure DI in Program.cs
- Update Swagger documentation
Phase 6: Testing Refactor
- Update unit tests for domain entities
- Add handler tests with mocks
- Create integration tests for repositories
- Add API integration tests
- Verify test coverage >= 80%
Dependency Injection Configuration
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Application services
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Application.AssemblyMarker).Assembly));
builder.Services.AddValidatorsFromAssembly(typeof(Application.AssemblyMarker).Assembly);
// Infrastructure services
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<I{{EntityName}}Repository, {{EntityName}}Repository>();
// API services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Middleware pipeline
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Quality Checklist
- Domain layer has no external dependencies
- Application layer depends only on Domain
- Infrastructure implements interfaces from Domain
- API controllers are thin (delegate to MediatR)
- Business logic in domain entities/handlers
- Repository pattern for data access
- CQRS with commands and queries
- DTOs for API contracts (no entities exposed)
- FluentValidation for input validation
- Exception handling middleware
- Dependency injection properly configured
- Tests updated for new structure
- Test coverage >= 80%
- No circular dependencies
- SOLID principles followed
Common Refactoring Patterns
Move Business Logic to Domain
Before: Logic in controller or service
public async Task<IActionResult> UpdatePrice(int id, decimal newPrice)
{
var product = await _context.Products.FindAsync(id);
if (newPrice <= 0) return BadRequest("Invalid price");
if (newPrice > product.MaxPrice) return BadRequest("Price too high");
product.Price = newPrice;
await _context.SaveChangesAsync();
return Ok();
}
After: Logic in entity
// Domain Entity
public void UpdatePrice(decimal newPrice)
{
if (newPrice <= 0)
throw new DomainException("Price must be positive");
if (newPrice > MaxPrice)
throw new DomainException($"Price cannot exceed {MaxPrice}");
Price = newPrice;
}
// Controller
var command = new UpdateProductPriceCommand(id, request.NewPrice);
var result = await _mediator.Send(command);
return result.IsSuccess ? Ok() : BadRequest(result.Error);
Extract to CQRS Handler
Before: Controller with business logic
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var entity = await _context.Entities
.Include(e => e.Related)
.FirstOrDefaultAsync(e => e.Id == id);
if (entity == null) return NotFound();
var dto = new EntityDto
{
Id = entity.Id,
Name = entity.Name,
// Complex mapping...
};
return Ok(dto);
}
After: Query handler
// Query
public sealed record GetEntityQuery(int Id) : IRequest<Result<EntityResponse>>;
// Handler
public sealed class GetEntityHandler : IRequestHandler<GetEntityQuery, Result<EntityResponse>>
{
private readonly IEntityRepository _repository;
public async Task<Result<EntityResponse>> Handle(
GetEntityQuery query,
CancellationToken cancellationToken)
{
var entity = await _repository.GetByIdAsync(query.Id, cancellationToken);
if (entity == null)
return Result<EntityResponse>.Failure("Entity not found");
return Result<EntityResponse>.Success(EntityMapper.ToResponse(entity));
}
}
// Controller
[HttpGet("{id}")]
public async Task<ActionResult<EntityResponse>> Get(int id)
{
var query = new GetEntityQuery(id);
var result = await _mediator.Send(query);
return result.IsSuccess ? Ok(result.Value) : NotFound();
}
Generate refactoring plan and execute transformation to Clean Architecture.
