DEV Community

ismail Cagdas
ismail Cagdas

Posted on

An Opinionated Solution Structure for ASP.NET Core Projects


As software projects grow, maintaining a clean and organized codebase becomes increasingly challenging. A well-thought-out solution structure can make the difference between a maintainable application and a tangled mess of dependencies. In this article, I'll walk you through the opinionated solution structure we use in ABP Framework — a structure that has proven itself in thousands of production applications.

Why Solution Structure Matters

Before diving into the details, let's understand why investing time in your solution structure pays off:

  • Maintainability: Clear boundaries make code easier to understand and modify
  • Testability: Well-separated layers are easier to test in isolation
  • Team Collaboration: Developers can work on different layers without conflicts
  • Scalability: A good structure grows with your application

The Root Level

We have a sample project named Acme.BookStore. I will try to explain the solution structure using this sample ASP.NET Core application.
Let's start with what you'll find at the root of the solution:

Acme.BookStore/
├── src/                    # Source projects
├── test/                   # Test projects
├── Acme.BookStore.sln      # Solution file
├── common.props            # Shared MSBuild properties
├── global.json             # SDK version pinning
└── NuGet.Config            # Package source configuration
Enter fullscreen mode Exit fullscreen mode

The separation of src/ and test/ folders provides immediate clarity. The common.props file centralizes build configurations, ensuring consistency across all projects.

Source Projects: A Layered Architecture

The src/ folder contains the heart of your application, organized into distinct layers:

Domain Shared (Acme.BookStore.Domain.Shared)

This is the foundation layer with zero dependencies on other project layers. It contains:

  • Constants and enums
  • Shared value objects
  • Localization resources
  • Exception codes
// Example: BookType enum that can be shared across all layers
public enum BookType
{
    Adventure,
    Biography,
    Fiction,
    Science
}
Enter fullscreen mode Exit fullscreen mode

Why separate this? These elements are often needed by multiple layers and even external clients. Keeping them in a dedicated project prevents circular dependencies.

Domain (Acme.BookStore.Domain)

The core business logic lives here:

  • Entities and aggregate roots
  • Repository interfaces (not implementations!)
  • Domain services
  • Domain events
public class Book : AggregateRoot<Guid>
{
    public string Name { get; set; }
    public BookType Type { get; set; }
    public DateTime PublishDate { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This layer is infrastructure-agnostic — it doesn't know about databases, APIs, or UI frameworks.

Application Contracts (Acme.BookStore.Application.Contracts)

This project defines the application layer's public API:

  • Data Transfer Objects (DTOs)
  • Application service interfaces
  • Permission definitions
public interface IBookAppService : IApplicationService
{
    Task<BookDto> GetAsync(Guid id);
    Task<PagedResultDto<BookDto>> GetListAsync(GetBookListDto input);
    Task<BookDto> CreateAsync(CreateBookDto input);
}
Enter fullscreen mode Exit fullscreen mode

Key benefit: This project can be shared with clients (like a Blazor WebAssembly app) without exposing implementation details.

Application (Acme.BookStore.Application)

Implementation of the application services:

  • Application service implementations
  • Object mapping configurations
  • Business workflow orchestration
public class BookAppService : ApplicationService, IBookAppService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public async Task<BookDto> GetAsync(Guid id)
    {
        var book = await _bookRepository.GetAsync(id);
        return ObjectMapper.Map<Book, BookDto>(book);
    }
}
Enter fullscreen mode Exit fullscreen mode

Entity Framework Core (Acme.BookStore.EntityFrameworkCore)

Infrastructure layer for data access:

