DEV Community

Cover image for Onion Architecture in .NET Core: Keeping Your Core Clean (Part 3)
Pouria Ghadiri
Pouria Ghadiri

Posted on

Onion Architecture in .NET Core: Keeping Your Core Clean (Part 3)

Onion Architecture in .NET Core: Keeping Your Core Clean (Part 3)

Alright, here we go—part three! If you’ve been following along, you already know why good architecture matters and how the old-school layered thing works. Now let’s talk about Onion Architecture. This one’s a game-changer if you’re sick of spaghetti code or business logic that’s tangled with database weirdness.

So, What’s Onion Architecture Anyway?

Picture this: your app is an onion (cue the Shrek jokes). The juicy stuff—your domain, the business rules—sits in the middle. Everything else? Just layers wrapping around, like armor. The golden rule: dependencies only point inwards. That means your core logic couldn’t care less about which database you use, what the UI looks like, or what random third-party library you’ve glued on.

Why bother? Clean core. If you want to swap out your database, you can. If the latest JavaScript framework drops and everyone loses their minds, your business rules don’t flinch. No more pollution from infrastructure code sneaking into your logic.

Why Use It?

Because it keeps your business logic clean and untouched by infrastructure chaos. If you’re switching from SQL Server to PostgreSQL, no problem. Building a new UI? Go for it. The core remains the same.

Core Principles of Onion Architecture

  • The domain (business logic) sits at the center.
  • Each layer wraps around the one inside it.
  • Outer layers depend on inner ones—not the other way.
  • Infrastructure is plugged in via interfaces.
  • Code stays clean, testable, and easy to reason about.

How the Layers Stack Up

Here’s a quick look at the architecture:

  • Center: Domain Layer (Entities, Interfaces, Business Rules)
  • Next Layer: Application Layer (Use Cases, Services)
  • Outer Layer: Infrastructure Layer (Database, Email, Logging)
  • Outermost: Web/API/UI Layer (Controllers, Views, Pages)

Each layer only talks to the one just inside it. That’s the key.

A Simple .NET Project Structure

Your folders might look like this:

/MyApp
  /Domain
    Order.cs
    IOrderRepository.cs
  /Application
    OrderService.cs
    IOrderService.cs
  /Infrastructure
    OrderRepository.cs
    EmailSender.cs
  /WebAPI
    OrdersController.cs
Enter fullscreen mode Exit fullscreen mode

Straightforward and clean.

Quick Code Peek

Domain Layer
Defines your Order entity and a repository interface—no database stuff here.

public class Order {
    public Guid Id { get; set; }
    public decimal Total { get; set; }
    public DateTime CreatedAt { get; set; }
}

public interface IOrderRepository {
    Task SaveAsync(Order order);
}
Enter fullscreen mode Exit fullscreen mode

Application Layer
Handles business use cases—like creating an order—but stays blissfully ignorant about where that order gets saved.

public interface IOrderService {
    Task<Guid> CreateAsync(decimal total);
}

public class OrderService : IOrderService {
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository) {
        _repository = repository;
    }

    public async Task<Guid> CreateAsync(decimal total) {
        var order = new Order {
            Id = Guid.NewGuid(),
            Total = total,
            CreatedAt = DateTime.UtcNow
        };

        await _repository.SaveAsync(order);
        return order.Id;
    }
}
Enter fullscreen mode Exit fullscreen mode

Infrastructure Layer
This is where you actually touch the database. It implements those interfaces from the Domain layer.

public class OrderRepository : IOrderRepository {
    private readonly AppDbContext _context;

    public OrderRepository(AppDbContext context) {
        _context = context;
    }

    public async Task SaveAsync(Order order) {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

Web/API Layer
Takes HTTP requests, calls your service, returns IDs. No business logic hiding in here.

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase {
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService) {
        _orderService = orderService;
    }

    [HttpPost]
    public async Task<IActionResult> Create(decimal total) {
        var id = await _orderService.CreateAsync(total);
        return Ok(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Onion vs Layered Architecture

Here’s a quick comparison:

Feature Layered Architecture Onion Architecture
Dependency Direction Top-down Inward only
Domain Focus Not enforced Core of everything
Testability Average Excellent
Infra Isolation Weak Strong
Code Reuse Low High

When Should You Use It?

  • If your app has more logic than just CRUD.
  • If you care about testability and clean architecture.
  • If you're building APIs or microservices.
  • If you want to avoid vendor lock-in (like a specific DB).

Migrating from Layered to Onion

Already have a layered structure? Here’s how to begin:

  • Move your business logic into a Domain project.
  • Use interfaces to abstract data and external services.
  • Use dependency injection to wire it all up.
  • Refactor slowly, step-by-step.

Final Thoughts

Onion Architecture is like giving your business logic its own safe room. Everything else works around it, but can’t mess with it. If you’re tired of fragile, messy code, this approach might just be what you need.

Next up in our series: Clean Architecture. Stay tuned!
Have questions or use this in production? Let’s discuss in the comments!

Top comments (1)

Collapse
 
alexandre_thomazcarvalho profile image
Alexandre Thomaz Carvalho Silva Ale

Great article! I really enjoyed how you broke down Onion Architecture in such a practical and approachable way. Your explanations about dependency direction and the importance of keeping business logic isolated from infrastructure are spot on. The code samples make it easy for anyone to see the benefits and the migration steps are super valuable, especially for teams moving away from traditional layered architectures.

I've also applied Onion Architecture in real-world .NET Core projects, and it really does make testing and maintaining the codebase much easier—especially as the system grows and requirements change. Seeing your perspective and the way you structured the article definitely added a fresh angle for me.

Looking forward to your next piece on Clean Architecture! Thanks for sharing your knowledge and starting this discussion.