DEV Community

nikosst
nikosst

Posted on

Προσθήκη Fluent Validator step by step

Σε αυτό το άρθρο, θα προσπαθήσουμε να περιγράφουμε την σημαντικότητα της επικύρωσης των δεδομένων, θα περιγράψουμε που γίνεται, σε ποιο layer, αλλά και μία βιβλιοθήκη για να καλύψουμε αυτή την ανάγκη. θα δούμε βήμα βήμα την υλοποίησή του μέσα στο project Clean Architecture με επεξήγηση κάθε βήματος. Πάμε λοιπόν να προσθέσουμε FluentValidation για όλες τις οντότητες (Student, Lesson, Department, User) — για τα Create και Update DTOs — με καθαρή δομή, ενιαία εγγραφή στο DI και παράδειγμα controller ώστε να δουλεύει όλο end-to-end.


Βήμα 0 — Πακέτο

Στην βιβλιοθήκη application κάνε εγκατάσταση το παρακάτω nuget.

dotnet add package FluentValidation.AspNetCore


Βήμα 1 — Δομή φακέλων (στο παράδειγμά μας μπορεί να υπάρχουν και υποφάκελοι με τις οντότητες Student, Lesson και Department)

Βάλε τους validators στο Application layer:

MySchool.Application/
├─ DTOs/
│ ├─ StudentCreateDto.cs
│ ├─ StudentUpdateDto.cs
│ ├─ LessonCreateDto.cs
│ ├─ LessonUpdateDto.cs
│ ├─ DepartmentCreateDto.cs
│ ├─ DepartmentUpdateDto.cs
│ ├─ UserCreateDto.cs
│ └─ UserUpdateDto.cs
└─ Validators/
├─ StudentCreateValidator.cs
├─ StudentUpdateValidator.cs
├─ LessonCreateValidator.cs
├─ LessonUpdateValidator.cs
├─ DepartmentCreateValidator.cs
├─ DepartmentUpdateValidator.cs
├─ UserCreateValidator.cs
└─ UserUpdateValidator.cs


Βήμα 2 — Παραδείγματα DTOs

(Δώσε προσοχή στα πεδία required που θέλεις να ελέγχεις.)

StudentCreateDto.cs