  • DbContext implementation
  • Repository implementations
  • Entity configurations
  • Database migrations
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Books { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.Entity<Book>(b =>
        {
            b.ToTable("Books");
            b.Property(x => x.Name).IsRequired().HasMaxLength(128);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API (Acme.BookStore.HttpApi)

API controllers that expose your application services:

[Route("api/books")]
public class BookController : AbpController
{
    private readonly IBookAppService _bookAppService;

    [HttpGet("{id}")]
    public Task<BookDto> GetAsync(Guid id)
    {
        return _bookAppService.GetAsync(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API Client (Acme.BookStore.HttpApi.Client)

This is a powerful pattern — typed HTTP client proxies for consuming your API:

// Consumers can use IBookAppService directly
// The proxy handles HTTP calls transparently
var books = await _bookAppService.GetListAsync(new GetBookListDto());
Enter fullscreen mode Exit fullscreen mode

This enables code sharing between server and client applications, reducing duplication and ensuring type safety.

Web (Acme.BookStore.Web)

The UI layer — whether Razor Pages, Blazor, or MVC:

  • Pages/Components
  • View models
  • Static assets
  • UI-specific configurations

Database Migrator (Acme.BookStore.DbMigrator)

A console application for:

  • Running database migrations
  • Seeding initial data
  • DevOps-friendly deployment scenarios (Like updating all tenant databases if you use database-per tenant approach)

Test Projects: Comprehensive Coverage

The test/ folder mirrors the source structure:

Project Purpose
TestBase Shared test infrastructure and utilities
Domain.Tests Unit tests for domain logic
Application.Tests Application service integration tests
EntityFrameworkCore.Tests Repository and database tests
HttpApi.Client.ConsoleTestApp End-to-end API testing
Web.Tests UI layer tests

This structure makes it clear what you're testing and enables focused test runs during development.

Key Benefits of This Structure

1. Clear Separation of Concerns

Each project has a single, well-defined responsibility. Developers immediately know where to find and place code.

2. Dependency Inversion in Practice

The domain layer defines repository interfaces; the infrastructure layer implements them. Your business logic remains pure.

3. Shareable Contracts

The .Domain.Shared and .Application.Contracts projects can be packaged as NuGet packages and shared:

  • Across microservices
  • With client applications
  • With external integrators

4. Testability by Design

Each layer can be tested in isolation with appropriate mocking strategies.

5. Database Independence

Switching from SQL Server to PostgreSQL? Only the EntityFrameworkCore project needs changes.

6. Ready for Growth

This structure naturally evolves into modular monolith or microservices architectures.

Beyond Monolith: Modular and Microservice Architectures

While this article focuses on a single application structure, the same principles scale beautifully:

Modular Monolith

You can organize your solution into independent modules, each following this layered structure. Modules communicate through well-defined interfaces, giving you the benefits of microservices without the operational complexity.

Microservices

Each microservice can follow this exact structure, with the .Domain.Shared and .Application.Contracts projects published as NuGet packages for inter-service communication.

ABP Framework provides comprehensive templates and guidance for both architectures. Check out the ABP Framework GitHub repository for:

  • Modular application templates
  • Microservice solution templates
  • Real-world sample applications

Getting Started

If you'd like to try this structure yourself, ABP Framework provides CLI tools to generate solutions with this exact architecture:

dotnet tool install -g Volo.Abp.Studio.Cli
abp new Acme.BookStore -m none --theme leptonx-lite -csf --connection-string "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True;TrustServerCertificate=true"
Enter fullscreen mode Exit fullscreen mode

Or, you can go to https://abp.io/get-started page and select other options like PostgreSQL, MongoDB, Angular, Blazor etc...

Conclusion

A well-organized solution structure is an investment that pays dividends throughout your project's lifecycle. The layered architecture presented here — with its clear separation between domain, application, infrastructure, and presentation layers — provides a solid foundation for building maintainable, testable, and scalable ASP.NET Core applications.

I encourage you to explore the ABP Framework on GitHub to see this structure in action across numerous modules and sample applications. As one of the developers of ABP Framework, I've seen firsthand how this architecture helps teams build robust applications efficiently.

Whether you're building a simple CRUD application or an enterprise-grade distributed system, starting with a clean structure will serve you well. Happy coding!


Have questions or feedback? Feel free to open an issue on the ABP Framework GitHub repository or join our community discussions.

An Opinionated Solution Structure for ASP.NET Core Projects

As software projects grow, maintaining a clean and organized codebase becomes increasingly challenging. A well-thought-out solution structure can make the difference between a maintainable application and a tangled mess of dependencies. In this article, I'll walk you through the opinionated solution structure we use in ABP Framework — a structure that has proven itself in thousands of production applications.

Why Solution Structure Matters

Before diving into the details, let's understand why investing time in your solution structure pays off:

  • Maintainability: Clear boundaries make code easier to understand and modify
  • Testability: Well-separated layers are easier to test in isolation
  • Team Collaboration: Developers can work on different layers without conflicts
  • Scalability: A good structure grows with your application

The Root Level

We have a sample project named Acme.BookStore. I will try to explain the solution structure using this sample ASP.NET Core application.
Let's start with what you'll find at the root of the solution:

Acme.BookStore/
├── src/                    # Source projects
├── test/                   # Test projects
├── Acme.BookStore.sln      # Solution file
├── common.props            # Shared MSBuild properties
├── global.json             # SDK version pinning
└── NuGet.Config            # Package source configuration
Enter fullscreen mode Exit fullscreen mode

The separation of src/ and test/ folders provides immediate clarity. The common.props file centralizes build configurations, ensuring consistency across all projects.

Source Projects: A Layered Architecture

The src/ folder contains the heart of your application, organized into distinct layers:

Domain Shared (Acme.BookStore.Domain.Shared)

This is the foundation layer with zero dependencies on other project layers. It contains:

  • Constants and enums
  • Shared value objects
  • Localization resources
  • Exception codes
// Example: BookType enum that can be shared across all layers
public enum BookType
{
    Adventure,
    Biography,
    Fiction,
    Science
}
Enter fullscreen mode Exit fullscreen mode

Why separate this? These elements are often needed by multiple layers and even external clients. Keeping them in a dedicated project prevents circular dependencies.

Domain (Acme.BookStore.Domain)

The core business logic lives here:

  • Entities and aggregate roots
  • Repository interfaces (not implementations!)
  • Domain services
  • Domain events
public class Book : AggregateRoot<Guid>
{
    public string Name { get; set; }
    public BookType Type { get; set; }
    public DateTime PublishDate { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

This layer is infrastructure-agnostic — it doesn't know about databases, APIs, or UI frameworks.

Application Contracts (Acme.BookStore.Application.Contracts)

This project defines the application layer's public API:

  • Data Transfer Objects (DTOs)
  • Application service interfaces
  • Permission definitions
public interface IBookAppService : IApplicationService
{
    Task<BookDto> GetAsync(Guid id);
    Task<PagedResultDto<BookDto>> GetListAsync(GetBookListDto input);
    Task<BookDto> CreateAsync(CreateBookDto input);
}
Enter fullscreen mode Exit fullscreen mode

Key benefit: This project can be shared with clients (like a Blazor WebAssembly app) without exposing implementation details.

Application (Acme.BookStore.Application)

Implementation of the application services:

  • Application service implementations
  • Object mapping configurations
  • Business workflow orchestration
public class BookAppService : ApplicationService, IBookAppService
{
    private readonly IRepository<Book, Guid> _bookRepository;

    public async Task<BookDto> GetAsync(Guid id)
    {
        var book = await _bookRepository.GetAsync(id);
        return ObjectMapper.Map<Book, BookDto>(book);
    }
}
Enter fullscreen mode Exit fullscreen mode

Entity Framework Core (Acme.BookStore.EntityFrameworkCore)

Infrastructure layer for data access:

  • DbContext implementation
  • Repository implementations
  • Entity configurations
  • Database migrations
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Books { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.Entity<Book>(b =>
        {
            b.ToTable("Books");
            b.Property(x => x.Name).IsRequired().HasMaxLength(128);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API (Acme.BookStore.HttpApi)

API controllers that expose your application services:

[Route("api/books")]
public class BookController : AbpController
{
    private readonly IBookAppService _bookAppService;

    [HttpGet("{id}")]
    public Task<BookDto> GetAsync(Guid id)
    {
        return _bookAppService.GetAsync(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

HTTP API Client (Acme.BookStore.HttpApi.Client)

This is a powerful pattern — typed HTTP client proxies for consuming your API:

// Consumers can use IBookAppService directly
// The proxy handles HTTP calls transparently
var books = await _bookAppService.GetListAsync(new GetBookListDto());
Enter fullscreen mode Exit fullscreen mode

This enables code sharing between server and client applications, reducing duplication and ensuring type safety.

Web (Acme.BookStore.Web)

The UI layer — whether Razor Pages, Blazor, or MVC:

  • Pages/Components
  • View models
  • Static assets
  • UI-specific configurations

Database Migrator (Acme.BookStore.DbMigrator)

A console application for:

  • Running database migrations
  • Seeding initial data
  • DevOps-friendly deployment scenarios (Like updating all tenant databases if you use database-per tenant approach)

Test Projects: Comprehensive Coverage

The test/ folder mirrors the source structure:

Project Purpose
TestBase Shared test infrastructure and utilities
Domain.Tests Unit tests for domain logic
Application.Tests Application service integration tests
EntityFrameworkCore.Tests Repository and database tests
HttpApi.Client.ConsoleTestApp End-to-end API testing
Web.Tests UI layer tests

This structure makes it clear what you're testing and enables focused test runs during development.

Key Benefits of This Structure

1. Clear Separation of Concerns

Each project has a single, well-defined responsibility. Developers immediately know where to find and place code.

2. Dependency Inversion in Practice

The domain layer defines repository interfaces; the infrastructure layer implements them. Your business logic remains pure.

3. Shareable Contracts

The .Domain.Shared and .Application.Contracts projects can be packaged as NuGet packages and shared:

  • Across microservices
  • With client applications
  • With external integrators

4. Testability by Design

Each layer can be tested in isolation with appropriate mocking strategies.

5. Database Independence

Switching from SQL Server to PostgreSQL? Only the EntityFrameworkCore project needs changes.

6. Ready for Growth

This structure naturally evolves into modular monolith or microservices architectures.

Beyond Monolith: Modular and Microservice Architectures

While this article focuses on a single application structure, the same principles scale beautifully:

Modular Monolith

You can organize your solution into independent modules, each following this layered structure. Modules communicate through well-defined interfaces, giving you the benefits of microservices without the operational complexity.

Microservices

Each microservice can follow this exact structure, with the .Domain.Shared and .Application.Contracts projects published as NuGet packages for inter-service communication.

ABP Framework provides comprehensive templates and guidance for both architectures. Check out the ABP Framework GitHub repository for:

  • Modular application templates
  • Microservice solution templates
  • Real-world sample applications

Getting Started

If you'd like to try this structure yourself, ABP Framework provides CLI tools to generate solutions with this exact architecture:

dotnet tool install -g Volo.Abp.Studio.Cli
abp new Acme.BookStore -m none --theme leptonx-lite -csf --connection-string "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True;TrustServerCertificate=true"
Enter fullscreen mode Exit fullscreen mode

Or, you can go to https://abp.io/get-started page and select other options like PostgreSQL, MongoDB, Angular, Blazor etc...

Conclusion

A well-organized solution structure is an investment that pays dividends throughout your project's lifecycle. The layered architecture presented here — with its clear separation between domain, application, infrastructure, and presentation layers — provides a solid foundation for building maintainable, testable, and scalable ASP.NET Core applications.

I encourage you to explore the ABP Framework on GitHub to see this structure in action across numerous modules and sample applications. As one of the developers of ABP Framework, I've seen firsthand how this architecture helps teams build robust applications efficiently.

Whether you're building a simple CRUD application or an enterprise-grade distributed system, starting with a clean structure will serve you well. Happy coding!


Have questions or feedback? Feel free to open an issue on the ABP Framework GitHub repository or join our community discussions.

Top comments (0)