Sajjad Arif GulSajjad Arif Gul
← All Notes
BackendSeptember 2, 2024

Clean Architecture in .NET, without the dogma

Clean Architecture pays off on long-lived systems — but only if you apply it with judgement. How I use it on real .NET Core projects without drowning in boilerplate.

Clean Architecture in .NET, without the dogma

Clean Architecture gets cited like scripture and applied like a checklist. After years of shipping .NET Core systems that other teams maintain, I have a more pragmatic view: the goal is replaceable edges and a protected core, not a folder for every noun.

In modern systems, this decoupling is especially valuable as AI integration becomes common. Here is how I set up Clean Architecture in .NET to decouple business logic from external dependencies (like AI endpoints) and ORMs without drowning in boilerplate.

The Core Concept: Protect the Domain

In Clean Architecture, your Domain (entities, business rules) and Application (use cases, interfaces) sit at the center. They have zero dependencies on external databases, web frameworks, or third-party client SDKs. External things (like Entity Framework, OpenAI SDKs, HTTP clients) live in the Infrastructure layer and implement interfaces defined in the center.

When building AI integration, we shouldn’t leak the OpenAI or DeepSeek NuGet packages into our core logic. Instead, we define an interface in the Application layer:

// Src/Core/Application/Common/Interfaces/IAiService.cs
namespace Portfolio.Application.Common.Interfaces;

public interface IAiService
{
    Task<string> GenerateTextAsync(string prompt, CancellationToken cancellationToken = default);
    Task<IReadOnlyList<float>> EmbedTextAsync(string text, CancellationToken cancellationToken = default);
}

Then, the concrete implementations live inside the Infrastructure layer:

// Src/Infrastructure/Services/DeepSeekAiService.cs
using Microsoft.Extensions.Options;
using Portfolio.Application.Common.Interfaces;

namespace Portfolio.Infrastructure.Services;

public class DeepSeekAiService : IAiService
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public DeepSeekAiService(HttpClient httpClient, IOptions<AiSettings> settings)
    {
        _httpClient = httpClient;
        _apiKey = settings.Value.ApiKey;
    }

    public async Task<string> GenerateTextAsync(string prompt, CancellationToken cancellationToken = default)
    {
        // Concrete HTTP request to DeepSeek API endpoint...
        return "Decoupled AI response";
    }

    public async Task<IReadOnlyList<float>> EmbedTextAsync(string text, CancellationToken cancellationToken = default)
    {
        // Custom embedding logic...
        return new float[] { 0.1f, 0.2f, 0.3f };
    }
}

By registering this with .NET Dependency Injection, swapping from DeepSeek to OpenAI or Ollama is literally a one-line change in our infrastructure configuration, with zero changes required in our core application handlers.

Repositories and the EF Question

A common source of dogma is writing a repository wrapper over Entity Framework Core. Since EF Core already implements the Unit of Work and Repository patterns (via DbContext and DbSet), a generic repository wrapper often adds redundant boilerplate.

Here is my pragmatic guideline:

  • Skip the Repository if you are writing simple CRUD queries. Inject the IDbContext directly into your Application handlers.
  • Add a Repository only when you need to hide complex SQL optimization, manage raw data mapping, or isolate database-specific queries (like vector search syntax in PostgreSQL vs SQL Server) from your business logic.

Takeaway

Use Clean Architecture as a tool for managing change, not as a dogmatic rulebook. Protect the parts of your codebase that dictate business policy, keep infrastructure swappable, and write clean, direct code for the rest. Your future self will thank you.

Written by Sajjad Arif Gul