Built-in Business Rule Validators
Sannr provides common business validation patterns as built-in attributes, eliminating the need for custom validators in most enterprise scenarios.
Overview
Business rule validators address common validation patterns that appear across different domains:
- Future Date Validation: Ensuring dates are in the future (appointments, deadlines, etc.)
- Allowed Values: Restricting properties to predefined sets of values
- Conditional Range: Applying numeric ranges based on other property values
FutureDate Attribute
The [FutureDate] attribute ensures that a DateTime or DateTime? property contains a date in the future.
Usage
public class Appointment
{
[Required]
public string CustomerName { get; set; }
[FutureDate]
public DateTime AppointmentDate { get; set; }
}
public class Order
{
[FutureDate]
public DateTime? DeliveryDate { get; set; } // Nullable also supported
}Client-Side Generation
{
"appointmentDate": { "futureDate": true },
"deliveryDate": { "futureDate": true }
}Validation Logic
- For non-nullable
DateTime: Must be >DateTime.Now - For nullable
DateTime?: If not null, must be >DateTime.Now - Uses
DateTime.Nowfor comparison (notDateTime.UtcNow)
AllowedValues Attribute
The [AllowedValues] attribute restricts a property to a predefined set of string values.
Usage
public class Product
{
[Required]
public string Name { get; set; }
[AllowedValues("active", "inactive", "discontinued", "pending")]
public string Status { get; set; }
[AllowedValues("USD", "EUR", "GBP", "JPY")]
public string Currency { get; set; }
}Constructor
public AllowedValuesAttribute(params string[] values)Properties
Values: Array of allowed string values
Client-Side Generation
{
"status": { "allowedValues": ["active", "inactive", "discontinued", "pending"] },
"currency": { "allowedValues": ["USD", "EUR", "GBP", "JPY"] }
}Validation Logic
- Property value must match one of the allowed values (case-sensitive)
- Works with
stringandstring?properties - Null/empty values are allowed unless combined with
[Required]
ConditionalRange Attribute
The [ConditionalRange] attribute applies numeric range validation only when another property matches a specific value.
Usage
public class LoanApplication
{
[Required]
[AllowedValues("personal", "business", "mortgage")]
public string LoanType { get; set; }
[ConditionalRange("LoanType", "personal", 1000, 50000)]
[ConditionalRange("LoanType", "business", 5000, 1000000)]
[ConditionalRange("LoanType", "mortgage", 50000, 5000000)]
public decimal Amount { get; set; }
}
public class UserProfile
{
public bool IsPremium { get; set; }
[ConditionalRange("IsPremium", true, 100, 1000)]
[ConditionalRange("IsPremium", false, 10, 100)]
public int Credits { get; set; }
}Constructor
public ConditionalRangeAttribute(string otherProperty, object targetValue, double minimum, double maximum)Properties
OtherProperty: Name of the property to checkTargetValue: Value that the other property must have for validation to applyMinimum: Minimum allowable valueMaximum: Maximum allowable value
Client-Side Generation
{
"amount": {
"minRange": 1000,
"maxRange": 50000,
"conditionProperty": "loanType",
"conditionValue": "personal"
},
"credits": {
"minRange": 100,
"maxRange": 1000,
"conditionProperty": "isPremium",
"conditionValue": true
}
}Validation Logic
- Range validation only applies when
OtherProperty == TargetValue - Works with numeric types:
int,long,float,double,decimal - Supports nullable numeric types
- When condition is not met, no range validation is performed
Multiple Conditional Ranges
You can apply multiple [ConditionalRange] attributes to the same property for different conditions:
public class ShippingOrder
{
[AllowedValues("standard", "express", "overnight")]
public string ShippingMethod { get; set; }
[ConditionalRange("ShippingMethod", "standard", 1, 30)] // 1-30 days
[ConditionalRange("ShippingMethod", "express", 1, 7)] // 1-7 days
[ConditionalRange("ShippingMethod", "overnight", 1, 2)] // 1-2 days
public int DeliveryDays { get; set; }
}Integration with Other Attributes
Business rule validators work seamlessly with other Sannr attributes:
public class AdvancedModel
{
[Required]
[AllowedValues("draft", "review", "approved", "published")]
public string Status { get; set; }
[Required]
[FutureDate]
public DateTime PublishDate { get; set; }
[ConditionalRange("Status", "published", 0, 1000000)]
[Range(0, 100)] // This range applies regardless of status
public int ViewCount { get; set; }
}Error Messages
All business rule validators support custom error messages:
public class CustomMessages
{
[FutureDate(ErrorMessage = "Delivery date must be in the future")]
public DateTime DeliveryDate { get; set; }
[AllowedValues("active", "inactive", ErrorMessage = "Status must be either active or inactive")]
public string Status { get; set; }
[ConditionalRange("Category", "premium", 100, 1000, ErrorMessage = "Premium items must cost between $100 and $1000")]
public decimal Price { get; set; }
}Performance Characteristics
- Zero Runtime Overhead: All validation logic is generated at compile-time
- AOT Compatible: No reflection or dynamic code generation
- Memory Efficient: No allocations for attribute metadata lookups
- Fast Execution: Direct static method calls
Client-Side Integration
Business rule validators generate JSON rules compatible with popular JavaScript validation libraries:
// jQuery Validation
$.validator.addMethod("futureDate", function(value, element) {
return new Date(value) > new Date();
});
$.validator.addMethod("allowedValues", function(value, element, params) {
return params.indexOf(value) !== -1;
});
// Example usage
const rules = JSON.parse(MyModelValidators.ValidationRulesJson);
$("#myForm").validate({ rules: rules });Migration from Custom Validators
If you're currently using custom validators for these patterns, migration is straightforward:
Before:
[CustomValidator(typeof(MyValidators), nameof(MyValidators.ValidateFutureDate))]
public DateTime DeliveryDate { get; set; }
public static class MyValidators
{
public static ValidationResult ValidateFutureDate(DateTime date)
{
return date > DateTime.Now
? ValidationResult.Success()
: ValidationResult.Error("Date must be in the future");
}
}After:
[FutureDate]
public DateTime DeliveryDate { get; set; }Best Practices
- Use AllowedValues for enums: When you need string-based enums without the overhead of actual enums
- Combine with Required: Use
[Required]with[AllowedValues]to ensure a value is both present and valid - Multiple ConditionalRanges: Stack multiple attributes for complex business logic
- Client-Side Consistency: Always use the generated JSON rules for client-side validation
- Error Messages: Provide clear, business-specific error messages
Limitations
ConditionalRangeonly supports equality conditions (not greater than, less than, etc.)AllowedValuesis case-sensitive (consider using[Sanitize(ToLower = true)]if needed)FutureDateuses local time (DateTime.Now) rather than UTC
Roadmap
Future enhancements may include:
- Support for more complex conditional logic (>, <, !=, etc.)
- Case-insensitive allowed values
- UTC-based future date validation
- Date range validation (between two dates)
- Regular expression-based allowed patterns