28  ASP.NET Clean Architecture

Figure 28.1: ASP.NET Core - 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

Figure 28.2: ASP.NET Unittest

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

Figure 28.3: ASP.NET Integration Test

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:

  1. Dependency Inversion: Infrastructure β†’ Core (not Core β†’ Infrastructure)
  2. Interface Segregation: Core defines interfaces, Infrastructure implements
  3. Separation of Concerns: Each project has ONE responsibility