Edit on GitHub

Declarative Validation

Declarative validation facilitate greater declarative functionality around ServiceStack Services where all existing Fluent Validation Property Validators can be annotated on Request DTOs using typed validation attributes which are decoupled from their Validator implementation so they’re suitable to be annotated on impl-free Service Model DTOs and exported in Add ServiceStack Reference Types.

As they’re decoupled the same rules could enable instant validation feedback on clients without server round trips

The validators are incorporated into ServiceStack’s existing Fluent Validation model so it works with existing UI form binding.

Property Validators

The Property Validator attributes provide an alternative way to apply Request DTO validation rules, the best way to demonstrate them is showing the same example below implemented using Fluent Validation APIs:

public class ExampleValidatorsValidator : AbstractValidator<ExampleValidators>
{
    public ExampleValidatorsValidator()
    {
        RuleFor(x => x.CreditCard).CreditCard();
        RuleFor(x => x.Email).EmailAddress();
        RuleFor(x => x.Empty).Empty();
        RuleFor(x => x.Equal).Equal("Equal");
        RuleFor(x => x.ExclusiveBetween).ExclusiveBetween(10, 20);
        RuleFor(x => x.GreaterThanOrEqual).GreaterThanOrEqualTo(10);
        RuleFor(x => x.GreaterThan).GreaterThan(10);
        RuleFor(x => x.InclusiveBetween).InclusiveBetween(10, 20);
        RuleFor(x => x.Length).Length(10);
        RuleFor(x => x.LessThanOrEqual).LessThanOrEqualTo(10);
        RuleFor(x => x.LessThan).LessThan(10);
        RuleFor(x => x.NotEmpty).NotEmpty();
        RuleFor(x => x.NotEqual).NotEqual("NotEqual");
        RuleFor(x => x.Null).Null();
        RuleFor(x => x.ScalePrecision).ScalePrecision(1,1);
        RuleFor(x => x.RegularExpression).Matches(@"^[a-z]*$");
    }
}

For each property validator above you can use a Typed Property Validation Attribute in the format [Validate*]:

public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse>
{
    [ValidateCreditCard]
    public string CreditCard { get; set; }
    [ValidateEmail]
    public string Email { get; set; }
    [ValidateEmpty]
    public string Empty { get; set; }
    [ValidateEqual("Equal")]
    public string Equal { get; set; }
    [ValidateLessThan(10)]
    public int LessThan { get; set; }
    [ValidateLessThanOrEqual(10)]
    public int LessThanOrEqual { get; set; }
    [ValidateGreaterThan(10)]
    public int GreaterThan { get; set; }
    [ValidateGreaterThanOrEqual(10)]
    public int GreaterThanOrEqual { get; set; }
    [ValidateExclusiveBetween(10, 20)]
    public int ExclusiveBetween { get; set; }
    [ValidateInclusiveBetween(10, 20)]
    public int InclusiveBetween { get; set; }
    [ValidateExactLength(10)]
    public string Length { get; set; }
    [ValidateNotEmpty]
    public string NotEmpty { get; set; }
    [ValidateNotEqual("NotEqual")]
    public string NotEqual { get; set; }
    [ValidateNull]
    public string Null { get; set; }
    [ValidateScalePrecision(1,1)]
    public decimal ScalePrecision { get; set; }
    [ValidateRegularExpression("^[a-z]*$")]
    public string RegularExpression { get; set; }
}

All Typed Validator Attributes above are just providing a typed subclass wrapper around the generic [Validate], so the implementation of the [ValidateLessThan] is just:

public class ValidateLessThanAttribute : ValidateAttribute
{
    public ValidateLessThanAttribute(int value) : base($"LessThan({value})") { }
}

So the same Typed Validator above is equivalent to using the untyped generic [Validate] attribute below:

