26 KiB
26 KiB
Validation Rules and Patterns - Centron Enterprise Application
Overview
This document provides comprehensive coverage of validation patterns and implementations within the Centron .NET 8 enterprise application. Validation patterns encompass input validation, field-level validation, cross-field validation, business rule validation, error handling, and user feedback mechanisms that ensure data integrity and business rule compliance.
Validation Architecture
Core Validation Patterns
1. Guard Clause Pattern
The foundation of parameter validation used throughout the application:
public class AccountBL : BaseBL
{
public Result<Account> SaveAccount(Account account)
{
// Parameter validation using Guard pattern
Guard.NotNull(account, nameof(account));
Guard.NotNull(account.CustomerNumber, nameof(account.CustomerNumber));
Guard.NotLessOrEqualThan(account.CustomerNumber, 0, nameof(account.CustomerNumber));
Guard.NotNullOrEmpty(account.CompanyName, nameof(account.CompanyName));
// Business logic continues...
return ProcessAccountSave(account);
}
}
// Guard implementation patterns
public static class Guard
{
public static void NotNull<T>(T value, string parameterName) where T : class
{
if (value == null)
throw new ArgumentNullException(parameterName);
}
public static void NotNullOrEmpty(string value, string parameterName)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException($"Parameter {parameterName} cannot be null or empty", parameterName);
}
public static void NotLessOrEqualThan(int value, int minimum, string parameterName)
{
if (value <= minimum)
throw new ArgumentException($"Parameter {parameterName} must be greater than {minimum}", parameterName);
}
}
2. Result Validation Pattern
Validation that integrates with the Result error handling system:
public class ValidationService
{
public static Result ValidateAccount(AccountDTO account)
{
var validator = new AccountValidator();
return validator.Validate(account);
}
}
public class AccountValidator
{
public Result Validate(AccountDTO account)
{
var validationErrors = new List<string>();
// Required field validation
if (string.IsNullOrWhiteSpace(account.CompanyName))
validationErrors.Add("Company name is required");
if (account.CustomerNumber <= 0)
validationErrors.Add("Customer number must be positive");
// Business rule validation
if (account.CreditLimit < 0 && account.CustomerType != CustomerType.Special)
validationErrors.Add("Negative credit limit only allowed for special customers");
// Email format validation
if (!string.IsNullOrEmpty(account.Email) && !IsValidEmail(account.Email))
validationErrors.Add("Email format is invalid");
return validationErrors.Any()
? Result.AsError(string.Join("; ", validationErrors))
: Result.AsSuccess();
}
private bool IsValidEmail(string email)
{
try
{
var mailAddress = new MailAddress(email);
return mailAddress.Address == email;
}
catch
{
return false;
}
}
}
Input Validation Patterns
1. Field-Level Validation
Data Type and Format Validation
public class FieldValidator
{
private readonly Dictionary<string, List<IFieldRule>> _fieldRules = new();
public FieldValidator AddRule(string fieldName, IFieldRule rule)
{
if (!_fieldRules.ContainsKey(fieldName))
_fieldRules[fieldName] = new List<IFieldRule>();
_fieldRules[fieldName].Add(rule);
return this;
}
public ValidationResult ValidateField(string fieldName, object value)
{
if (!_fieldRules.ContainsKey(fieldName))
return ValidationResult.Success();
var errors = new List<string>();
foreach (var rule in _fieldRules[fieldName])
{
var result = rule.Validate(value);
if (!result.IsValid)
errors.AddRange(result.Errors);
}
return errors.Any()
? ValidationResult.Failure(errors)
: ValidationResult.Success();
}
}
// Field validation rules
public class RequiredFieldRule : IFieldRule
{
public ValidationResult Validate(object value)
{
if (value == null || (value is string str && string.IsNullOrWhiteSpace(str)))
return ValidationResult.Failure("Field is required");
return ValidationResult.Success();
}
}
public class MaxLengthRule : IFieldRule
{
private readonly int _maxLength;
public MaxLengthRule(int maxLength)
{
_maxLength = maxLength;
}
public ValidationResult Validate(object value)
{
if (value is string str && str.Length > _maxLength)
return ValidationResult.Failure($"Field cannot exceed {_maxLength} characters");
return ValidationResult.Success();
}
}
public class NumericRangeRule : IFieldRule
{
private readonly decimal _min;
private readonly decimal _max;
public NumericRangeRule(decimal min, decimal max)
{
_min = min;
_max = max;
}
public ValidationResult Validate(object value)
{
if (value is decimal number)
{
if (number < _min || number > _max)
return ValidationResult.Failure($"Value must be between {_min} and {_max}");
}
return ValidationResult.Success();
}
}
// Usage example
public Result<Customer> ValidateCustomerInput(CustomerDTO customer)
{
var validator = new FieldValidator()
.AddRule(nameof(customer.CompanyName), new RequiredFieldRule())
.AddRule(nameof(customer.CompanyName), new MaxLengthRule(100))
.AddRule(nameof(customer.Email), new EmailFormatRule())
.AddRule(nameof(customer.CreditLimit), new NumericRangeRule(0, 1000000));
var companyNameResult = validator.ValidateField(nameof(customer.CompanyName), customer.CompanyName);
var emailResult = validator.ValidateField(nameof(customer.Email), customer.Email);
var creditLimitResult = validator.ValidateField(nameof(customer.CreditLimit), customer.CreditLimit);
var allErrors = new List<string>();
if (!companyNameResult.IsValid) allErrors.AddRange(companyNameResult.Errors);
if (!emailResult.IsValid) allErrors.AddRange(emailResult.Errors);
if (!creditLimitResult.IsValid) allErrors.AddRange(creditLimitResult.Errors);
return allErrors.Any()
? Result<Customer>.AsError(string.Join("; ", allErrors))
: Result<Customer>.AsSuccess(MapToCustomer(customer));
}
2. Cross-Field Validation Logic
Dependent Field Validation
public class CrossFieldValidator
{
public static Result ValidateAccountDependencies(AccountDTO account)
{
var validationErrors = new List<string>();
// Credit limit and payment terms dependency
if (account.CreditLimit > 50000 && account.PaymentTerms > 30)
validationErrors.Add("Accounts with credit limit over €50,000 must have payment terms ≤ 30 days");
// Customer type and discount rate dependency
if (account.CustomerType == CustomerType.Standard && account.DiscountRate > 0.10m)
validationErrors.Add("Standard customers cannot have discount rates above 10%");
// Address validation for shipping customers
if (account.RequiresShipping && string.IsNullOrEmpty(account.ShippingAddress?.Street))
validationErrors.Add("Shipping address is required for customers requiring shipping");
// VAT number validation for business customers
if (account.CustomerType == CustomerType.Business && string.IsNullOrEmpty(account.VatNumber))
validationErrors.Add("VAT number is required for business customers");
// Contact person validation for corporate accounts
if (account.CustomerType == CustomerType.Corporate && !account.ContactPersons.Any())
validationErrors.Add("Corporate accounts must have at least one contact person");
return validationErrors.Any()
? Result.AsError(string.Join("; ", validationErrors))
: Result.AsSuccess();
}
}
3. Business Rule Validation Framework
Complex Business Logic Validation
public class BusinessRuleValidationEngine
{
private readonly List<IBusinessRuleValidator> _validators = new();
public BusinessRuleValidationEngine RegisterValidator(IBusinessRuleValidator validator)
{
_validators.Add(validator);
return this;
}
public async Task<Result> ValidateAsync<T>(T entity) where T : class
{
var validationTasks = _validators
.Where(v => v.AppliesTo(typeof(T)))
.Select(v => v.ValidateAsync(entity))
.ToArray();
var results = await Task.WhenAll(validationTasks);
var errors = results
.Where(r => r.Status != ResultStatus.Success)
.SelectMany(r => r.Errors)
.ToList();
return errors.Any()
? Result.AsError(string.Join("; ", errors))
: Result.AsSuccess();
}
}
// Business rule validators
public class CustomerCreditLimitValidator : IBusinessRuleValidator
{
private readonly ICustomerRepository _customerRepository;
private readonly ICreditCheckService _creditCheckService;
public bool AppliesTo(Type entityType) => entityType == typeof(Customer);
public async Task<Result> ValidateAsync(object entity)
{
var customer = (Customer)entity;
if (customer.CreditLimit <= 0)
return Result.AsSuccess(); // No validation needed for zero credit
// Check existing customer debt
var existingDebt = await _customerRepository.GetCustomerDebt(customer.Id);
if (existingDebt.Status != ResultStatus.Success)
return Result.AsError("Unable to verify existing customer debt");
if (existingDebt.Data > customer.CreditLimit * 0.8m)
return Result.AsError("Customer's existing debt exceeds 80% of requested credit limit");
// External credit check for high-value customers
if (customer.CreditLimit > 100000)
{
var creditCheckResult = await _creditCheckService.PerformCreditCheck(customer);
if (creditCheckResult.Status != ResultStatus.Success || !creditCheckResult.Data.IsApproved)
return Result.AsError("External credit check failed for high-value customer");
}
return Result.AsSuccess();
}
}
public class InventoryAllocationValidator : IBusinessRuleValidator
{
private readonly IInventoryService _inventoryService;
public bool AppliesTo(Type entityType) => entityType == typeof(OrderItem);
public async Task<Result> ValidateAsync(object entity)
{
var orderItem = (OrderItem)entity;
// Check stock availability
var stockResult = await _inventoryService.GetAvailableStock(orderItem.ArticleId);
if (stockResult.Status != ResultStatus.Success)
return Result.AsError($"Unable to check stock for article {orderItem.ArticleId}");
if (stockResult.Data < orderItem.Quantity)
return Result.AsError($"Insufficient stock: Available {stockResult.Data}, Requested {orderItem.Quantity}");
// Check for discontinued items
var articleResult = await _inventoryService.GetArticle(orderItem.ArticleId);
if (articleResult.Status == ResultStatus.Success && articleResult.Data.IsDiscontinued)
return Result.AsError($"Article {orderItem.ArticleId} is discontinued and cannot be ordered");
return Result.AsSuccess();
}
}
Error Handling and User Feedback Patterns
1. Localized Error Messages
Multi-Language Error Message System
public class LocalizedValidationMessages
{
private readonly IResourceManager _resourceManager;
public LocalizedValidationMessages(IResourceManager resourceManager)
{
_resourceManager = resourceManager;
}
public string GetValidationMessage(string messageKey, params object[] parameters)
{
var template = _resourceManager.GetString($"Validation_{messageKey}");
return string.Format(template, parameters);
}
// Common validation messages
public string RequiredField(string fieldName) =>
GetValidationMessage("RequiredField", fieldName);
public string InvalidFormat(string fieldName, string expectedFormat) =>
GetValidationMessage("InvalidFormat", fieldName, expectedFormat);
public string ValueOutOfRange(string fieldName, object min, object max) =>
GetValidationMessage("ValueOutOfRange", fieldName, min, max);
public string DuplicateValue(string fieldName, object value) =>
GetValidationMessage("DuplicateValue", fieldName, value);
}
// Resource file entries (German - LocalizedStrings.resx)
// Validation_RequiredField = "Feld '{0}' ist erforderlich"
// Validation_InvalidFormat = "Feld '{0}' hat ein ungültiges Format. Erwartet: {1}"
// Validation_ValueOutOfRange = "Wert für '{0}' muss zwischen {1} und {2} liegen"
// Resource file entries (English - LocalizedStrings.en.resx)
// Validation_RequiredField = "Field '{0}' is required"
// Validation_InvalidFormat = "Field '{0}' has an invalid format. Expected: {1}"
// Validation_ValueOutOfRange = "Value for '{0}' must be between {1} and {2}"
public class ValidationResult
{
public bool IsValid { get; }
public List<string> Errors { get; }
public List<ValidationMessage> LocalizedErrors { get; }
public ValidationResult(bool isValid, List<string> errors = null, List<ValidationMessage> localizedErrors = null)
{
IsValid = isValid;
Errors = errors ?? new List<string>();
LocalizedErrors = localizedErrors ?? new List<ValidationMessage>();
}
public static ValidationResult Success() => new(true);
public static ValidationResult Failure(string error) => new(false, new List<string> { error });
public static ValidationResult Failure(List<string> errors) => new(false, errors);
}
public class ValidationMessage
{
public string Key { get; set; }
public string[] Parameters { get; set; }
public string FieldName { get; set; }
public ValidationSeverity Severity { get; set; }
}
public enum ValidationSeverity
{
Error,
Warning,
Information
}
2. UI Validation Integration Pattern
WPF Data Binding Validation
public class AccountViewModel : BindableBase, IDataErrorInfo
{
private readonly AccountValidator _validator = new();
private readonly LocalizedValidationMessages _messages;
private string _companyName;
private decimal _creditLimit;
private string _email;
public string CompanyName
{
get { return _companyName; }
set
{
if (SetProperty(ref _companyName, value))
{
ValidateProperty(nameof(CompanyName), value);
}
}
}
public decimal CreditLimit
{
get { return _creditLimit; }
set
{
if (SetProperty(ref _creditLimit, value))
{
ValidateProperty(nameof(CreditLimit), value);
}
}
}
// IDataErrorInfo implementation for WPF binding validation
public string Error => string.Empty;
public string this[string columnName]
{
get
{
return GetValidationError(columnName);
}
}
private readonly Dictionary<string, string> _validationErrors = new();
private void ValidateProperty(string propertyName, object value)
{
var validationResult = _validator.ValidateProperty(propertyName, value);
if (validationResult.IsValid)
{
_validationErrors.Remove(propertyName);
}
else
{
_validationErrors[propertyName] = validationResult.Errors.First();
}
RaisePropertyChanged(nameof(HasValidationErrors));
}
private string GetValidationError(string propertyName)
{
return _validationErrors.ContainsKey(propertyName)
? _validationErrors[propertyName]
: string.Empty;
}
public bool HasValidationErrors => _validationErrors.Any();
public bool CanSave => !HasValidationErrors && IsFormComplete();
private bool IsFormComplete()
{
return !string.IsNullOrWhiteSpace(CompanyName) &&
CreditLimit >= 0 &&
!string.IsNullOrWhiteSpace(Email);
}
// Command with validation
public DelegateCommand SaveCommand { get; }
private async void ExecuteSave()
{
// Final comprehensive validation before save
var account = MapViewModelToDTO();
var validationResult = await _validator.ValidateCompleteAsync(account);
if (!validationResult.IsValid)
{
ShowValidationErrors(validationResult.LocalizedErrors);
return;
}
// Proceed with save operation
var saveResult = await _accountsLogic.SaveAccountAsync(account, true);
if (saveResult.Status != ResultStatus.Success)
{
ShowErrorMessage(saveResult.Error);
}
}
private void ShowValidationErrors(List<ValidationMessage> errors)
{
var errorMessages = errors.Select(e =>
_messages.GetValidationMessage(e.Key, e.Parameters)).ToList();
// Display in UI - could be message box, validation summary, etc.
MessageBox.Show(string.Join("\n", errorMessages), "Validation Errors");
}
}
3. Async Validation Pattern
Server-Side Validation for UI
public class AsyncValidationService
{
private readonly IAccountsLogic _accountsLogic;
private readonly CancellationTokenSource _cancellationTokenSource = new();
public async Task<ValidationResult> ValidateUniqueEmailAsync(string email)
{
try
{
// Debounce rapid changes
await Task.Delay(300, _cancellationTokenSource.Token);
var existingAccountResult = await _accountsLogic.GetAccountByEmailAsync(email);
if (existingAccountResult.Status == ResultStatus.Success && existingAccountResult.Data != null)
return ValidationResult.Failure("Email address is already in use");
return ValidationResult.Success();
}
catch (OperationCanceledException)
{
return ValidationResult.Success(); // Validation was cancelled, assume valid
}
}
public async Task<ValidationResult> ValidateVatNumberAsync(string vatNumber, string country)
{
try
{
var vatValidationResult = await ValidateVatWithExternalService(vatNumber, country);
if (!vatValidationResult.IsValid)
return ValidationResult.Failure($"VAT number is invalid: {vatValidationResult.Message}");
return ValidationResult.Success();
}
catch (Exception ex)
{
// Don't fail validation due to external service issues
return ValidationResult.Success(); // Log warning but allow save
}
}
public void CancelValidation()
{
_cancellationTokenSource.Cancel();
}
}
// UI Integration
public class AccountViewModel : BindableBase
{
private readonly AsyncValidationService _asyncValidator = new();
private readonly Timer _validationTimer = new();
private string _email;
public string Email
{
get { return _email; }
set
{
if (SetProperty(ref _email, value))
{
// Cancel previous validation
_asyncValidator.CancelValidation();
// Start new validation with delay
_validationTimer.Stop();
_validationTimer.Interval = TimeSpan.FromMilliseconds(500);
_validationTimer.Tick += async (s, e) =>
{
_validationTimer.Stop();
await ValidateEmailAsync(value);
};
_validationTimer.Start();
}
}
}
private async Task ValidateEmailAsync(string email)
{
if (string.IsNullOrEmpty(email)) return;
IsValidatingEmail = true;
try
{
var result = await _asyncValidator.ValidateUniqueEmailAsync(email);
if (result.IsValid)
{
EmailValidationError = string.Empty;
}
else
{
EmailValidationError = result.Errors.First();
}
}
finally
{
IsValidatingEmail = false;
}
}
}
Validation Framework Patterns
1. Fluent Validation Pattern
Chainable Validation Rules
public class FluentValidator<T>
{
private readonly List<ValidationRule<T>> _rules = new();
public FluentValidator<T> RuleFor<TProperty>(Expression<Func<T, TProperty>> property)
{
var propertyName = GetPropertyName(property);
var rule = new ValidationRule<T>(propertyName, property.Compile());
_rules.Add(rule);
return this;
}
public ValidationResult Validate(T instance)
{
var errors = new List<string>();
foreach (var rule in _rules)
{
var ruleResult = rule.Validate(instance);
if (!ruleResult.IsValid)
errors.AddRange(ruleResult.Errors);
}
return errors.Any()
? ValidationResult.Failure(errors)
: ValidationResult.Success();
}
}
public class ValidationRule<T>
{
private readonly string _propertyName;
private readonly Func<T, object> _propertySelector;
private readonly List<IValidationCondition> _conditions = new();
public ValidationRule(string propertyName, Func<T, object> propertySelector)
{
_propertyName = propertyName;
_propertySelector = propertySelector;
}
public ValidationRule<T> NotNull()
{
_conditions.Add(new NotNullCondition(_propertyName));
return this;
}
public ValidationRule<T> NotEmpty()
{
_conditions.Add(new NotEmptyCondition(_propertyName));
return this;
}
public ValidationRule<T> MaximumLength(int maxLength)
{
_conditions.Add(new MaxLengthCondition(_propertyName, maxLength));
return this;
}
public ValidationRule<T> Must(Func<object, bool> predicate, string errorMessage)
{
_conditions.Add(new CustomCondition(_propertyName, predicate, errorMessage));
return this;
}
public ValidationResult Validate(T instance)
{
var propertyValue = _propertySelector(instance);
var errors = new List<string>();
foreach (var condition in _conditions)
{
if (!condition.IsValid(propertyValue))
errors.Add(condition.GetErrorMessage());
}
return errors.Any()
? ValidationResult.Failure(errors)
: ValidationResult.Success();
}
}
// Usage example
public class CustomerValidator : FluentValidator<Customer>
{
public CustomerValidator()
{
RuleFor(c => c.CompanyName)
.NotNull()
.NotEmpty()
.MaximumLength(100);
RuleFor(c => c.Email)
.NotEmpty()
.Must(email => IsValidEmail(email.ToString()), "Email format is invalid");
RuleFor(c => c.CreditLimit)
.Must(limit => (decimal)limit >= 0, "Credit limit cannot be negative");
RuleFor(c => c.VatNumber)
.Must((customer, vatNumber) => ValidateVatForCustomerType(customer, vatNumber?.ToString()),
"VAT number is required for business customers");
}
private bool ValidateVatForCustomerType(Customer customer, string vatNumber)
{
if (customer.CustomerType == CustomerType.Business)
return !string.IsNullOrEmpty(vatNumber);
return true;
}
}
Validation Best Practices and Guidelines
1. Validation Layer Architecture
- Client-Side Validation: Immediate user feedback for format and required field validation
- Server-Side Validation: Comprehensive business rule validation and data integrity checks
- Database Constraints: Final validation layer for data consistency
- External Service Validation: Third-party validation for specialized data (VAT, addresses, etc.)
2. Performance Optimization
- Lazy Validation: Validate only when necessary (on blur, on save, etc.)
- Async Validation: Non-blocking validation for external service calls
- Caching: Cache expensive validation results where appropriate
- Debouncing: Prevent excessive validation calls during rapid user input
3. Error Message Guidelines
- Clear and Specific: Error messages clearly explain what's wrong and how to fix it
- Localized: All error messages support multiple languages
- Contextual: Messages relate to the specific business context
- Actionable: Users can understand what action to take to resolve the error
4. Validation Testing Strategies
- Unit Tests: Test individual validation rules in isolation
- Integration Tests: Test validation chains and cross-field dependencies
- UI Tests: Verify validation messages appear correctly in the user interface
- Performance Tests: Ensure validation doesn't impact application performance
Conclusion
The Centron application implements comprehensive validation patterns that ensure:
- Data Integrity: All data is validated at multiple layers before persistence
- User Experience: Clear, immediate feedback helps users correct errors quickly
- Business Rule Compliance: Complex business logic is properly enforced
- Maintainability: Validation rules are organized and easy to modify
- Internationalization: All validation messages support multiple languages
- Performance: Validation is optimized to minimize impact on user experience
These validation patterns provide robust data quality assurance while maintaining excellent user experience and system performance.