public class StudentCreateDto
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public int DepartmentId { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

StudentUpdateDto.cs

public class StudentUpdateDto
{
    public int Id { get; set; }
    public string? FirstName { get; set; }    // optional on update
    public string? LastName { get; set; }
    public string? Email { get; set; }
    public int? DepartmentId { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

(Αντίστοιχα για LessonCreateDto, LessonUpdateDto, DepartmentCreateDto, DepartmentUpdateDto, UserCreateDto, UserUpdateDto — ό,τι πεδία χρειάζεσαι.)


Βήμα 3 — validators (παραδείγματα)
StudentCreateValidator

StudentCreateValidator.cs

using FluentValidation;

public class StudentCreateValidator : AbstractValidator<StudentCreateDto>
{
    public StudentCreateValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty().WithMessage("Το όνομα είναι υποχρεωτικό.")
            .MaximumLength(50);

        RuleFor(x => x.LastName)
            .NotEmpty().WithMessage("Το επώνυμο είναι υποχρεωτικό.")
            .MaximumLength(50);

        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Το email είναι υποχρεωτικό.")
            .EmailAddress().WithMessage("Μη έγκυρη διεύθυνση email.");

        RuleFor(x => x.DepartmentId)
            .GreaterThan(0).WithMessage("Το DepartmentId πρέπει να είναι > 0.");
    }
}

Enter fullscreen mode Exit fullscreen mode

StudentUpdateValidator

StudentUpdateValidator.cs

using FluentValidation;

public class StudentUpdateValidator : AbstractValidator<StudentUpdateDto>
{
    public StudentUpdateValidator()
    {
        RuleFor(x => x.Id)
            .GreaterThan(0).WithMessage("Μη έγκυρο Id.");

        // τα πεδία update είναι optional — αν δοθούν, ελέγχουμε περιορισμούς
        When(x => x.FirstName != null, () =>
        {
            RuleFor(x => x.FirstName).MaximumLength(50);
        });

        When(x => x.Email != null, () =>
        {
            RuleFor(x => x.Email).EmailAddress().WithMessage("Μη έγκυρο email.");
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

UserCreateValidator (παράδειγμα για password/role)

UserCreateValidator.cs

public class UserCreateValidator : AbstractValidator<UserCreateDto>
{
    public UserCreateValidator()
    {
        RuleFor(x => x.Username)
            .NotEmpty().MaximumLength(50);

        RuleFor(x => x.Password)
            .NotEmpty().MinimumLength(8)
            .WithMessage("Ο κωδικός πρέπει να έχει τουλάχιστον 8 χαρακτήρες.");

        RuleFor(x => x.Email)
            .EmailAddress().When(x => !string.IsNullOrEmpty(x.Email));

        RuleFor(x => x.Role)
            .NotEmpty()
            .Must(role => role == "admin" || role == "user")
            .WithMessage("Role πρέπει να είναι 'admin' ή 'user'.");
    }
}

Enter fullscreen mode Exit fullscreen mode

Βήμα 4 — Επαναχρησιμοποίηση κανόνων (DRY)

Αν έχεις κανόνες που επαναλαμβάνονται (π.χ. EmailRule, NameRule), σήκωσέ τους σε helper:

public static class ValidationRules
{
    public static IRuleBuilderOptions<T, string> ValidName<T>(this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder.NotEmpty().MaximumLength(50);
    }
}

Enter fullscreen mode Exit fullscreen mode

Και στο validator:


RuleFor(x => x.FirstName).ValidName();

Enter fullscreen mode Exit fullscreen mode

Μπορείς επίσης να φτιάξεις base validator:


public abstract class BaseCreateValidator<T> : AbstractValidator<T> where T : class
{
    protected void ApplyNameRules(IRuleBuilderInitial<T, string> rule) { /*...*/ }
}

Enter fullscreen mode Exit fullscreen mode

Βήμα 5 — Εγγραφή στο DI / Program.cs (μία φορά)

Στο Presentation/Program.cs:

using FluentValidation.AspNetCore;
using MySchool.Application.Validators; // reference to assembly

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddFluentValidation(fv =>
    {
        fv.RegisterValidatorsFromAssemblyContaining<StudentCreateValidator>();
        // αυτό θα βρει όλους τους validators στο ίδιο assembly
    });

// υπόλοιπο DI, DbContext, authentication κλπ.

Enter fullscreen mode Exit fullscreen mode

Βήμα 6 — Controller: πώς έρχεται το error response (έτοιμο)

Με FluentValidation ενσωματωμένο στα MVC model binders, όταν το DTO αποτυγχάνει validation, το framework ακυρώνει την action και επιστρέφει 400 Bad Request με ModelState errors. Μπορείς να το διαμορφώσεις αν θέλεις.

Παράδειγμα StudentsController:

[ApiController]
[Route("api/[controller]")]
public class StudentsController : ControllerBase
{
    private readonly IStudentService _service;
    public StudentsController(IStudentService service) => _service = service;

    [HttpPost]
    public IActionResult Create([FromBody] StudentCreateDto dto)
    {
        // αν το dto χάνει validation, δεν θα φτάσει εδώ (ModelState invalid)
        var created = _service.Create(dto);
        return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, [FromBody] StudentUpdateDto dto)
    {
        dto.Id = id;
        var ok = _service.Update(dto);
        if (!ok) return NotFound();
        return NoContent();
    }
}

Enter fullscreen mode Exit fullscreen mode

Προτεινόμενο σχήμα error (μπορείς να το customize)

Αν θες custom format, πρόσθεσε middleware για να μετατρέπεις ModelState σε συγκεκριμένο JSON.

Παράδειγμα middleware για errors:

app.Use(async (context, next) =>
{
    await next();

    if (context.Response.StatusCode == 400 && !context.Response.HasStarted)
    {
        var problemDetails = new ValidationProblemDetails(context.Features.Get<ModelStateFeature>()?.ModelState ?? new ModelStateDictionary());
        context.Response.ContentType = "application/problem+json";
        var json = JsonSerializer.Serialize(problemDetails);
        await context.Response.WriteAsync(json);
    }
});

Enter fullscreen mode Exit fullscreen mode

(Σημείωση: υπάρχει και built-in ProblemDetails support όταν χρησιμοποιείς [ApiController].)


Extra: Validation που χρειάζεται πρόσβαση σε DB (π.χ. μοναδικό email)

Μερικοί κανόνες θέλουν repository (π.χ. να μην υπάρχει ήδη email). Μην βάλεις DB κλήσεις μέσα στα domain entities — κάνε το στο validator injectando ένα repository:

public class UserCreateValidator : AbstractValidator<UserCreateDto>
{
    private readonly IUserRepository _repo;

    public UserCreateValidator(IUserRepository repo)
    {
        _repo = repo;

        RuleFor(x => x.Username)
            .NotEmpty().MustAsync(async (username, ct) => !await _repo.ExistsByUsernameAsync(username))
            .WithMessage("Το username υπάρχει ήδη.");
    }
}

Enter fullscreen mode Exit fullscreen mode

Για να λειτουργήσει, ο validator πρέπει να εγγραφεί στο DI (το FluentValidation το κάνει αυτό αυτομάτως όταν το register-άρεις).


Testing: παράδειγμα unit test για validator

Γρήγορο unit test με xUnit:

[Fact]
public void StudentCreateValidator_InvalidEmail_ShouldHaveError()
{
    var validator = new StudentCreateValidator();
    var dto = new StudentCreateDto { FirstName="A", LastName="B", Email="bad" };
    var result = validator.TestValidate(dto);
    result.ShouldHaveValidationErrorFor(x => x.Email);
}

Enter fullscreen mode Exit fullscreen mode

(Χρησιμοποίησε FluentValidation.TestHelper package αν θέλεις helpers.)


Συμπέρασμα / Good practices (σύντομα)

  • Βάλε τους validators στο Application layer (όχι Domain).
  • Register-άρισε μία φορά το assembly όπου βρίσκονται (DRY).
  • Χρησιμοποίησε CreateDto / UpdateDto ξεχωριστά και validators που αντιστοιχούν.
  • Για κανόνες που χρειάζονται DB, inject-άρε repository στον validator.
  • Χρησιμοποίησε helpers / base validators για κοινό validation (DRY).
  • Έλεγξε το error shape και καθόρισε αν θέλεις custom middleware για formatting.

Κάθε παρέμβαση μέσα στον κώδικα έχει στόχο να καλύψει μια ανάγκη, να βελτιώσει και να προλάβει οποιαδήποτε αστοχία. Για αυτό το λόγο χρειάζεται να γίνεται απολύτος συνηδητά και με κριτική σκέψη, να γνωρίζουμε ακριβώς το γιατί πρέπει να την κάνουμε, χρειάζεται λοιπόν μελέτη η κάθε βιβλιοθήκη πριν την χρησημοποιήσουμε. Παρακάτω παραθέτω την βιβλιοθήκη FluentValidation για την δική σας μελέτη.


nikosst

Top comments (0)