# 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<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<T> Validation Pattern**
Validation that integrates with the Result<T> 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<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**
```csharp
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**
```csharp
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**
```csharp
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**
```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<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**
```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<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**
```csharp
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**
```csharp
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.