# Step-by-Step Approach to Use Onion Architecture in .NET

Alright, let's start coding! Before we dive into the code, let's quickly recap the core principles of Onion Architecture. If you want a more in-depth look, you can always check out my previous post on [https://anilgurau.hashnode.dev/why-choose-onion-architecture-for-better-software-development-practicese](https://anilgurau.hashnode.dev/why-choose-onion-architecture-for-better-software-development-practices).

But for those who just need a quick refresher, here's the gist:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743036577620/c692b514-09b0-4697-a94f-d5275d7a1f4e.png align="center")

As you can see, the key idea is that dependencies point inwards, towards the domain core. But there's another crucial aspect: layers communicate exclusively through abstractions, not concrete implementations. This keeps our application flexible, maintainable, and testable. Each layer has a specific responsibility, and we'll be exploring how to implement them step by step in the following posts.

We're going to focus on the practical application of these principles, so let's get started!

## Project Setup: Creating the Solution and Layers

1. **Create Solution and Solution Folders:**
    
    1. In Visual Studio, create a new "Blank Solution" named "`OnionArchitectureGuide`".
        
    2. Create Solution Folders named "Core", "Infrastructure", and "Presentation".
        
2. **Create Domain Layer:**
    
    1. Inside the "Core" Solution Folder, add a new "Class Library (.NET Standard/.NET Library)" project named "`OnionArchitectureGuide.Domain`".
        
3. **Create Application Layer:**
    
    1. Inside the "Core" Solution Folder, add a new "Class Library (.NET Standard/.NET Library)" project named "`OnionArchitectureGuide.Application.Abstraction`".
        
    2. Inside the "Core" Solution Folder, add a new "Class Library (.NET Standard/.NET Library)" project named "`OnionArchitectureGuide.Application.Implementation`".
        
4. **Create Infrastructure Layer:**
    
    1. Inside the "Infrastructure" Solution Folder, add a new "Class Library (.NET Standard/.NET Library)" project named "`OnionArchitectureGuide.Persistence`".
        
5. **Create Presentation Layer:**
    
    1. Inside the "Presentation" Solution Folder, add a new "ASP.NET Core Web API" project named "`OnionArchitectureGuide.Api`".
        
    
    **Note:** Application layer is split into `OnionArchitectureGuide.Application.Abstraction` and `OnionArchitectureGuide.Application.Implementation`. The Presentation layer communicates with the Application layer, but it should not have direct access to implementations. Placing service interfaces and their implementations together violates the principles of Onion Architecture and creates tight coupling between layers. The outer layer should interact with the inner layer only through abstractions, not concrete implementations.
    

Your Folder Structure Look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743043079247/e4962ad9-99a9-42c9-9295-ee3c04ddf58c.png align="left")

## **Setting up Project Dependencies:**

Now, we need to set up the project dependencies according to the Onion Architecture principles. Remember, dependencies should point inwards.

1. `OnionArchitectureGuide.Application.Implementation` references `OnionArchitectureGuide.Domain` and `OnionArchitectureGuide.Application.Abstraction`.
    
2. `OnionArchitectureGuide.Infrastructure` references `OnionArchitectureGuide.Application.Abstraction` and `OnionArchitectureGuide.Domain`.
    
3. `OnionArchitectureGuide.Api` references `OnionArchitectureGuide.Application.Abstraction`
    

Here is a visual Representation of the Project Dependencies.

![Project dependencies Diagram](https://cdn.hashnode.com/res/hashnode/image/upload/v1743032957611/850a0a70-c1d6-406c-83de-d2b494b30ec8.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743032908281/91aa2e64-01fb-48e3-8601-04be4febf710.png align="center")

This dependency structure is crucial. The API depends on the Application layer, and Infrastructure depends on the Application and Domain Layer, and finally Application layer depends on the Domain layer. This keeps our core logic independent and allows us to easily swap out implementations. This inward dependency flow also ensures that changes in outer layers have minimal impact on the inner layers, enhancing stability. Furthermore, it allows for better testability as the core business logic can be tested in isolation.

We now have the basic project structure set up. In the next post, we'll start implementing Onion Architecture layer by layer.

## Layer-by-Layer Implementation

Alright, we've laid the foundation, now lets construct Domain layer.

### Constructing Domain Layer

Now, let's define our domain entities and repository interfaces within the `OnionArchitectureGuide.Domain` project. This layer holds our core business objects and their contracts.

Create Folder named `Entities` inside `OnionArchitectureGuide.Core` project and add classes:

```csharp
public class Book
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public DateTime PublishedOn { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}
```

```csharp
public class Author
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public List<Book> Books { get; set; }
}
```

Create `Contracts` Folder inside `OnionArchitectureGuide.Core` project and add Repository Interfaces:

```csharp
public interface IBookRepository
{
    Task<List<Book>> GetAllAsync();
}
```

```csharp
public interface IAuthorRepository
{
    Task<List<Author>> GetAllAsync();
}
```

Your Domain layer should look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743046093544/6960b24f-d4d3-4886-8017-3f100c534f99.png align="left")

