Table of Contents

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