惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Hacker News - Newest:
Hacker News - Newest: "LLM"
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 最新话题
Cloudbric
Cloudbric
N
News and Events Feed by Topic
S
Secure Thoughts
Vercel News
Vercel News
S
Security @ Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
SegmentFault 最新的问题
Hacker News: Ask HN
Hacker News: Ask HN
博客园 - 聂微东
WordPress大学
WordPress大学
Google Online Security Blog
Google Online Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
PCI Perspectives
PCI Perspectives
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
Blog — PlanetScale
Blog — PlanetScale
Apple Machine Learning Research
Apple Machine Learning Research
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threat Research - Cisco Blogs
博客园 - 司徒正美
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
About on SuperTechFans
P
Proofpoint News Feed
大猫的无限游戏
大猫的无限游戏
V
V2EX
I
Intezer
H
Hacker News: Front Page
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
Lohrmann on Cybersecurity
F
Fortinet All Blogs
Schneier on Security
Schneier on Security
博客园 - 叶小钗
The Cloudflare Blog
月光博客
月光博客
W
WeLiveSecurity
T
Tenable Blog
P
Proofpoint News Feed
aimingoo的专栏
aimingoo的专栏
Help Net Security
Help Net Security
L
LangChain Blog
C
CERT Recently Published Vulnerability Notes
T
The Exploit Database - CXSecurity.com
美团技术团队
B
Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Clean Architecture in .NET 8: A 2026 Starter Template with 4 Projects, EF Core, and JWT Auth
Avinash Zala · 2026-06-22 · via DEV Community

I joined a team where the controller was 800 lines long, the business rules were scattered between the controller and the DbContext, and "to run the tests, spin up a SQL Server in Docker" was a sentence I heard every week. The fix was Clean Architecture. The argument I had with the team lead was about how to actually structure it. We argued for two weeks. Then I built this template so the next person wouldn't have to.

This is the Clean Architecture .NET 8 starter template I wish someone had handed me on day one. Four projects, strict dependency direction, domain entities that own their own invariants, and an Application layer you can unit test with Moq — no database required. The whole repo is on GitHub, MIT-licensed, runs with dotnet run, and ships with xUnit tests, JWT auth, Swagger, Docker, and CI.

This post is the explanation of why each project exists, what goes in it, and what I learned the hard way about getting Clean Architecture right in .NET.

The problem Clean Architecture solves

The naive way to build a .NET Web API is one project, one folder structure, and "everything talks to everything":

MyApp/
  Controllers/
    ProductsController.cs    ← HTTP stuff
    OrdersController.cs      ← HTTP stuff + business rules
  Services/
    ProductService.cs        ← business rules + DbContext.SaveChanges
  Data/
    AppDbContext.cs          ← EF Core, entities
  Models/
    Product.cs               ← POCO with public setters

This works for the first 1,000 lines. By 5,000 lines, the controller is doing five things at once. By 10,000, "to test this, I need a database" is the answer to every test question, and your CI takes 20 minutes because every test run spins up SQL Server.

Clean Architecture says: separate the business rules from the HTTP boundary, separate the database from the business rules, and enforce it with project references. A controller is allowed to call a service. A service is allowed to call a repository. A repository is allowed to know about EF Core. Nothing is allowed to know about anything "above" it in the chain. That's the rule.

The .NET implementation is four projects:

                ┌─────────────────────────────────┐
                │           Api project           │  ← HTTP, DTOs, Swagger
                │   (Controllers, Program.cs)     │
                └────────────┬────────────────────┘
                             │ references
                ┌────────────▼────────────────────┐
                │       Application project       │  ← Use cases, services
                │  (DTOs, Interfaces, Services)   │
                └────────────┬────────────────────┘
                             │ references
                ┌────────────▼────────────────────┐
                │      Infrastructure project     │  ← EF Core, JWT
                │ (DbContext, Repositories)       │
                └────────────┬────────────────────┘
                             │ references
                ┌────────────▼────────────────────┐
                │        Domain project           │  ← Zero dependencies
                │  (Entities, Interfaces)         │
                └─────────────────────────────────┘