The Core layer holds our domain model, free from external dependencies. This ensures core logic is reusable and maintainable.

Next, we'll create abstractions in the `OnionArchitectureGuide.Application.Abstraction` before implementing these contracts in the `OnionArchitectureGuide.Application.Implementation`.

### **Building Application Contracts (Abstraction)**

Now that we've defined our domain entities and repository contracts in the Domain layer, let's move on to creating the application contracts in the `OnionArchitectureGuide.Application.Abstraction` project. This layer will define the DTOs and interfaces that our application services will implement.

Inside the `OnionArchitectureGuide.Application.Abstraction` project, create a folder named `Contracts`. This folder will hold our service interface definitions.

```csharp
public interface IBookService
{
    Task<List<BookDto>> GetAllBooks();
}
```

```csharp
public interface IAuthorService
{
    Task<List<AuthorDto>> GetAllAuthors();
}
```

Create `DTOs` Folder inside `OnionArchitectureGuide.Application.Abstraction` project and add Data transfer objects:

```csharp
public class BookDto
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public DateTime PublishedOn { get; set; }
}
```

```csharp
public class AuthorDto
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
}
```

Your `OnionArchitectureGuide.Application.Abstraction` project should look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743053205746/e88eccbb-0ddf-40a5-9c38-95dc585be9c5.png align="left")

By defining these service interfaces and DTOs in a separate `OnionArchitectureGuide.Application.Abstraction` layer, we decouple our application's core logic from the specific implementations and the data transfer details. This allows us to easily switch out implementations or change data transfer formats without affecting the rest of the application.

Next, we'll implement these service contracts in the `OnionArchitectureGuide.Application.Implementation`.

### **Implementing Application Services**

Alright, let's move on to the `OnionArchitectureGuide.Application.Implementation` project. We'll be focusing on implementing the service contracts and utilizing the DTOs we defined in the `OnionArchitectureGuide.Application.Abstraction`.

Before Implementing this layer, lets first install required packages from Nuget package manager or package manager console.

```bash
NuGet\Install-Package AutoMapper -Version 14.0.0
```

Lets Create Mapping Profiles now. Create Folder named `Mappings` inside `OnionArchitectureGuide.Application.Implementation` project and add AutoMapper profiles:

```csharp
internal class ApplicationProfile: Profile
{
    public ApplicationProfile()
    {
        CreateMap<BookDto, Book>().ReverseMap();
        CreateMap<Author, AuthorDto>().ReverseMap();
    }
}
```

Create Folder named `Services` inside `OnionArchitectureGuide.Application.Implementation` project and add service implementations:

```csharp
public class BookService : IBookService
{
    private readonly IBookRepository _bookRepository;
    private readonly IMapper _mapper;
    public BookService(IBookRepository bookRepository, IMapper mapper)
    {
        _bookRepository = bookRepository;
        _mapper = mapper;
    }
    public async Task<List<BookDto>> GetAllBooks()
    {
        return _mapper.Map<List<BookDto>>(await _bookRepository.GetAllAsync());
    }
}
```

```csharp
internal class AuthorService : IAuthorService
{
    private readonly IAuthorRepository _authorRepository;
    private readonly IMapper _mapper;
    public AuthorService(IAuthorRepository authorRepository, IMapper mapper)
    {
        _authorRepository = authorRepository;
        _mapper = mapper;
    }
    public async Task<List<AuthorDto>> GetAllAuthors()
    {
        return _mapper.Map<List<AuthorDto>>(await _authorRepository.GetAllAsync());
    }
}
```

This layer contains the core business logic of our application. By keeping it separate from the presentation and infrastructure layers, we ensure that our business logic is reusable and testable.

Before implementing Infrastructure layer, lets register all the Dependencies for this layer.

Create a file named `ApplicationServiceExtensions.cs` in the root level of the `OnionArchitectureGuide.Application.Implementation` project.

The `OnionArchitectureGuide.Application.Implementation` layer should look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743059521789/ff40c4ad-5359-4e92-a72a-85d95e2a9382.png align="center")

Now, register all the dependencies for this project:

