ValidationResult Class
Property-level validation errors with fluent API and DataAnnotations interop.
Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll
public sealed class ValidationResult
Overview
ValidationResult provides:
- Property-level errors — Map errors to specific properties
- Entity-level errors — General validation failures
- Fluent API — Chain
AddError()calls - DataAnnotations interop — Convert to/from standard validation
Quick Start
ValidationResult ValidateProduct(Product? product) {
if (product is null)
return ValidationResult.Error("Product cannot be null");
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(product.Name))
result.AddError(nameof(Product.Name), "Name is required");
if (product.Price < 0)
result.AddError(nameof(Product.Price), "Price cannot be negative");
return result;
}
Properties
| Property | Type | Description |
|---|---|---|
HasErrors |
bool |
Any errors exist |
PropertyNames |
IEnumerable<string> |
Properties with errors |
AllErrors |
IReadOnlyList<string> |
Flat list of all messages |
Static Properties
| Property | Description |
|---|---|
Success |
Singleton for no errors |
Creating Results
Success (No Errors)
// Use the singleton
return ValidationResult.Success;
// Or create empty
var result = new ValidationResult();
// result.HasErrors == false
Single Error
// Property error
var result = ValidationResult.Error("Name", "Name is required");
// Entity-level error
var result = ValidationResult.Error("Invalid data format");
Multiple Errors (Fluent)
var result = new ValidationResult()
.AddError("Name", "Name is required")
.AddError("Name", "Name must be 3-50 characters")
.AddError("Price", "Price cannot be negative")
.AddError("General validation failed"); // Entity-level
Methods
AddError
Add errors with fluent chaining.
// Property error
result.AddError("PropertyName", "Error message");
// Entity-level error (null or empty property)
result.AddError(null, "Entity-level error");
result.AddError("Entity-level error"); // Shorthand
AddErrors
Add multiple errors at once.
result.AddErrors("Name", new[] { "Error 1", "Error 2" });
GetErrors
Retrieve errors for a property.
IEnumerable<string> nameErrors = result.GetErrors("Name");
IEnumerable<string> entityErrors = result.GetErrors(null);
Merge
Combine validation results.
var result1 = ValidateName(obj);
var result2 = ValidatePrice(obj);
var combined = result1.Merge(result2);
Factory Methods
FromErrors
Create from error list (entity-level).
var errors = new[] { "Error 1", "Error 2" };
var result = ValidationResult.FromErrors(errors);
FromDictionary
Create from property-to-errors mapping.
var errors = new Dictionary<string, IEnumerable<string>> {
["Name"] = new[] { "Required", "Too short" },
["Price"] = new[] { "Must be positive" }
};
var result = ValidationResult.FromDictionary(errors);
DataAnnotations Interop
FromDataAnnotations
Convert from System.ComponentModel.DataAnnotations.
var context = new ValidationContext(product);
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
if (!Validator.TryValidateObject(product, context, results, validateAllProperties: true)) {
return ValidationResult.FromDataAnnotations(results);
}
return ValidationResult.Success;
ToDataAnnotations
Convert to System.ComponentModel.DataAnnotations.
var result = new ValidationResult()
.AddError("Name", "Required")
.AddError("Price", "Invalid");
IReadOnlyList<System.ComponentModel.DataAnnotations.ValidationResult> daResults
= result.ToDataAnnotations();
Implicit Conversions
For backward compatibility:
// From string array
ValidationResult result = new[] { "Error 1", "Error 2" };
// From List<string>
ValidationResult result = new List<string> { "Error 1" };
Use in validation functions:
var state = State<User>.Create(
loadFunc: LoadUserAsync,
validateFunc: user => user?.Email == null
? new[] { "Email is required" } // Implicit conversion
: Array.Empty<string>());
Common Patterns
Comprehensive Validation
ValidationResult ValidateCustomer(Customer? customer) {
if (customer is null)
return ValidationResult.Error("Customer cannot be null");
var result = new ValidationResult();
// Required fields
if (string.IsNullOrWhiteSpace(customer.FirstName))
result.AddError(nameof(customer.FirstName), "First name is required");
if (string.IsNullOrWhiteSpace(customer.LastName))
result.AddError(nameof(customer.LastName), "Last name is required");
// Format validation
if (customer.Email != null && !IsValidEmail(customer.Email))
result.AddError(nameof(customer.Email), "Invalid email format");
// Business rules
if (customer.Age.HasValue && customer.Age < 18)
result.AddError(nameof(customer.Age), "Must be 18 or older");
// Cross-field validation
if (customer.EndDate < customer.StartDate)
result.AddError("End date must be after start date");
return result;
}
Using with State<T>
var customerState = State<Customer>.Create(
loadFunc: LoadCustomerAsync,
saveFunc: SaveCustomerAsync,
validateFunc: ValidateCustomer);
// Check validation
if (customerState.HasErrors) {
// Get all errors
foreach (var error in customerState.ValidationErrors) {
Console.WriteLine(error);
}
// Get property-specific errors
var nameErrors = customerState.ValidationResult.GetErrors("FirstName");
}
// Save only if valid
if (!customerState.HasErrors) {
await customerState.SaveAsync();
}
INotifyDataErrorInfo Integration
State<T> implements INotifyDataErrorInfo:
// XAML automatically shows validation errors
// <TextBox Text="{Binding State.CurrentValue.Name}"
// Validation.ErrorTemplate="{StaticResource ErrorTemplate}"/>
// The State.GetErrors() method returns errors for binding
IEnumerable errors = state.GetErrors("Name");
Composing Validators
ValidationResult ValidateOrder(Order? order) {
if (order is null)
return ValidationResult.Error("Order required");
var result = new ValidationResult();
// Validate order details
if (order.Items.Count == 0)
result.AddError(nameof(order.Items), "Order must have items");
// Validate each item
for (int i = 0; i < order.Items.Count; i++) {
var itemResult = ValidateOrderItem(order.Items[i]);
foreach (var error in itemResult.AllErrors) {
result.AddError($"Items[{i}]", error);
}
}
// Validate customer
result.Merge(ValidateCustomer(order.Customer));
return result;
}
Tips
Always Return ValidationResult.Success
// ✅ Good
return result.HasErrors ? result : ValidationResult.Success;
// ✅ Also good - check at end
if (!result.HasErrors) return ValidationResult.Success;
return result;
Use Property Names Consistently
// ✅ Use nameof for compile-time safety
result.AddError(nameof(Product.Name), "Required");
// ❌ Avoid magic strings
result.AddError("name", "Required"); // Case mismatch issues
Prefer Specific Over Generic
// ✅ Good - user knows what to fix
result.AddError(nameof(Product.Price), "Price must be between 0.01 and 9999.99");
// ❌ Bad - vague
result.AddError(nameof(Product.Price), "Invalid");
See Also
State<T>— Editable state with validation- System.ComponentModel.DataAnnotations