Implementing Clean Architecure by Steve “Ardalis” Smith GitHub Repository

Implementing Clean Architecure by Steve “Ardalis” Smith GitHub Repository

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.

We will be creating full CRUD Operations on this repository. This full repository is available on my GitHub account. You will also find other features implemented, which are:
  1. Number based paging for reading records.
  2. Authentication and Authorization with ASP.NET Core Identity.
  3. All the Unit, Functional and Integration tests.

What is Clean Architecture

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:

Clean Architecture

The layers of Clean Architecture are given below:

  1. Domain / Application Core => contains entities for business logic and interfaces which are implemented by the outer layers..
  2. Infrastructure => contains infrastructure related codes like Entity Framework Core for DB operations, JWT Tokens, Identity, Repositories, etc.
  3. Presentation / UI => forms the UI of the application.

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.

Structure of Clean Architecture “Ardalis” Repository

Ardalis repo contains 4 important projects:

  1. Clean.Architecture.Core – represents the Domain / Application Core layer and contains entities and interfaces forming the business logic.
  2. Clean.Architecture.Infrastructure – represents the Infrastructure layer and contains data access, entity framework core, Identity and email providers.
  3. Clean.Architecture.UseCases – contains CQRS (Command and Query Responsibility Segregation) pattern codes. This pattern separates read and update operations for a data store.
  4. Clean.Architecture.Web – represents UI ayer and contains razor views that forms the user interface.

I have shown this in the below image.

Ardalis Repository Structure

The database used is an SQLite database. You will find database.sqlite file in the Clean.Architecture.Web project.

Ardalis Database

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"
}

Implement Ardalis Clean Architecture

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:

  1. String type “Name” property.
  2. String type “Standard” property.
  3. Int type “Rank” property.

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.

Student Entity Core

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.

I recently wrote about – How to use Self Signed Certificate in ASP.NET Core. Do check it after this tutorial.

Next, inside the Infrastructure project, there is AppDbContext.cs file which is the Database Context for the app.

Ardalis DB Context

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>();

  ....
}
Learn Complete Entity Framework Core from start till finish from our tutorial series.

Now we are all set to perform the CRUD Operations on Students.

Create Student feature

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.

Ardalis CQRS

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:

Ardalis Fast Endpoints

Create.CreateStudentRequest.cs
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.

Create.CreatStudentValidator.cs
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.

Create.Create.CreateStudentResponse.cs
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.

Create.cs
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.

Dont’ worry about all these codes as I have already added them to my GitHub repository.

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.

Ardalis Controller

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?}");
View for Create Student UI

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.

Ardalis Views

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.

Create Feature Ardalis

Now check the database where the record is created.

Record Created Ardalis

Dont’ worry about the 404 error as we are yet to create the Read feature.

Read Student 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:

ListStudentQuery.cs
using Ardalis.Result;
using Ardalis.SharedKernel;

namespace Clean.Architecture.UseCases.Student.List;
public record ListStudentQuery(int? Skip, int? Take) : Iquery<Result<IEnumerable<StudentDTO>>>;
IListStudentQueryService.cs
namespace Clean.Architecture.UseCases.Student.List;
public interface IListStudentQueryService
{
  Task<IEnumerable<StudentDTO>> ListAsync();
}
ListStudentHandler.cs
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.

Ardalis Read UseCases

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.

View for Read Student UI

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.

Ardalis Read Feature

Update Student feature

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:

UpdateStudentCommand.cs
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>>;
UpdateStudentCommand.cs
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);
  }
}
GetStudentQuery.cs
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:

Update.cs
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;
    }
  }
}
Update.UpdateStudentRequest.cs
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; }
}
Update.UpdateStudentResponse.cs
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; }
}
Update.UpdateStudentValidator.cs
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:

GetById.cs
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);
    }
  }
}
GetById.GetStudentByIdRequest.cs
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; }
}
GetById.GetStudentValidator.cs
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();
}
View for Update Student UI

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.

Ardalis Update Feature

Read Student by Id feature

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>
View for Read Student by Id UI

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.

Read Student By ID

The student details image is shown below.

Student Details

Other than CRUD operations the Repository also contains Login and Logout feature using Identity. I have written complete tutorial series on ASP.NET Core Identity, do check it.

Delete Student feature

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:

StudentDeletedEvent.cs
using Ardalis.Result;
using Ardalis.SharedKernel;

namespace Clean.Architecture.UseCases.Student.Delete;
public record DeleteStudentCommand(int StudentId) : ICommand<Result>;
DeleteStudentHandler.cs
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:

Delete.cs
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
  }
}
Delete.DeleteStudentRequest.cs
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; }
}
Delete.DeleteStudentValidator.cs
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.

Conclusion

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.

SHARE THIS ARTICLE

  • linkedin
  • reddit
yogihosting

ABOUT THE AUTHOR

I hope you enjoyed reading this tutorial. If it helped you then consider buying a cup of coffee for me. This will help me in writing more such good tutorials for the readers. Thank you. Buy Me A Coffee donate

Leave a Reply

Your email address will not be published. Required fields are marked *