The arrows only go down. The Domain project has zero NuGet dependencies outside the BCL. The Application project references Domain only. The Infrastructure project references both, but Domain and Application never reference Infrastructure. The compiler enforces the direction. A circular reference is a build error, not a code review comment.

Part 1 — the Domain layer

The Domain project contains the entities and the abstract interfaces that the Application layer depends on. No EF Core, no ASP.NET, no HTTP, no JSON. Just business rules.

The interesting file is Product.cs. It's not a POCO with public setters. It's a class with a DecreaseStock method that enforces its own invariants:

public class Product : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }

    // Domain logic — entity enforces its own invariants
    public bool IsInStock(int requestedQuantity) => StockQuantity >= requestedQuantity;

    public void DecreaseStock(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive.", nameof(quantity));
        if (!IsInStock(quantity))
            throw new InvalidOperationException($"Insufficient stock for product '{Name}'. " +
                                               $"Requested {quantity}, available {StockQuantity}.");

        StockQuantity -= quantity;
        UpdatedAt = DateTime.UtcNow;
    }
}

The crucial thing is that DecreaseStock is the only way to change StockQuantity from outside the class. The setter is public because EF Core needs it, but the domain logic is "you can't decrease stock by a negative number, and you can't decrease stock below zero." That logic lives on the entity, in Domain. The service, the controller, the test — none of them have to remember to validate. They call product.DecreaseStock(quantity) and the entity decides.

This is the part of Clean Architecture that actually matters. The dependency-direction rule is hygiene. The "entities own their invariants" rule is the architecture. If you take one thing from this post, take that: business rules belong on the business objects, not on the services that manipulate them.

Part 2 — the Application layer

The Application project contains the use cases. A use case is a verb at the level of the user, not the database. "Create a product" is a use case. "Insert a row into the products table" is not.

ProductService.cs is the canonical example:

public class ProductService : IProductService
{
    private readonly IUnitOfWork _unitOfWork;

    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<ProductDto?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        var product = await _unitOfWork.Products.GetByIdAsync(id, ct);
        return product is null ? null : MapToDto(product);
    }

    public async Task<ProductDto> CreateAsync(CreateProductDto dto, CancellationToken ct = default)
    {
        var product = new Product
        {
            Name = dto.Name,
            Description = dto.Description,
            Price = dto.Price,
            StockQuantity = dto.StockQuantity
        };

        await _unitOfWork.Products.AddAsync(product, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        return MapToDto(product);
    }
}

Two things to notice.

First, the service takes IUnitOfWork, not AppDbContext. This is the testability win. IUnitOfWork is an interface declared in the Domain project. The Infrastructure project provides the EF Core implementation. The Application project never knows EF Core exists. In a unit test, I write a fake IUnitOfWork in 10 lines and the test runs in 0 ms, with no database.

Second, the service returns DTOs, not entities. DTOs are flat data shapes designed for the wire. Entities are rich domain objects with behaviour. If I returned Product from the service, the controller would have access to product.DecreaseStock, and the controller could break invariants. The DTO is a contract: "this is what the service exposes to the outside world, and nothing more."

The "decrease stock" use case is the most interesting one because it touches three layers:

public async Task<OrderDto> CreateOrderAsync(CreateOrderDto dto, CancellationToken ct = default)
{
    var product = await _unitOfWork.Products.GetByIdAsync(dto.ProductId, ct)
        ?? throw new InvalidOperationException($"Product {dto.ProductId} not found.");

    // Domain logic — product enforces its own invariants
    product.DecreaseStock(dto.Quantity);

    var order = new Order
    {
        CustomerId = dto.CustomerId,
        Items = new List<OrderItem> { new() { ProductId = product.Id, Quantity = dto.Quantity } }
    };

    await _unitOfWork.Orders.AddAsync(order, ct);
    await _unitOfWork.SaveChangesAsync(ct);

    return MapToDto(order);
}

The service does three things: load the product, ask the product to decrease its own stock, save the order. The validation logic — "you can't order 5 of something that has 3 in stock" — is in the entity. The service doesn't know about that rule. The service couldn't bypass it even if it tried, because DecreaseStock throws and the exception propagates up.

Part 3 — the Infrastructure layer

