Clean Architecture in Blazor Projects
Blazor makes it easy to build interactive web applications with C# and .NET. But as a project grows, the biggest problem is usually not the UI framework itself. The real problem is structure.
If all logic ends up inside Razor components, the application quickly becomes hard to maintain. Components start loading data, applying business rules, talking to the database, handling validation and managing UI state all at once.
That works in a small demo, but it becomes painful in a real project.
Clean Architecture helps solve this by separating responsibilities. The goal is simple: the UI should not control the whole application. It should call application services, render data and respond to user interaction.
The basic idea
Clean Architecture separates the project into layers.
A common structure is:
- Domain
- Application
- Infrastructure
- Web or UI
Each layer has a clear responsibility.
The Domain layer contains the core business concepts. The Application layer contains use cases and service contracts. The Infrastructure layer contains external details such as databases, APIs and file storage. The Blazor UI layer contains pages, components and user interaction.
The most important rule is dependency direction.
The UI can depend on the Application layer. The Infrastructure layer can implement interfaces defined by the Application layer. But the Domain layer should not depend on the UI or database.
This keeps the core of the application independent.
Domain layer
The Domain layer contains the most important business concepts.
Examples:
- entities
- value objects
- enums
- domain rules
- domain exceptions
For a blog application, the Domain layer might include a BlogPost entity.
public class BlogPost { public Guid Id { get; private set; } public string Title { get; private set; } public string Slug { get; private set; } public string Content { get; private set; } public DateTime PublishedAt { get; private set; }public BlogPost(string title, string slug, string content) { Id = Guid.NewGuid(); Title = title; Slug = slug; Content = content; PublishedAt = DateTime.UtcNow; }
}
This layer should not know anything about Blazor, Entity Framework, HTTP or the database.
It represents the business model.
Application layer
The Application layer defines what the system can do.
Examples:
- create a blog post
- publish a blog post
- load latest posts
- update a category
- send a notification
- validate a command
This layer usually contains service interfaces, DTOs, commands, queries and use-case services.
Example:
public interface IBlogPostService
{
Task<IReadOnlyList<BlogPostDto>> GetLatestPostsAsync();
Task<BlogPostDto?> GetBySlugAsync(string slug);
Task CreateAsync(CreateBlogPostRequest request);
}
The Blazor UI can call this interface without knowing how the data is stored.
That is the key benefit.
The UI does not need to know whether posts come from PostgreSQL, SQLite, an API, Redis or a file.
Infrastructure layer
The Infrastructure layer contains technical details.
Examples:
- Entity Framework Core DbContext
- repository implementations
- email sender
- file storage
- external API clients
- caching
- authentication providers
For example:
public class BlogPostService : IBlogPostService { private readonly AppDbContext _dbContext;public BlogPostService(AppDbContext dbContext) { _dbContext = dbContext; } public async Task<IReadOnlyList<BlogPostDto>> GetLatestPostsAsync() { return await _dbContext.BlogPosts .OrderByDescending(x => x.PublishedAt) .Select(x => new BlogPostDto(x.Id, x.Title, x.Slug)) .ToListAsync(); }
}
The Infrastructure layer implements the contracts defined by the Application layer.
The Blazor UI does not need to depend directly on DbContext.
Blazor UI layer
The Blazor UI layer should focus on presentation and interaction.
A page should ask the Application layer for data and render the result.
Example:
@page "/posts" @inject IBlogPostService BlogPostService<h1>Latest Posts</h1>
@if (posts is null) { <p>Loading...</p> } else { foreach (var post in posts) { <article> <h2>@post.Title</h2> <a href="/posts/@post.Slug">Read more</a> </article> } }
@code { private IReadOnlyList<BlogPostDto>? posts;
protected override async Task OnInitializedAsync() { posts = await BlogPostService.GetLatestPostsAsync(); }
}
This component is simple. It loads data and displays it.
It does not contain SQL queries, database logic or business rules.
Why this matters
Clean Architecture gives you several benefits:
- components stay smaller
- business logic becomes easier to test
- database details are isolated
- the UI can change without rewriting the whole application
- services can be reused outside Blazor
- the project is easier to reason about
This is especially useful in Blazor applications because it is very easy to put too much logic into .razor files.
A Razor component should not become a service, repository and controller at the same time.
A practical project structure
A simple structure could look like this:
src/
MyApp.Domain/
MyApp.Application/
MyApp.Infrastructure/
MyApp.Web/
The references might look like this:
MyApp.Web -> MyApp.Application
MyApp.Infrastructure -> MyApp.Application
MyApp.Application -> MyApp.Domain
The Web project registers the services:
builder.Services.AddScoped<IBlogPostService, BlogPostService>();
The UI consumes abstractions:
@inject IBlogPostService BlogPostService
This keeps the UI clean and the architecture flexible.
Common mistake: putting everything in components
A common beginner mistake is writing all logic inside the Razor component.
For example:
- calling
DbContextdirectly - applying business rules inside
@code - mapping entities to view models inside components
- sending emails from components
- handling complex validation inside components
This makes the component difficult to test and difficult to reuse.
Instead, move logic into services.
The component should ask for a result, not know every technical step needed to produce it.
Common mistake: too many layers too early
Clean Architecture is useful, but it should not become ceremony.
For a small personal project, you do not need to create dozens of abstractions before there is a real need.
The goal is not to overengineer. The goal is to keep dependencies clean.
Start simple:
- keep components clean
- move business logic into services
- avoid direct database access from the UI
- use interfaces where they provide real value
- keep domain concepts independent
That is enough for many projects.
Final thoughts
Blazor works best when the UI is not responsible for everything.
Clean Architecture gives your project a clear structure. The Domain layer contains business concepts. The Application layer defines use cases. The Infrastructure layer handles technical details. The Blazor UI layer renders the interface and reacts to user actions.
This separation makes the application easier to maintain, easier to test and easier to extend.
A clean Blazor project is not just about beautiful components. It is about putting the right logic in the right place.