```csharp
public static class ApplicationServiceExtensions
{
    public static void AddApplicationServices(this IServiceCollection services)
    {
        //Registering all the automapper profiles
        services.AddAutoMapper(Assembly.GetExecutingAssembly());

        services.AddScoped<IBookService,BookService>();
        services.AddScoped<IAuthorService,AuthorService>();
    }
}
```

Next, we'll integrate the infrastructure layer to handle data access and implement repositories.

### Integrating Infrastructure

Now that we've implemented our application services in the `OnionArchitectureGuide.Application.Implementation` project, let's integrate the Infrastructure layer (`OnionArchitectureGuide.Persistence`). The main purpose of this layer is to handle data access, external API integrations, and other infrastructure-related concerns. Since this is a simple implementation, we will only focus on data access and Repository implementation.

Make sure you have installed `Microsoft.EntityFrameworkCore` , `Microsoft.EntityFrameworkCore.SqlServer` and `Microsoft.EntityFrameworkCore.Tools` packages for SQL Server connection, Database operations and Migrations.

Run these commands in Package Manager Console:

```bash
NuGet\Install-Package Microsoft.EntityFrameworkCore -Version 9.0.3
```

```bash
NuGet\Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 9.0.3
```

```bash
NuGet\Install-Package Microsoft.EntityFrameworkCore.Tools -Version 9.0.3
```

After packages are installed, create `Data` Folder, which will hold our data access(DbContext).

```csharp
internal class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Book> Books { get; set; }
    public DbSet<Author> Authors { get; set; }
}
```

Create Folder named `Repositories`, which holds the repository implementations that we declared in the domain layer.

```csharp
internal class BookRepository : IBookRepository
{
    private readonly ApplicationDbContext _context;

    public BookRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<List<Book>> GetAllAsync()
    {
        return await _context.Books.ToListAsync();
    }
}
```

```csharp
internal class AuthorRepository : IAuthorRepository
{
    private readonly ApplicationDbContext _context;

    public AuthorRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<List<Author>> GetAllAsync()
    {
        return await _context.Authors.ToListAsync();
    }
}
```

Now lets register all the dependencies for the Infrastructure layer in the Dependency Injection container.

Create a file named `InfrastructureServiceExtensions.cs` in the root level of the `OnionArchitectureGuide.Persistence` project.

Your `OnionArchitectureGuide.Presistence` should look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743098582438/6ec74067-8fbf-4aa3-8835-a50d21da0850.png align="left")

Register Infrastructure Dependencies as:

```csharp
public static class InfrastructureServiceExtensions
{
    public static void AddInfrastructureServices(this IServiceCollection services, string connectionString)
    {
        //Registering Application DbContext and Seeding Values
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlServer(connectionString);
            options.UseSeeding((context, _) =>
            {
                var book = context.Set<Book>().FirstOrDefault();
                if (book == null)
                {
                    context.Set<Book>().AddRange(BookSeeds());
                    context.SaveChanges();
                }
            })
            .UseAsyncSeeding(async (context, _, cancellationToken) =>
                {
                    var book = context.Set<Book>().FirstOrDefaultAsync();
                    if (book == null)
                    {
                        context.Set<Book>().AddRange(BookSeeds());
                        await context.SaveChangesAsync();
                    }
                });

        });
        services.AddScoped<IBookRepository, BookRepository>();
        services.AddScoped<IAuthorRepository, AuthorRepository>();
    }
    private static List<Book> BookSeeds()
    {
        return new List<Book> { 
            new Book { Title = "Book 1", Author= new Author(){ Name ="XXX"} }, 
            new Book { Title = "Book 2", Author= new Author(){ Name ="YYY"} }
            };
    }
}
```

Infrastructure layer isolates our application's core logic from specific data access and external dependencies. This allows us to easily switch out data access implementations or integrate with different external systems without affecting the rest of the application.

Next, we'll expose our application through an API in the Presentation layer.

### Exposing the Application (API)

Now that we've integrated the Infrastructure layer, let's expose our application's functionality through an API in the Presentation layer. This layer will handle incoming requests and outgoing responses. We'll also configure our dependencies and set up the connection string for our data access.

Inside the `OnionArchitectureGuide.Api/Controllers`, add new API controllers `BookController.cs` and `AuthorController.cs` and create endpoints as shown below.

```csharp
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
    private readonly IBookService _bookService;
    public BookController(IBookService bookService)
    {
        _bookService = bookService;
    }

    [HttpGet("get-books")]
    public async Task<IActionResult> GetBooks()
    {
        return Ok(await _bookService.GetAllBooks());
    }
}
```

