Validation¶
Overview¶
TinyResult provides a robust validation system that helps you validate your data in a clean and type-safe way. The validation features allow you to collect validation errors and handle them appropriately.
Key Concepts¶
1. ValidationResult¶
The ValidationResult
type represents the result of a validation operation:
public class ValidationResult
{
public bool IsValid { get; }
public IReadOnlyDictionary<string, string> Errors { get; }
}
2. Creating Validation Results¶
// Create an empty validation result
var validationResult = ValidationResult.Create();
// Create a validation result with errors
var validationResult = ValidationResult.Create()
.AddError("Name", "Name is required")
.AddError("Email", "Email is invalid");
Basic Validation¶
1. Simple Validation¶
public Result<User> ValidateUser(User user)
{
var validationResult = ValidationResult.Create();
if (string.IsNullOrEmpty(user.Name))
{
validationResult.AddError("Name", "Name is required");
}
if (user.Age < 18)
{
validationResult.AddError("Age", "User must be at least 18 years old");
}
return validationResult.IsValid
? Result<User>.Success(user)
: Result<User>.Failure(validationResult);
}
2. Using Validation Rules¶
public class UserValidationRules
{
public static ValidationResult Validate(User user)
{
var result = ValidationResult.Create();
// Name validation
if (string.IsNullOrEmpty(user.Name))
{
result.AddError("Name", "Name is required");
}
else if (user.Name.Length < 3)
{
result.AddError("Name", "Name must be at least 3 characters");
}
// Email validation
if (string.IsNullOrEmpty(user.Email))
{
result.AddError("Email", "Email is required");
}
else if (!IsValidEmail(user.Email))
{
result.AddError("Email", "Email is invalid");
}
return result;
}
private static bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
}
Advanced Validation¶
1. Fluent Validation¶
public class UserValidator
{
private readonly ValidationResult _result;
public UserValidator()
{
_result = ValidationResult.Create();
}
public UserValidator ValidateName(string name)
{
if (string.IsNullOrEmpty(name))
{
_result.AddError("Name", "Name is required");
}
else if (name.Length < 3)
{
_result.AddError("Name", "Name must be at least 3 characters");
}
return this;
}
public UserValidator ValidateEmail(string email)
{
if (string.IsNullOrEmpty(email))
{
_result.AddError("Email", "Email is required");
}
else if (!IsValidEmail(email))
{
_result.AddError("Email", "Email is invalid");
}
return this;
}
public ValidationResult GetResult() => _result;
}
// Usage
var validator = new UserValidator()
.ValidateName(user.Name)
.ValidateEmail(user.Email);
var result = validator.GetResult();
2. Validation with Metadata¶
public class ValidationError
{
public string Field { get; }
public string Message { get; }
public Dictionary<string, object> Metadata { get; }
public ValidationError(string field, string message, Dictionary<string, object> metadata = null)
{
Field = field;
Message = message;
Metadata = metadata ?? new Dictionary<string, object>();
}
}
public class EnhancedValidationResult
{
private readonly List<ValidationError> _errors = new();
public bool IsValid => _errors.Count == 0;
public IReadOnlyList<ValidationError> Errors => _errors.AsReadOnly();
public void AddError(string field, string message, Dictionary<string, object> metadata = null)
{
_errors.Add(new ValidationError(field, message, metadata));
}
}
3. Cross-Field Validation¶
public class OrderValidator
{
public ValidationResult Validate(Order order)
{
var result = ValidationResult.Create();
// Basic field validation
if (order.Quantity <= 0)
{
result.AddError("Quantity", "Quantity must be greater than 0");
}
if (order.Price <= 0)
{
result.AddError("Price", "Price must be greater than 0");
}
// Cross-field validation
if (order.Quantity > 0 && order.Price > 0)
{
var total = order.Quantity * order.Price;
if (total > order.Customer.CreditLimit)
{
result.AddError("Total",
"Order total exceeds customer credit limit",
new Dictionary<string, object>
{
{ "Total", total },
{ "CreditLimit", order.Customer.CreditLimit }
});
}
}
return result;
}
}
Integration with Result Pattern¶
1. Converting Validation Results¶
public static class ValidationResultExtensions
{
public static Result<T> ToResult<T>(this ValidationResult validationResult, T value)
{
return validationResult.IsValid
? Result<T>.Success(value)
: Result<T>.Failure(validationResult);
}
}
// Usage
var validationResult = ValidateUser(user);
var result = validationResult.ToResult(user);
2. Combining Results¶
public Result<Order> ValidateAndCreateOrder(OrderRequest request)
{
// Validate customer
var customerResult = ValidateCustomer(request.Customer);
if (customerResult.IsFailure)
{
return Result<Order>.Failure(customerResult.Error);
}
// Validate order items
var itemsResult = ValidateOrderItems(request.Items);
if (itemsResult.IsFailure)
{
return Result<Order>.Failure(itemsResult.Error);
}
// Create order
var order = new Order
{
Customer = customerResult.Value,
Items = itemsResult.Value
};
return Result<Order>.Success(order);
}
Best Practices¶
1. Keep Validation Rules Separate¶
// Avoid
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public bool IsValid()
{
// Validation logic here
}
}
// Prefer
public class UserValidator
{
public ValidationResult Validate(User user)
{
// Validation logic here
}
}
2. Use Descriptive Error Messages¶
// Avoid
result.AddError("Name", "Invalid");
// Prefer
result.AddError("Name", "Name must be between 3 and 50 characters");
3. Include Context in Error Messages¶
// Avoid
result.AddError("Age", "Invalid age");
// Prefer
result.AddError("Age",
"Age must be between 18 and 100",
new Dictionary<string, object>
{
{ "MinAge", 18 },
{ "MaxAge", 100 },
{ "CurrentAge", age }
});
Common Use Cases¶
1. Form Validation¶
public Result<User> ValidateRegistrationForm(RegistrationForm form)
{
var validationResult = ValidationResult.Create();
// Validate username
if (string.IsNullOrEmpty(form.Username))
{
validationResult.AddError("Username", "Username is required");
}
else if (form.Username.Length < 3)
{
validationResult.AddError("Username", "Username must be at least 3 characters");
}
// Validate password
if (string.IsNullOrEmpty(form.Password))
{
validationResult.AddError("Password", "Password is required");
}
else if (form.Password.Length < 8)
{
validationResult.AddError("Password", "Password must be at least 8 characters");
}
return validationResult.ToResult(new User
{
Username = form.Username,
Password = form.Password
});
}
2. Business Rule Validation¶
public Result<Order> ValidateOrder(Order order)
{
var validationResult = ValidationResult.Create();
// Validate order status
if (order.Status == OrderStatus.Cancelled && order.Items.Any())
{
validationResult.AddError("Status", "Cannot cancel order with items");
}
// Validate payment
if (order.PaymentStatus == PaymentStatus.Paid && order.TotalAmount == 0)
{
validationResult.AddError("Payment", "Cannot mark empty order as paid");
}
return validationResult.ToResult(order);
}
3. Data Integrity Validation¶
public Result<DatabaseRecord> ValidateRecord(DatabaseRecord record)
{
var validationResult = ValidationResult.Create();
// Validate required fields
if (record.Id == Guid.Empty)
{
validationResult.AddError("Id", "Id is required");
}
if (record.CreatedAt > DateTime.UtcNow)
{
validationResult.AddError("CreatedAt", "Creation date cannot be in the future");
}
// Validate relationships
if (record.ParentId.HasValue && !_repository.Exists(record.ParentId.Value))
{
validationResult.AddError("ParentId", "Parent record does not exist");
}
return validationResult.ToResult(record);
}