This solution follows Clean Architecture principles and is divided into four main layers:
-
Domain Layer:
Contains the core business entities (such as User, Comic, Character, etc.) and any business rules that are inherent to those entities. This layer is completely independent of any frameworks or external technologies. -
Application Layer:
Contains the application’s business logic. Here, you find the CQRS (Command Query Responsibility Segregation) implementation using MediatR. It holds:- Commands/Command Handlers: For write operations (e.g., creating a user).
- Queries/Query Handlers: For read operations.
- DTOs (Data Transfer Objects): Such as
CreateUserDTOin the Contracts project. - Validations: Using FluentValidation (e.g.,
CreateUserValidator) and also pipeline behaviors (such asValidationBehaviour) that ensure each command/query is valid before reaching its handler. - Common Classes: Such as
OperationResultto standardize responses.
-
Infrastructure Layer:
Implements all persistence logic and external integrations. It includes:- Entity Framework Core (EF Core) Context: In
ComicDbContext, which is configured with Fluent APIs (in the DomainSettings folder) to set up entity configurations. - Generic Repositories: For commands and queries. For example,
BaseCommandRepository<T>andBaseQueryRepository<T>provide generic operations. Specific repositories (e.g.,UserCommandRepositoryandUserQueryRepository) inherit from these. - Unit of Work Pattern: Implemented in
UnitOfWorkto handle transaction management. - Extensions: Such as
InfrastructureExtensions, which encapsulate dependency injection registrations.
- Entity Framework Core (EF Core) Context: In
-
Presentation Layer (Blazor Web UI):
This is the front end built using Blazor. It interacts with the Application Layer (often via an API or directly through dependency-injected services) to send commands and fetch data. This layer is composed of Razor Components (which serve as the “pages” in a Blazor Server or WASM app).
Take a look at the file ComicApp/Program.cs:
using ComicApp.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
// Map razor components with interactive rendering mode.
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();-
Service Registrations:
- AddRazorComponents() / AddInteractiveServerComponents():
These extension methods register the Blazor components. In Blazor Server apps, this allows for interactive (real-time) communication between the client and server.
- AddRazorComponents() / AddInteractiveServerComponents():
-
Middleware Pipeline Configuration:
- Exception Handling:
For non-development environments, the app uses a global exception handler (UseExceptionHandler) that forwards to an/Errorpage. - HTTPS Redirection, Static Files, and Antiforgery:
Standard middleware that ensures security (HTTPS, antiforgery tokens) and serves static content.
- Exception Handling:
-
Mapping Razor Components:
- app.MapRazorComponents():
This line maps the root Blazor component (App) to the request pipeline and specifies the render mode, which in this case is interactive server render mode.
- app.MapRazorComponents():
In this project, “Razor pages” are implemented as Blazor components. Let’s examine the Users component:
@page "/users"
@inject NavigationManager _navigationManager;
@inherits UsersBase
<PageTitle>Users</PageTitle>
<h3>@welcomeText</h3>
<table>
<thead>
<tr>
<th>Id</th>
<th>Username</th>
</tr>
</thead>
<tbody>
<!-- Data rows would be rendered here -->
</tbody>
</table>
@code {
// The component’s code logic can be added here (if not using code-behind)
}-
@page "/users":
This directive tells Blazor that the component should handle requests at the/usersroute. -
Dependency Injection:
The@inject NavigationManager _navigationManager;statement injects a service to manage navigation. -
Inheritance:
@inherits UsersBaselinks this component to its code-behind (found inUsers.razor.cs), where additional logic can be maintained separately from the view markup. -
PageTitle Component:
<PageTitle>Users</PageTitle>sets the browser tab title. -
HTML Markup:
The<h3>tag displays a welcome text, and a basic table is prepared to eventually show user data.
using Microsoft.AspNetCore.Components;
using ComicApp.Contracts;
namespace ComicApp.Components.Pages
{
public class UsersBase : ComponentBase
{
// You might inject services here for data retrieval.
// For example, an HttpClient or a custom IUserService.
// private readonly HttpClient _httpClient;
// private readonly NavigationManager _navigationManager;
// Constructor injection is an option if you register these services.
// public UsersBase(HttpClient http, NavigationManager navigationManager)
// {
// _httpClient = http;
// _navigationManager = navigationManager;
// }
protected string welcomeText = "Welcome to 'Users'";
// You could add methods here to call your API through MediatR.
// For example:
// public async Task CreateUser(CreateUserDTO createUserDTO)
// {
// var result = await _httpClient.PostAsJsonAsync("api/user", createUserDTO);
// }
}
}-
Separation of Concerns:
The code-behind (or “base” class) encapsulates the logic for the component, keeping the Razor markup clean. This separation is useful for unit testing and for maintaining larger components. -
Placeholder for Service Calls:
Although commented out in the provided code, you can see that there is room to inject and use services (e.g., anHttpClientor a dedicatedIUserService) to call the API endpoints (which, in this project, would interact with the Application layer through CQRS).
-
Presentation Interaction:
The Blazor UI (Razor components) in the Presentation layer consumes services that eventually trigger application commands or queries via MediatR. For example, if you need to create a new user, your component could call a method (like the commented-outCreateUserinUsersBase) that would:- Prepare a DTO (
CreateUserDTO). - Dispatch a command to the Application layer.
- The command handler (
CreateUserCommandHandler) would perform business logic (e.g., checking for duplicates) and then use the repositories in the Infrastructure layer to persist the new user.
- Prepare a DTO (
-
Data Flow:
- From UI to Domain:
The Razor component sends user input (perhaps via forms) to a service. - Through Application Layer:
The service uses MediatR to send a command or query to the appropriate handler. - Persistence:
The handler uses Generic Repositories and the Unit of Work pattern to interact with the SQL Server database through EF Core. - Response Handling:
If an error occurs (for instance, during validation by FluentValidation), the Exception Middleware intercepts the exception and returns a standardized JSON error response.
- From UI to Domain:
-
Create the Razor File:
- In the
ComicApp/Components/Pagesfolder, add a new file named, for example,NewFeature.razor.
- In the
-
Add the @page Directive:
- At the top of your file, specify a route:
@page "/new-feature"
- At the top of your file, specify a route:
-
Inject Required Services:
- Use
@injectto include services. For instance:@inject NavigationManager NavigationManager @inject IUserService UserService
- This allows you to use navigation and any custom service (which you would register via DI in your Program.cs or startup configuration).
- Use
-
Define the Markup and Bind Data:
- Build your UI with standard HTML and Blazor’s data-binding syntax. For example:
<h1>New Feature</h1> <input @bind="newUser.Username" placeholder="Enter username" /> <input @bind="newUser.Password" placeholder="Enter password" type="password" /> <button @onclick="HandleCreateUser">Create User</button> @if (message != null) { <p>@message</p> }
- Build your UI with standard HTML and Blazor’s data-binding syntax. For example:
-
Create the Code Section or a Code-behind File:
- You can either include an
@codeblock at the bottom of the.razorfile or use a code-behind file (e.g.,NewFeature.razor.cs). For the inline version:@code { private CreateUserDTO newUser = new CreateUserDTO(); private string message; private async Task HandleCreateUser() { // Assuming UserService.CreateUser returns a Task<OperationResult> var result = await UserService.CreateUser(newUser); if (result.Success) { message = "User created successfully!"; } else { message = string.Join(", ", result.ErrorMessages); } } }
- This method sends the user data to your backend via the injected service.
- You can either include an
-
Register Your Service:
- In your DI configuration (possibly in the Program.cs or in a dedicated Extensions class), make sure that
IUserServiceis registered so it can be injected into your component.
- In your DI configuration (possibly in the Program.cs or in a dedicated Extensions class), make sure that
-
Test and Debug:
- Run your application. Navigate to
/new-featurein your browser. Use browser developer tools and Blazor’s built-in error messages to debug any issues.
- Run your application. Navigate to
When implementing more interactive pages, you’ll likely want to trigger commands or queries that are handled by the Application layer. For example:
-
Dispatching a Command:
- In your Razor component, you might inject an
IMediatorinstance:@inject IMediator Mediator
- Then, you could dispatch a command like this:
var command = new CreateUser { Username = newUser.Username, Password = newUser.Password }; var result = await Mediator.Send(command);
- The
CreateUserCommandHandlerin the Application layer receives the command, performs validations (using the pipeline behaviorValidationBehaviour), checks business rules, and then uses the Infrastructure repositories to persist the user.
- In your Razor component, you might inject an
-
Handling Responses and Exceptions:
- If the command fails validation (or any other exception occurs), the ExceptionMiddleware in the API layer (or similar middleware in your Blazor Server app) will catch the exception, wrap it in a standardized DTO (
TaskResultDTO), and return it. Your UI can then display error messages accordingly.
- If the command fails validation (or any other exception occurs), the ExceptionMiddleware in the API layer (or similar middleware in your Blazor Server app) will catch the exception, wrap it in a standardized DTO (
-
Separation of Concerns:
Keep UI logic (data binding, user interactions) separate from business logic. Let the Application layer handle complex scenarios (validation, business rules, persistence). -
Validation:
Leverage the FluentValidation library in the Application layer to ensure data integrity. TheValidationBehaviourin the MediatR pipeline automatically validates commands before they reach the handlers. -
Generic Repositories and Unit of Work:
These patterns help encapsulate data access logic, making the code easier to test and maintain. They also ensure that multiple operations can be grouped together in a single transaction. -
Middleware for Exception Handling:
Centralizing exception handling using custom middleware (as shown inExceptionMiddleware) reduces code duplication and provides a consistent error response format across your application. -
Blazor Specifics:
When working with Blazor, remember that you have the flexibility to use code-behind classes (likeUsersBase) to keep your markup clean. This approach not only improves maintainability but also simplifies unit testing of your component logic.
This project is a modern web application built with .NET 8 that leverages a layered, clean architecture. It combines Blazor for the interactive UI, CQRS via MediatR for clear separation between commands and queries, FluentValidation for robust input checking, and a generic repository/unit-of-work pattern for data access via EF Core.
Implementing and extending Razor pages (Blazor components) in this context involves:
- Creating new
.razorfiles with routing and DI. - Separating UI markup from business logic using code-behind classes.
- Wiring up user actions to dispatch commands/queries that flow through the Application layer and finally persist data via the Infrastructure layer.
By following these practices, you ensure that your application remains modular, testable, and maintainable as it scales.
Feel free to ask if you need further details on any specific part of the project!