28 ASP.NET Clean Architecture
28.1 The Three Projects Structure
ASP.NET Core Clean Architecture typically uses three separate projects (not just folders!):
Solution: CTStrokeAI.sln
β
βββ CTStrokeAI.Core/ β Application Core Project (Red)
βββ CTStrokeAI.Infrastructure/ β Infrastructure Project (Green)
βββ CTStrokeAI.Web/ β ASP.NET Core Web App (Blue)
28.2 Project 1: Application Core (Red) π΄
28.2.1 Whatβs Inside
From your first screenshot, the Application Core contains:
CTStrokeAI.Core/
βββ Entities/
β βββ Patient.cs
β βββ CTScan.cs
β βββ StrokeResult.cs
βββ Interfaces/
β βββ IScanRepository.cs
β βββ IStrokeDetector.cs
β βββ INotificationService.cs
βββ Services/ (Domain Services)
β βββ AspectsCalculator.cs
β βββ ClinicalAdvisor.cs
βββ Aggregates/
β βββ StrokeAssessmentAggregate.cs
βββ ValueObjects/
β βββ AspectsScore.cs
β βββ PatientAge.cs
βββ Specifications/
β βββ StrokeEligibilitySpec.cs
βββ DomainEvents/
β βββ StrokeDiagnosedEvent.cs
βββ Exceptions/
βββ InvalidAspectsScoreException.cs
28.2.2 Key Characteristics
β
Contains:
β’ Pure business logic
β’ Domain entities
β’ Business rules
β’ Interface definitions
β Does NOT contain:
β’ Database code
β’ HTTP requests
β’ File I/O
β’ External service calls
β’ ANY infrastructure
Dependencies: NONE (or minimal like System.Collections)
28.2.3 Example Code: Entity (POCO - Plain Old CLR Object)
// Core/Entities/Patient.cs
namespace CTStrokeAI.Core.Entities
{
public class Patient
{
public int Id { get; private set; }
public string HospitalNumber { get; private set; }
public string Name { get; private set; }
public DateTime DateOfBirth { get; private set; }
// Pure business logic - no database concerns
public int GetAge()
{
var today = DateTime.Today;
var age = today.Year - DateOfBirth.Year;
if (DateOfBirth.Date > today.AddYears(-age)) age--;
return age;
}
// Domain rule
public bool IsEligibleForThrombolysis()
{
int age = GetAge();
return age >= 18 && age <= 80; // Business rule
}
}
}28.2.4 Example Code: Interface
// Core/Interfaces/IScanRepository.cs
namespace CTStrokeAI.Core.Interfaces
{
public interface IScanRepository
{
Task<CTScan> GetByIdAsync(int scanId);
Task<List<CTScan>> GetByPatientAsync(int patientId);
Task<CTScan> AddAsync(CTScan scan);
Task UpdateAsync(CTScan scan);
Task DeleteAsync(int scanId);
}
// Core defines WHAT it needs, not HOW it's implemented
}28.2.5 Example Code: Domain Service
// Core/Services/AspectsCalculator.cs
namespace CTStrokeAI.Core.Services
{
public class AspectsCalculator
{
// Pure business logic
public int CalculateScore(CTScan scan)
{
int score = 10; // Start with 10 points
// Business rules for ASPECTS scoring
if (HasInfarct(scan.M1Region)) score--;
if (HasInfarct(scan.M2Region)) score--;
if (HasInfarct(scan.M3Region)) score--;
// ... more regions
return score;
}
public string GetRecommendation(int aspectsScore)
{
// Clinical decision rules
return aspectsScore switch
{
>= 8 => "Good candidate for thrombolysis",
>= 6 => "Discuss with stroke team",
_ => "Large infarct - thrombolysis not recommended"
};
}
private bool HasInfarct(RegionData region)
{
// Domain logic
return region.Hypodensity > 0.5;
}
}
}28.3 Project 2: Infrastructure (Green) π’
28.3.1 Whatβs Inside
From your first screenshot, Infrastructure contains the implementations:
CTStrokeAI.Infrastructure/
βββ Data/
β βββ EfCoreDbContext.cs β Entity Framework
β βββ Repositories/
β βββ EfScanRepository.cs β Implements IScanRepository
β βββ CacheScanRepository.cs
βββ Services/
β βββ SmsService.cs β Implements INotificationService
β βββ EmailService.cs
β βββ RedisCacheService.cs
βββ ExternalApis/
β βββ PacsConnector.cs
β βββ AzureServiceBusAccessor.cs
β βββ GithubApiClient.cs
βββ InMemoryCache/
βββ MemoryCacheAdapter.cs
28.3.2 Key Characteristics
β
Contains:
β’ Database implementations (EF Core)
β’ External API clients
β’ File system operations
β’ Cache implementations
β’ Email/SMS services
β’ Implements interfaces from Core
Dependencies:
β’ Application Core (implements its interfaces)
β’ External packages (EF Core, Redis, etc.)
28.3.3 Example Code: Repository Implementation
// Infrastructure/Data/Repositories/EfScanRepository.cs
using CTStrokeAI.Core.Entities;
using CTStrokeAI.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace CTStrokeAI.Infrastructure.Data.Repositories
{
public class EfScanRepository : IScanRepository
{
private readonly AppDbContext _dbContext;
public EfScanRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<CTScan> GetByIdAsync(int scanId)
{
// Entity Framework implementation
return await _dbContext.CTScans
.Include(s => s.Patient)
.FirstOrDefaultAsync(s => s.Id == scanId);
}
public async Task<List<CTScan>> GetByPatientAsync(int patientId)
{
return await _dbContext.CTScans
.Where(s => s.PatientId == patientId)
.OrderByDescending(s => s.AcquisitionDate)
.ToListAsync();
}
public async Task<CTScan> AddAsync(CTScan scan)
{
await _dbContext.CTScans.AddAsync(scan);
await _dbContext.SaveChangesAsync();
return scan;
}
// ... other methods
}
}28.3.4 Example Code: External Service Implementation
// Infrastructure/Services/EmailService.cs
using CTStrokeAI.Core.Interfaces;
using SendGrid;
using SendGrid.Helpers.Mail;
namespace CTStrokeAI.Infrastructure.Services
{
public class EmailService : INotificationService
{
private readonly SendGridClient _client;
private readonly string _fromEmail;
public EmailService(string apiKey, string fromEmail)
{
_client = new SendGridClient(apiKey);
_fromEmail = fromEmail;
}
public async Task NotifyAsync(string recipient, string message)
{
var from = new EmailAddress(_fromEmail);
var to = new EmailAddress(recipient);
var msg = MailHelper.CreateSingleEmail(from, to,
"Stroke Detection Alert", message, message);
await _client.SendEmailAsync(msg);
}
}
}28.3.5 Example Code: DbContext
// Infrastructure/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using CTStrokeAI.Core.Entities;
namespace CTStrokeAI.Infrastructure.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Patient> Patients { get; set; }
public DbSet<CTScan> CTScans { get; set; }
public DbSet<StrokeResult> StrokeResults { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// EF Core configuration
modelBuilder.Entity<Patient>()
.HasMany(p => p.Scans)
.WithOne(s => s.Patient)
.HasForeignKey(s => s.PatientId);
}
}
}28.4 Project 3: ASP.NET Core Web App (Blue) π΅
28.4.1 Whatβs Inside
From your first screenshot, the Web App contains:
CTStrokeAI.Web/
βββ Controllers/
β βββ StrokeDetectionController.cs
β βββ PatientController.cs
βββ ViewModels/
β βββ StrokeResultViewModel.cs
β βββ PatientViewModel.cs
βββ Views/
β βββ Home/
β βββ Stroke/
βββ Filters/
β βββ ResponseCachingFilter.cs
β βββ ModelValidationFilter.cs
βββ Program.cs β Entry point
βββ Startup.cs β DI configuration
βββ appsettings.json
28.4.2 Key Characteristics
β
Contains:
β’ MVC Controllers
β’ Razor Views
β’ ViewModels (DTOs)
β’ Filters/Middleware
β’ Dependency injection setup
β’ HTTP concerns
Dependencies:
β’ Application Core
β’ Infrastructure (at startup only)
28.4.3 Example Code: Controller
// Web/Controllers/StrokeDetectionController.cs
using Microsoft.AspNetCore.Mvc;
using CTStrokeAI.Core.Interfaces;
using CTStrokeAI.Core.Services;
namespace CTStrokeAI.Web.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class StrokeDetectionController : ControllerBase
{
private readonly IScanRepository _scanRepo;
private readonly AspectsCalculator _calculator;
private readonly INotificationService _notifier;
public StrokeDetectionController(
IScanRepository scanRepo,
AspectsCalculator calculator,
INotificationService notifier)
{
_scanRepo = scanRepo;
_calculator = calculator;
_notifier = notifier;
}
[HttpPost("analyze/{scanId}")]
public async Task<IActionResult> AnalyzeScan(int scanId)
{
// Get scan using interface (don't care about implementation!)
var scan = await _scanRepo.GetByIdAsync(scanId);
if (scan == null)
return NotFound();
// Use business logic from Core
int aspectsScore = _calculator.CalculateScore(scan);
string recommendation = _calculator.GetRecommendation(aspectsScore);
// Notify using interface
if (aspectsScore <= 5)
{
await _notifier.NotifyAsync(
"stroke-team@ramathibodi.go.th",
$"Critical: Low ASPECTS score ({aspectsScore}) for scan {scanId}"
);
}
// Return ViewModel
return Ok(new StrokeResultViewModel
{
ScanId = scanId,
AspectsScore = aspectsScore,
Recommendation = recommendation
});
}
}
}28.4.4 Example Code: Dependency Injection Setup
// Web/Program.cs (ASP.NET Core 6+)
using CTStrokeAI.Core.Interfaces;
using CTStrokeAI.Core.Services;
using CTStrokeAI.Infrastructure.Data;
using CTStrokeAI.Infrastructure.Data.Repositories;
using CTStrokeAI.Infrastructure.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
// Database
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")
));
// Register Core services
builder.Services.AddScoped<AspectsCalculator>();
// Register Infrastructure implementations
builder.Services.AddScoped<IScanRepository, EfScanRepository>();
builder.Services.AddScoped<INotificationService, EmailService>(sp =>
new EmailService(
builder.Configuration["SendGrid:ApiKey"],
builder.Configuration["SendGrid:FromEmail"]
));
// Add caching
builder.Services.AddMemoryCache();
builder.Services.AddScoped<INotificationService, SmsService>();
var app = builder.Build();
// Configure the HTTP request pipeline
app.UseRouting();
app.MapControllers();
app.Run();28.5 Dependency Flow Visualization
28.5.1 Compile-Time Dependencies
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASP.NET Core Web App (Blue) β
β β
β Controllers, Views, Filters β
βββββββββββββββ¬βββββββββββββββββ¬βββββββββββββββββββ
β β
β references β references
β β (startup only)
βΌ βΌ
βββββββββββββββββββββββ ββββββββββββββββββββββββββ
β Application Core β β Infrastructure β
β (Red) β β (Green) β
β β β β
β β’ Entities β β β’ EF Core DbContext β
β β’ Interfaces ββββ€ β’ Repositories (impl) β
β β’ Domain Services β β β’ External APIs β
βββββββββββββββββββββββ β β’ Email/SMS Services β
β² ββββββββββββββββββββββββββ
β β
β NO dependency! β uses
β βΌ
β βββββββββββββββββββββββ
β β External Resources β
β β β’ SQL Database β
βββββββββββββββββ€ β’ Redis Cache β
β β’ SendGrid API β
βββββββββββββββββββββββ
Key: Infrastructure depends on Core
Core depends on NOTHING
Web depends on both (but uses Core interfaces)
28.5.2 Runtime Dependencies (via Dependency Injection)
HTTP Request arrives
β
βΌ
βββββββββββββββββββ
β Controller β
β β
β Needs: β
β β’ IScanRepo βββββ
β β’ INotifier βββββ€ DI Container injects
βββββββββββββββββββ β concrete implementations
β
β
βββββββββββββββ΄ββββββββββββββ
β β
βββββββββΌβββββββββ ββββββββββΌβββββββββββ
β EfScanRepo β β EmailService β
β (from Infra) β β (from Infra) β
ββββββββββββββββββ βββββββββββββββββββββ
Controller only knows about interfaces!
DI provides the implementations at runtime
28.6 Testing Strategy
28.6.1 Unit Tests (Figure 5-10) - Test Application Core
From your second screenshot, Unit Tests target Application Core in isolation:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Unit Test Project β
β (Green) β
β β
β Tests pure business logic with NO external β
β dependencies β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
β tests
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Application Core (Red) β
β β
β βββββββββββββββββββββββββββββ β
β β Entities + Interfaces β β
β β Domain Services β β
β βββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββ
NO Database, NO APIs, NO External Services!
Fast, isolated, reliable tests
Example - Unit Test:
// Tests/Core/Services/AspectsCalculatorTests.cs
using Xunit;
using CTStrokeAI.Core.Services;
using CTStrokeAI.Core.Entities;
namespace CTStrokeAI.Tests.Core.Services
{
public class AspectsCalculatorTests
{
[Fact]
public void CalculateScore_NoInfarcts_ReturnsFullScore()
{
// Arrange
var calculator = new AspectsCalculator();
var scan = new CTScan
{
M1Region = new RegionData { Hypodensity = 0.1 },
M2Region = new RegionData { Hypodensity = 0.1 },
M3Region = new RegionData { Hypodensity = 0.1 }
// All regions normal
};
// Act
int score = calculator.CalculateScore(scan);
// Assert
Assert.Equal(10, score);
}
[Fact]
public void GetRecommendation_HighScore_RecommendsThrombolysis()
{
// Arrange
var calculator = new AspectsCalculator();
// Act
string recommendation = calculator.GetRecommendation(9);
// Assert
Assert.Contains("thrombolysis", recommendation.ToLower());
}
[Theory]
[InlineData(10, "Good candidate")]
[InlineData(8, "Good candidate")]
[InlineData(7, "Discuss with stroke team")]
[InlineData(5, "not recommended")]
public void GetRecommendation_VariousScores_ReturnsExpected(
int score, string expectedText)
{
// Arrange
var calculator = new AspectsCalculator();
// Act
string recommendation = calculator.GetRecommendation(score);
// Assert
Assert.Contains(expectedText, recommendation);
}
}
}Unit Test with Mock (Test with Interface):
// Tests/Core/Services/PatientServiceTests.cs
using Xunit;
using Moq;
using CTStrokeAI.Core.Interfaces;
using CTStrokeAI.Core.Services;
using CTStrokeAI.Core.Entities;
namespace CTStrokeAI.Tests.Core.Services
{
public class PatientServiceTests
{
[Fact]
public async Task ProcessStrokeScan_CriticalCase_SendsNotification()
{
// Arrange
var mockNotifier = new Mock<INotificationService>();
var mockRepo = new Mock<IScanRepository>();
var scan = new CTScan { Id = 1, PatientId = 100 };
mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(scan);
var calculator = new AspectsCalculator();
var service = new StrokeAnalysisService(
mockRepo.Object,
calculator,
mockNotifier.Object
);
// Act
await service.AnalyzeScanAsync(1);
// Assert
mockNotifier.Verify(
n => n.NotifyAsync(
It.IsAny<string>(),
It.IsAny<string>()),
Times.Once
);
}
}
}Benefits of Unit Tests:
β
FAST: No database, no network, no I/O
β
ISOLATED: Test business logic alone
β
RELIABLE: No external dependencies = no flaky tests
β
EASY TO WRITE: Simple setup, no infrastructure
Test Speed:
Unit Tests: ~1-10 ms per test
Integration Tests: ~100-1000 ms per test
28.6.2 Integration Tests (Figure 5-11) - Test Infrastructure
From your third screenshot, Integration Tests target Infrastructure with real dependencies:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Integration Test Project β
β (Green) β
β β
β Tests infrastructure implementations with β
β REAL external dependencies β
ββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β
β tests
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure (Yellow) β
β β
β β’ Repositories β
β β’ External API clients β
ββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β
β connects to
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β External Dependencies β
β β’ SQL Database (test DB) β
β β’ Cloud Storage β
β β’ Email service β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Uses REAL infrastructure
Tests actual implementations
Example - Integration Test:
// Tests/Infrastructure/Data/EfScanRepositoryTests.cs
using Xunit;
using Microsoft.EntityFrameworkCore;
using CTStrokeAI.Infrastructure.Data;
using CTStrokeAI.Infrastructure.Data.Repositories;
using CTStrokeAI.Core.Entities;
namespace CTStrokeAI.Tests.Infrastructure.Data
{
public class EfScanRepositoryTests : IDisposable
{
private readonly AppDbContext _context;
private readonly EfScanRepository _repository;
public EfScanRepositoryTests()
{
// Use in-memory database for testing
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new AppDbContext(options);
_repository = new EfScanRepository(_context);
}
[Fact]
public async Task AddAsync_ValidScan_SavesToDatabase()
{
// Arrange
var patient = new Patient { HospitalNumber = "HN001" };
var scan = new CTScan
{
Patient = patient,
AcquisitionDate = DateTime.Now,
Modality = "CT"
};
// Act
var result = await _repository.AddAsync(scan);
// Assert
Assert.NotEqual(0, result.Id); // ID was assigned
var savedScan = await _repository.GetByIdAsync(result.Id);
Assert.NotNull(savedScan);
Assert.Equal("CT", savedScan.Modality);
}
[Fact]
public async Task GetByPatientAsync_MultipleScans_ReturnsAllScans()
{
// Arrange
var patient = new Patient { HospitalNumber = "HN002" };
await _context.Patients.AddAsync(patient);
await _context.SaveChangesAsync();
await _repository.AddAsync(new CTScan
{
PatientId = patient.Id,
AcquisitionDate = DateTime.Now.AddDays(-2)
});
await _repository.AddAsync(new CTScan
{
PatientId = patient.Id,
AcquisitionDate = DateTime.Now.AddDays(-1)
});
// Act
var scans = await _repository.GetByPatientAsync(patient.Id);
// Assert
Assert.Equal(2, scans.Count);
Assert.True(scans[0].AcquisitionDate > scans[1].AcquisitionDate);
// Should be ordered by date descending
}
public void Dispose()
{
_context.Dispose();
}
}
}Integration Test with Real Database:
// Tests/Infrastructure/ExternalApis/PacsConnectorTests.cs
using Xunit;
using CTStrokeAI.Infrastructure.ExternalApis;
namespace CTStrokeAI.Tests.Infrastructure.ExternalApis
{
// These tests require a real test PACS server
[Collection("PACS Tests")]
public class PacsConnectorTests
{
private readonly string _testPacsHost = "test-pacs.ramathibodi.local";
private readonly int _testPacsPort = 11112;
[Fact]
public async Task QueryStudy_ValidStudyId_ReturnsData()
{
// Arrange
var connector = new PacsConnector(_testPacsHost, _testPacsPort);
string testStudyId = "1.2.840.113619.2.55.3.test";
// Act
var study = await connector.QueryStudyAsync(testStudyId);
// Assert
Assert.NotNull(study);
Assert.Equal(testStudyId, study.StudyInstanceUID);
}
[Fact]
public async Task RetrieveSeries_ExistingSeries_DownloadsImages()
{
// Arrange
var connector = new PacsConnector(_testPacsHost, _testPacsPort);
// Act
var images = await connector.RetrieveSeriesAsync(
"1.2.840.test.series"
);
// Assert
Assert.NotEmpty(images);
Assert.True(images.Count > 0);
}
}
}28.7 Complete Radiology AI Example
28.7.1 Project Structure
CTStrokeAI.sln
β
βββ src/
β βββ CTStrokeAI.Core/ β Application Core
β β βββ Entities/
β β β βββ Patient.cs
β β β βββ CTScan.cs
β β β βββ StrokeResult.cs
β β βββ Interfaces/
β β β βββ IScanRepository.cs
β β β βββ IModelService.cs
β β β βββ INotificationService.cs
β β βββ Services/
β β βββ AspectsCalculator.cs
β β βββ StrokeAnalysisService.cs
β β
β βββ CTStrokeAI.Infrastructure/ β Infrastructure
β β βββ Data/
β β β βββ AppDbContext.cs
β β β βββ Repositories/
β β β βββ EfScanRepository.cs
β β βββ Services/
β β β βββ TensorFlowModelService.cs
β β β βββ SendGridEmailService.cs
β β βββ ExternalApis/
β β βββ PacsConnector.cs
β β
β βββ CTStrokeAI.Web/ β ASP.NET Core Web
β βββ Controllers/
β β βββ StrokeController.cs
β βββ ViewModels/
β β βββ StrokeResultViewModel.cs
β βββ Program.cs
β βββ appsettings.json
β
βββ tests/
βββ CTStrokeAI.UnitTests/ β Unit Tests
β βββ Core/
β βββ Services/
β βββ AspectsCalculatorTests.cs
β
βββ CTStrokeAI.IntegrationTests/ β Integration Tests
βββ Infrastructure/
βββ Data/
βββ EfScanRepositoryTests.cs
28.7.2 Dependency Graph
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CTStrokeAI.Web β
β β
β Program.cs ββ β
β β configures DI β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββ
β
βββββββββ΄βββββββββ¬ββββββββββββββββββ
β β β
βΌ βΌ β
βββββββββββββββ ββββββββββββββββββ β
β Core β β Infrastructure β β
β (Pure) ββββ€ (Implements) β β
βββββββββββββββ ββββββββββ¬ββββββββ β
β β
βΌ β
ββββββββββββββββββ β
β EF Core β β
β SendGrid β β
β pydicom β β
ββββββββββββββββββ β
β
ββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββ
β Unit Tests β (test Core)
ββββββββββββββββββ
ββββββββββββββββββ
β Integration β (test Infrastructure)
β Tests β
ββββββββββββββββββ
28.8 Key Benefits of This Architecture
28.8.1 1. Testability
Unit Tests:
β
Test business logic in isolation
β
Fast (milliseconds)
β
No external dependencies
β
Run on every commit
Integration Tests:
β
Test infrastructure with real systems
β
Catch integration issues
β
Confidence before deployment
28.8.2 2. Flexibility
Switch Database:
SQL Server β PostgreSQL
Just change Infrastructure project
Core remains unchanged!
Switch Notification:
Email β SMS β Slack
Just change Infrastructure project
Core remains unchanged!
28.8.3 3. Maintainability
Clear separation:
β’ Core = Business rules (rarely changes)
β’ Infrastructure = Tech details (changes often)
β’ Web = UI/API (changes frequently)
Each team can work independently
28.8.4 4. Framework Independence
Application Core has NO dependency on:
β ASP.NET Core
β Entity Framework
β Any external library
Can move to:
β
Different web framework (Blazor, gRPC)
β
Different ORM (Dapper, NPoco)
β
Different platform (Azure Functions, AWS Lambda)
28.9 Summary
ASP.NET Core Clean Architecture uses three projects:
βββββββββββββββββββββββββββββββββββββββββββ
β Web (Blue) - ASP.NET Core β
β β’ Controllers, Views, Filters β
β β’ Coordinates requests β
βββββββββ¬ββββββββββββββββββββββββββββββββββ
β depends on both
βββββ΄βββββ¬βββββββββββββββββββββ
βΌ βΌ β
ββββββββββ ββββββββββββββββββββ β
β Core β β Infrastructure β β
β (Red) ββββ€ (Green) β β
β β β β β
β Pure β β Implements β β
β Logic β β interfaces β β
ββββββββββ ββββββββ¬ββββββββββββ β
β β
βΌ β
ββββββββββββββββ β
β External β β
β Systems β β
ββββββββββββββββ β
β
ββββββββββββββββββββ β
β Unit Tests ββββ
β (Green) β
ββββββββββββββββββββ
ββββββββββββββββββββ
β Integration β
β Tests (Green) β
ββββββββββββββββββββ
Three key principles:
- Dependency Inversion: Infrastructure β Core (not Core β Infrastructure)
- Interface Segregation: Core defines interfaces, Infrastructure implements
- Separation of Concerns: Each project has ONE responsibility


