Designing an application people can follow
A business application has several boundaries: user input, domain policy, persistence and delivery. Each boundary should expose what can happen there. Ordinary classes, actions and interfaces provide most of the structure; direct .NET libraries supply the platform work.
FluentValidation / selectors: validate application input
FluentValidation is a normal .NET dependency. Reference its package and import its namespace so its extension methods are available. This program validates a customer, reports all failures and registers only valid input:
import Global.FluentValidation.*
public class Customer
public Name as String = ""
public Email as String = ""
public Country as String = ""
public CreditLimit as Decimal = 0
end class
public class CustomerValidator
inherits Global.FluentValidation.AbstractValidator <of Customer>
public constructor()
RuleFor(.Name).NotEmpty().Length(2, 100)
RuleFor(.Email).NotEmpty().EmailAddress()
RuleFor(.Country).NotEmpty().Length(2, 2)
RuleFor(.CreditLimit).InclusiveBetween(0, 50000)
end constructor
end class
public class CustomerOnboarding
private readonly validator as Global.FluentValidation.IValidator <of Customer>
public constructor(validator as Global.FluentValidation.IValidator <of Customer>)
require arguments.validator is not nothing
self.validator = arguments.validator
end constructor
public action Register(customer as Customer)
returns Boolean
require customer is not nothing
let validation = validator.Validate(customer)
if not validation.IsValid then
for_each failure in validation.Errors
Global.System.Console.WriteLine("{{failure.PropertyName}}: {{failure.ErrorMessage}}")
end for_each
return false
end if
Global.System.Console.WriteLine("Register {{customer.Name}} in {{customer.Country}}")
return true
end action
end class
public class OnboardingApplication
public static action Run()
let customer = new Customer()
customer.Name = "Ana Petrova"
customer.Email = "ana@example.com"
customer.Country = "MK"
customer.CreditLimit = 2500
let validator = new CustomerValidator()
let onboarding = new CustomerOnboarding(validator)
let accepted = onboarding.Register(customer)
Global.System.Console.WriteLine(if(accepted, "Accepted", "Please correct the input"))
end action
end class
The validator describes input constraints through ordinary library calls. Its expressions become the expression trees that RuleFor expects. CustomerOnboarding depends on the interface, so a caller can supply the concrete validator or another compatible implementation.
The Boolean result means this small program distinguishes accepted input from validation failure. A web application needs a richer response containing field failures, and a persisted workflow needs a repository and transaction policy. Keep those additions at their appropriate boundaries instead of adding database or HTTP behavior to the validator.
The two-character Country rule validates shape only. It does not prove that a country exists. Similarly, EmailAddress checks the library's accepted format; it does not prove ownership or deliverability. A verification workflow remains a separate operation.
classes / actions: separate application responsibilities
This validator uses an ordinary base class. If an application has many validators with the same repeated inheritance and interface policy, a class kind can standardize that header. The constructor and rule calls stay ordinary code.
That order also applies to other frameworks. An ASP.NET controller is an ordinary class with framework attributes and action methods. A Dapper repository calls normal extension methods on a connection. EF Core configuration uses ordinary generic interfaces. A compiler feature adapter can help a real integration problem, but it should earn its place by simplifying an established pattern.
require / validation: choose the right boundary
Validation at an input boundary is not a substitute for a domain invariant. Two valid requests can still conflict with each other. Reserving the last item, transferring money or changing a credit limit requires a policy about current state and concurrent changes.
For a typical order workflow, use this division:
| Component | Responsibility | Typical result |
|---|---|---|
| Request model and validator | Check supplied fields and report correctable input errors | Field failures or valid input |
| Application action | Arrange a use case and call its dependencies | A success or meaningful business outcome |
| Domain object or policy action | Protect rules about a valid state transition | New state or a rejected operation |
| Repository | Load and save through the database's transaction rules | Stored values or a persistence failure |
| HTTP/controller boundary | Translate application outcomes into protocol responses | HTTP status and response body |
Use the same rule in one obvious place. A controller, validator and repository each reimplementing the same discount policy makes later changes hard to review.
needs / constructors: expose dependencies
Constructor parameters work well for a small number of dependencies. The needs and service constructs support a reusable dependency contract and composition for larger applications. In either case, the dependency's type should tell readers what the component can do.
Avoid a service locator call in every action when the dependency could be stated once. Avoid global mutable service state. Keep resource ownership distinct from usage: a caller-provided shared HttpClient is not owned by each action that sends a request.
The DirectSharp standard library supplies language-adjacent contracts such as Gate and consumable sources. The existing .NET ecosystem supplies logging, configuration, database drivers, validation, testing and web hosting. Use each directly where its contract fits.
try / catch: preserve useful failure information
An application boundary can turn an expected business rejection into a response. It should keep unexpected faults distinguishable from those rejections. A database outage is not "customer invalid," and a canceled request is not "order declined."
Pass cancellation to imported asynchronous APIs using their standard token parameters. Keep a resource's lifetime around the actual async work. Use structured concurrency when several independent operations must finish together, and avoid shared captured writes without explicit coordination.
For more complete applications, see the examples and .NET integration. The standard library explains its core contracts; the reference provides syntax and diagnostics for individual constructs.
Practice: application boundaries
Follow one validation change through the Customer service, HTTP boundary and storage. Use the walkthrough for the connected declarations.