public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse>
{
    [Validate("CreditCard")]
    public string CreditCard { get; set; }
    [Validate("Email")]
    public string Email { get; set; }
    [Validate("Empty")]
    public string Empty { get; set; }
    [Validate("Equal('Equal')")]
    public string Equal { get; set; }
    [Validate("ExclusiveBetween(10, 20)")]
    public int ExclusiveBetween { get; set; }
    [Validate("GreaterThanOrEqual(10)")]
    public int GreaterThanOrEqual { get; set; }
    [Validate("GreaterThan(10)")]
    public int GreaterThan { get; set; }
    [Validate("InclusiveBetween(10, 20)")]
    public int InclusiveBetween { get; set; }
    [Validate("ExactLength(10)")]
    public string Length { get; set; }
    [Validate("LessThanOrEqual(10)")]
    public int LessThanOrEqual { get; set; }
    [Validate("LessThan(10)")]
    public int LessThan { get; set; }
    [Validate("NotEmpty")]
    public string NotEmpty { get; set; }
    [Validate("NotEqual('NotEqual')")]
    public string NotEqual { get; set; }
    [Validate("Null")]
    public string Null { get; set; }
    [Validate("RegularExpression('^[a-z]*$')")]
    public string RegularExpression { get; set; }
    [Validate("ScalePrecision(1,1)")]
    public decimal ScalePrecision { get; set; }
}

Where the Validator Expression is a #Script Expression that returns a Fluent Validation IPropertyValidator defined in the built-in ValidateScripts.cs:

public class ValidateScripts : ScriptMethods
{
    public IPropertyValidator Null() => new NullValidator();
    public IPropertyValidator Empty() => new EmptyValidator(null);
    public IPropertyValidator Empty(object defaultValue) => new EmptyValidator(defaultValue);
    public IPropertyValidator Equal(object value) => new EqualValidator(value);
    public IPropertyValidator NotNull() => new NotNullValidator();
    public IPropertyValidator NotEmpty() => new NotEmptyValidator(null);
    public IPropertyValidator NotEmpty(object defaultValue) => new NotEmptyValidator(defaultValue);
    public IPropertyValidator NotEqual(object value) => new NotEqualValidator(value);
    public IPropertyValidator CreditCard() => new CreditCardValidator();
    public IPropertyValidator Email() => new AspNetCoreCompatibleEmailValidator();
    public IPropertyValidator Length(int min, int max) => new LengthValidator(min, max);
    public IPropertyValidator ExactLength(int length) => new ExactLengthValidator(length);
    public IPropertyValidator MaximumLength(int max) => new MaximumLengthValidator(max);
    public IPropertyValidator MinimumLength(int min) => new MinimumLengthValidator(min);
    public IPropertyValidator InclusiveBetween(IComparable from, IComparable to) =>
        new InclusiveBetweenValidator(from, to);
    public IPropertyValidator ExclusiveBetween(IComparable from, IComparable to) =>
        new ExclusiveBetweenValidator(from, to);
    public IPropertyValidator LessThan(int value) => new LessThanValidator(value);
    public IPropertyValidator LessThanOrEqual(int value) => new LessThanOrEqualValidator(value);
    public IPropertyValidator GreaterThan(int value) => new GreaterThanValidator(value);
    public IPropertyValidator GreaterThanOrEqual(int value) => new GreaterThanOrEqualValidator(value);
    public IPropertyValidator ScalePrecision(int scale, int precision) =>
        new ScalePrecisionValidator(scale, precision);
    public IPropertyValidator RegularExpression(string regex) => 
        new RegularExpressionValidator(regex, RegexOptions.Compiled);
}

Validated Validator Expressions

Despite using untyped string Expressions, Validator expressions still provide early error detection as on Startup each #Script expression is evaluated and verified that it resolves to a valid IPropertyValidator instance otherwise fails with a Startup Exception. If the instance returned is valid it’s merged with any other AbstractValidator<T> that may also be defined for the same Request DTO Type, where it lets you mix n’ match declarative attributes together with Fluent Validation rules.

Defining Multiple Validators

You can specify multiple Property Validators should be applied within a single Validator expression by using [] Array notation, alternatively you can apply multiple Validate attributes and use C# syntax to combine them in a single line:

public class ExampleValidators
{
    [Validate("[NotNull,InclusiveBetween(13,100)]")]
    public int? ValidateAge { get; set; }

