"createdAt" у "Created At", 142857 у "142 857", DateTime.Now.AddMinutes(-5) у "5 хвилин тому". Guard Clauses замінюють численні if (x == null) throw new ArgumentNullException() на один читабельний рядок.Humanizer — бібліотека для перетворення даних у людиночитабельний текст. Її місія: зробити те, що комп'ютер представляє внутрішньо, зрозумілим для людей.
Приклади завдань, що вирішує Humanizer:
"camelCaseString" → "Camel case string"DateTime.Now.AddHours(-3) → "3 years ago" / "3 години тому"1500000 → "1.5 million" / "1 500 000"UserAccountStatus.PendingVerification → "Pending verification""TheQuickBrownFox" → "The quick brown fox"dotnet add package Humanizer
dotnet add package Humanizer.Core.uk # Українська локалізація
Humanizer надає потужний набір extension methods для рядків:
using Humanizer;
// Гуманізація рядків
"camelCaseString".Humanize() // → "Camel case string"
"PascalCaseString".Humanize() // → "Pascal case string"
"UPPER_CASE_IDENTIFIER".Humanize() // → "Upper case identifier"
"some_database_column".Humanize() // → "Some database column"
// Зміна регістру
"some title".Pascalize() // → "SomeTitle"
"some title".Camelize() // → "someTitle"
"some title".Underscore() // → "some_title"
"some title".Kebaberize() // → "some-title"
"SomeTitle".Titleize() // → "Some Title"
// Truncate (скорочення)
"The quick brown fox".Truncate(10)
// → "The qui..." (10 символів з "..." наприкінці)
"The quick brown fox".Truncate(10, Truncator.FixedNumberOfCharacters)
// → "The qui..."
"The quick brown fox".Truncate(3, Truncator.FixedNumberOfWords)
// → "The quick brown..."
// Transform
"Encode this string!".Transform(
To.LowerCase,
To.TitleCase)
// → "Encode This String!"
Humanizer корисний для генерації читабельних ключів і значень у відповідях API:
public static class EnumExtensions
{
// Перетворює значення Enum у читабельний рядок для фронтенду
public static string ToHumanReadable<TEnum>(this TEnum value)
where TEnum : Enum
{
return value.ToString().Humanize(LetterCasing.Title);
}
}
// Використання:
OrderStatus.PendingPayment.ToHumanReadable() // → "Pending Payment"
UserRole.SystemAdministrator.ToHumanReadable() // → "System Administrator"
[HttpGet("statuses")]
public IActionResult GetOrderStatuses()
{
var statuses = Enum.GetValues<OrderStatus>()
.Select(s => new
{
Value = (int)s,
Label = s.ToString().Humanize(LetterCasing.Title),
Code = s.ToString()
})
.ToList();
return Ok(statuses);
}
// → [{ "value": 0, "label": "Pending Payment", "code": "PendingPayment" }, ...]
Перетворення дат у відносний час (timeago) — одна з найкорисніших функцій Humanizer:
using Humanizer;
using System.Globalization;
// Встановлюємо культуру для усієї програми
CultureInfo.CurrentCulture = new CultureInfo("uk-UA");
CultureInfo.CurrentUICulture = new CultureInfo("uk-UA");
// Відносний час (від поточного моменту)
DateTime.Now.AddSeconds(-45).Humanize() // → "хвилину тому"
DateTime.Now.AddMinutes(-3).Humanize() // → "3 хвилини тому"
DateTime.Now.AddHours(-2).Humanize() // → "2 години тому"
DateTime.Now.AddDays(-1).Humanize() // → "вчора"
DateTime.Now.AddDays(-3).Humanize() // → "3 дні тому"
DateTime.Now.AddMonths(-2).Humanize() // → "2 місяці тому"
DateTime.Now.AddYears(-1).Humanize() // → "рік тому"
// Майбутнє
DateTime.Now.AddMinutes(10).Humanize() // → "за 10 хвилин"
DateTime.Now.AddDays(2).Humanize() // → "через 2 дні"
// Різниця між двома датами
var start = new DateTime(2024, 1, 1);
var end = new DateTime(2024, 3, 15);
(end - start).Humanize() // → "2 місяці"
(end - start).Humanize(precision: 2) // → "2 місяці 2 тижні"
// Форматування DateTimeOffset
DateTimeOffset.Now.AddHours(-5).Humanize() // → "5 годин тому"
public class ArticleDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
// Автоматично обчислюване поле
public string CreatedAtHuman => CreatedAt.Humanize();
// → "5 хвилин тому", "вчора", "3 місяці тому"
}
// Або через Mapster afar mapping:
config.NewConfig<Article, ArticleDto>()
.Map(dest => dest.CreatedAtHuman,
src => src.CreatedAt.Humanize());
using Humanizer;
// Ordinal (порядковий номер)
1.ToOrdinalWords() // → "first" (англ.)
5.ToOrdinalWords() // → "fifth"
42.ToOrdinalWords() // → "forty-second"
// Ukrainian ordinals (через CultureInfo)
CultureInfo.CurrentUICulture = new CultureInfo("uk-UA");
1.ToOrdinalWords() // → "перший"
5.ToOrdinalWords() // → "п'ятий"
// Words (число прописом)
1234.ToWords() // → "one thousand two hundred and thirty-four"
42.ToWords() // → "forty-two"
// Metric (SI-префікси)
1_500_000.ToMetric() // → "1.5M"
1_234.ToMetric() // → "1.234k"
0.001.ToMetric() // → "1m" (мілі)
// Bits/Bytes
10_240.Bytes().Humanize() // → "10 KB"
1_073_741_824L.Bytes().Humanize()// → "1 GB"
(2.5).Gigabytes().Humanize() // → "2.5 GB"
public class FileDto
{
public string FileName { get; set; } = string.Empty;
public long SizeBytes { get; set; }
public string SizeHuman => SizeBytes.Bytes().Humanize();
// → "1.45 MB", "256 KB", "3.2 GB"
}
// Pluralize (однина → множина)
"user".Pluralize() // → "users"
"category".Pluralize() // → "categories"
"person".Pluralize() // → "people"
"ox".Pluralize() // → "oxen"
// Singularize (множина → однина)
"users".Singularize() // → "user"
"categories".Singularize() // → "category"
"people".Singularize() // → "person"
// Conditioned pluralize (з числом)
"user".ToQuantity(1) // → "1 user"
"user".ToQuantity(5) // → "5 users"
"user".ToQuantity(0) // → "0 users"
// Прописом
"user".ToQuantity(1, ShowQuantityAs.Words) // → "one user"
"user".ToQuantity(5, ShowQuantityAs.Words) // → "five users"
Guard Clause (захисне твердження) — перевірка на початку методу, що «захищає» від некоректного вхідного стану. Класичний підхід:
public async Task<Order> CreateOrderAsync(
int customerId,
List<OrderItem> items,
string? promoCode)
{
if (customerId <= 0)
throw new ArgumentException("CustomerId must be positive.", nameof(customerId));
if (items == null)
throw new ArgumentNullException(nameof(items));
if (items.Count == 0)
throw new ArgumentException("Items cannot be empty.", nameof(items));
if (items.Any(i => i.Quantity <= 0))
throw new ArgumentException("All items must have positive quantity.", nameof(items));
// Лише тут починається реальна логіка...
var order = new Order { CustomerId = customerId };
// ...
return order;
}
8 рядків перевірок до початку реальної логіки. Множте на кожен метод — і читабельність стрімко падає.
Ardalis.GuardClauses від Steve Smith (Ardalis) — бібліотека, що перетворює захисні перевірки на один читабельний рядок:
dotnet add package Ardalis.GuardClauses
using Ardalis.GuardClauses;
public async Task<Order> CreateOrderAsync(
int customerId,
List<OrderItem> items,
string? promoCode)
{
// Замість 8 рядків перевірок — 2 виклики Guard
Guard.Against.NegativeOrZero(customerId, nameof(customerId));
Guard.Against.NullOrEmpty(items, nameof(items));
// Одразу реальна логіка
var order = new Order { CustomerId = customerId };
// ...
return order;
}
using Ardalis.GuardClauses;
// Null перевірки
Guard.Against.Null(obj, nameof(obj));
Guard.Against.NullOrEmpty(str, nameof(str));
Guard.Against.NullOrWhiteSpace(str, nameof(str));
// Числові перевірки
Guard.Against.Zero(value, nameof(value));
Guard.Against.Negative(value, nameof(value));
Guard.Against.NegativeOrZero(value, nameof(value));
Guard.Against.OutOfRange(value, nameof(value), min: 1, max: 100);
// Колекції
Guard.Against.NullOrEmpty(list, nameof(list));
Guard.Against.OutOfRange(index, nameof(index), min: 0, max: list.Count - 1);
// Рядки
Guard.Against.StringTooLong(str, nameof(str), maxLength: 255);
// Enum
Guard.Against.EnumOutOfRange(enumValue, nameof(enumValue));
Guard.Against.InvalidInput(
input: str,
parameterName: nameof(str),
predicate: s => s.Contains("@"),
message: "Must be a valid email format.");
// Generic expression
Guard.Against.InvalidInput(customerId,
nameof(customerId),
id => id > 0 && id < 1_000_000,
"CustomerId must be between 1 and 999999.");
Для домен-специфічних перевірок Ardalis.GuardClauses дозволяє розширювати IGuardClause:
using Ardalis.GuardClauses;
public static class DomainGuards
{
// Перевірка українського номера телефону
public static string ValidUkrainianPhone(
this IGuardClause guardClause,
string input,
string parameterName)
{
Guard.Against.NullOrWhiteSpace(input, parameterName);
var phoneRegex = new Regex(@"^\+380\d{9}$");
if (!phoneRegex.IsMatch(input))
throw new ArgumentException(
$"'{parameterName}' повинен бути у форматі +380XXXXXXXXX. Отримано: '{input}'.",
parameterName);
return input;
}
// Перевірка ЄДРПОУ (код підприємства в Україні)
public static string ValidEdrpou(
this IGuardClause guardClause,
string input,
string parameterName)
{
Guard.Against.NullOrWhiteSpace(input, parameterName);
if (input.Length != 8 || !input.All(char.IsDigit))
throw new ArgumentException(
$"ЄДРПОУ повинен містити рівно 8 цифр. Отримано: '{input}'.",
parameterName);
return input;
}
// Перевірка позитивної ціни
public static decimal PositivePrice(
this IGuardClause guardClause,
decimal price,
string parameterName)
{
if (price <= 0m)
throw new ArgumentException(
$"Ціна повинна бути більше 0. Отримано: {price:N2}.",
parameterName);
if (price > 1_000_000m)
throw new ArgumentException(
$"Ціна не може перевищувати 1 000 000. Отримано: {price:N2}.",
parameterName);
return price;
}
}
public class ProductService
{
public async Task<Product> CreateAsync(CreateProductRequest request)
{
// Валідація через Guards — декларативно та стисло
Guard.Against.NullOrWhiteSpace(request.Name, nameof(request.Name));
Guard.Against.PositivePrice(request.Price, nameof(request.Price));
Guard.Against.ValidUkrainianPhone(request.SupplierPhone,
nameof(request.SupplierPhone));
Guard.Against.NegativeOrZero(request.Stock, nameof(request.Stock));
// Лише бізнес-логіка — жодного «шуму» перевірок
var product = request.Adapt<Product>();
// ...
return product;
}
}
Guards кидають виключення — це чудово для внутрішніх перевірок програмних інваріантів. Але для публічних API методів краще повертати ErrorOr:
public async Task<ErrorOr<Order>> CreateAsync(CreateOrderRequest request)
{
// ArgumentException від Guard — несподівана ситуація (баг у коді)
// Дозволяємо їй «спливти» до GlobalExceptionHandler
Guard.Against.Null(request, nameof(request));
// Бізнес-валідація — очікувана ситуація → ErrorOr
var customer = await _db.Users.FindAsync(request.CustomerId);
if (customer is null)
return UserErrors.NotFound;
if (request.Items.Count == 0)
return Error.Validation("Order.EmptyItems",
"Замовлення повинно містити принаймні один товар.");
// Тут Guard використовуємо для програмних інваріантів
// (ситуацій, що НІКОЛИ не мають траплятися якщо код правильний)
Guard.Against.Negative(customer.Id,
nameof(customer.Id),
"Customer.Id must be positive after DB retrieval.");
var order = new Order { /* ... */ };
return order;
}
Ключове правило: Guard Clauses — для програмних інваріантів (невірний аргумент = баг у виклику). ErrorOr — для бізнес-правил (невірний стан = очікуваний сценарій).
| Ситуація | Підхід | Чому |
|---|---|---|
null аргумент (програмний баг) | Guard.Against.Null() | Fast fail, легко знайти в stacktrace |
| Негативний ID (програмний баг) | Guard.Against.NegativeOrZero() | Інваріант домену |
| Запис не знайдений у БД | ErrorOr → Error.NotFound | Очікуваний сценарій |
| Бізнес-правило (недостатньо коштів) | ErrorOr → Error.Conflict | Очікувана бізнес-помилка |
| Невалідний формат (з флронтенду) | FluentValidation | Валідація вхідних даних |
OrderDto обчислювані поля: CreatedAtHuman (наприклад "3 дні тому"), TotalFormatted (наприклад "₴1 234.56"), StatusLabel (Enum.Humanize()). Встановіть українську локаль.ValidEmail(this IGuardClause, string, string), що перевіряє формат email через regex. Застосуйте його у UserService.CreateAsync() поряд із вбудованими Guards. Напишіть юніт-тест, що перевіряє кидання ArgumentException при невалідному email.InvoiceService.GenerateAsync(int orderId): 1) Guards для програмних інваріантів, 2) ErrorOr для бізнес-правил, 3) Humanizer для форматування суми та дати у тілі рахунку (QuestPDF), 4) Bogus для seed-тестових даних рахунків. Покрийте юніт-тестами.Humanizer: DateTime
createdAt.Humanize() → "3 хвилини тому". Одна рядок замість перетворення вручну.Humanizer: Strings
"camelCase".Humanize() → "Camel case". Автоматична конвертація для UI.Guard: Null/Range
Guard.Against.NullOrEmpty() замість 3 рядків if/throw. Декларативно та стисло.Guard: Extensible
Guard.Against.ValidEmail() — ваш власний Guard.Посилання: