Clean Architecture is a popular software development architecture which can be used in .NET apps. In this tutorial we are going to implement the Clean Architecure GitHub Repository by Steve “Ardalis” Smith, it can be downloaded from the GitHub Link.
Page Contents
Clean Architecture also know as Onion Architecture is a way to organize your codes into multiple concentric layers where each layer has been given a particular role to play. The inner-most layer is Domain / Application Core while othermost layer is the Presentation / UI. The below image shows this:
The layers of Clean Architecture are given below:
Domain layer has no dependencies on other layers. The Application layer has a dependency on the Domain Layer. Similarly Infrastructure layer has a dependency for Application layer and UI layer depends on Infrastructure layer. Instead of having business logic (which the Domain layer contains) depend on data access or other infrastructure concerns (contained by Infrastructure Layer), this dependency is inverted. That is, the Infrastructure and Application layers depend on the business logic so this means we have Dependency Inversion Principle applied in this architecture.
Ardalis repo contains 4 important projects:
I have shown this in the below image.
The database used is an SQLite database. You will find database.sqlite file in the Clean.Architecture.Web project.
To work with this database you need to install SQLite and SQL Server Compact Toolbox extension for Visual Studio. Once installed open it from the “Tools” menu in Visual Studio.
The database connection string is provided in the appsettings.json file in the Clean.Architecture.Web project. Below is the database connection string found on this file.
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true",
"SqliteConnection": "Data Source=database.sqlite"
}
Let’s start the implementation of this repo. We will be creating CRUD Opertions feature on a Student entity. The Student entity contain 3 fields:
We start with the Application Core layer where we will define the Student entity. We do this since application layer is totally independent layer which does not depend on other things.
Inside the Core project create a new folder called StudentAggregate and to it create a new class called Student.cs.
Add the following code to this class.
using Ardalis.GuardClauses;
using Ardalis.SharedKernel;
namespace Clean.Architecture.Core.StudentAggregate;
public class Student(string name, string standard, int rank) : EntityBase, IAggregateRoot
{
public string Name { get; private set; } = Guard.Against.NullOrEmpty(name, nameof(name));
public string Standard { get; private set; } = Guard.Against.NullOrEmpty(standard, nameof(standard));
public int Rank { get; set; } = Guard.Against.Zero(rank, nameof(rank));
public void UpdateStudent(string newName, string standard, int rank)
{
Name = Guard.Against.NullOrEmpty(newName, nameof(newName));
Standard = Guard.Against.NullOrEmpty(standard, nameof(standard));
Rank = Guard.Against.Zero(rank, nameof(rank));
}
}
The class contains student’s fields and UpdateStudent() method for updating the records. There are also guard clause for preventing misformed data.
Next, inside the Infrastructure project, there is AppDbContext.cs file which is the Database Context for the app.
Here we add the entry for the Student class as shown below.
using System.Reflection;
using Ardalis.SharedKernel;
using Clean.Architecture.Core.ContributorAggregate;
using Microsoft.EntityFrameworkCore;
using Clean.Architecture.Core.StudentAggregate;
namespace Clean.Architecture.Infrastructure.Data;
public class AppDbContext : DbContext
{
private readonly IDomainEventDispatcher? _dispatcher;
public AppDbContext(DbContextOptions<AppDbContext> options,
IDomainEventDispatcher? dispatcher)
: base(options)
{
_dispatcher = dispatcher;
}
public DbSet<Contributor> Contributors => Set<Contributor>();
public DbSet<Student> Student => Set<Student>();
....
}
Now we are all set to perform the CRUD Operations on Students.
Inside the UseCases project we will write CQRS Command for the student.
In this project create “Student” folder then inside it, add a new folder called “Create”.
To the “Create” folder add a new class called CreateStudentCommand.cs. It’s code is given below.
using Ardalis.Result;
namespace Clean.Architecture.UseCases.Student.Create;
public record CreateStudentCommand(string Name, string Standard, int Rank) : Ardalis.SharedKernel.ICommand<Result<int>>;
Add another class called CreateStudentHandler.cs to the same “Create” folder with the following code.
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.Create;
public class CreateStudentHandler(IRepository<Core.StudentAggregate.Student> _repository)
: ICommandHandler<CreateStudentCommand, Result<int>>
{
public async Task<Result<int>> Handle(CreateStudentCommand request,
CancellationToken cancellationToken)
{
var newStudent = new Core.StudentAggregate.Student(request.Name, request.Standard, request.Rank);
var createdItem = await _repository.AddAsync(newStudent, cancellationToken);
return createdItem.Id;
}
}
I have shown it in the below image.
Next, we move to the Web project. Here we will do a number of things like creating Endpoints, Controllers and Views.
The Ardalis repository uses Fast Endpoints package instead of API Controllers. So we will have no choice but to use it. Anyways it’s easy and I will explain it’s code to you as we go with the development process.
To the “Web” project create a folder called “Student”. Now add 4 classes to it which are given below:
using System.ComponentModel.DataAnnotations;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class CreateStudentRequest
{
public const string Route = "/Student";
[Required]
public string? Name { get; set; }
[Required]
public string? Standard { get; set; }
[Range(1, 3)]
public int Rank { get; set; }
}
This class will form the endpoints for the Student CRUD operations which for us will be /Student.
using Clean.Architecture.Infrastructure.Data.Config;
using FastEndpoints;
using FluentValidation;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class CreatStudentValidator : Validator<CreateStudentRequest>
{
public CreatStudentValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required.")
.MinimumLength(2)
.MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
RuleFor(x => x.Standard)
.NotEmpty()
.WithMessage("Standard is required.")
.MinimumLength(2)
.MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
RuleFor(x => x.Rank)
.NotEmpty()
.WithMessage("Rank is required.")
.InclusiveBetween(1, 3)
.WithMessage("Rank 1-3 allowed");
}
}
This class performs validations for the Student, it uses Fluent Validations package. There are some restrictions on Rank which should be only from 1 to 3 and Name, Standard are made required fields.
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class CreateStudentResponse
{
public CreateStudentResponse(int id, string name, string standard, int rank)
{
Id = id;
Name = name;
Standard = standard;
Rank = rank;
}
public int Id { get; set; }
public string Name { get; set; }
public string Standard { get; set; }
public int Rank { get; set; }
}
This class handles the response of the operations.
using Clean.Architecture.UseCases.Student.Create;
using FastEndpoints;
using MediatR;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class Create(IMediator _mediator)
: Endpoint<CreateStudentRequest, CreateStudentResponse>
{
public override void Configure()
{
Post(CreateStudentRequest.Route);
AllowAnonymous();
Summary(s =>
{
s.ExampleRequest = new CreateStudentRequest { Name = "Student Name" };
});
}
public override async Task HandleAsync(
CreateStudentRequest request,
CancellationToken cancellationToken)
{
var result = await _mediator.Send(new CreateStudentCommand(request.Name!, request.Standard!, request.Rank));
if (result.IsSuccess)
{
Response = new CreateStudentResponse(result.Value, request.Name!, request.Standard!, request.Rank!);
return;
}
}
}
This is the most important class where we write our Fast Endpoints code to receieve a new Student data from the View and it then transfers this data to the CQRS code (which we created earlier) for insertion to the database.
The Post(CreateStudentRequest.Route) is where the route (/Student) is set. The HandleAsync() method calls the CQRS with mediator pattern.
Now it’s time to create a Student Controller inside the “Controllers” folder. Create “Controllers” folder to the “Web” project and to this folder add a new controller called StudentController.cs. Here we write the code for the Create action method.
This code is shown below.
using Clean.Architecture.Web.Endpoints.StudentEndpoints;
using Microsoft.AspNetCore.Mvc;
namespace Clean.Architecture.Web.Controllers;
public class StudentController : Controller
{
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(CreateStudentRequest s)
{
if (ModelState.IsValid)
{
using (var httpClient = new HttpClient())
{
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{Request.Scheme}://{Request.Host}/Student", s);
return RedirectToAction("Read");
}
}
else
return View();
}
}
Here in the controller we are making HTTP POST request to the student endpoint as "{Request.Scheme}://{Request.Host}/Student"
.
We also have to add support for MVC and endpoints to the controllers, in the Program.cs class, given on the “Web” project. The 2 codes to be added to this class are given below.
builder.Services.AddControllersWithViews();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Student}/{action=Index}/{id?}");
The views will form the UI where we will be doing CRUD operations. So add Views folder to the “Web” project, next add “Student” folder to it. Now add Create.cshtml razor view file to the Student folder. In this file we will add a student form that should be filled and submitted before the student is inserted to the database.
The Create.cshtml code is given below.
@model CreateStudentRequest
@{
ViewData["Title"] = "Create a Student";
}
<h1 class="bg-info text-white">Create a Student</h1>
<a asp-action="Read" class="btn btn-secondary">View all Students</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Standard"></label>
<input type="text" asp-for="Standard" class="form-control" />
<span asp-validation-for="Standard" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Rank"></label>
<input type="text" asp-for="Rank" class="form-control" />
<span asp-validation-for="Rank" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
Let’s also do some proper designing by including Layout file. First add “Shared” folder inside the “Views” folder. Next, add _Layout.cshtml to the “Shared” folder with the following code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>@ViewData["Title"] - Clean Architecture "Ardalis"</title>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Student" asp-action="Read">Clean Architecture</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Student" asp-action="Read">Read Students</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Student" asp-action="ReadByPaging" asp-route-id="1">Read Students by Paging</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2024 - Clean Architecture "Ardalis"
</div>
</footer>
</body>
</html>
Create another view file called _ViewImports.cshtml inside the Views folder and add the following code to it.
@using Clean.Architecture.Web.Endpoints.StudentEndpoints;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<p>Also add <span class="term">_ViewStart.cshtml</span> inside the same "Views" folder.</p>
@{
Layout = "_Layout";
}
I have show all the view files in the below image.
Let’s now test our feature. First delete the database file database.sqlite. Don’t worry it will be automatically recreated and this time it will also contain the “Student” table. Now run the app and then go to the url – http://localhost:57679/Student/Create.
Fill the student’s entry and submit the form.
Now check the database where the record is created.
Dont’ worry about the 404 error as we are yet to create the Read feature.
Let’s move forward to the Read Student feature. First in the Infrastructure project’s Data ➤ Queries folder, create a new class called ListStudentQueryService.cs. The code of this class is given below:
using Clean.Architecture.UseCases.Student;
using Clean.Architecture.UseCases.Student.List;
using Microsoft.EntityFrameworkCore;
namespace Clean.Architecture.Infrastructure.Data.Queries;
public class ListStudentQueryService(AppDbContext _db) : IListStudentQueryService
{
public async Task<IEnumerable<StudentDTO>> ListAsync()
{
var result = await _db.Database.SqlQuery<StudentDTO>($"SELECT Id, Name, Standard, Rank FROM Student").ToListAsync();
return result;
}
}
In this class we have a raw SQL Select query to read the students records from the database.
Next in the UseCases project, create a new class called StudentDTO.cs inside the Student folder. Add the below code to it.
namespace Clean.Architecture.UseCases.Student;
public record StudentDTO(int Id, string Name, string Standard, int Rank);
This class will serve as a DTO for the student entity.
Now create a new folder called List inside the Student folder. In this folder we will create 3 classes for handling the Student read feature initiated by the client. We used CQRS Queries for doing this work. These classes are:
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.List;
public record ListStudentQuery(int? Skip, int? Take) : Iquery<Result<IEnumerable<StudentDTO>>>;
namespace Clean.Architecture.UseCases.Student.List;
public interface IListStudentQueryService
{
Task<IEnumerable<StudentDTO>> ListAsync();
}
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.List;
public class ListStudentHandler(IListStudentQueryService _query)
: IQueryHandler<ListStudentQuery, Result<IEnumerable<StudentDTO>>>
{
public async Task<Result<IEnumerable<StudentDTO>>> Handle(ListStudentQuery request, CancellationToken cancellationToken)
{
var result = await _query.ListAsync();
return Result.Success(result);
}
}
In the below image I have shown all these classes.
It’s now time to register CQRS Queries in the AutofacInfrastructureModule.cs class given on the Infrastructure project.
So first add the using block on the top.
using Clean.Architecture.UseCases.Student.List;
Next, Insde the RegisterQueries() function register them as shown below.
private void RegisterQueries(ContainerBuilder builder)
{
builder.RegisterType<ListContributorsQueryService>()
.As<IListContributorsQueryService>()
.InstancePerLifetimeScope();
builder.RegisterType<ListStudentQueryService>()
.As<IListStudentQueryService>()
.InstancePerLifetimeScope();
}
We move to the Web project. Here, inside the Student folder, create List.cs with the following code.
using Clean.Architecture.UseCases.Student.List;
using FastEndpoints;
using MediatR;
namespace Clean.Architecture.Web.StudentEndpoints;
public class List(IMediator _mediator) : EndpointWithoutRequest<StudentListResponse>
{
public override void Configure()
{
Get("/Student");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var result = await _mediator.Send(new ListStudentQuery(null, null));
if (result.IsSuccess)
{
Response = new StudentListResponse
{
Student = result.Value.Select(c => new StudentRecord(c.Id, c.Name, c.Standard, c.Rank)).ToList()
};
}
}
}
In this class we wrote fast endpoints code to call the UseCases project classes we created just a moment ago. So we will get the student records from the database.
To the same Student folder, create anothe class – List.StudentListResponse.cs with the following code.
namespace Clean.Architecture.Web.StudentEndpoints;
public class StudentListResponse
{
public List<StudentRecord> Student { get; set; } = new();
}
Also create a new class called StudentRecord.cs inside the Student folder with the following code.
namespace Clean.Architecture.Web.StudentEndpoints;
public record StudentRecord(int Id, string Name, string Standard, int Rank);
Now to the StudentController.cs, add Read action method whose code is given below.
using Clean.Architecture.Web.Endpoints.StudentEndpoints;
using Clean.Architecture.Web.StudentEndpoints;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
namespace Clean.Architecture.Web.Controllers;
public class StudentController : Controller
{
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(CreateStudentRequest s)
{
if (ModelState.IsValid)
{
using (var httpClient = new HttpClient())
{
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{Request.Scheme}://{Request.Host}/Student", s);
return RedirectToAction("Read");
}
}
else
return View();
}
public async Task<IActionResult> Read()
{
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync($"{Request.Scheme}://{Request.Host}/Student"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
var s = JsonConvert.DeserializeObject<StudentListResponse>(apiResponse);
return View(s);
}
}
}
}
We are just calling the Fast Enpoints “List.cs” class we created earlier so that the students records are fetched.
Now create a razor view file called Read.cshtml inside the Views ➤ Student folder with the following code.
@model StudentListResponse
@{
ViewData["Title"] = "Students";
}
<h1 class="bg-info text-white">Students</h1>
<a asp-action="Create" class="btn btn-secondary">Create a Student</a>
<table class="table table-sm table-bordered">
<tr>
<th>Id</th>
<th>Name</th>
<th>Standard</th>
<th>Rank</th>
<th></th>
<th></th>
</tr>
@foreach (var s in Model.Student)
{
<tr>
<td><a class="link-info" asp-action="ReadById" asp-route-id="@s.Id">@s.Id</a></td>
<td>@s.Name</td>
<td>@s.Standard</td>
<td>@s.Rank</td>
<td>
<a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@s.Id">
Update
</a>
</td>
<td>
<form asp-action="Delete" asp-route-id="@s.Id" method="post">
<button type="submit" class="btn btn-sm btn-danger">
Delete
</button>
</form>
</td>
</tr>
}
</table>
Finally, we need to import the namespace on the _ViewImports.cshtml. So add the following code to it.
@using Clean.Architecture.Web.StudentEndpoints
Let’s run the app and open the url – http://localhost:57679/Student/Read. Here you will see the student record which we created earlier.
The Student.cs class present on the “Core” project already has the Update() method which is used to update the student. I have shown it below.
public void UpdateStudent(string newName, string standard, int rank)
{
Name = Guard.Against.NullOrEmpty(newName, nameof(newName));
Standard = Guard.Against.NullOrEmpty(standard, nameof(standard));
Rank = Guard.Against.Zero(rank, nameof(rank));
}
We will also need a method to get a particular student record by his Id. For this add a new folder called Specifications inside the StudentAggregate folder. Now add a new class called StudentByIdSpec.cs to inside this new folder. Add the following code to it.
using Ardalis.Specification;
namespace Clean.Architecture.Core.StudentAggregate.Specifications;
public class StudentByIdSpec : Specification<Student>
{
public StudentByIdSpec(int studentId)
{
Query
.Where(a => a.Id == studentId);
}
}
Next, we move to the UseCases project where we will add the Update CQRS Code for the student. So in this project create Student ➤ Update folder and add 2 classes to the Update folder. These classes are:
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.Update;
public record UpdateStudentCommand(int StudentId, string NewName, string Standard, int Rank) : ICommand<Result<StudentDTO>>;
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.Update;
public class UpdateStudentHandler(IRepository<Core.StudentAggregate.Student> _repository)
: ICommandHandler<UpdateStudentCommand, Result<StudentDTO>>
{
public async Task<Result<StudentDTO>> Handle(UpdateStudentCommand request, CancellationToken cancellationToken)
{
var existingStudent = await _repository.GetByIdAsync(request.StudentId, cancellationToken);
if (existingStudent == null)
{
return Result.NotFound();
}
existingStudent.UpdateStudent(request.NewName!, request.Standard, request.Rank);
await _repository.UpdateAsync(existingStudent, cancellationToken);
return Result.Success(new StudentDTO(existingStudent.Id, existingStudent.Name, existingStudent.Standard, existingStudent.Rank));
}
}
In the same “UseCases” project, create Get folder inside the Student folder. Now add 2 classes to the “Get” folder. These classes are:
<div class="note">GetStudentHandler.cs</div>
using Ardalis.Result;
using Ardalis.SharedKernel;
using Clean.Architecture.Core.StudentAggregate.Specifications;
namespace Clean.Architecture.UseCases.Student.Get;
/// <summary>
/// Queries don't necessarily need to use repository methods, but they can if it's convenient
/// </summary>
public class GetStudentHandler(IReadRepository<Core.StudentAggregate.Student> _repository)
: IQueryHandler<GetStudentQuery, Result<StudentDTO>>
{
public async Task<Result<StudentDTO>> Handle(GetStudentQuery request, CancellationToken cancellationToken)
{
var spec = new StudentByIdSpec(request.StudentId);
var entity = await _repository.FirstOrDefaultAsync(spec, cancellationToken);
if (entity == null) return Result.NotFound();
return new StudentDTO(entity.Id, entity.Name, entity.Standard, entity.Rank);
}
}
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.Get;
public record GetStudentQuery(int StudentId) : IQuery<Result<StudentDTO>>;
The work of these classes are to fetch a student by his id.
Next, we move to the Web project. Here create 4 classes for Fast Endpoints inside the Student folder. These classes are:
using Ardalis.Result;
using Clean.Architecture.UseCases.Student.Get;
using Clean.Architecture.UseCases.Student.Update;
using Clean.Architecture.Web.Endpoints.StudentEndpoints;
using FastEndpoints;
using MediatR;
namespace Clean.Architecture.Web.StudentEndpoints;
public class Update(IMediator _mediator)
: Endpoint<UpdateStudentRequest, UpdateStudentResponse>
{
public override void Configure()
{
Put(UpdateStudentRequest.Route);
AllowAnonymous();
}
public override async Task HandleAsync(
UpdateStudentRequest request,
CancellationToken cancellationToken)
{
var result = await _mediator.Send(new UpdateStudentCommand(request.Id, request.Name!, request.Standard!, request.Rank));
if (result.Status == ResultStatus.NotFound)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var query = new GetStudentQuery(request.StudentId);
var queryResult = await _mediator.Send(query);
if (queryResult.Status == ResultStatus.NotFound)
{
await SendNotFoundAsync(cancellationToken);
return;
}
if (queryResult.IsSuccess)
{
var dto = queryResult.Value;
Response = new UpdateStudentResponse(new StudentRecord(dto.Id, dto.Name, dto.Standard, dto.Rank));
return;
}
}
}
using System.ComponentModel.DataAnnotations;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class UpdateStudentRequest
{
public const string Route = "/Student/{StudentId:int}";
public static string BuildRoute(int StudentId) => Route.Replace("{StudentId:int}", StudentId.ToString());
public int StudentId { get; set; }
public int Id { get; set; }
[Required]
public string? Name { get; set; }
[Required]
public string? Standard { get; set; }
[Range(1, 3)]
public int Rank { get; set; }
}
using Clean.Architecture.Web.StudentEndpoints;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class UpdateStudentResponse
{
public UpdateStudentResponse(StudentRecord student)
{
Student = student;
}
public StudentRecord Student { get; set; }
}
using Clean.Architecture.Infrastructure.Data.Config;
using FastEndpoints;
using FluentValidation;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class UpdateStudentValidator : Validator<UpdateStudentRequest>
{
public UpdateStudentValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required.")
.MinimumLength(2)
.MaximumLength(DataSchemaConstants.DEFAULT_NAME_LENGTH);
RuleFor(x => x.StudentId)
.Must((args, studentId) => args.Id == studentId)
.WithMessage("Route and body Ids must match; cannot update Id of an existing resource.");
}
}
These classes calls the CQRS classes in the UseCases project to perform the update of a student based on his id.
We will also need to create a feature for getting a student by his id. So to the same “Student” folder create 3 classes, these are:
using Ardalis.Result;
using FastEndpoints;
using MediatR;
using Clean.Architecture.UseCases.Student.Get;
using Clean.Architecture.Web.Endpoints.StudentEndpoints;
namespace Clean.Architecture.Web.StudentEndpoints;
public class GetById(IMediator _mediator)
: Endpoint<GetStudentByIdRequest, StudentRecord>
{
public override void Configure()
{
Get(GetStudentByIdRequest.Route);
AllowAnonymous();
}
public override async Task HandleAsync(GetStudentByIdRequest request,
CancellationToken cancellationToken)
{
var command = new GetStudentQuery(request.studentId);
var result = await _mediator.Send(command);
if (result.Status == ResultStatus.NotFound)
{
await SendNotFoundAsync(cancellationToken);
return;
}
if (result.IsSuccess)
{
Response = new StudentRecord(result.Value.Id, result.Value.Name, result.Value.Standard, result.Value.Rank);
}
}
}
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public class GetStudentByIdRequest
{
public const string Route = "/Student/{StudentId:int}";
public static string BuildRoute(int studentId) => Route.Replace("{StudentId:int}", studentId.ToString());
public int studentId { get; set; }
}
using FastEndpoints;
using FluentValidation;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
/// <summary>
/// See: https://fast-endpoints.com/docs/validation
/// </summary>
public class GetStudentValidator : Validator<GetStudentByIdRequest>
{
public GetStudentValidator()
{
RuleFor(x => x.studentId)
.GreaterThan(0);
}
}
Next, we add Update action method to the StudentController.cs class. The actions code is given below.
public async Task<IActionResult> Update(int id)
{
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync($"{Request.Scheme}://{Request.Host}/Student/{id}"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
var s = JsonConvert.DeserializeObject<UpdateStudentRequest>(apiResponse);
return View(s);
}
}
}
[HttpPost]
public async Task<IActionResult> Update(int id, UpdateStudentRequest s)
{
if (ModelState.IsValid)
{
using (var httpClient = new HttpClient())
{
HttpResponseMessage response = await httpClient.PutAsJsonAsync($"{Request.Scheme}://{Request.Host}/Student/{id}", s);
return RedirectToAction("Read");
}
}
else
return View();
}
The final thing is to add the Update.cshtml view file to the Views ➤ Student folder with the following code.
@model UpdateStudentRequest
@{
ViewData["Title"] = "Update a Student";
}
<h1 class="bg-info text-white">Update a Student</h1>
<a asp-action="Read" class="btn btn-secondary">View all Students</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Standard"></label>
<input type="text" asp-for="Standard" class="form-control" />
<span asp-validation-for="Standard" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Rank"></label>
<input type="text" asp-for="Rank" class="form-control" />
<span asp-validation-for="Rank" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
You can now test the update feature by going to the url – http://localhost:57679/Student/Read. Then click the update button against a record to update it.
We have already created most of this feature in the Update section. We just have to add some controller code and razor view to complete it. So in the StudentController.cs file add the action method given below:
public async Task<IActionResult> ReadById(int id)
{
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.GetAsync($"{Request.Scheme}://{Request.Host}/Student/{id}"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
var s = JsonConvert.DeserializeObject<StudentRecord>(apiResponse!);
return View(s);
}
}
}
Inside the Views ➤ Student folder add ReadById.cshtml file with the following code.
@model StudentRecord
@{
ViewData["Title"] = "Student";
}
<h1 class="bg-info text-white">Student</h1>
<a asp-action="Create" class="btn btn-secondary">Create a Student</a>
<table class="table table-sm table-bordered">
<tr>
<td class="bg-warning">Id</td>
<td>@Model.Id</td>
</tr>
<tr>
<td class="bg-warning">Name</td>
<td>@Model.Name</td>
</tr>
<tr>
<td class="bg-warning">Standard</td>
<td>@Model.Standard</td>
</tr>
<tr>
<td class="bg-warning">Rank</td>
<td>@Model.Rank</td>
</tr>
</table>
On the read view you will see student id before a student record. Click it to see the student details. I have marked it on the below image.
The student details image is shown below.
Let’s create the final feature which is deleting a student by his id. First let us update “Core” project. Here inside the Interfaces folder create a new class called IDeleteStudentService.cs with the following code:
using Ardalis.Result;
namespace Clean.Architecture.Core.Interfaces;
public interface IDeleteStudentService
{
public Task<Result> DeleteStudent(int studentId);
}
Next, inside the Services folder create DeleteStudentService.cs with the follwong code.
using Ardalis.Result;
using Ardalis.SharedKernel;
using MediatR;
using Microsoft.Extensions.Logging;
using Clean.Architecture.Core.StudentAggregate;
using Clean.Architecture.Core.StudentAggregate.Events;
using Clean.Architecture.Core.Interfaces;
namespace Clean.Architecture.Core.Services;
public class DeleteStudentService(IRepository<Student> _repository,
IMediator _mediator,
ILogger<DeleteStudentService> _logger) : IDeleteStudentService
{
public async Task<Result> DeleteStudent(int studentId)
{
_logger.LogInformation("Deleting Student {studentId}", studentId);
var aggregateToDelete = await _repository.GetByIdAsync(studentId);
if (aggregateToDelete == null) return Result.NotFound();
await _repository.DeleteAsync(aggregateToDelete);
var domainEvent = new StudentDeletedEvent(studentId);
await _mediator.Publish(domainEvent);
return Result.Success();
}
}
We implemented the previous defined interface in this class and here we delete a student record whose id is provided. Once the student is deleted we publish a CQRS notification to the StudentDeletedEvent.cs class.
Create a folder called Events inside “StudentAggregate” folder. To this new folder, add StudentDeletedEvent.cs with the following code.
using Ardalis.SharedKernel;
namespace Clean.Architecture.Core.StudentAggregate.Events;
internal sealed class StudentDeletedEvent(int studentId) : DomainEventBase
{
public int StudentId { get; init; } = studentId;
}
This class acts as a doman event and it gets notified when a student record is deleted. If we want to do some extra work after a student record is deleted then this class is the perfect choice.
Now we need to register DeleteStudentService in the DefaultCoreModule.cs located on the root of the “Core” project. So inside the Load() method add the below code:
builder.RegisterType<DeleteStudentService>()
.As<IDeleteStudentService>().InstancePerLifetimeScope();
Let’s now move to UseCases project. First create Delete folder inside the “Student” folder. Next add 2 classes to the “Delete” folder, these classes acts as CQRS Command and are given below:
using Ardalis.Result;
using Ardalis.SharedKernel;
namespace Clean.Architecture.UseCases.Student.Delete;
public record DeleteStudentCommand(int StudentId) : ICommand<Result>;
using Ardalis.Result;
using Ardalis.SharedKernel;
using Clean.Architecture.Core.Interfaces;
namespace Clean.Architecture.UseCases.Student.Delete;
public class DeleteStudentHandler(IDeleteStudentService _deleteStudentService)
: ICommandHandler<DeleteStudentCommand, Result>
{
public async Task<Result> Handle(DeleteStudentCommand request, CancellationToken cancellationToken)
{
return await _deleteStudentService.DeleteStudent(request.StudentId);
}
}
Moving to the “Web” project. Inside Student folder, create 3 classes, these are:
using Ardalis.Result;
using FastEndpoints;
using MediatR;
using Clean.Architecture.UseCases.Student.Delete;
using Clean.Architecture.Web.Endpoints.StudentEndpoints;
namespace Clean.Architecture.Web.StudentEndpoints;
public class Delete(IMediator _mediator)
: Endpoint<DeleteStudentRequest>
{
public override void Configure()
{
Delete(DeleteStudentRequest.Route);
AllowAnonymous();
}
public override async Task HandleAsync(
DeleteStudentRequest request,
CancellationToken cancellationToken)
{
var command = new DeleteStudentCommand(request.StudentId);
var result = await _mediator.Send(command);
if (result.Status == ResultStatus.NotFound)
{
await SendNotFoundAsync(cancellationToken);
return;
}
if (result.IsSuccess)
{
await SendNoContentAsync(cancellationToken);
};
// TODO: Handle other issues as needed
}
}
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
public record DeleteStudentRequest
{
public const string Route = "/Student/{StudentId:int}";
public static string BuildRoute(int studentId) => Route.Replace("{StudentId:int}", studentId.ToString());
public int StudentId { get; set; }
}
using FastEndpoints;
using FluentValidation;
namespace Clean.Architecture.Web.Endpoints.StudentEndpoints;
/// <summary>
/// See: https://fast-endpoints.com/docs/validation
/// </summary>
public class DeleteStudentValidator : Validator<DeleteStudentRequest>
{
public DeleteStudentValidator()
{
RuleFor(x => x.StudentId)
.GreaterThan(0);
}
}
Moving to the StudentController.cs, we add the Delete action method.
[HttpPost]
public async Task<IActionResult> Delete(int id)
{
using (var httpClient = new HttpClient())
{
using (var response = await httpClient.DeleteAsync($"{Request.Scheme}://{Request.Host}/Student/{id}"))
{
string apiResponse = await response.Content.ReadAsStringAsync();
}
}
return RedirectToAction("Read");
}
Well that’s it. You can now check the delete feature by yourself and this completes this tutorial.
In this long tutorial we implemented Clean Architecture Ardalis repository from complete beginning and also build CRUD operations. We also looked into all the fields and structures. I hope you will like this tutorial, let me know your thoughts on the comments section below.