    [ValidateNotNull,ValidateInclusiveBetween(13,100)]
    public int? TypedAge { get; set; }
}

Registering Custom Declarative Validators

As [Validate*] attributes just execute a Script Method they’re easily extensible by defining and register your own, e.g:

public class MyValidateScripts : ScriptMethods
{
    public IPropertyValidator Custom(int arg) => new MyCustomValidator(arg);
}

Which can be registered, either directly on your Script Pages plugin if your AppHost uses one:

Plugins.Add(new SharpPagesFeature {
    ScriptMethods = { new CustomScriptMethods() }
});

Otherwise you can use the AppHost’s new ScriptContext which adds it to the AppHost’s empty ScriptContext:

ScriptContext.ScriptMethods.Add(new CustomScriptMethods());

ScriptContext also returns SharpPagesFeature if registered, in which case both registration examples are equivalent

After which you’ll immediately be able to use it with the [Validate] attribute:

[Validate("Custom(1)")]
public int Test { get; set; }

Likewise you can create a typed Validate attribute around it which you can use instead:

public class ValidateCustomAttribute : ValidateAttribute
{
    public ValidateCustomAttribute(int arg) : base($"Custom({arg})") { }
}
//...

[ValidateCustom(1)]
public int Test { get; set; }

Custom Script Validation

Fluent Validation Validators are a nice model for defining reusable validation rules however they can require a bit of boilerplate if you only need to define a one-off validation check. In these cases we can provide an even lighter weight solution by being able to defining our validation condition inline with #Script by specifying it in the Condition attribute, e.g:

public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse>
{
    [Validate(Condition = "it.isOdd()")]
    public int IsOddCondition { get; set; }

    [Validate(Condition = "it.isOdd() && it.log10() > 2")]
    public int IsOddAndOverTwoDigitsCondition { get; set; }

    [Validate(Condition = "it.isOdd() || it.log10() > 2")]
    public int IsOddOrOverTwoDigitsCondition { get; set; }
}

Script Conditions are valid if they return a truthy value and have access to the following arguments within their Expression:

If you’re reusing the same Expression a nice solution for maintaining them is in a static class where you can use the AllConditions and AnyConditions helper properties to compose individual checks, e.g:

public static class ValidationConditions
{
    public const string IsOdd = "it.isOdd()";
    public const string IsOver2Digits = "it.log10() > 2";
}

public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse>
{
    [Validate(Condition = ValidationConditions.IsOdd)]
    public int IsOddCondition { get; set; }

    [Validate(AllConditions = new[]{ ValidationConditions.IsOdd, ValidationConditions.IsOver2Digits })]
    public int IsOddAndOverTwoDigitsCondition { get; set; }

    [Validate(AnyConditions = new[]{ ValidationConditions.IsOdd, ValidationConditions.IsOver2Digits })]
    public int IsOddOrOverTwoDigitsCondition { get; set; }
}

Despite not using a validator all #Script Conditions are executed using a custom Fluent Validation IPredicateValidator (called ScriptConditionValidator) so it able to slot right in with all other Property Validators.

Custom Error Codes and Messages

The other aspect of validators that can be overridden declaratively are the ErrorCode and Error Message returned in ServiceStack’s structured Error Response, specified using the ErrorCode and Message Attribute properties:

public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse>
{
    [ValidateNotNull(ErrorCode = "ZError")]
    public string CustomErrorCode { get; set; }
    
    // Overrides both ErrorCode & Message
    [ValidateInclusiveBetween(1,2, ErrorCode = "ZError", 
        Message = "{PropertyName} has to be between {From} and {To}, you: {PropertyValue}")]
    public int CustomErrorCodeAndMessage { get; set; }

    // Overrides ErrorCode & uses Message from Validators
    [ValidateNotNull(ErrorCode = "RuleMessage")]
    public string ErrorCodeRule { get; set; }

    [Validate(Condition = ValidationConditions.IsOdd)]
    public int IsOddCondition { get; set; }

    [Validate(AllConditions = new[]{ ValidationConditions.IsOdd, ValidationConditions.IsOver2Digits }, 
        ErrorCode = "RuleMessage")]
    public int IsOddAndOverTwoDigitsCondition { get; set; }
}

