Versuche und Ergebnisse Umstrukturiert
This commit is contained in:
822
Versuche/Versuch 02/Ergenisse/software/Validation_Rules.md
Normal file
822
Versuche/Versuch 02/Ergenisse/software/Validation_Rules.md
Normal file
@@ -0,0 +1,822 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user