On this page

Model and validation

A customer form needs to explain what is wrong with input without throwing an exception for every correction. FluentValidation supplies the rules and a result containing property failures. DirectSharp supplies ordinary classes and calls.

This is the first application step. The model below will be used unchanged by the repository, web API and customer dashboard. Id identifies a stored customer; IsActive is an application state, not a validation result. New input starts inactive. Activation is a separate application decision.

The model and rules

Place these declarations in Customer.ds and CustomerValidator.ds. The validator file imports FluentValidation because its fluent calls include extension methods. Reference the FluentValidation package in the surrounding .NET project.

import Global.System
import Global.FluentValidation

public class Customer
    public Id as Guid = Guid.Empty
    public Name as String = ""
    public Email as String = ""
    public IsActive as Boolean = false

    public constructor()
    end constructor
end class

public class CustomerValidator
    inherits Global.FluentValidation.AbstractValidator <of Customer>

    public constructor()
        RuleFor(.Name).NotEmpty().Length(2, 100)
        RuleFor(.Email).NotEmpty().EmailAddress()
    end constructor
end class

.Name and .Email are typed property selectors. RuleFor's parameter asks for an expression tree, so the library can inspect which property is being validated. These are not the current String values of the properties. The same lambda syntax can target a delegate when an API expects one.

The rules describe this application's input policy. EmailAddress checks the library's email-format policy; it does not prove that a mailbox exists or belongs to the user. Keep ownership verification in a separate workflow.

Calling the validator

This class keeps the validator reusable and handles errors at a console boundary. Invoke Run from the host's entry point. Check returns the actual library result so a web boundary can use structured failures instead of parsing console messages.

import Global.System
import Global.FluentValidation.Results

public class CustomerOnboarding
    private readonly validator as CustomerValidator

    public constructor(customerValidator as CustomerValidator)
        require customerValidator is not nothing
        validator = customerValidator
    end constructor

    public action Check(customer as Customer)
        returns ValidationResult
        require customer is not nothing
        return validator.Validate(customer)
    end action

    public action Run(name as String, email as String)
        returns Boolean
        let customer = new Customer()
        customer.Id = Guid.NewGuid()
        customer.Name = name
        customer.Email = email

        let validation = Check(customer)
        if not validation.IsValid then
            Console.WriteLine("Please correct the following fields:")
            for_each failure in validation.Errors
                Console.WriteLine("{{failure.PropertyName}}: {{failure.ErrorMessage}}")
            end for_each
            return false
        end if

        Console.WriteLine("Ready to register {{customer.Name}}")
        return true
    end action
end class

Check uses a contract because passing an absent model is a programming error at this boundary. An empty name is expected user input and produces a validation failure. The two cases need different treatment.

Run does not persist or activate the customer. It first constructs the input, then validates it once, then reports the outcome. This leaves the timing visible to a reader and keeps partially filled form state out of property setters.

Reusing a validator header with a class kind

If an application has many validators, a kind can share their inheritance and exposed interface. Replace the earlier CustomerValidator declaration with this pair; keep the Customer model and CustomerOnboarding caller.

import Global.FluentValidation

public class kind Validator <of T>
    exposes Global.FluentValidation.IValidator <of T>
    inherits Global.FluentValidation.AbstractValidator <of T>
    default scope internal
end class kind

public class CustomerValidator is Validator <of Customer>
    public constructor()
        RuleFor(.Name).NotEmpty().Length(2, 100)
        RuleFor(.Email).NotEmpty().EmailAddress()
    end constructor
end class

The kind contributes the class header. CustomerValidator still owns its constructor and rules. There is no extra runtime Validator base class between it and AbstractValidator<Customer>.

is Validator <of Customer> applies the kind to a class declaration. In a type position, Validator <of Customer> denotes its exposed IValidator<Customer> contract. A public modifier on the concrete class overrides the kind's internal default scope.

Use the ordinary class when this is the only validator. Introduce the kind when the shared declaration actually helps maintain a family of validators.

Useful checks

Input Application expectation
Ana, ana@example.com Valid
Empty name, valid email Name failure
One-character name, valid email Name-length failure
101-character ASCII name, valid email Name-length failure
Valid name, empty email Email failure
Valid name, invalid email Email-format failure

Test property names and validity rather than localized error text. Library version and cascade configuration can affect the exact set and order of errors. The web API example turns the result into a structured HTTP response; the repository handles storage independently.

Next: the application service. Back to the showcase.