FluentValidation — AbstractValidator, kuchli validatsiya qoidalari

~25 daq · .NET Backend · Web API — Controller va Routing

Muammodan boshlaylik

Buyurtma DTO si:

public record BuyurtmaDto(
    string Telefon,
    string YetkazishTuri,           // "kuryer" yoki "olib ketish"
    string? Manzil,                 // kuryer bo'lsa — majburiy
    DateTime? YetkazishVaqti,       // kelajakda, ish vaqtida
    List<BuyurtmaQatoriDto> Qatorlar);   // kamida bitta, har biri to'g'ri

Qoidalar: "kuryer bo'lsa manzil majburiy", "yetkazish vaqti 9:00–21:00 oralig'ida va kelajakda", "har bir qatorda soni 1–99", "promokod bazada mavjud bo'lsin". Atributlar (46-dars) bilan — shartli qoidalar, ichki ro'yxatlar va bazaga murojaat — noqulay yoki imkonsiz. DTO atributlar bilan to'lib, o'qib bo'lmaydigan holga keladi.


FluentValidation nima?

FluentValidation — validatsiya qoidalarini DTO dan alohida klassda, zanjirli (fluent) sintaksis bilan yozish kutubxonasi:

dotnet add package FluentValidation.DependencyInjectionExtensions
using FluentValidation;

public class BuyurtmaDtoValidator : AbstractValidator<BuyurtmaDto>
{
    public BuyurtmaDtoValidator()
    {
        RuleFor(x => x.Telefon)
            .NotEmpty().WithMessage("Telefon majburiy")
            .Matches(@"^\+998\d{9}
quot;).WithMessage("Telefon +998XXXXXXXXX formatida"); RuleFor(x => x.YetkazishTuri) .Must(t => t is "kuryer" or "olib ketish") .WithMessage("Yetkazish turi: 'kuryer' yoki 'olib ketish'"); RuleFor(x => x.Manzil) .NotEmpty().WithMessage("Kuryer uchun manzil majburiy") .When(x => x.YetkazishTuri == "kuryer"); RuleFor(x => x.YetkazishVaqti) .Must(v => v > DateTime.Now).WithMessage("Vaqt kelajakda bo'lsin") .Must(v => v!.Value.Hour is >= 9 and < 21).WithMessage("Yetkazish 9:00–21:00 oralig'ida") .When(x => x.YetkazishVaqti.HasValue); RuleFor(x => x.Qatorlar) .NotEmpty().WithMessage("Savat bo'sh"); RuleForEach(x => x.Qatorlar).SetValidator(new BuyurtmaQatoriValidator()); } } public class BuyurtmaQatoriValidator : AbstractValidator<BuyurtmaQatoriDto> { public BuyurtmaQatoriValidator() { RuleFor(x => x.MahsulotId).GreaterThan(0); RuleFor(x => x.Soni).InclusiveBetween(1, 99); } }

DTO — toza, faqat ma'lumot; qoidalar — o'qishli, alohida klassda.


Hayotiy o'xshatma

Bino qurilishidagi inspektor va uning tekshiruv varaqasi. Qoidalar binoning devorlariga yopishtirilmaydi (atributlar) — inspektorning alohida, tartibli ro'yxatida turadi: "agar bino 5 qavatdan baland bo'lsa — lift majburiy", "har bir xonada — deraza". Ro'yxatni o'zgartirish — binoni qayta qurishni talab qilmaydi.


Asosiy qoidalar

Qoida Ma'nosi
NotEmpty() / NotNull() Bo'sh emas / null emas
Length(min, max), MaximumLength(n) Uzunlik
GreaterThan(x), InclusiveBetween(a, b) Son oralig'i
EmailAddress() Email
Matches(regex) Regex
Must(predicate) O'z sharti
Equal(x => x.Parol) Boshqa maydonga teng
IsInEnum() Enum ning to'g'ri qiymati
.When(...) / .Unless(...) Shartli qoida
.WithMessage("...") Xabar
.WithName("Telefon raqam") Xabardagi maydon nomi
RuleForEach(...) Kolleksiyaning har bir elementi
SetValidator(...) Ichki ob'ekt uchun validator

Xato bo'lganda to'xtash

RuleFor(x => x.Email)
    .Cascade(CascadeMode.Stop)          // birinchi xatoda keyingi qoidalarni tekshirmaslik
    .NotEmpty()
    .EmailAddress();

Asinxron qoidalar — bazaga murojaat