All Error Messages can reference the {PropertyName} and {PropertyValue} in their messages along with any other MessageFormatter placeholders defined by the validator, e.g. the InclusiveBetweenValidator.cs used above also defines the {From}, {To} and {Value} placeholders.

#Script Conditions can define their Error codes in the centralized ConditionErrorCodes Dictionary in the ValidationFeature Plugin where all IsOdd conditions will return the NotOdd custom error code.

The Error Messages can also be defined in the centralized ErrorCodeMessages Dictionary which defines the Error Messages that all failed NotOdd or RuleMessage rules will use, e.g:

Plugins.Add(new ValidationFeature {
    ConditionErrorCodes = {
        [ValidationConditions.IsOdd] = "NotOdd",
    },
    ErrorCodeMessages = {
        ["NotOdd"] = "{PropertyName} must be odd",
        ["RuleMessage"] = "ErrorCodeMessages for RuleMessage",
    }
});

Type Validators

In addition to Property Validators there’s also new support for Type Validators which can be declaratively added to perform top-level validation on Request DTOs.

They behave and function the same as Property Validators where you can use either the typed or the generic [ValidateRequest] attribute.

ServiceStack includes built-in Type Validator attributes for all Authorization Filter Attributes but as they’re decoupled from any implementation they can be safely annotated on Request DTOs without requiring any implementation dependencies.

[ValidateIsAuthenticated]            // or [ValidateRequest("IsAuthenticated")]
[ValidateIsAdmin]                    // or [ValidateRequest("IsAdmin")]
[ValidateHasRole(role)]              // or [ValidateRequest($"HasRole(`{role}`)")]
[ValidateHasPermission(permission)]  // or [ValidateRequest($"HasPermission(`{permission}`)")

Just like Property Validators, the Typed Validator attributes are wrappers around the generic [ValidateRequest] attribute, e.g:

public class ValidateIsAuthenticatedAttribute : ValidateRequestAttribute
{
    public ValidateIsAuthenticatedAttribute() : base("IsAuthenticated") { }
}

Which are also defined in ValidateScripts.cs but instead return a ITypeValidator:

public class ValidateScripts : ScriptMethods
{
    public ITypeValidator IsAuthenticated() => new IsAuthenticatedValidator();
    public ITypeValidator IsAuthenticated(string provider) => new IsAuthenticatedValidator(provider);
    public ITypeValidator HasRole(string role) => new HasRolesValidator(role);
    public ITypeValidator HasRoles(string[] roles) => new HasRolesValidator(roles);
    public ITypeValidator HasPermission(string permission) => new HasPermissionsValidator(permission);
    public ITypeValidator HasPermissions(string[] permission) => new HasPermissionsValidator(permission);
    public ITypeValidator IsAdmin() => new HasRolesValidator(RoleNames.Admin);
}

Custom Type Attributes

The easiest way to create a an ITypeValidator is to inherit from the TypeValidator base class, including both the ErrorCode and Error Message failed requests should return.

An example where you might use one is when testing the pre-condition state of an entity which doesn’t logically map to a property. In the example below we’re validating to ensure that the entity doesn’t have any Foreign Key References:

public class NoRockstarAlbumReferences : TypeValidator
{
    public NoRockstarAlbumReferences() 
        : base("HasForeignKeyReferences", "Has RockstarAlbum References") {}

    public override async Task<bool> IsValidAsync(object dto, IRequest request)
    {
        //Example of using compiled accessor delegates to access `Id` property
        //var id = TypeProperties.Get(dto.GetType()).GetPublicGetter("Id")(dto).ConvertTo<int>();

        var id = ((IHasId<int>)dto).Id;
        using var db = HostContext.AppHost.GetDbConnection(request);
        return !await db.ExistsAsync<RockstarAlbum>(x => x.RockstarId == id);
    }
}

Then we need to register it as a custom script method to be able to reference it in [ValidateRequest]:

