Files
Masterarbeit/Versuche/Versuch 03/Ergenisse/software/Validation_Rules.md
2026-02-19 11:21:18 +01:00

822 lines
26 KiB
Markdown

# 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.