У курсі Razor Pages ви ознайомилися з базовою валідацією через DataAnnotations: [Required], [StringLength], [Range], [EmailAddress]. Це чудово для простих форм — але що з правилами, які стосуються кількох полів одночасно? Або правилами що потребують звернення до бази даних (чи зайнятий цей email)? Або правилами що занадто складні для атрибутів (якщо метод доставки "кур'єр", тоді адреса обов'язкова)?
ASP.NET Core MVC пропонує три рівні відповіді на ці питання. Від простого до потужного:
IValidatableObject — крос-field правила безпосередньо в моделіValidationAttribute — повторювані перевірки у вигляді атрибутів[Remote] validation — перевірки на сервері без перезавантаження сторінкиНайпростіший спосіб валідації кількох полів одночасно — реалізація інтерфейсу IValidatableObject. Метод Validate викликається після успішної валідації всіх DataAnnotations, тому можна бути впевненим що базові обмеження вже перевірено.
using System.ComponentModel.DataAnnotations;
namespace ShopApp.Models;
public class CheckoutDto : IValidatableObject
{
[Required(ErrorMessage = "Вкажіть ім'я")]
[StringLength(100, MinimumLength = 2)]
public string FullName { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
[Required(ErrorMessage = "Оберіть метод доставки")]
public string DeliveryMethod { get; set; } = ""; // "courier" або "pickup"
// Обов'язкове ЛИШЕ для кур'єрської доставки
public string? DeliveryAddress { get; set; }
// Payment method
[Required]
public string PaymentMethod { get; set; } = ""; // "card" або "cash"
// Card fields — обов'язкові ЛИШЕ якщо PaymentMethod == "card"
public string? CardNumber { get; set; }
public string? CardExpiry { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Сума замовлення має бути більшою за нуль")]
public decimal OrderTotal { get; set; }
// IValidatableObject: крос-field валідація
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Правило 1: кур'єрська доставка потребує адреси
if (DeliveryMethod == "courier" && string.IsNullOrWhiteSpace(DeliveryAddress))
{
yield return new ValidationResult(
"Для кур'єрської доставки вкажіть адресу",
[nameof(DeliveryAddress)] // ← до якого поля прив'язати помилку
);
}
// Правило 2: оплата карткою потребує номера та терміну дії
if (PaymentMethod == "card")
{
if (string.IsNullOrWhiteSpace(CardNumber))
{
yield return new ValidationResult(
"Введіть номер картки",
[nameof(CardNumber)]
);
}
if (string.IsNullOrWhiteSpace(CardExpiry))
{
yield return new ValidationResult(
"Введіть термін дії картки",
[nameof(CardExpiry)]
);
}
}
// Правило 3: самовивіз не підтримується для замовлень більше 5000 грн
if (DeliveryMethod == "pickup" && OrderTotal > 5000)
{
yield return new ValidationResult(
"Самовивіз доступний лише для замовлень до 5000 ₴. " +
"Для великих замовлень оберіть кур'єрську доставку.",
[nameof(DeliveryMethod), nameof(OrderTotal)] // ← прив'язка до двох полів
);
}
}
}
Розберемо ключові моменти реалізації:
yield return new ValidationResult(...) — метод Validate є генератором. Кожен yield return — це окрема помилка валідації. Якщо всі правила виконані — жодного yield return, метод повертає порожню колекцію.
Другий аргумент ValidationResult — масив імен властивостей, до яких прив'язана помилка. Це важливо: MVC відображає помилку у <span asp-validation-for="DeliveryAddress"> саме завдяки цьому. Якщо передати null — помилка стає «загальною» (ModelState[""]) і відображається в asp-validation-summary.
Порядок виконання: спочатку перевіряються усі [Required], [StringLength] тощо. Лише якщо всі вони пройшли — Validate(). Тобто в Validate() гарантовано ненульові required-поля.
IValidatableObject.Validate не потрібно викликати вручну. ASP.NET Core MVC робить це автоматично як частину ModelState.IsValid. Аналогічно — на клієнті jQuery Validation виклик Validate()не підтримується: цей метод виконується лише на сервері.ValidationAttributeКоли одне й те саме правило потрібно в кількох моделях — IValidatableObject починає дублюватися. Тоді правило виносять у власний атрибут.
Власний атрибут — це клас, що успадковує ValidationAttribute та перевизначає метод IsValid:
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
namespace ShopApp.Attributes;
// Атрибут: забороняє HTML-теги у рядках (захист від XSS)
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class NoHtmlAttribute : ValidationAttribute
{
private static readonly Regex HtmlRegex =
new("<[^>]+>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public NoHtmlAttribute()
{
// Повідомлення за замовчуванням (якщо не передано через ErrorMessage)
ErrorMessage = "Поле '{0}' не може містити HTML-теги";
}
protected override ValidationResult? IsValid(object? value, ValidationContext context)
{
if (value is null) return ValidationResult.Success; // необов'язкове поле — ОК
var str = value.ToString()!;
if (HtmlRegex.IsMatch(str))
{
// context.DisplayName — локалізована назва поля (з [Display(Name="...")])
var errorMessage = FormatErrorMessage(context.DisplayName);
return new ValidationResult(errorMessage, [context.MemberName!]);
}
return ValidationResult.Success;
}
}
using System.ComponentModel.DataAnnotations;
namespace ShopApp.Attributes;
// Атрибут: дата має бути в майбутньому
public class FutureDateAttribute : ValidationAttribute
{
public int MinDaysAhead { get; set; } = 0; // мінімум X днів у майбутньому
protected override ValidationResult? IsValid(object? value, ValidationContext context)
{
if (value is not DateTime date) return ValidationResult.Success;
var minDate = DateTime.Today.AddDays(MinDaysAhead);
if (date < minDate)
{
var msg = MinDaysAhead > 0
? $"Дата має бути щонайменше через {MinDaysAhead} дн. від сьогодні"
: "Дата має бути в майбутньому";
return new ValidationResult(msg, [context.MemberName!]);
}
return ValidationResult.Success;
}
}
Використання — як звичайні DataAnnotations:
using ShopApp.Attributes;
public class CreateEventDto
{
[Required]
[NoHtml] // ← власний атрибут
public string Title { get; set; } = "";
[NoHtml]
public string? Description { get; set; }
[Required]
[FutureDate(MinDaysAhead = 3)] // ← з параметром
public DateTime EventDate { get; set; }
}
Власні атрибути за замовчуванням валідуються лише на сервері. Щоб додати клієнтську валідацію через jQuery Validation, реалізуйте IClientModelValidator:
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
// Додати до FutureDateAttribute:
public class FutureDateAttribute : ValidationAttribute, IClientModelValidator
{
// ...
public void AddValidation(ClientModelValidationContext context)
{
// Додаємо data-атрибути для jQuery Validation
context.Attributes["data-val"] = "true";
context.Attributes["data-val-futuredate"] =
$"Дата має бути в майбутньому";
context.Attributes["data-val-futuredate-mindays"] =
MinDaysAhead.ToString();
}
}
// Потрібно зареєструвати адаптер jQuery Validation у Scripts
$.validator.addMethod("futuredate", function(value, element, params) {
const minDays = parseInt(params.mindays);
const minDate = new Date();
minDate.setDate(minDate.getDate() + minDays);
const inputDate = new Date(value);
return inputDate >= minDate;
});
$.validator.unobtrusive.adapters.add("futuredate", ["mindays"], function(options) {
options.rules["futuredate"] = { mindays: options.params.mindays };
options.messages["futuredate"] = options.message;
});
[Remote] — атрибут що дозволяє перевіряти значення поля ajax-запитом до сервера без перезавантаження сторінки. Ідеальний сценарій: перевірка унікальності email під час реєстрації.
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace BlogApp.Models;
public class RegisterDto
{
[Required, StringLength(50, MinimumLength = 3)]
[Display(Name = "Нікнейм")]
// Remote: при blur або change поля — надсилається GET на /account/checkusername?Username=...
[Remote(
action: "CheckUsername", // ← назва Action-методу
controller: "Account", // ← назва Controller
ErrorMessage = "Цей нікнейм вже зайнятий"
)]
public string Username { get; set; } = "";
[Required, EmailAddress]
[Display(Name = "Email")]
[Remote("CheckEmail", "Account",
ErrorMessage = "Цей email вже зареєстрований",
AdditionalFields = nameof(Id) // ← передати додаткове поле разом з запитом
)]
public string Email { get; set; } = "";
// Потрібно для AdditionalFields при редагуванні (щоб не блокувати власний email)
public int Id { get; set; }
[Required, DataType(DataType.Password)]
[StringLength(100, MinimumLength = 8)]
public string Password { get; set; } = "";
[Required, DataType(DataType.Password)]
[Display(Name = "Підтвердження паролю")]
[Compare(nameof(Password), ErrorMessage = "Паролі не збігаються")]
public string ConfirmPassword { get; set; } = "";
}
Action-методи для Remote validation повертають JsonResult з true (valid) або рядком помилки:
using Microsoft.AspNetCore.Mvc;
using BlogApp.Services;
namespace BlogApp.Controllers;
public class AccountController : Controller
{
private readonly IUserService _users;
public AccountController(IUserService users)
{
_users = users;
}
// Remote validation: GET /account/checkusername?Username=mylogin
[AcceptVerbs("GET", "POST")] // jQuery Validation надсилає GET; POST — для форм з antiforgery
public async Task<IActionResult> CheckUsername(string username)
{
var isTaken = await _users.IsUsernameTakenAsync(username);
// true → валідне; рядок → повідомлення про помилку
return isTaken
? Json($"Нікнейм «{username}» вже зайнятий")
: Json(true);
}
// Remote з AdditionalFields: GET /account/checkemail?Email=x@x.com&Id=5
[AcceptVerbs("GET", "POST")]
public async Task<IActionResult> CheckEmail(string email, int id)
{
// Якщо id > 0 — редагування: не блокуємо власний email
var existingUser = await _users.FindByEmailAsync(email);
var isTaken = existingUser is not null && existingUser.Id != id;
return isTaken
? Json($"Email «{email}» вже використовується")
: Json(true);
}
[HttpGet]
public IActionResult Register() => View(new RegisterDto());
[HttpPost]
public async Task<IActionResult> Register(RegisterDto dto)
{
if (!ModelState.IsValid) return View(dto);
await _users.CreateAsync(dto.Username, dto.Email, dto.Password);
TempData["Success"] = $"Акаунт «{dto.Username}» успішно створено!";
return RedirectToAction("Login");
}
}
[Remote] спрацьовує лише при наявності клієнтських скриптів валідації: jQuery Validation + Unobtrusive Validation (_ValidationScriptsPartial). На сервері при ModelState.IsValid Remote перевірка також виконується автоматично як звичайний атрибут.HttpMethod = "POST"). Ніколи не виконуйте в ньому операцій що змінюють стан (запис, оновлення даних). Remote — лише для читання з метою перевірки.DataAnnotations та IValidatableObject добре для простих сценаріїв, але мають суттєві обмеження:
FluentValidation — бібліотека що дозволяє описувати правила валідації у вигляді окремих класів-валідаторів з fluent API:
// DataAnnotations підхід (в моделі)
[Required]
[StringLength(100, MinimumLength = 2)]
[RegularExpression(@"^[\w\s]+$")]
public string Title { get; set; } = "";
// FluentValidation підхід (в окремому класі)
RuleFor(x => x.Title)
.NotEmpty().WithMessage("Заголовок обов'язковий")
.Length(2, 100).WithMessage("Від 2 до 100 символів")
.Matches(@"^[\w\s]+$").WithMessage("Лише букви та пробіли");
dotnet add package FluentValidation.AspNetCore
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// Реєструємо FluentValidation — він автоматично знаходить валідатори у збірці
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// або явно: builder.Services.AddScoped<IValidator<OrderDto>, OrderDtoValidator>();
AddFluentValidationAutoValidation() інтегрує FluentValidation у ModelState: якщо є зареєстрований валідатор для типу, він виконується автоматично при ModelState.IsValid у Controller. Жодних змін у Controller-коді.
using FluentValidation;
using BlogApp.Models;
using BlogApp.Services;
namespace BlogApp.Validators;
public class RegisterDtoValidator : AbstractValidator<RegisterDto>
{
private readonly IUserService _users;
// DI — валідатор може отримувати сервіси!
public RegisterDtoValidator(IUserService users)
{
_users = users;
// Правила для Username
RuleFor(x => x.Username)
.NotEmpty().WithMessage("Нікнейм обов'язковий")
.Length(3, 50).WithMessage("Від 3 до 50 символів")
.Matches(@"^[a-zA-Z0-9_]+$")
.WithMessage("Лише латинські букви, цифри та підкреслення")
.MustAsync(BeUniqueUsername)
.WithMessage(x => $"Нікнейм «{x.Username}» вже зайнятий");
// Правила для Email
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email обов'язковий")
.EmailAddress().WithMessage("Некоректний формат email")
.MustAsync(BeUniqueEmail)
.WithMessage(x => $"Email «{x.Email}» вже зареєстрований");
// Правила для Password
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Пароль обов'язковий")
.MinimumLength(8).WithMessage("Мінімум 8 символів")
.Matches("[A-Z]").WithMessage("Хоча б одна велика буква")
.Matches("[0-9]").WithMessage("Хоча б одна цифра")
.Matches("[^a-zA-Z0-9]").WithMessage("Хоча б один спеціальний символ");
// Крос-field правило: ConfirmPassword
RuleFor(x => x.ConfirmPassword)
.Equal(x => x.Password)
.WithMessage("Паролі не збігаються");
}
// Async правила через MustAsync — можна звертатися до БД
private async Task<bool> BeUniqueUsername(string username, CancellationToken ct)
=> !await _users.IsUsernameTakenAsync(username);
private async Task<bool> BeUniqueEmail(RegisterDto dto, string email, CancellationToken ct)
{
var existing = await _users.FindByEmailAsync(email);
return existing is null || existing.Id == dto.Id;
}
}
Ось де FluentValidation дійсно сяє — складні умовні правила, вкладені об'єкти, валідація колекцій:
namespace ShopApp.Models;
public class OrderDto
{
public string DeliveryMethod { get; set; } = ""; // "courier" | "pickup" | "nova-poshta"
public string? DeliveryAddress { get; set; }
public string? NovaPoshtaBranch { get; set; } // відділення Нової Пошти
public string PaymentMethod { get; set; } = ""; // "card" | "cash" | "invoice"
public string? CardNumber { get; set; }
public List<OrderItemDto> Items { get; set; } = [];
public string? PromoCode { get; set; }
public string CustomerEmail { get; set; } = "";
}
public class OrderItemDto
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
using FluentValidation;
using ShopApp.Models;
using ShopApp.Services;
namespace ShopApp.Validators;
public class OrderDtoValidator : AbstractValidator<OrderDto>
{
public OrderDtoValidator(IPromoCodeService promoCodes)
{
// ── Базові правила ─────────────────────────────────────────────
RuleFor(x => x.CustomerEmail)
.NotEmpty().EmailAddress()
.WithMessage("Введіть коректний email");
// ── Умовна валідація доставки ───────────────────────────────────
// When/Unless — умовне застосування правил
RuleFor(x => x.DeliveryAddress)
.NotEmpty().WithMessage("Для кур'єрської доставки вкажіть адресу")
.When(x => x.DeliveryMethod == "courier");
RuleFor(x => x.NovaPoshtaBranch)
.NotEmpty().WithMessage("Вкажіть відділення Нової Пошти")
.Matches(@"^\d+$").WithMessage("Номер відділення — лише цифри")
.When(x => x.DeliveryMethod == "nova-poshta");
// ── Умовна валідація оплати ─────────────────────────────────────
RuleFor(x => x.CardNumber)
.NotEmpty().WithMessage("Введіть номер картки")
.CreditCard().WithMessage("Некоректний номер картки")
.When(x => x.PaymentMethod == "card");
// Unless — протилежне: виконувати ЯКЩО NOT умова
RuleFor(x => x.DeliveryMethod)
.Must(m => m != "pickup")
.WithMessage("Накладений платіж доступний лише при самовивозі")
.Unless(x => x.PaymentMethod == "cash"); // якщо не cash — обмеження знімається
// ── Валідація колекцій через RuleForEach ───────────────────────
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Замовлення має містити хоча б один товар")
.Must(items => items.Count <= 50)
.WithMessage("Максимум 50 позицій в одному замовленні");
// RuleForEach — правило для кожного елемента List<>
RuleForEach(x => x.Items).SetValidator(new OrderItemValidator());
// ── Async правило з сервісом ────────────────────────────────────
When(x => !string.IsNullOrEmpty(x.PromoCode), () =>
{
RuleFor(x => x.PromoCode!)
.MustAsync(async (code, ct) =>
await promoCodes.IsValidAsync(code))
.WithMessage(x => $"Промо-код «{x.PromoCode}» недійсний або використаний");
});
// ── Transform: конвертація значення перед валідацією ────────────
// Нормалізуємо email (trim + lowercase) перед перевіркою
Transform(x => x.CustomerEmail, v => v?.Trim().ToLowerInvariant() ?? "");
}
}
// Окремий валідатор для вкладеного типу
public class OrderItemValidator : AbstractValidator<OrderItemDto>
{
public OrderItemValidator()
{
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("Некоректний ідентифікатор товару");
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 100)
.WithMessage("Кількість від 1 до 100");
RuleFor(x => x.UnitPrice)
.GreaterThan(0).WithMessage("Ціна має бути більшою за нуль");
}
}
Декілька ключових конструкцій FluentValidation що потрібно запам'ятати:
When(condition, rules) — застосовує правила лише якщо умова виконана. Ідеально для залежних полів.
Unless(condition, rules) — протилежне: застосовує правила лише якщо умова не виконана.
RuleForEach(x => x.Collection) — правила для кожного елемента колекції. Помилки прив'язуються до Items[0].Quantity, Items[1].ProductId тощо.
MustAsync(async (value, ct) => ...) — async правило із зверненням до сервісів.
Transform(x => x.Field, v => transform(v)) — перетворити значення перед валідацією (нормалізація).
using Microsoft.AspNetCore.Mvc;
using ShopApp.Models;
namespace ShopApp.Controllers;
public class OrderController : Controller
{
private readonly IOrderService _orders;
public OrderController(IOrderService orders)
{
_orders = orders;
}
[HttpGet]
public IActionResult Checkout() => View(new OrderDto());
[HttpPost]
public async Task<IActionResult> Checkout(OrderDto dto)
{
// ModelState.IsValid — FluentValidation виконується автоматично
// Жодних змін у Controller коді порівняно з DataAnnotations
if (!ModelState.IsValid) return View(dto);
var order = await _orders.CreateAsync(dto);
TempData["Success"] = $"Замовлення #{order.Id} оформлено!";
return RedirectToAction("Confirmation", new { id = order.Id });
}
}
Ключова перевага FluentValidation — валідатор є звичайним C# класом, що легко тестувати:
using FluentValidation.TestHelper;
using ShopApp.Models;
using ShopApp.Validators;
namespace ShopApp.Tests;
public class OrderDtoValidatorTests
{
private readonly OrderDtoValidator _validator;
public OrderDtoValidatorTests()
{
// Mock сервіс промо-кодів
var promoServiceMock = new Mock<IPromoCodeService>();
promoServiceMock.Setup(x => x.IsValidAsync("VALID10")).ReturnsAsync(true);
promoServiceMock.Setup(x => x.IsValidAsync("EXPIRED")).ReturnsAsync(false);
_validator = new OrderDtoValidator(promoServiceMock.Object);
}
[Fact]
public async Task Should_Require_Address_For_Courier_Delivery()
{
var dto = new OrderDto
{
DeliveryMethod = "courier",
DeliveryAddress = null // ← помилка
};
var result = await _validator.TestValidateAsync(dto);
// FluentValidation TestHelper — чіткий синтаксис перевірки
result.ShouldHaveValidationErrorFor(x => x.DeliveryAddress)
.WithErrorMessage("Для кур'єрської доставки вкажіть адресу");
}
[Fact]
public async Task Should_Not_Require_Address_For_Pickup()
{
var dto = new OrderDto
{
DeliveryMethod = "pickup",
DeliveryAddress = null // ← ОК, самовивіз — адреса не потрібна
};
var result = await _validator.TestValidateAsync(dto);
result.ShouldNotHaveValidationErrorFor(x => x.DeliveryAddress);
}
}
TestValidateAsync та ShouldHaveValidationErrorFor — методи з пакета FluentValidation.TestHelper що роблять тестування валідаторів надзвичайно читабельним.
| DataAnnotations | IValidatableObject | Custom Attribute | FluentValidation | |
|---|---|---|---|---|
| Просте поле | ✅ Ідеально | Оverkill | Overkill | Ок |
| Крос-field правила | ❌ Не підтримує | ✅ Ідеально | Складно | ✅ When() |
| Правило з DI/БД | ❌ | ❌ | Складно | ✅ MustAsync |
| Remote validation | [Remote] | ❌ | ❌ | Окремо |
| Тестованість | ❌ Погано | ❌ Середньо | Середньо | ✅ Відмінно |
| Різні правила для різних контекстів | ❌ | ❌ | ❌ | ✅ Окремі валідатори |
| Клієнтська валідація | ✅ Автоматична | ❌ | Потрібен адаптер | ❌ Лише серверна |
Помилки FluentValidation автоматично потрапляють у ModelState — тому стандартні Tag Helpers працюють без змін:
@model ShopApp.Models.OrderDto
@* Summary — загальні помилки (без прив'язки до поля) *@
<div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>
<form asp-action="Checkout" method="post">
<div class="mb-3">
<label asp-for="CustomerEmail" class="form-label">Email</label>
<input asp-for="CustomerEmail" class="form-control" type="email">
@* Помилки FluentValidation з'являються тут *@
<span asp-validation-for="CustomerEmail" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="DeliveryMethod" class="form-label">Метод доставки</label>
<select asp-for="DeliveryMethod" class="form-select"
onchange="updateDeliveryFields(this.value)">
<option value="">— Оберіть —</option>
<option value="pickup">Самовивіз</option>
<option value="courier">Кур'єр</option>
<option value="nova-poshta">Нова Пошта</option>
</select>
<span asp-validation-for="DeliveryMethod" class="text-danger"></span>
</div>
@* Умовні поля — показуємо/приховуємо через JS *@
<div id="address-field" class="mb-3" style="display:none;">
<label asp-for="DeliveryAddress" class="form-label">Адреса доставки</label>
<input asp-for="DeliveryAddress" class="form-control">
<span asp-validation-for="DeliveryAddress" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Оформити замовлення</button>
</form>
@section Scripts {
<partial name="_ValidationScriptsPartial"/>
<script>
function updateDeliveryFields(method) {
document.getElementById("address-field").style.display =
method === "courier" ? "block" : "none";
}
</script>
}
Завдання 1.1. Для моделі UserProfileDto (Username, Bio, WebsiteUrl, BirthDate) реалізуйте IValidatableObject.Validate з такими крос-field правилами:
Bio не порожнє і містить WebsiteUrl — повернути помилку «Вкажіть сайт у відповідному полі, а не в біографії»BirthDate < DateTime(1900, 1, 1) — повернути помилку «Некоректна дата народження»BirthDate > DateTime.Today.AddYears(-13) — «Вік має бути більше 13 років»Завдання 1.2. Створіть власний атрибут [NotContains(string forbidden)] що перевіряє відсутність певного підрядка у значенні. Застосуйте: [NotContains("admin")] до поля Username. Повідомлення: «Поле не може містити "{forbidden}"».
Завдання 2.1. Реалізуйте Remote validation для поля Username:
AccountController.CheckUsernameAvailability(string username)admin, root, support, api) та не зайнятий у БД"Цей нікнейм зарезервований системою", якщо зайнятий — "Вже використовується", інакше — trueAdditionalFields = nameof(Id) для підтримки редагування профілюЗавдання 2.2. Напишіть AbstractValidator<CheckoutDto> (FluentValidation) що замінює IValidatableObject з початку статті. Забезпечте всі три крос-field правила через When() та Unless(). Напишіть 3 юніт-тести Should_Require_Address_When_Courier, Should_Not_Require_Card_When_Cash, Should_Block_Pickup_For_Large_Order_With_Cash.
Завдання 3.1. Реалізуйте повноцінну форму реєстрації з кількома кроками (wizard):
AccountInfoDto (Username, Email, Password, ConfirmPassword) — FluentValidation з MustAsync для перевірки унікальностіPersonalInfoDto (FirstName, LastName, BirthDate, PhoneNumber) — власний PhoneUaAttribute що валідує формат +380XXXXXXXXXPreferencesDto (NotificationsEnabled, PreferredLanguage, AgreeToTerms) — [Remote] для перевірки чи вибрана PreferredLanguage підтримується системоюTempData (серіалізація в JSON). Завершення — лише якщо ВСІ три кроки пройшли валідаціюIValidatableObject — крос-field правила в тілі моделі через Validate(). Простий але не тестовий, не масштабується до складних правилValidationAttribute — повторювані правила у вигляді атрибутів. Клієнтська підтримка через IClientModelValidator[Remote] — ajax-перевірка поля на сервері в реальному часі. Action повертає Json(true) або Json("повідомлення"). Лише GET-запит, лише читанняWhen/Unless (умовні правила), RuleForEach (колекції), MustAsync (async з DI-сервісами), Transform (нормалізація)ModelState через AddFluentValidationAutoValidation() — Controller-код не змінюєтьсяTestValidateAsync + ShouldHaveValidationErrorForУ наступній статті — HTMX: філософія «HTML over the wire», core атрибути (hx-get/post, hx-target, hx-swap, hx-trigger) та практичне застосування для живих інтерфейсів без SPA.
Display та Editor Templates
Display та Editor Templates в ASP.NET Core MVC: папки DisplayTemplates та EditorTemplates, Html.DisplayFor та Html.EditorFor, атрибут [UIHint], шаблони для складних типів (Money, DateRange, Address). Демо: ProductCard Display Template та MoneyEditor Editor Template.
HTMX: інтерактивність через HTML-атрибути
HTMX — бібліотека для побудови динамічних інтерфейсів без JavaScript-фреймворків. Філософія «HTML over the wire»: hx-get/post/put/delete, hx-target, hx-swap (всі режими), hx-trigger, hx-include, hx-indicator, hx-push-url, hx-boost. Out-of-band swaps. Server-Sent Events. Порівняння з Alpine.js та React. Демо: живий список задач.