public class MyValidators : ScriptMethods
{
    public ITypeValidator NoRockstarAlbumReferences() => new NoRockstarAlbumReferences();
}

Which we can now declaratively reference by script method name:

[ValidateRequest("NoRockstarAlbumReferences")]
public class ExampleValidators : ICreateDb<Rockstar>, IReturn<RockstarWithIdResponse>, IHasId<int>
{
    public int Id { get; set; }
    
    [ValidateNotNull] //doesn't get validated if ValidateRequest is invalid
    public string NotNull { get; set; }
}

Type Validators are executed before any property validators, which if failed wont be executed.

Type Script Conditions

Type Validators can also execute #Script expressions where we could implement the above FK check inline using a sync Database Script:

[ValidateRequest(Condition = "!dbExistsSync('SELECT * FROM RockstarAlbum WHERE RockstarId = @Id', { it.Id })", 
    ErrorCode = "HasForeignKeyReferences")]
public class ExampleValidators : ICreateDb<Rockstar>, IReturn<RockstarWithIdResponse>
{
    public int Id { get; set; }
    
    [ValidateNotNull] //doesn't get validated if ValidateRequest is invalid
    public string NotNull { get; set; }
}

Note the condition needs to return a truthy value so you’d need to use the sync DB Script APIs to return a boolean instead of an async Task.

Type Validators can also specify custom Error Codes and Error Messages, they can also specify a custom HTTP Error StatusCode that failed requests should return.

[ValidateRequest(Condition = "it.Test.isOdd() && it.Test.log10() > 2",
    ErrorCode = "NotOddAndOver2Decimals", Message = "Pre-condition Failed", StatusCode = 401)]
public class ExampleValidators : ICreateDb<ExampleValidator>, IReturn<EmptyResponse> { }

DB Validation Rules

Both Property and Type Validators can also be sourced from a dynamic source with both Memory and RDBMS implementations included along with a Management HTTP API to be able to manage them remotely. Dynamic Validation Rules are cacheable locally giving them the same performance profile as declarative attributes in code whose caches are only invalidated once they’ve been updated, upon which they’ll come into immediate effect.

Here’s a Modular Startup class you can drop into a ServiceStack Project to enable maintaining declarative Validation Rules in your configured RDBMS:

public class ConfigureValidation : IConfigureServices, IConfigureAppHost
{
    public void Configure(IServiceCollection services)
    {
        // Add support for dynamically generated db rules
        services.AddSingleton<IValidationSource>(c => 
            new OrmLiteValidationSource(c.Resolve<IDbConnectionFactory>()));
    }

    public void Configure(IAppHost appHost)
    {
        appHost.Plugins.Add(new ValidationFeature());
        appHost.Resolve<IValidationSource>().InitSchema();
    }
}

DB Validation rules can be added programmatically, this example below adds 1x Type Validator and 2x Property Validators to the DynamicRules Request DTO:

var validationSource = container.Resolve<IValidationSource>();
validationSource.SaveValidationRulesAsync(new List<ValidationRule> {
    new ValidationRule { Type  = nameof(DynamicRules), Validator = "IsAuthenticated" },
    new ValidationRule { Type  = nameof(DynamicRules), Validator = "NotNull", 
                         Field = nameof(DynamicRules.LastName) },
    new ValidationRule { Type  = nameof(DynamicRules), Validator = "InclusiveBetween(13,100)", 
                         Field = nameof(DynamicRules.Age) },
});

Admin Users can also manage these rules remotely using the ModifyValidationRules Service defined below:

public class ModifyValidationRules : IReturnVoid
{
    public string AuthSecret { get; set; }

    public List<ValidationRule> SaveRules { get; set; }

    public int[] DeleteRuleIds { get; set; }

    public int[] SuspendRuleIds { get; set; }

    public int[] UnsuspendRuleIds { get; set; }
    
    public bool? ClearCache { get; set; }
}

ServiceStack Studio Validators UI

ServiceStack Studio utilizes the above ModifyValidationRules for its support for managing DB Validation rules, with an optimized UX that lets you quickly select & configure all built-in & registered property & type validators where they’re instantly applied.