A Guide to Onion Architecture in C#

Introduction

Onion Architecture was introduced by Jeffrey Palermo in 2008. This architecture is designed to increase sustainability, testability, and flexibility in software development. Unlike traditional layered architectures, Onion Architecture focuses on the core business logic and controls the direction of dependencies outward. In this article, we will explore how to implement Onion Architecture using C#.

Why Onion Architecture?

  • Dependency Inversion Principle: Business logic is not dependent on external layers such as data access or user interface.
  • Testability: The core of the application (domain layer) is isolated from external dependencies, making unit testing easier.
  • Flexibility and Sustainability: The application can more easily integrate with different technologies and frameworks and is more adaptable to changes.

Layers of the Architecture

When implementing Onion Architecture, we typically talk about four main layers:

  1. Domain Layer (Core Layer):
    • This is where the business logic and business rules reside.
    • Entities, Value Objects, Interfaces, and Services are included in this layer.
  2. Application Layer:
    • This layer contains services that manage user interactions and business workflows.
    • It contains Use Cases, Command/Query Handlers, and Data Transfer Objects (DTOs).
  3. Infrastructure Layer:
    • This layer handles infrastructure concerns like data access and integration with external services.
    • It typically includes ORM tools like Entity Framework, Dapper, as well as email services and other external dependencies.
  4. Presentation Layer:
    • This is the user interface and API layer.
    • UI frameworks like ASP.NET Core, Angular, or React are placed in this layer.

Example of Onion Architecture in C#

Below is a basic structure to implement Onion Architecture using C#. In this example, we will create a simple “Product Management” application.

1. Domain Layer

// Entities/Product.cs
namespace Domain.Entities
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

// Interfaces/IProductRepository.cs
namespace Domain.Interfaces
{
    public interface IProductRepository
    {
        Product GetProductById(int id);
        IEnumerable<Product> GetAllProducts();
        void AddProduct(Product product);
    }
}

2. Application Layer

// Services/ProductService.cs
using Domain.Entities;
using Domain.Interfaces;

namespace Application.Services
{
    public class ProductService
    {
        private readonly IProductRepository _productRepository;

        public ProductService(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        public Product GetProduct(int id)
        {
            return _productRepository.GetProductById(id);
        }

        public IEnumerable<Product> GetAllProducts()
        {
            return _productRepository.GetAllProducts();
        }

        public void AddProduct(Product product)
        {
            _productRepository.AddProduct(product);
        }
    }
}

3. Infrastructure Layer

// Repositories/ProductRepository.cs
using Domain.Entities;
using Domain.Interfaces;
using System.Collections.Generic;
using System.Linq;

namespace Infrastructure.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly List<Product> _products = new List<Product>();

        public Product GetProductById(int id)
        {
            return _products.FirstOrDefault(p => p.Id == id);
        }

        public IEnumerable<Product> GetAllProducts()
        {
            return _products;
        }

        public void AddProduct(Product product)
        {
            _products.Add(product);
        }
    }
}

4. Presentation Layer

// Controllers/ProductController.cs
using Application.Services;
using Domain.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Presentation.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductController : ControllerBase
    {
        private readonly ProductService _productService;

        public ProductController(ProductService productService)
        {
            _productService = productService;
        }

        [HttpGet("{id}")]
        public IActionResult Get(int id)
        {
            var product = _productService.GetProduct(id);
            if (product == null)
                return NotFound();
            return Ok(product);
        }

        [HttpGet]
        public IActionResult GetAll()
        {
            var products = _productService.GetAllProducts();
            return Ok(products);
        }

        [HttpPost]
        public IActionResult Create(Product product)
        {
            _productService.AddProduct(product);
            return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
        }
    }
}

Conclusion

In this article, we provided a basic guide on how to implement Onion Architecture with C#. Onion Architecture offers a more sustainable, flexible, and testable software development process by controlling dependencies. This structure simplifies the architecture, especially in large and complex projects, and ensures the longevity of the software.

Leave a Reply

Your email address will not be published. Required fields are marked *