
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
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
}
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; }
}
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);
}
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);
}
}
Entity Framework Core (Acme.BookStore.EntityFrameworkCore)
Infrastructure layer for data access:
-
DbContextimplementation - 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);
});
}
}
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);
}
}
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());
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"
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
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
}
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; }
}
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);
}
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);
}
}
Entity Framework Core (Acme.BookStore.EntityFrameworkCore)
Infrastructure layer for data access:
-
DbContextimplementation - 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);
});
}
}
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);
}
}
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());
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"
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)