# 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: ```csharp public class AccountBL : BaseBL { public Result 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 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: ```csharp 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(); // 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** ```csharp public class FieldValidator { private readonly Dictionary> _fieldRules = new(); public FieldValidator AddRule(string fieldName, IFieldRule rule) { if (!_fieldRules.ContainsKey(fieldName)) _fieldRules[fieldName] = new List(); _fieldRules[fieldName].Add(rule); return this; } public ValidationResult ValidateField(string fieldName, object value) { if (!_fieldRules.ContainsKey(fieldName)) return ValidationResult.Success(); var errors = new List(); 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 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(); 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.AsError(string.Join("; ", allErrors)) : Result.AsSuccess(MapToCustomer(customer)); } ``` ### **2. Cross-Field Validation Logic** #### **Dependent Field Validation** ```csharp public class CrossFieldValidator { public static Result ValidateAccountDependencies(AccountDTO account) { var validationErrors = new List(); // 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** ```csharp public class BusinessRuleValidationEngine { private readonly List _validators = new(); public BusinessRuleValidationEngine RegisterValidator(IBusinessRuleValidator validator) { _validators.Add(validator); return this; } public async Task ValidateAsync(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 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 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** ```csharp 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 Errors { get; } public List LocalizedErrors { get; } public ValidationResult(bool isValid, List errors = null, List localizedErrors = null) { IsValid = isValid; Errors = errors ?? new List(); LocalizedErrors = localizedErrors ?? new List(); } public static ValidationResult Success() => new(true); public static ValidationResult Failure(string error) => new(false, new List { error }); public static ValidationResult Failure(List 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** ```csharp 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 _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 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** ```csharp public class AsyncValidationService { private readonly IAccountsLogic _accountsLogic; private readonly CancellationTokenSource _cancellationTokenSource = new(); public async Task 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 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** ```csharp public class FluentValidator { private readonly List> _rules = new(); public FluentValidator RuleFor(Expression> property) { var propertyName = GetPropertyName(property); var rule = new ValidationRule(propertyName, property.Compile()); _rules.Add(rule); return this; } public ValidationResult Validate(T instance) { var errors = new List(); 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 { private readonly string _propertyName; private readonly Func _propertySelector; private readonly List _conditions = new(); public ValidationRule(string propertyName, Func propertySelector) { _propertyName = propertyName; _propertySelector = propertySelector; } public ValidationRule NotNull() { _conditions.Add(new NotNullCondition(_propertyName)); return this; } public ValidationRule NotEmpty() { _conditions.Add(new NotEmptyCondition(_propertyName)); return this; } public ValidationRule MaximumLength(int maxLength) { _conditions.Add(new MaxLengthCondition(_propertyName, maxLength)); return this; } public ValidationRule Must(Func 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(); 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 { 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.