On this page

Constructing and validating a customer

Find the invariant owner and the permitted mutation paths. Compare native constructors, property setters and validation results instead of counting declarations. See readability under real business rules for policy branches, initialization and coordination.

An account-opening screen supplies a positive customer ID, a name and an email address. Construction removes leading and trailing ASCII spaces from the name. The ID and name are fixed after construction; the email can be corrected later. Construction rejects a nonpositive ID and absent name or email.

A separate validation operation returns all applicable errors in order. The name must contain 2–60 characters. For this deliberately small application rule, email must contain exactly one @, with text on both sides and no ASCII spaces. This is not a general email-address parser. A production application can use a validation library and a delivery-verification workflow.

To make length comparisons equivalent, this example's name inputs are ASCII. .NET String length counts UTF-16 code units; Python String length counts Unicode code points. Applications accepting international names should choose and document an appropriate length policy.

DirectSharp

// Customer construction: keep validation and the object invariant together.
public class Customer
    public readonly CustomerId as Integer
    public readonly Name as String
    public Email as String

    public constructor(id as Integer, inputName as String, inputEmail as String)
        require id > 0 otherwise new Global.System.ArgumentOutOfRangeException("id")
        require inputName is not nothing otherwise new Global.System.ArgumentNullException("inputName")
        require inputEmail is not nothing otherwise new Global.System.ArgumentNullException("inputEmail")

        CustomerId = id
        Name = inputName.Trim(" " as Char)
        Email = inputEmail
    end constructor

    public action Validate()
        returns Global.System.Collections.Generic.List <of String>
        let errors = new Global.System.Collections.Generic.List <of String>()

        if Name.Length < 2 or Name.Length > 60 then
            errors.Add("Name must contain 2 to 60 characters")
        end if

        if Global.System.String.IsNullOrEmpty(Email) then
            errors.Add("Email is required")
        else
            let at = Email.IndexOf("@")
            if at <= 0 or at >= Email.Length - 1 or at <> Email.LastIndexOf("@") or Email.IndexOf(" ") >= 0 then
                errors.Add("Email must contain one @ and no spaces")
            end if
        end if

        return errors
    end action
end class

Public stored members are properties. readonly allows assignment during construction and prevents reassignment afterward, including from another action of this class. An ordinary constructor parameter is not stored automatically: the three assignments are explicit. Email remains a writable property; validation handles its absence even if application code later assigns nothing.

The fully qualified collection name and String API names identify ordinary .NET library calls. They are not validation keywords.

C#

// Customer construction: keep validation and the object invariant together.
using System;
using System.Collections.Generic;

public class Customer
{
    public int CustomerId { get; }
    public string Name { get; }
    public string? Email { get; set; }

    public Customer(int id, string inputName, string inputEmail)
    {
        if (id <= 0)
            throw new ArgumentOutOfRangeException(nameof(id));
        if (inputName is null)
            throw new ArgumentNullException(nameof(inputName));
        if (inputEmail is null)
            throw new ArgumentNullException(nameof(inputEmail));

        CustomerId = id;
        Name = inputName.Trim(' ');
        Email = inputEmail;
    }

    public List<string> Validate()
    {
        var errors = new List<string>();
        if (Name.Length < 2 || Name.Length > 60)
            errors.Add("Name must contain 2 to 60 characters");

        if (string.IsNullOrEmpty(Email))
        {
            errors.Add("Email is required");
        }
        else
        {
            int at = Email.IndexOf('@');
            if (at <= 0 || at >= Email.Length - 1 ||
                at != Email.LastIndexOf('@') || Email.IndexOf(' ') >= 0)
                errors.Add("Email must contain one @ and no spaces");
        }
        return errors;
    }
}

Getter-only auto-properties express constructor-assigned state. The nullable annotation on Email describes a possible later absence to C#'s flow analysis. It does not automatically produce a runtime guard. Constructor checks remain useful when calls arrive from reflection, deserialization or another .NET language.

VB.NET

' Customer construction: keep validation and the object invariant together.
Option Strict On
Option Explicit On
Option Infer On
Imports System
Imports System.Collections.Generic

Public Class Customer
    Public ReadOnly Property CustomerId As Integer
    Public ReadOnly Property Name As String
    Public Property Email As String

    Public Sub New(id As Integer, inputName As String, inputEmail As String)
        If id <= 0 Then Throw New ArgumentOutOfRangeException(NameOf(id))
        If inputName Is Nothing Then Throw New ArgumentNullException(NameOf(inputName))
        If inputEmail Is Nothing Then Throw New ArgumentNullException(NameOf(inputEmail))

        CustomerId = id
        Name = inputName.Trim(" "c)
        Email = inputEmail
    End Sub

    Public Function Validate() As List(Of String)
        Dim errors As New List(Of String)()
        If Name.Length < 2 OrElse Name.Length > 60 Then
            errors.Add("Name must contain 2 to 60 characters")
        End If

        If String.IsNullOrEmpty(Email) Then
            errors.Add("Email is required")
        Else
            Dim at As Integer = Email.IndexOf("@"c)
            If at <= 0 OrElse at >= Email.Length - 1 OrElse
                at <> Email.LastIndexOf("@"c) OrElse Email.IndexOf(" "c) >= 0 Then
                errors.Add("Email must contain one @ and no spaces")
            End If
        End If
        Return errors
    End Function
