Practical Steps for Applying Clean Architecture with ASP.NET Core
You want to learn the steps for using Clean Architecture with ASP.NET Core. Clean Architecture helps you keep your code neat and easy to test. It also makes your code simple to change. These ideas work on many platforms, but you will see examples with ASP.NET Core. You can start this even if you have never tried Clean Architecture before.
Key Takeaways
Split your ASP.NET Core app into four layers. The Domain layer holds business rules. The Application layer has use cases. The Infrastructure layer handles data access. The Presentation layer is for the user interface.
Keep business logic away from UI and data code. This makes your app easier to test. It also helps you change and grow your app.
Use dependency injection to link services together. This keeps your code flexible. It also makes it easy to take care of.
Make clear projects and folders for each layer. This keeps your solution neat. It also makes it simple to use.
Write unit tests for Domain and Application layers. This checks if your business logic works. You do not need databases or UI for these tests.
Clean Architecture Basics
Principles
You want your code to be simple to change and test. Clean Architecture helps by keeping each part of your app separate. You do not put business rules with user interface or data access code. This makes your code neat and easy to follow.
Clean Architecture keeps business logic away from frameworks, databases, and user interfaces.
You can test business rules without thinking about the database or web pages.
You split your app into small modules. Each module has one job. This helps you take care of your code and lets your team work faster.
You can add new features or change old ones. You do not break other parts of your app.
If you want to use microservices later, Clean Architecture helps. Your modules are already separate.
The Decorator pattern fits well with Clean Architecture in ASP.NET Core. You can add logging or validation to your services. You do not change the main logic. This keeps your code neat and easy to change. You can test each part alone.
Layers
Clean Architecture uses layers to keep your code organized. Each layer has its own job.
You start with the Domain layer. This layer knows your business rules. The Application layer uses these rules to help users. The Infrastructure layer talks to the database or other services. The Presentation layer shows things to users or sends data through APIs.
Each layer only talks to the one below it. You keep dependencies pointing inward. This makes your code simple to test and change.
Solution Structure
Projects
You can make a Visual Studio solution with different projects for each layer. This way, your code stays neat and simple to work with. Here is a usual way to set up your solution:
Domain (Class Library): This project has your business logic and main entities.
Application (Class Library): This project has use cases, interfaces, and services for your app.
Infrastructure (Class Library): This project has code for data access, outside APIs, and other tech stuff.
WebUI (ASP.NET Core Web Application): This project handles the user interface, API endpoints, and dependency injection.
Set Project References
The WebUI project points to the Application, Infrastructure, and Domain projects.
The Application project points to the Domain project.
The Infrastructure project points to both Application and Domain.
The Domain project does not point to any other project.
Use Consistent Naming
Give your projects clear names. For example, use
YourApp.Domain
,YourApp.Application
,YourApp.Infrastructure
, andYourApp.WebUI
.Clear names help you find and understand each part of your solution.
Add Shared and Test Projects (Optional)
You can add a
SharedKernel
project for tools that many projects use.Make test projects for each layer, like
YourApp.Domain.Tests
.
Tip:
Use a.gitignore
file to keep your code clean. AddDirectory.Build.props
andDirectory.Packages.props
to manage shared settings and package versions for all projects.
Organization
Keeping your folders and files organized in each project helps your codebase stay tidy and easy to grow. Here is a table that shows common folder names and what they are for:
You can follow these steps to keep your solution easy to change and ready to grow:
Centralize Package Management
Use oneDirectory.Packages.props
file for NuGet package versions. This keeps all projects using the same versions.Separate Responsibilities
Put business logic in the Domain project. Put data access and outside connections in Infrastructure. This makes your code easier to test and fix.Refactor Existing Solutions
If you already have a project, move business logic into a new Domain project. Move data access code into Infrastructure. Keep controllers and UI code in WebUI. Register dependencies in the WebUI project’sStartup.cs
orProgram.cs
file.Plan for Modularity and Scalability
Modular design lets you add new features without breaking old code. You can grow your app by splitting modules into new services if needed. Use CI/CD pipelines to test and deploy each module automatically.Keep Dependencies Clear
Only reference what you need in each project. Do not make circular references. This helps Clean Architecture and keeps your solution simple.
Note:
Modular solutions help you support more users and add new features. They also make it easier to test and update your code as time goes on.
By following these steps, you build a strong base for Clean Architecture in your ASP.NET Core projects. This setup helps you keep your code easy to fix, test, and grow in the future.
Domain Layer
Entities
You begin by making main business entities in the Domain Layer. Entities are important things in your app, like User
, Order
, or Product
. Each entity has its own identity and follows business rules.
Here are steps to make strong entities and value objects:
Make a domain project. This keeps business logic apart from other code.
Choose what is an entity and what is a value object. Entities have special IDs. Value objects show details, like address or money, and do not have IDs.
Make value objects so they cannot change. You compare them by their details, not by their ID.
Sort your domain layer with folders. Use folders like
Entities
andValueObjects
to keep things neat.Use aggregates and aggregate roots. Put related entities together. Commands should go to aggregate roots.
Put business rules inside your entities. This keeps your logic safe and easy to find.
Keep domain logic away from infrastructure code. Your domain model stays clean and can be reused.
Use value objects for hard ideas, like
Address
orCurrency
. This makes your code easier to read and fix.
Tip:
Do not mix different jobs in your entities. Keep UI and validation logic out of the Domain Layer. Protect private data by making fields private or read-only.
Here is a table to help you sort your Domain Layer:
You avoid mistakes by keeping entities focused on business rules. Do not connect entities to outside tech. Do not make entities that only hold data. Always put business logic in the Domain Layer.
Interfaces
Interfaces help keep your Domain Layer clean and flexible. You use interfaces to say what your app can do, without picking how it does it.
Interfaces show business actions and rules. You do not tie them to any tech or framework.
Other layers, like Infrastructure, use these interfaces. This keeps your Domain Layer pure.
You use dependency injection to add code at runtime. This makes your code easy to test and change.
You often make interfaces for repositories and services. You put these interfaces in the Domain Layer and use them somewhere else.
This way helps you build apps that are easy to fix and grow. You keep jobs clear and separate.
Note:
When you use interfaces, you make your app easier to test. You can switch code or use fake code for testing.
You keep your Domain Layer as the heart of your app. You set ideas, rules, and actions here. Other layers handle saving data or talking to outside services.
Clean Architecture helps you build strong, reusable domain models. You focus on business logic and keep your code easy to test and grow.
Application Layer
Use Cases
In the Application Layer, you split your business logic into use cases. Each use case is a special action, like registering a user or placing an order. You keep these use cases away from infrastructure and UI code. This helps your app stay easy to test and fix.
To make a use case, do these steps:
Check the input data. Make sure it follows your rules.
See if the entity is already there. For example, check if a user with the same email exists.
Hide sensitive data by hashing things like passwords.
Make a new entity with your domain models.
Save the entity using a repository interface. This keeps your code free from database details.
Give back the result, like a success message or the new entity’s ID.
You use dependency injection to get services, like repositories or password hashers. This makes your code flexible and simple to test. Name your use cases clearly, like RegisterUserHandler
. The Application Layer connects your domain logic to the outside world.
Tip:
Always put your business logic in the Application Layer. Do not mix it with data access or UI code.
DTOs
Data Transfer Objects (DTOs) help you move data between layers without making tight links. You use DTOs to send only the needed information. This keeps your Application Layer free from infrastructure and UI.
You can update one layer without breaking others.
DTOs hide things you do not want to share, like passwords or inside IDs.
Use simple, flat DTOs for each use case. Do not add extra fields or nest objects too much.
Make different DTOs for different actions, like creating or updating data.
Keep DTOs unchangeable to avoid mistakes.
Use tools like AutoMapper to quickly map between domain models and DTOs.
Note:
Never put business logic in your DTOs. They should only carry data.
By doing these steps, you keep your Application Layer clean, easy to test, and ready for new changes.
Infrastructure & Presentation
Data Access
You work with data in the Infrastructure Layer. This part connects your app to databases, APIs, or files. First, make classes that use repository interfaces from the Domain Layer. These classes use tools like Entity Framework Core or Dapper to talk to your database.
Here are steps for setting up data access:
Put your database context class in the Infrastructure project. Entity Framework Core works well with ASP.NET Core.
Make repository classes. Each class matches an interface from the Domain Layer, like
IUserRepository
orIOrderRepository
.Register your database context and repositories in the dependency injection container. Do this in the
Startup.cs
orProgram.cs
file in your WebUI project.If you need ASP.NET Core Identity, add it in the Infrastructure Layer. Set it up to use your database context for managing users.
Tip:
Keep data access code out of the Domain and Application Layers. This keeps your business logic clean and easy to test.
Controllers
Controllers are in the Presentation Layer. They handle HTTP requests and send back responses. Each controller should call the Application Layer to do things. Do not put business logic in controllers.
Here are some good ways to connect controllers to the Application Layer:
The best ways to connect controllers and endpoints to the Application Layer in ASP.NET Core are:
Use asynchronous programming by returning
Task
instead ofasync void
. This helps with the HTTP request lifecycle.Do not use
HttpContext
after the request is done. Only use it during the request.Do not use
HttpContext
or scoped services in background threads. Make new scopes for background work.Do not change response headers after the body starts. Use
HttpResponse.OnStarting
to change headers.Use in-process hosting with IIS for better speed.
You can test your Infrastructure and Presentation Layers well by doing these steps:
Split your app into clear layers.
Put interfaces and business logic in the Application Core.
Make interfaces work in the Infrastructure Layer for outside communication.
Put controllers and UI parts in the Presentation Layer.
Use dependency injection to test each layer by itself.
For Infrastructure, use unit tests with fake Application Core interfaces.
For Presentation, use unit or integration tests with abstractions and dependency injection.
If you follow these steps, your code stays clean, easy to test, and ready for changes.
Dependency Injection
Service Registration
You use dependency injection (DI) in ASP.NET Core to link interfaces and classes. DI helps your code stay neat and easy to test. It also makes changes simple. Here is how you do it:
Put your service registrations in
Program.cs
. UseAddScoped
,AddTransient
, orAddSingleton
to set how long each service lasts.Make interfaces in your core layer, like
IEmailService
. Write the code for these interfaces in the infrastructure layer, such asSmtpEmailService
.Register your services with code like:
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
Use constructor injection in your classes. Add the needed service as a parameter in the class constructor.
Keep your service registrations tidy with extension methods. This helps your
Program.cs
file stay clean and lets your project grow.Always use the Dependency Inversion Principle. Your core layer should depend on abstractions, not real classes. This makes it easy to swap code or use mocks for tests.
Tip:
ASP.NET Core’s DI container lets you get both framework and app services easily. This setup helps with unit testing and keeps your code flexible.
References
You need to manage project references to keep your solution neat and modular. Follow these best practices:
Keep the core layer on its own. Do not let it point to other layers.
Let outer layers, like API and infrastructure, point to the core layer. The core layer holds your business logic and interfaces.
Put the real code in the infrastructure layer. This layer talks to databases and outside services.
Use a
DependencyInjection.cs
extension class in each layer. This helps you register services in a clear way.Register services at runtime with DI. This helps keep your code loosely connected and easy to test.
Do not use the service locator pattern. Do not call
GetService
or use factories to get services at runtime.Do not use
HttpContext
in a static way. This can cause problems with tests and service lifetimes.Never call
BuildServiceProvider
insideConfigureServices
. This can make more than one container and cause bugs.
Note:
Always check that your service lifetimes match. For example, do not let a singleton use a scoped service. This stops errors and keeps your app stable.
If you follow these steps, your ASP.NET Core project will be easy to keep up, test, and grow. Dependency injection helps you build strong, clean solutions that follow Clean Architecture principles.
You can now build strong ASP.NET Core apps. First, set up four layers. These layers are Domain, Application, Infrastructure, and Presentation. Here are the steps you should follow:
Make domain entities and value objects.
Put use cases and business logic in the application layer.
Link to databases and services in the infrastructure layer.
Take care of user requests in the presentation layer.
Test each layer with unit and integration tests.
Keep learning and practicing new skills. Your projects will be easier to fix and will grow better.
FAQ
How do you start refactoring an existing ASP.NET Core project to Clean Architecture?
Start by moving business logic into a new Domain project. Next, put data access code in Infrastructure. Put controllers in Presentation. Change project references as needed. Register dependencies in your startup file. Test each layer while you work.
What tools help you organize Clean Architecture projects in ASP.NET Core?
You can use Visual Studio or JetBrains Rider. Both let you make many projects in one solution. Use solution folders to group layers together. Add a .gitignore
file to keep your codebase neat.
Can you use Entity Framework Core with Clean Architecture?
Yes, you can use it. Put your DbContext
and migrations in Infrastructure. Keep domain entities free from EF Core attributes. Use repository interfaces in Domain and write their code in Infrastructure.
How do you test business logic in Clean Architecture?
Write unit tests for Domain and Application layers. Use fake repositories or services. Test business rules without using the database or UI. This keeps tests quick and focused.
What is the best way to manage dependencies between layers?
Only reference what you need. Domain should not point to other layers. Use dependency injection in Presentation to link interfaces and code. Do not make circular references. This keeps your solution clean.