SpecExpress: The Complete Guide for Data ValidationSpecExpress is a .NET validation library designed to make validating complex business objects clear, maintainable, and testable. This guide explains what SpecExpress is, why you might use it, how it works, common patterns, advanced features, performance considerations, testing strategies, migration tips, and alternatives — with examples to help you get productive quickly.
What is SpecExpress?
SpecExpress is a fluent, rule-based validation library for .NET that lets you define validation rules for your domain models in a centralized, expressive way. Instead of scattering validation logic across controllers, services, or UI layers, SpecExpress encourages keeping validation concerns in dedicated “specifications” (specs), improving separation of concerns and testability.
Why use SpecExpress?
- Centralized validation: rules live in one place.
- Fluent API: readable, self-documenting rule definitions.
- Reusability: share specs across layers (server, background jobs, tests).
- Testability: easily unit-test validation logic.
- Extensibility: add custom rules and message providers.
- Supports complex scenarios: conditional rules, nested objects, collections.
Core concepts
- Specifications (Specs): classes that describe validation rules for a specific type.
- RuleFor: defines a rule for a property or expression.
- Conditions: enable rules only when certain predicates are true.
- Nested specs: validate complex object graphs by referencing other specs.
- Rule sets: group rules so you can run subsets (e.g., create vs update).
- Message formatting: customize error messages and localization.
Basic example
Below is a simple SpecExpress specification for a User model.
public class User { public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public DateTime? DateOfBirth { get; set; } public IList<Address> Addresses { get; set; } } public class UserSpecification : Validates<User> { public UserSpecification() { IsDefaultForType(); Check(u => u.FirstName) .Required() .MaxLength(50); Check(u => u.LastName) .Required() .MaxLength(50); Check(u => u.Email) .Required() .Email(); Check(u => u.DateOfBirth) .Optional() .Is((dob) => dob <= DateTime.UtcNow, "Date of birth must be in the past"); Check(u => u.Addresses) .Optional() .RequiredCollection() .ListCount(1, 5) .EachItem().SetValidator(new AddressSpecification()); } }
Notes:
- IsDefaultForType registers this spec as the default validator for the User type.
- Check defines property-level validation.
- EachItem with SetValidator applies nested spec validation to each collection element.
Validation for nested objects and collections
SpecExpress handles nested objects and collections naturally by referencing other specs.
public class Address { public string Line1 { get; set; } public string City { get; set; } public string PostalCode { get; set; } } public class AddressSpecification : Validates<Address> { public AddressSpecification() { IsDefaultForType(); Check(a => a.Line1).Required(); Check(a => a.City).Required(); Check(a => a.PostalCode).Required().Matches("^[0-9A-Za-z -]{3,10}$"); } }
Conditional validation
Run rules only when certain conditions are met.
Check(u => u.DateOfBirth) .Required() .When(u => u.Age < 18, ApplyCondition.If);
Or use When/Unless to enable/disable rule groups.
Rule sets (contexts)
Define different groups of rules for different operations, such as Create vs Update.
public UserSpecification() { IsDefaultForType(); Check(u => u.Email).Required().ApplyFor("Create"); Check(u => u.Id).Required().ApplyFor("Update"); }
When validating, specify the rule set:
ValidationResult result = ValidationService.Validate(user, "Create");
Custom rules and validators
You can write custom validation functions or reusable rule extensions.
Check(u => u.Username) .IsValid((username) => MyCustomUsernameCheck(username), "Username is invalid"); public static bool MyCustomUsernameCheck(string value) { // custom logic }
Create reusable rule extensions to keep specs concise.
Localization and message customization
SpecExpress supports customizing error messages and localizing them by providing custom message providers or resource strings. Keep messages user-friendly and map technical messages to UI-friendly text.
Performance considerations
- Cache specs: SpecExpress typically compiles and caches specs; avoid re-registering specs repeatedly.
- Validate only necessary rule sets to reduce work.
- For large collections, consider validating in parallel with caution (thread-safety of specs).
- Measure hotspots with profiling; validation is usually I/O-free CPU work but can be heavy for deep graphs.
Testing validation logic
Unit-test specs directly:
[Test] public void UserSpec_InvalidEmail_ReturnsError() { var user = new User { Email = "not-an-email" }; var result = ValidationService.Validate(user); Assert.IsFalse(result.IsValid); Assert.That(result.Errors.Any(e => e.Property == "Email")); }
Write tests for boundary conditions, conditional branches, nested object failures, and rule set behavior.
Integration with frameworks
- ASP.NET / ASP.NET Core: plug SpecExpress into model binding or action filters; validate in middleware or controller actions before business logic runs.
- Use in background workers, microservices, and desktop apps — same specs can be reused.
Migration tips (from DataAnnotations, FluentValidation, etc.)
- Map DataAnnotations attributes to equivalent SpecExpress checks.
- Move complex rules into specs rather than attribute-heavy models.
- Create wrapper validators for common validation patterns during incremental migration.
Alternatives and when to choose them
- FluentValidation: similar fluent API, large community and active maintenance.
- DataAnnotations: simpler, attribute-based; good for small models and simple rules.
- Custom in-house validators: sometimes necessary for unique requirements.
Use SpecExpress when you want centralized, testable, expressive specs and your project is .NET-based. If you prefer wider community support or a more active project, evaluate FluentValidation as well.
Troubleshooting common issues
- Duplicate rule registrations: ensure specs are registered once and AreDefaultForType used consistently.
- Error message mapping: verify property names and paths if front-end mapping fails.
- Thread-safety: avoid mutating shared state in spec constructors.
Example: Full end-to-end flow
- Define specs for models (User, Address).
- Register specs at application startup.
- Validate model instances in controllers/services using ValidationService.Validate.
- Map ValidationResult to HTTP 400 responses with structured error payloads.
- Unit-test specs and integration tests for endpoints that depend on validation.
Summary
SpecExpress is a powerful, fluent validation library for .NET that centralizes and clarifies validation logic through specifications. It supports nested objects, conditionals, rule sets, customization, and testing-friendly patterns. Choose it when you need readable, maintainable, and reusable validation definitions — verify community activity and compare with FluentValidation if long-term maintenance and ecosystem are priorities.
Leave a Reply