End Class

ReadOnly Property expresses the same public construction-only state. String can hold Nothing; this declaration does not add C#-style nullable reference annotations. OrElse keeps the Boolean evaluation short-circuiting.

Python

# Customer construction: keep validation and the object invariant together.
class Customer:
    def __init__(self, customer_id: int, input_name: str, input_email: str):
        if type(customer_id) is not int:
            raise TypeError("customer_id must be an integer")
        if customer_id <= 0:
            raise ValueError("customer_id must be positive")
        if input_name is None:
            raise ValueError("input_name is required")
        if input_email is None:
            raise ValueError("input_email is required")
        if not isinstance(input_name, str) or not isinstance(input_email, str):
            raise TypeError("name and email must be strings")

        self._customer_id = customer_id
        self._name = input_name.strip(" ")
        self.email: str | None = input_email

    @property
    def customer_id(self) -> int:
        return self._customer_id

    @property
    def name(self) -> str:
        return self._name

    def validate(self) -> list[str]:
        errors: list[str] = []
        if len(self.name) < 2 or len(self.name) > 60:
            errors.append("Name must contain 2 to 60 characters")

        if self.email is None or self.email == "":
            errors.append("Email is required")
        else:
            at = self.email.find("@")
            if (at <= 0 or at >= len(self.email) - 1
                    or at != self.email.rfind("@") or " " in self.email):
                errors.append("Email must contain one @ and no spaces")
        return errors

Python getter properties reject ordinary assignment to name or customer_id. The underscore convention does not enforce private storage: code can still mutate _name directly. The static versions enforce their property rules through the type system and CLR member surface. Python's constructor includes runtime type checks because annotations alone do not enforce the contract; the later email assignment assumes application code respects its annotation.

Calling the same model

Operation Result
Construct ID 42, name " Ana ", email "ana@example.com" Name becomes "Ana"; validation returns no errors
Construct ID 0 Construction fails
Construct with absent name or email Construction fails
Construct name " A ", email "bad" Construction succeeds; validation returns two errors in order
Replace email with absent value Validation returns "Email is required"
Assign Name after construction Rejected by the public property contract

Construction guards protect the object's required state. Validation reports editable input problems together. Putting every rule in the constructor would prevent creation of an object that a form needs to display with error messages. Leaving every rule out of construction would permit a meaningless identity.

Using FluentValidation on .NET

C#, VB.NET and DirectSharp can share FluentValidation. With the NuGet package and FluentValidation namespace available, these validators register the same name-length rule for their Customer model. The full email policy above remains separate; substituting a library's email validator would change that policy.

DirectSharp:

import Global.FluentValidation

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

    public constructor()
        RuleFor(.Name).Length(2, 60)
    end constructor
end class

C#:

using FluentValidation;

public class CustomerNameValidator : AbstractValidator<Customer>
{
    public CustomerNameValidator()
    {
        RuleFor(customer => customer.Name).Length(2, 60);
    }
}

VB.NET:

Imports FluentValidation

Public Class CustomerNameValidator
    Inherits AbstractValidator(Of Customer)

    Public Sub New()
        RuleFor(Function(customer) customer.Name).Length(2, 60)
    End Sub
End Class

The DirectSharp leading-dot selector is target-typed by RuleFor as an expression tree over Customer. The other versions write that expression-tree lambda explicitly. All three call ordinary library methods. Python's native validators use a different ecosystem; a Python implementation of the small rule is shown above without pretending it executes FluentValidation.

Reusable DirectSharp class kind declarations can abbreviate repeated validator headers. They do not replace validation libraries or introduce a domain-specific rule language. See classes and validation.

Keeping application code ordinary

The customer and its validator are ordinary classes. A service can construct them, inject collaborators through its normal dependency mechanism and call validation before saving. FluentValidation remains a normal .NET dependency, so the application keeps access to its documented APIs, integrations and diagnostics. A DirectSharp-specific adapter is not required.

Use class kind when several application classes share a recurring declaration pattern, such as the same generic validator base. Keep the concrete class responsible for the actual rules. C# and VB.NET can share behavior through ordinary base classes and library helpers; Python can use ordinary classes and functions. These choices concern how much repeated structure a team wants to name, rather than whether validation needs new language grammar.

Construction-only properties also have limits. They prevent reassignment of the stored binding; they do not make an entire object graph immutable. A readonly reference to a mutable list can still expose changes to that list. Likewise, validating this customer does not reserve an email address, check a database uniqueness constraint or make a later save atomic. Those are application and persistence contracts, handled explicitly in every version.

Next: aggregating an import · All comparisons