Validation

התקנה

נתקין את ה-package של FluentValidation, FluentValidation.DependencyInjection, FluentValidation.AspNetCore מתוך nuget.

מודל הנתונים שלנו של האוביקט שמגיע מה-client הוא:

קובץ ProductDto.cs

public class ProductDto
{
    public required int id { get; init; }
    public string? name { get; init; }
    public string? description { get; init; }
    public decimal price { get; init; }
}

אנחנו רוצים להוסיף חוקיות לאובייקט הזה.

יצירת ה-validator

ניצור תיקיית validation בתוכה ניצור class בשם ProductDtoValidator. אנחנו יורשים מ-AbstractValidator ושולחים את האובייקט שאנחנו עושים לו ולידציה.

קובץ ProductDtoValidator.cs

public class ProductDtoValidator : AbstractValidator<ProductDto>
{
}

אנחנו נגדיר constructor ובתוכו נשתמש בפונקציות שקבלנו מה-parent.

אנחנו מגדירים חוקים עבור שדות מסויימים. למשל פה נגדיר שהשדה name לא יהיה ריק.

public class ProductDtoValidator : AbstractValidator<ProductDto>
{
    public ProductDtoValidator() {
        RuleFor(x => x.name).NotEmpty();
    }  
}

אפשר לשרשר חוקים לאותו השדה, למשל להוסיף שהוא גם יהיה לא null.

RuleFor(x => x.name).NotEmpty().NotNull();

נוסיף עוד ולידציות:

public ProductDtoValidator() {
        RuleFor(x => x.name).NotEmpty().NotNull();
        RuleFor(x => x.price).GreaterThan(0);
        RuleFor(x => x.description).Length(5, 20);
}

לא משנה מה הולידציות שעושים ב-client, תמיד צריך לעשות את הולידציות גם ב-server.

נשתמש ב-DI גם פה ונוסיף את ה-validator לקובץ ה-program.

קובץ program.cs

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<CourseDbContext>(options => options.UseInMemoryDatabase("CourseDb"));

builder.Services.AddCors(Action => Action.AddPolicy("aspnet-course",
    config => config.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));

// Resolve DI
builder.Services.AddScoped<IProductsRepository, ProductsRepository>();
builder.Services.AddScoped<IValidator<ProductDto>, ProductDtoValidator>();

var app = builder.Build();

כשנרצה לעבוד עם ה-validator לא ניצור class אלא נשתמש ב-DI.

שימוש ב-Validator

נראה איך קוראים ל-validator למשל כשרוצים להוסיף מוצר חדש.

  1. הוספת IValidator ל-DI.
  2. קריאה ל-validator, קבלה של התוצאה.
  3. אם חזר false מחזירים את התוצאה של ה-validator.
  4. אם הכל בסדר ממשיכים עם פעולת הוספת המוצר.

קובץ ProductsEndPoints.cs

static async Task<Results<Created<ProductDto>, ValidationProblem>> AddNewProduct(
  ProductDto product, 
  IProductsRepository repo, 
  IMapper mapper, 
  IValidator<ProductDto> validator) {
    var validationResult = validator.Validate(product);

    if (!validationResult.IsValid) {
        return TypedResults.ValidationProblem(validationResult.ToDictionary());
    }

    var mappedResult = mapper.Map<Product>(product);
    Product addProduct = await repo.AddNewProduct(mappedResult);
    var productResult = mapper.Map<ProductDto>(addProduct);

    return TypedResults.Created($"api/products/{addProduct.id}", productResult);
}

הוספה אוטומטית של validation

הוספנו לקובץ ה-program את ה-validator שלנו בצורה הזאת:

builder.Services.AddScoped<IValidator<ProductDto>, ProductDtoValidator>();

במידה ויהיו לנו הרבה ישויות יהיו לנו פה הרבה שורות של validation. אפשר לעשות את זה בפעם אחת על ידי הוספת AddValidatorsFromAssembly ובעליית מערכת הוא יסרוק את כל ה-assembly ויחקור את כולו ויחלץ ממנו את ה-validators הרלוונטים.

קובץ program.cs

builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
builder.Services.AddDbContext<CourseDbContext>(options => options.UseInMemoryDatabase("CourseDb"));

builder.Services.AddCors(Action => Action.AddPolicy("aspnet-course",
    config => config.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));

// Resolve DI
builder.Services.AddScoped<IProductsRepository, ProductsRepository>();

var app = builder.Build();

Custom Validation

אם יש לי ולידציה שהיא לא סטנדרטית אפשר לעשות ולידציה מותאמת. נניח שאני רוצה לוודא שמספר של ת.ז תקין או מספר אשראי. אנחנו בונים חוק Custom, שהוא פונקציה שמקבלת את ה-value ואת ה-context. בפונקציה אני מבצעת את הבדיקות ומכניסה את השגיאות ל-context.

נעשה בדיקה לדוגמא, אנחנו רוצים לוודא שהמחיר הוא מספר זוגי.

קובץ ProductDtoValidator.cs

public ProductDtoValidator() {
    RuleFor(x => x.name).NotEmpty().NotNull();
    RuleFor(x => x.price)
        .GreaterThan(0)
        .Custom((value, context) => {
            if(value % 2 != 0) {
                context.AddFailure("Price is not even");
            }
        });
    RuleFor(x => x.description).Length(5, 20);
}