The Infrastructure project is where the rest of the world lives. EF Core, the SQL Server or SQLite provider, the JWT signing service, anything that talks to a network or a file system. Domain and Application never know this project exists.

AppDbContext.cs is the EF Core surface:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

The repositories are the bridge between the service layer and EF Core:

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _db;
    public ProductRepository(AppDbContext db) { _db = db; }

    public async Task<Product?> GetByIdAsync(int id, CancellationToken ct = default) =>
        await _db.Products.FindAsync(new object[] { id }, ct);

    public async Task<IReadOnlyList<Product>> GetAllAsync(CancellationToken ct = default) =>
        await _db.Products.AsNoTracking().ToListAsync(ct);

    public async Task<IReadOnlyList<Product>> GetInStockAsync(CancellationToken ct = default) =>
        await _db.Products.AsNoTracking().Where(p => p.StockQuantity > 0).ToListAsync(ct);

    public async Task AddAsync(Product product, CancellationToken ct = default) =>
        await _db.Products.AddAsync(product, ct);
}

Note IProductRepository is declared in the Domain project, not Infrastructure. The Application layer's IUnitOfWork references IProductRepository and gets the EF Core implementation injected at startup. The Application layer doesn't know it's EF Core. Swap Repository<Product> for a Dapper implementation, or an in-memory fake for tests — the service code doesn't change.

Part 4 — the API layer

The API project is the HTTP boundary. Controllers are thin. They take a request, hand it to a service, return the result. No business logic. No DbContext. No SQL.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;

    public ProductsController(IProductService service) { _service = service; }

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<ProductDto>>> GetAll(CancellationToken ct)
        => Ok(await _service.GetAllAsync(ct));

    [HttpGet("{id:int}")]
    public async Task<ActionResult<ProductDto>> GetById(int id, CancellationToken ct)
    {
        var p = await _service.GetByIdAsync(id, ct);
        return p is null ? NotFound() : Ok(p);
    }

    [HttpPost]
    [Authorize(Roles = "Admin")]
    public async Task<ActionResult<ProductDto>> Create(CreateProductDto dto, CancellationToken ct)
    {
        var created = await _service.CreateAsync(dto, ct);
        return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
    }
}

The controller is 30 lines. There's nothing to test here that isn't already tested in the service. In a real codebase I'd skip controller unit tests entirely and rely on integration tests with WebApplicationFactory.

The auth flow is a stripped-down demo. POST /api/auth/login with any email and any password returns a JWT. In production, replace this with ASP.NET Identity or an external IdP. The JWT plumbing, the [Authorize] attribute, and the Swagger "Authorize" button are all real and work — only the credential check is fake.

Part 5 — what I got wrong

Five things, in order of how much they cost.

5.1 Calling the entity an "anemic domain model" prematurely

When I first wrote Product as a POCO with public setters and put all the logic in ProductService, I called it Clean Architecture. It was not. It was an anemic domain model with extra project references.

The fix was hard. I had to move DecreaseStock, IsInStock, and the audit-timestamp logic from the service into the entity. The service stopped doing math. The tests changed — instead of testing the service, I tested the entity. The result is that the controller, the service, the test, and the future "bulk update from an admin tool" all share the same invariants. The rule is on the data, not on the caller.

The lesson: "Clean Architecture" without rich entities is just folders. The architecture is the placement of behaviour, not the placement of files.

5.2 Repositories with too many methods

I started with IProductRepository.GetByIdAsync, GetByIdWithOrdersAsync, GetActiveProductsAsync, GetProductsByCategoryAsync, GetProductsByPriceRangeAsync, etc. Each method was a one-off query. The repository became a dumping ground for every query in the system.

The fix is the generic IRepository<T> plus the IUnitOfWork aggregate root pattern. The generic repo handles GetById, Add, Update, Remove. Domain-specific queries (GetInStockAsync, GetProductsByCategoryAsync) live on aggregate-specific repos that extend the generic interface:

public interface IProductRepository : IRepository<Product>
{
    Task<IReadOnlyList<Product>> GetInStockAsync(CancellationToken ct = default);
}

