Clean Architecture provides a clear way to organize ASP.NET Core projects, making it especially beneficial for healthcare systems that demand high standards of security, compliance, and scalability. By separating application layers, development teams can ensure better maintainability, easier testing, and a more robust deployment pipeline. This guide outlines the core principles, structure, and implementation details for using Clean Architecture in a healthcare-focused ASP.NET Core application.
Overview of Clean Architecture
- Maintainability: Each layer has a single responsibility. Business rules, data operations, and presentation concerns remain isolated from each other.
- Testability: Core logic can be tested without bringing up a web server, database, or other infrastructure services.
- Scalability: Well-defined boundaries allow specific layers or services to evolve independently or migrate into separate microservices.
- Security & Compliance: Healthcare environments often have stringent requirements (e.g., HIPAA/HITECH). Clean Architecture makes it easier to audit and secure data access pathways.
Recommended Folder and Layer Structure
A typical Clean Architecture solution for .NET might have the following high-level organization:
├── src
│ ├── HealthcareApp.Domain
│ ├── HealthcareApp.Application
│ ├── HealthcareApp.Infrastructure
│ └── HealthcareApp.Web
└── tests
├── HealthcareApp.UnitTests
└── HealthcareApp.IntegrationTests
Layer Descriptions:
- Domain Layer: Entities, Value Objects, Domain Events. Independent of any specific data storage or external services.
- Application Layer: Use cases (Commands and Queries), Interfaces, DTOs. Orchestrates domain models and ensures business logic is applied consistently.
- Infrastructure Layer: EF Core DbContext, repository implementations, integrations with external systems (e.g., Azure Services, email providers).
- Presentation (Web) Layer: ASP.NET Core controllers, Razor Pages, or Minimal APIs. Serves as the entry point for HTTP requests, delegating logic to the Application layer.
Domain Layer
Purpose
Holds the core business logic of the healthcare system, such as patient entities, appointment scheduling, and domain rules for insurance claims.
Example: Patient Entity
namespace HealthcareApp.Domain.Entities
{
public class Patient
{
public Guid PatientId { get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public DateTime DateOfBirth { get; private set; }
public string InsuranceNumber { get; private set; }
// Private constructor for EF or reflection-based creation
private Patient() { }
public Patient(string firstName, string lastName, DateTime dateOfBirth, string insuranceNumber)
{
PatientId = Guid.NewGuid();
FirstName = firstName;
LastName = lastName;
DateOfBirth = dateOfBirth;
InsuranceNumber = insuranceNumber;
}
}
}
Key Notes
- No direct database dependencies in domain classes.
- Domain models reflect real-world business concepts and rules.
Application Layer
Purpose
Implements use cases that coordinate domain entities and enforce business rules. Contains commands and queries (often implemented via MediatR) to separate write and read operations.
Example: Command and Handler
using MediatR;
namespace HealthcareApp.Application.Patients.Commands.RegisterPatient
{
public record RegisterPatientCommand(
string FirstName,
string LastName,
DateTime DateOfBirth,
string InsuranceNumber
) : IRequest<Guid>;
public class RegisterPatientCommandHandler : IRequestHandler<RegisterPatientCommand, Guid>
{
private readonly IPatientRepository _patientRepository;
public RegisterPatientCommandHandler(IPatientRepository patientRepository)
{
_patientRepository = patientRepository;
}
public async Task<Guid> Handle(RegisterPatientCommand request, CancellationToken cancellationToken)
{
var patient = new Domain.Entities.Patient(
request.FirstName,
request.LastName,
request.DateOfBirth,
request.InsuranceNumber);
await _patientRepository.AddAsync(patient);
return patient.PatientId;
}
}
}
Key Notes
- The
IPatientRepository
interface is defined here but implemented in the Infrastructure layer. - Handlers contain application logic (orchestration) without mixing infrastructure concerns.
Infrastructure Layer
Purpose
Manages data persistence, network calls, and any third-party or external integrations. Implements interfaces declared in the Application layer.
EF Core DbContext
using HealthcareApp.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace HealthcareApp.Infrastructure.Persistence
{
public class HealthcareDbContext : DbContext
{
public HealthcareDbContext(DbContextOptions<HealthcareDbContext> options)
: base(options) { }
public DbSet<Patient> Patients => Set<Patient>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Patient>(entity =>
{
entity.HasKey(p => p.PatientId);
entity.Property(p => p.FirstName).HasMaxLength(100);
entity.Property(p => p.LastName).HasMaxLength(100);
// Additional mapping configurations...
});
}
}
}
Repository Implementation
using HealthcareApp.Application.Common.Interfaces;
using HealthcareApp.Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace HealthcareApp.Infrastructure.Repositories
{
public class PatientRepository : IPatientRepository
{
private readonly HealthcareDbContext _dbContext;
public PatientRepository(HealthcareDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task AddAsync(Patient patient)
{
await _dbContext.Patients.AddAsync(patient);
await _dbContext.SaveChangesAsync();
}
// Additional CRUD operations...
}
}
Presentation (Web) Layer
Purpose
Receives HTTP requests, parses input, and delegates application tasks to handlers in the Application layer.
Example: ASP.NET Core Controller
using HealthcareApp.Application.Patients.Commands.RegisterPatient;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace HealthcareApp.Web.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class PatientsController : ControllerBase
{
private readonly IMediator _mediator;
public PatientsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> RegisterPatient([FromBody] RegisterPatientCommand command)
{
var patientId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetPatient), new { id = patientId }, null);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetPatient(Guid id)
{
return Ok($"Patient details for {id} will be retrieved here.");
}
}
}
Key Notes
- Controllers are kept lightweight; they should not contain domain or data access logic.
- Minimal APIs can also be used in .NET 6+ to simplify routing and reduce boilerplate.
CI/CD with Azure DevOps
Sample Pipeline Configuration
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration Release'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration Release'
- task: AzureWebApp@1
inputs:
azureSubscription: 'MyAzureSubscription'
appName: 'HealthcareApp'
package: '$(System.DefaultWorkingDirectory)/**/*.zip'
Automation Highlights
- Build and Test steps ensure code integrity and coverage.
- Deployment can target Azure App Service, Azure Kubernetes Service, or another hosting environment.
Performance and Security Considerations
- Caching: Integrate Azure Cache for Redis or in-memory caching for frequently accessed data.
- Database Tuning: Monitor slow queries, apply indexing, and consider partitioning large tables.
- Logging and Observability: Implement Application Insights or another logging/tracing solution for troubleshooting and performance monitoring.
- Encryption and Compliance: Use TLS/HTTPS, Azure Key Vault for secrets management, and consider Transparent Data Encryption for SQL data at rest.
Conclusion
Implementing Clean Architecture in an ASP.NET Core application for healthcare:
- Ensures separation of concerns across domain, application, infrastructure, and presentation layers.
- Simplifies testing by isolating core business logic from external dependencies.
- Provides a scalable foundation for adding new features or transitioning to microservices if traffic demands grow.
- Helps maintain security and compliance due to clear boundaries and well-defined data access pathways.
For projects dealing with sensitive healthcare data, the benefits of Clean Architecture—including maintainability, testability, and streamlined compliance—make it a highly recommended approach. As the application scales, each layer can be independently optimized or moved to its own service, further enhancing resilience and performance in a demanding healthcare environment.