```csharp
[Route("api/[controller]")]
[ApiController]
public class AuthorController : ControllerBase
{
    private readonly IAuthorService _authorService;

    public AuthorController(IAuthorService authorService)
    {
        _authorService = authorService;
    }

    [HttpGet("get-authors")]
    public async Task<IActionResult> GetBooks()
    {
        return Ok(await _authorService.GetAllAuthors());
    }
}
```

We're almost there, but first, we must register all necessary dependencies from `OnionArchitectureGuide.Application.Implementation` and `OnionArchitectureGuide.Infrastructure`.

Currently, our `OnionArchitectureGuide.Api` project only references `OnionArchitectureGuide.Application.Abstraction`. While we *could* add references to `OnionArchitectureGuide.Application.Implementation` and `OnionArchitectureGuide.Infrastructure` directly, I prefer to handle dependency registration in a separate class library dedicated to managing these dependencies. This keeps our API project cleaner.

The `OnionArchitectureGuide.Api` project should not directly communicate with `OnionArchitectureGuide.Application.Implementation` or `OnionArchitectureGuide.Infrastructure` via concrete implementations. We should avoid using `ApplicationDbContext`, repositories, or service implementations directly in the API project. This is why all implementations have `internal` access modifiers.

To achieve this, let's create a new "Class Library (.NET Standard/.NET Library)" project named OnionArchitectureGuide.DependencyManager within the 'Presentation' solution folder. Then, add project references to OnionArchitectureGuide.Application.Implementation and OnionArchitectureGuide.Infrastructure to this new project.

The Dependency Flow should look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743104670592/dac9c1fa-0d5f-4e10-9105-38932aea1be4.png align="center")

Here is a screenshot of the `OnionArchitectureGuide.Api` and Dependency Manager.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743104844512/6eb78d7b-964f-44c1-beee-9dfd5b2ecdbc.png align="left")

Lets manage dependencies:

```csharp
public static class DependencyManager
{
    public static void RegisterApplicationDependencies(this IServiceCollection services)
    {
        services.AddApplicationServices();
    }

    public static void RegisterInfrastructureDependencies(this IServiceCollection services, string connectionString)
    {
        services.AddInfrastructureServices(connectionString);
    }
}
```

Register all Dependencies in `Program.cs` of `OnionArchitectureGuide.Api` project.

```csharp
//Register Application and Infrastructure services using Dependency manager
// you can also register directly from OnionArchitectureGuide.Application.Implementation and OnionArchitectureGuide.Presistence
//E.g. builder.Services.AddApplicationServices(); and builder.Services.AddInfrastructureServices(builder.Configuration.GetConnectionString("DefaultConnection") ?? String.Empty);

builder.Services.RegisterApplicationDependencies();
builder.Services.RegisterInfrastructureDependencies(builder.Configuration.GetConnectionString("DefaultConnection")?? String.Empty);
```

Add ConnectionString to `appsettings.json`

```json
"ConnectionStrings": {
  "DefaultConnection":  "Your connection string"
}
```

One last step before we run our application.

**Running Migration:**

Install `Microsoft.EntityFrameworkCore.Design` package in `OnionArchitectureGuide.Api` project, for the migration to work.

```bash
NuGet\Install-Package Microsoft.EntityFrameworkCore.Design -Version 9.0.3
```

* Open Package Manager Console, set Default project to `OnionArchitectureGuide.`Persistence.
    
* Set a Project Startup to `OnionArchitectureGuide.Api`.
    
* Run: `Add-Migration InitialMigration` then `Update-Database`
    

```bash
add-migration initialMigration
```

```bash
update-database
```

We're now ready to run our application. Ensure all dependencies are correctly registered and the connection string is properly configured before proceeding.

* Run the `OnionArchitectureGuide.Api` project.
    
* Use Postman or Swagger to send requests to the API endpoints.
    

Example Output:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1743112220719/3df86dcd-78e5-41a5-81ac-8b3b0f892720.png align="center")

Congratulations! You've successfully built a simple application using Onion Architecture. We've navigated through each layer, implementing the core logic and exposing it through an API.

Now that you've grasped the fundamentals, it's time to put this knowledge into practice. Start by exploring how you can apply these principles to your own projects. Consider:

* **Refactoring existing code:** Identify areas where you can apply Onion Architecture to improve maintainability.
    
* **Expanding your application:** Add new features and functionalities, ensuring they fit within the established architecture.
    
* **Experimenting with different technologies:** Explore how Onion Architecture can be adapted to various databases, frameworks, and external systems.
    

Keep exploring, keep coding, and keep building robust applications. Thank you for joining me on this practical journey. Happy Coding!
