On this page

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.

Previous: Services and events · Next: Advanced integration