public class RoyxatDtoValidator : AbstractValidator<RoyxatDto>
{
    public RoyxatDtoValidator(DokonDbContext db)        // DI orqali
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(async (email, ct) =>
                !await db.Foydalanuvchilar.AnyAsync(f => f.Email == email.ToLower(), ct))
            .WithMessage("Bu email allaqachon ro'yxatdan o'tgan");
    }
}

[!WARNING] Asinxron "noyoblik" tekshiruvi — poyga holatidan (race condition) himoya qilmaydi: ikki bir vaqtdagi so'rov ikkalasi ham "bo'sh" javob olib, ikkalasi ham yozilishi mumkin. Haqiqiy kafolat — bazadagi unique index (51-dars). Validator — faqat foydalanuvchiga tushunarli xabar uchun.


Ro'yxatdan o'tkazish va ishlatish

// Program.cs — barcha validator larni avtomatik topish
builder.Services.AddValidatorsFromAssemblyContaining<Program>();

Controller da

[HttpPost]
public async Task<IActionResult> Yarat(
    BuyurtmaDto dto,
    [FromServices] IValidator<BuyurtmaDto> validator,
    CancellationToken ct)
{
    var natija = await validator.ValidateAsync(dto, ct);
    if (!natija.IsValid)
    {
        return ValidationProblem(new ValidationProblemDetails(natija.ToDictionary()));
    }

    var b = await service.YaratAsync(dto, ct);
    return CreatedAtAction(nameof(Ol), new { id = b.Id }, b);
}

Javob formati — 46-darsdagi bilan bir xil ProblemDetails.

Avtomatik — filtr bilan

Har bir action da qo'lda chaqirmaslik uchun — umumiy action filter yoki Minimal API endpoint filter (75-dars) yozish mumkin:

public class ValidatsiyaFiltri<T>(IValidator<T> validator) : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
    {
        var arg = ctx.ActionArguments.Values.OfType<T>().FirstOrDefault();
        if (arg is not null)
        {
            var r = await validator.ValidateAsync(arg);
            if (!r.IsValid)
            {
                ctx.Result = new BadRequestObjectResult(new ValidationProblemDetails(r.ToDictionary()));
                return;
            }
        }
        await next();
    }
}

[!INFO] Eski FluentValidation.AspNetCore paketidagi "avtomatik MVC validatsiyasi" (AddFluentValidationAutoValidation) endi tavsiya etilmaydi — kutubxona mualliflari aniq, qo'lda chaqirishni (yoki o'z filtringizni) maslahat beradi, chunki avtomatik rejim asinxron qoidalarni to'g'ri qo'llab-quvvatlamaydi.


Test qilish

var validator = new BuyurtmaQatoriValidator();
var natija = validator.TestValidate(new BuyurtmaQatoriDto(MahsulotId: 1, Soni: 0));
natija.ShouldHaveValidationErrorFor(x => x.Soni);

Qoidalar — oddiy klass, server ishga tushirmasdan test qilinadi (76-dars).


Data Annotations yoki FluentValidation?

Data Annotations FluentValidation
Qayerda DTO property larida Alohida klass
Oddiy qoidalar ✅ juda qisqa ✅
Shartli, ichki ro'yxat, bir nechta maydon Noqulay ✅
Asinxron (baza) ❌ ✅ MustAsync
Test Qiyinroq Oson
Paket O'rnatilgan NuGet

[!TIP] Oddiy CRUD — Data Annotations yetarli. Murakkab formalar va qoidalar ko'p bo'lsa — FluentValidation. Bitta loyihada bitta usulni izchil tanlang.


Tez-tez uchraydigan xatolar

Validator ro'yxatdan o'tmagan — Unable to resolve service for type 'IValidator<BuyurtmaDto>'; AddValidatorsFromAssemblyContaining.

.When() ni noto'g'ri qatorga qo'yish — u faqat oldingi qoida(lar)ga taalluqli.

Validate (sinxron) bilan MustAsync — AsyncValidatorInvokedSynchronouslyException; ValidateAsync.


Xulosa

FluentValidation — qoidalar DTO dan alohida AbstractValidator<T> klassida: RuleFor(x => x.Maydon).NotEmpty().Matches(...).WithMessage(...). Shartli qoidalar — .When(), kolleksiyalar — RuleForEach + SetValidator, o'z sharti — Must, baza — MustAsync (lekin unique index baribir kerak).

Ro'yxatdan o'tkazish — AddValidatorsFromAssemblyContaining<Program>(); ishlatish — IValidator<T>.ValidateAsync va ValidationProblem (yoki umumiy filtr). Oson test qilinadi. Oddiy holatlar uchun Data Annotations ham yetarli.


Keyingi dars

48-dars: HTTP Status kodlar — 200, 201, 400, 401, 404, Ok(), BadRequest()