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.