Validation Pattern
Overview
This pattern defines comprehensive validation strategies used across MepDash dashboards. Validation ensures data integrity, business rule compliance, and provides clear feedback to users about data issues.
Core Concepts
- Input Validation: User input verification
- Business Rule Validation: Domain-specific constraints
- Data Integrity: Database constraint validation
- Cross-Field Validation: Multi-field dependencies
- Async Validation: Server-side validation
Pattern Implementation
Validation Framework
Base validation infrastructure:
// Pattern: Validation Framework
public interface IValidatable
{
ValidationResult Validate();
Task<ValidationResult> ValidateAsync();
bool IsValid { get; }
}
public class ValidationResult
{
public bool IsValid => !Errors.Any() && !HasCriticalErrors;
public List<ValidationError> Errors { get; } = new();
public List<ValidationWarning> Warnings { get; } = new();
public bool HasCriticalErrors { get; private set; }
public void AddError(string propertyName, string errorMessage, bool isCritical = false)
{
Errors.Add(new ValidationError
{
PropertyName = propertyName,
ErrorMessage = errorMessage,
IsCritical = isCritical
});
if (isCritical)
HasCriticalErrors = true;
}
public void AddWarning(string propertyName, string warningMessage)
{
Warnings.Add(new ValidationWarning
{
PropertyName = propertyName,
Message = warningMessage
});
}
public void Merge(ValidationResult other)
{
Errors.AddRange(other.Errors);
Warnings.AddRange(other.Warnings);
HasCriticalErrors = HasCriticalErrors || other.HasCriticalErrors;
}
}
public class ValidationError
{
public string PropertyName { get; set; }
public string ErrorMessage { get; set; }
public bool IsCritical { get; set; }
}
public class ValidationWarning
{
public string PropertyName { get; set; }
public string Message { get; set; }
}
Field-Level Validation
Validating individual fields:
// Pattern: Field Validators
public static class FieldValidators
{
public static ValidationResult ValidateRequired(
string value,
string fieldName)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(value))
{
result.AddError(fieldName, $"{fieldName} is required");
}
return result;
}
public static ValidationResult ValidateLength(
string value,
string fieldName,
int minLength,
int maxLength)
{
var result = new ValidationResult();
if (value?.Length < minLength)
{
result.AddError(fieldName,
$"{fieldName} must be at least {minLength} characters");
}
if (value?.Length > maxLength)
{
result.AddError(fieldName,
$"{fieldName} cannot exceed {maxLength} characters");
}
return result;
}
public static ValidationResult ValidateRange<T>(
T value,
string fieldName,
T min,
T max) where T : IComparable<T>
{
var result = new ValidationResult();
if (value.CompareTo(min) < 0)
{
result.AddError(fieldName,
$"{fieldName} must be at least {min}");
}
if (value.CompareTo(max) > 0)
{
result.AddError(fieldName,
$"{fieldName} cannot exceed {max}");
}
return result;
}
public static ValidationResult ValidateEmail(
string email,
string fieldName)
{
var result = new ValidationResult();
if (!string.IsNullOrEmpty(email))
{
var emailRegex = new Regex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
if (!emailRegex.IsMatch(email))
{
result.AddError(fieldName, "Invalid email format");
}
}
return result;
}
public static ValidationResult ValidateDate(
DateTime date,
string fieldName,
DateTime? minDate = null,
DateTime? maxDate = null)
{
var result = new ValidationResult();
if (minDate.HasValue && date < minDate.Value)
{
result.AddError(fieldName,
$"{fieldName} cannot be before {minDate.Value:d}");
}
if (maxDate.HasValue && date > maxDate.Value)
{
result.AddError(fieldName,
$"{fieldName} cannot be after {maxDate.Value:d}");
}
return result;
}
}
Business Rule Validation
Domain-specific validation logic:
// Pattern: Business Rule Validator
public class BusinessRuleValidator<T> where T : class
{
private readonly List<IBusinessRule<T>> _rules = new();
private readonly ILogger<BusinessRuleValidator<T>> _logger;
public BusinessRuleValidator(ILogger<BusinessRuleValidator<T>> logger)
{
_logger = logger;
}
public void AddRule(IBusinessRule<T> rule)
{
_rules.Add(rule);
}
public async Task<ValidationResult> ValidateAsync(T entity)
{
var result = new ValidationResult();
foreach (var rule in _rules)
{
try
{
var ruleResult = await rule.ValidateAsync(entity);
result.Merge(ruleResult);
if (ruleResult.HasCriticalErrors)
{
_logger.LogWarning("Critical validation error in rule {RuleName}",
rule.GetType().Name);
break; // Stop on critical errors
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Rule validation failed for {RuleName}",
rule.GetType().Name);
result.AddError("System", "Validation error occurred");
}
}
return result;
}
}
public interface IBusinessRule<T>
{
Task<ValidationResult> ValidateAsync(T entity);
}
// Example business rule
public class CustomerCreditLimitRule : IBusinessRule<Order>
{
private readonly ICustomerService _customerService;
public async Task<ValidationResult> ValidateAsync(Order order)
{
var result = new ValidationResult();
var customer = await _customerService.GetCustomerAsync(order.CustomerId);
if (customer == null)
{
result.AddError("Customer", "Customer not found", isCritical: true);
return result;
}
var totalExposure = customer.OutstandingBalance + order.TotalAmount;
if (totalExposure > customer.CreditLimit)
{
result.AddError("CreditLimit",
$"Order exceeds credit limit. Available: {customer.CreditLimit - customer.OutstandingBalance:C}");
if (customer.IsOnHold)
{
result.AddError("Customer", "Customer account is on hold", isCritical: true);
}
}
return result;
}
}
Cross-Field Validation
Validating field dependencies:
// Pattern: Cross-Field Validation
public class CrossFieldValidator
{
public ValidationResult ValidateDateRange(
DateTime startDate,
DateTime endDate,
string context)
{
var result = new ValidationResult();
if (endDate < startDate)
{
result.AddError("DateRange",
$"{context}: End date cannot be before start date");
}
var daysDifference = (endDate - startDate).TotalDays;
if (daysDifference > 365)
{
result.AddWarning("DateRange",
$"{context}: Date range exceeds one year");
}
return result;
}
public ValidationResult ValidateConditionalRequired(
bool condition,
string conditionalValue,
string fieldName,
string conditionDescription)
{
var result = new ValidationResult();
if (condition && string.IsNullOrWhiteSpace(conditionalValue))
{
result.AddError(fieldName,
$"{fieldName} is required when {conditionDescription}");
}
return result;
}
public ValidationResult ValidateMutuallyExclusive(
params (bool HasValue, string FieldName)[] fields)
{
var result = new ValidationResult();
var setFields = fields.Where(f => f.HasValue).ToList();
if (setFields.Count == 0)
{
result.AddError("Required",
$"One of {string.Join(", ", fields.Select(f => f.FieldName))} is required");
}
else if (setFields.Count > 1)
{
result.AddError("MutuallyExclusive",
$"Only one of {string.Join(", ", setFields.Select(f => f.FieldName))} can be specified");
}
return result;
}
}
Async Validation
Server-side and database validation:
// Pattern: Async Validation
public class AsyncValidator
{
private readonly IDataService _dataService;
private readonly ILogger<AsyncValidator> _logger;
public async Task<ValidationResult> ValidateUniqueAsync<T>(
string value,
string fieldName,
Expression<Func<T, bool>> predicate) where T : class
{
var result = new ValidationResult();
try
{
var exists = await _dataService.ExistsAsync(predicate);
if (exists)
{
result.AddError(fieldName,
$"{fieldName} '{value}' already exists");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to validate uniqueness for {FieldName}",
fieldName);
result.AddWarning(fieldName, "Could not validate uniqueness");
}
return result;
}
public async Task<ValidationResult> ValidateReferenceAsync(
string referenceId,
string fieldName,
Func<string, Task<bool>> checkExistence)
{
var result = new ValidationResult();
if (!string.IsNullOrEmpty(referenceId))
{
var exists = await checkExistence(referenceId);
if (!exists)
{
result.AddError(fieldName,
$"{fieldName} '{referenceId}' not found");
}
}
return result;
}
public async Task<ValidationResult> ValidateAvailabilityAsync(
string stockCode,
decimal requestedQty)
{
var result = new ValidationResult();
var availability = await _dataService.GetStockAvailabilityAsync(stockCode);
if (availability == null)
{
result.AddError("StockCode", $"Stock item {stockCode} not found");
}
else if (availability.AvailableQty < requestedQty)
{
result.AddError("Quantity",
$"Insufficient stock. Available: {availability.AvailableQty}");
if (availability.OnOrder > 0)
{
result.AddWarning("Quantity",
$"{availability.OnOrder} units on order, due {availability.NextDelivery:d}");
}
}
return result;
}
}
UI Validation Integration
Integrating validation with WPF UI:
// Pattern: IDataErrorInfo Implementation
public abstract class ValidatableViewModel : BaseViewModel, IDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = new();
public string Error => string.Join(Environment.NewLine,
_errors.SelectMany(e => e.Value));
public string this[string propertyName]
{
get
{
if (_errors.ContainsKey(propertyName))
{
return string.Join(Environment.NewLine, _errors[propertyName]);
}
return string.Empty;
}
}
public bool HasErrors => _errors.Any();
protected void ValidateProperty(string propertyName, object value)
{
ClearErrors(propertyName);
var result = OnValidateProperty(propertyName, value);
if (!result.IsValid)
{
AddErrors(propertyName, result.Errors.Select(e => e.ErrorMessage));
}
OnPropertyChanged(nameof(HasErrors));
}
protected virtual ValidationResult OnValidateProperty(
string propertyName,
object value)
{
// Override in derived classes
return new ValidationResult();
}
protected void AddError(string propertyName, string error)
{
if (!_errors.ContainsKey(propertyName))
{
_errors[propertyName] = new List<string>();
}
_errors[propertyName].Add(error);
OnPropertyChanged(propertyName);
}
protected void AddErrors(string propertyName, IEnumerable<string> errors)
{
foreach (var error in errors)
{
AddError(propertyName, error);
}
}
protected void ClearErrors(string propertyName = null)
{
if (propertyName == null)
{
_errors.Clear();
}
else
{
_errors.Remove(propertyName);
}
OnPropertyChanged(propertyName);
}
}
Validation Composition
Combining multiple validators:
// Pattern: Validation Pipeline
public class ValidationPipeline<T> where T : class
{
private readonly List<Func<T, Task<ValidationResult>>> _validators = new();
private readonly ILogger<ValidationPipeline<T>> _logger;
public ValidationPipeline<T> AddValidator(
Func<T, ValidationResult> validator)
{
_validators.Add(entity => Task.FromResult(validator(entity)));
return this;
}
public ValidationPipeline<T> AddAsyncValidator(
Func<T, Task<ValidationResult>> validator)
{
_validators.Add(validator);
return this;
}
public async Task<ValidationResult> ValidateAsync(T entity)
{
var finalResult = new ValidationResult();
foreach (var validator in _validators)
{
try
{
var result = await validator(entity);
finalResult.Merge(result);
// Stop on critical errors
if (result.HasCriticalErrors)
{
_logger.LogWarning("Validation pipeline stopped due to critical error");
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Validator failed in pipeline");
finalResult.AddError("Validation", "Validation process failed");
}
}
return finalResult;
}
}
// Usage example
public class OrderValidator
{
private readonly ValidationPipeline<Order> _pipeline;
public OrderValidator(IServiceProvider services)
{
_pipeline = new ValidationPipeline<Order>(services.GetService<ILogger<ValidationPipeline<Order>>>())
.AddValidator(ValidateRequiredFields)
.AddValidator(ValidateOrderItems)
.AddAsyncValidator(ValidateCustomerAsync)
.AddAsyncValidator(ValidateInventoryAsync)
.AddValidator(ValidateBusinessRules);
}
public Task<ValidationResult> ValidateOrderAsync(Order order)
{
return _pipeline.ValidateAsync(order);
}
}
Dashboard-Specific Implementations
AR Payment Reversal
- Queue validation before processing
- Payment existence validation
- Period validation for reversals
- See: AR Payment Validation
Inventory Mini MRP
- Stock availability validation
- Supplier validation
- MRP calculation validation
- See: Inventory Custom Validation
AP EFT Remittance
- Email format validation
- Payment selection validation
- Remittance data validation
- See: AP Payment Validation
Best Practices
- Validate early and often - Catch errors as soon as possible
- Provide clear messages - Help users understand and fix issues
- Distinguish errors from warnings - Not all issues are critical
- Use async validation sparingly - Only when necessary for performance
- Cache validation results - Avoid redundant validations
- Implement client and server validation - Defense in depth
- Log validation failures - For audit and debugging
- Make validation testable - Separate validation logic from UI
Common Pitfalls
- No validation - Assuming data is always valid
- Validation only in UI - Missing server-side validation
- Cryptic error messages - Users can't understand issues
- Over-validation - Too many rules frustrate users
- Synchronous database validation - Blocking UI thread