If a query is one-off and used in only one place, it can be a LINQ chain in the service. It doesn't need a repository method. Repositories are for queries that are reused, that have business meaning, or that the test needs to mock. Everything else is service code.

5.3 Returning entities from the service layer

First version of ProductService.GetByIdAsync returned Product. The controller passed it directly to Ok(product). ASP.NET serialized all the entity fields, including navigation properties. A Product with an Orders collection returned a JSON blob with 50 nested orders and 200 line items. The response was 2 MB for a single product.

The fix is DTOs. Always DTOs. The service is the boundary between "rich domain object" and "flat data shape for the wire." The service's job is to translate between them. MapToDto is the explicit acknowledgement that the wire format is a contract, not an accident.

5.4 Putting validation in the service

I started with if (dto.Price < 0) throw new ValidationException(...) at the top of every service method. It was repetitive, easy to forget, and mixed validation with business logic.

The fix is FluentValidation. Each DTO has a Validator<T> that lives in the Application project. The validators are auto-registered via DependencyInjection.cs. The service trusts that if it received a DTO, the DTO is valid. The controller, or an action filter, runs the validators before the service is called.

public class CreateProductDtoValidator : AbstractValidator<CreateProductDto>
{
    public CreateProductDtoValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Price).GreaterThan(0);
        RuleFor(x => x.StockQuantity).GreaterThanOrEqualTo(0);
    }
}

This is the "parse, don't validate" principle. The DTO arrives pre-validated. The service code reads as if the data is good. The failure modes are at the boundary, not scattered through the use cases.

5.5 The "test pyramid in reverse"

I wrote integration tests first, because "they test the real thing." Each test spun up a WebApplicationFactory, hit the API with HttpClient, and asserted on the response. The first 5 tests took 8 seconds. The full suite took 90 seconds. CI took 4 minutes. Adding a test meant a 90-second wait.

The fix is the test pyramid: lots of fast unit tests at the bottom, a few integration tests in the middle, and a handful of end-to-end tests at the top. Domain tests are pure functions, no I/O — 6 tests in 50 ms. Service tests use Moq for IUnitOfWork — 5 tests in 200 ms. Controller tests use WebApplicationFactory — 0 tests in my repo, because the controllers are 30-line pass-throughs that don't need their own tests.

The current test count is 11. The current test time is 1.2 seconds. That's the right ratio.

The repo and how to run it

The full source is at github.com/ZalaAvinash/dotnet-clean-architecture-starter. MIT license. 11 tests passing on every push.

git clone https://github.com/ZalaAvinash/dotnet-clean-architecture-starter.git
cd dotnet-clean-architecture-starter
dotnet run --project src/CleanArchitecture.Api
# Swagger UI: http://localhost:5xxx/swagger

Or with Docker:

docker compose up --build
# API at http://localhost:8080/swagger

Run the tests:

dotnet test
# 11 passed, 0 failed, ~1.2s

Closing

Clean Architecture in .NET is not a folder structure. It is a constraint on where code is allowed to live, enforced by the project system. A controller cannot call DbContext because the project reference doesn't allow it. A service cannot return an entity to the controller because the convention is "services return DTOs." A test can run in 5 ms because the unit test never touches the database, because the project structure makes that the natural way to write the test.

If your team is having the "where does the business logic go" argument, this template is the answer. Fork it, rename Product to Invoice or Patient or Trade, and ship the thing. The architecture is the same. The folder names are the same. The patterns are the same. The only thing that changes is the business.

If you fork it and add a feature — CQRS, MediatR, FluentValidation on the entity, multi-tenancy — send me a PR. I'd especially like to see a real auth flow with ASP.NET Identity or Auth0. The demo AuthController works for the template, but it shouldn't ship to production as-is.


Build with: .NET 8 · ASP.NET Core · EF Core 8 · SQLite / SQL Server · xUnit · FluentAssertions · Moq · FluentValidation · Serilog · Swagger · JWT · Docker

Repo: ZalaAvinash/dotnet-clean-architecture-starter

About the author: Avinash Zala is a senior .NET engineer in Surat, India, with 7+ years building enterprise web apps, APIs, and ERP systems. He writes about what he learns on the job. GitHub · LinkedIn