Design by Contract library for .NET 8+
Based on Java uContract 2.0.1
A modern Design by Contract (DBC) library for .NET. This is a direct port of the Java uContract 2.0.1 library with .NET-specific improvements.
uContract.NET follows Semantic Versioning; the public API is stable as of 1.0.0. Changes between releases are tracked in CHANGELOG.md. The current version is shown by the NuGet badge above.
- Quick Start
- Features
- API Reference - Quick overview (Complete API Docs →)
- Configuration - Environment variables (Detailed Config →)
- Examples - Minimal example (15+ More Examples →)
- Requirements
- Differences from Java Version
- Documentation
- License
dotnet add package uContractusing uContract;
public class BankAccount
{
private decimal _balance;
private string _owner;
public BankAccount(string owner, decimal initialBalance)
{
// Preconditions: Validate inputs
Contract.RequireNotNull("Owner", owner);
Contract.Require("Initial balance non-negative", () => initialBalance >= 0);
_owner = owner;
_balance = initialBalance;
}
public void Transfer(decimal amount)
{
// Precondition: Valid amount
Contract.Require("Amount positive", () => amount > 0);
// Capture old state for postcondition
var oldBalance = Contract.Old(() => _balance);
// Business logic
_balance -= amount;
// Postcondition: Balance decreased correctly
Contract.Ensure("Balance decreased", () => _balance == oldBalance - amount);
}
private void CheckInvariant()
{
// Invariant: Balance never negative
Contract.Invariant("Balance non-negative", () => _balance >= 0);
}
}- ✅ Preconditions: Validate method inputs and caller state
- ✅ Postconditions: Verify method results and state changes
- ✅ Invariants: Enforce class-level constraints
- ✅ Check Assertions: Runtime checks for general conditions
- ✅ State Capture:
Old<T>()for comparing before/after states - ✅ Field Comparison:
EnsureAssignable<T>()with reflection-based validation
- 🚀 Zero overhead when disabled: Lazy evaluation via
Func<bool>delegates - ⚙️ Runtime configuration: Control contracts via environment variables
- 🎯 DDD-focused: Designed for domain models and aggregate roots
- 📦 Zero external dependencies: Uses only .NET built-in APIs
- 🔒 Thread-safe: AsyncLocal support for async/await
- 💎 Strongly typed: Generic constraints ensure type safety
Traditional Design by Contract tools face a sustainability challenge - most extensions are abandoned or dormant because they require custom compilers/preprocessors that must constantly keep up with language evolution.
Root contracting solves this with a simple utility library approach:
- ✅ No custom tooling - Uses standard C# features only (no IL weaving, no code generation)
- ✅ Language-independent design - Same approach can be ported to Java, Go, Python, Rust
- ✅ Zero dependencies - Only .NET built-in APIs (
System.Reflection)
Learn more:
- 📖 Research paper - Formal foundation, experiments, and comparisons with other DbC tools
- 📋 Architecture Decision Records - Design rationale and trade-offs for each decision
- 📚 DDD Integration Guide - How to apply contracts with DDD patterns
📖 Complete Documentation: API_REFERENCE.md — configuration, exceptions, best practices, and performance
uContract.NET's public API is organized into 4 categories:
| Category | Methods | Purpose |
|---|---|---|
| Preconditions | Require, RequireNotNull, RequireNotEmpty |
Validate inputs at method entry |
| Postconditions | Ensure, EnsureNotNull, EnsureResult, EnsureImmutableCollection, EnsureAssignable |
Verify results and state changes |
| Invariants | Invariant, InvariantNotNull |
Enforce class-level constraints |
| Helpers | Check, Ignore, Old, Imply, IfAndOnlyIf, FollowsFrom, CheckUnsupportedOperation |
Runtime assertions and utilities |
⚙️ Detailed Configuration: USAGE_EXAMPLES.md - Configuration Examples
Control contract evaluation at runtime via environment variables.
| Variable | Values | Default | Purpose |
|---|---|---|---|
DBC |
on/off |
on |
Default for every type that sets no flag of its own |
DBC_PRE |
on/off |
on |
Preconditions — overrides DBC |
DBC_POST |
on/off |
on |
Postconditions — overrides DBC |
DBC_INV |
on/off |
on |
Invariants — overrides DBC |
DBC_CHECK |
on/off |
on |
Check statements — overrides DBC |
DBC_DOC |
on/off |
off |
Diagnostic output (independent of the above) |
Resolution order, highest first: the per-type flag, then DBC, then the default.
The per-type flags override DBC rather than selecting a subset of it — so DBC=on DBC_INV=off
leaves invariants disabled, and DBC=off DBC_PRE=on enables preconditions only. See
ADR-0004, which rejects the
alternative where a per-type flag could not re-enable what DBC had disabled.
Configuration is read once and frozen at process start, so setting a variable afterwards has no
effect. To see what actually resolved rather than what you asked for, read
ContractConfiguration.PreconditionsSource and its siblings — each names whichever of the per-type
flag, DBC, or the default decided that type — or set DBC_DOC=on to have the summary
written to stderr.
Contracts are enabled by default. These commands turn them off:
Windows (PowerShell):
$env:DBC="off"Linux/macOS:
export DBC=off- Default: All contracts enabled in every build configuration; set
DBC=offto disable - Parameter validation: Always executed (even when DBC is disabled). Two exceptions.
EnsureAssignable<T>()never rejects anullactualorexpected: it comparesnullas a value, and returns without comparing with postconditions off or while another contract check is running; its pattern array is always checked.EnsureImmutableCollection<T>()checkscollectionfornullonly with postconditions on and not while another contract check is running - What
EnsureAssignable<T>()compares: what the declaredTshows. Astring, a delegate, a nullable value type, aFrozenSet<T>or aFrozenDictionary<TKey, TValue>, or an array that is passed asactualandexpectedis compared as a whole only whenTis that type (for an array: an array type orSystem.Array). Passed from a member or a local typed asobject,Tisobjectand nothing is compared beyondnull, even for two different strings or for two arrays with a different element. (A member typed asobjectinside the compared object is compared as the type it holds.) A base class asTthat is not compared as a whole (System.Array,Delegate,MulticastDelegate, aFrozenSet<T>and aFrozenDictionary<TKey, TValue>are) compares the members it declares or inherits, an interface only its own properties, in neither case what the runtime type adds; so two arrays passed as an interface (IList<int>,IEnumerable<int>) are not compared element by element. For everyTthat is compared by its members, and for each nested object or element that the comparison compares by its members (listed from its runtime type), the private fields declared on its base classes are compared too (#40). The walk up the base classes stops at the first class of the .NET runtime and libraries: a class whose assembly is signed with a public key token of the .NET 8 shared frameworks (the runtime libraries, ASP.NET Core,Microsoft.Extensions.*, WPF and Windows Forms). A library that is signed otherwise or not signed is walked. The fields of a framework base class that are not private (List<T>._size) are compared, as every inherited field that is not private is; its private fields are not (measured forException,ObservableCollection<T>andDeflateStream). Static fields are not compared. An auto-property that is not public and is declared on a base class is compared as its backing field (measured: a private and a protected one; the entry reads<Note>k__BackingField); a public auto-property of a base class, also one with a protected setter, is compared once, through the property. A nested collection class of yours is still compared by its elements. A change of such a field is a violation unless the member is listed as assignable: raising a domain event that a base aggregate class keeps in a private_domainEventslist is reported as_domainEvents(measured). The entries most likely to surprise are lazily cached values (for example a cached hash code in a value-object base class, set the first time the object is hashed inside the checked method) and event subscriptions (the backing field of an event declared on a base class that is not part of .NET); inside a nested object, such a change is reported under the top-level member. List the member as assignable, or check withContract.Ensure. Under Native AOT these fields are compared only when the base class is preserved: see Trimming and Native AOT. One instance of a type thatOld<T>()shares, and its state can change (aStream, aTask, aCancellationTokenSource, a class derived fromComponent), passed asactualand asexpectedis not compared: the method throwsInvalidOperationException, whateverTis,objectincluded. Check each state member withContract.Ensureand a value taken withOld; to check only that a reference was not replaced, useContract.EnsurewithReferenceEquals. Astring, a delegate, aType, aMemberInfo, aRegexand a frozen collection are shared too, and fixed: this rule does not apply to them. In a class derived from aRegex, aTypeor aMemberInfo,Old(() => this)returnsthistoo, so the pair onthisreports no change of the class's own state, and it throwsTargetInvocationExceptionwhen a getter of a public property ofTthrows (measured for a class derived fromTypeDelegatorand one derived fromMemberInfo): check each state member withContract.Ensurethere too. The full list is in Types thatOld<T>()shares - Zero overhead: Conditions are never evaluated when disabled (lazy evaluation), except by
Ignore, which is never disabled
📚 More Examples: USAGE_EXAMPLES.md (15+ real-world scenarios)
- DDD Quick Start: Minimal aggregate root example → USAGE_EXAMPLES.md
- State Capture: Using
Old<T>()for complex objects → USAGE_EXAMPLES.md - Field Validation: Using
EnsureAssignable<T>()→ USAGE_EXAMPLES.md - Real-World Scenarios: Banking, e-commerce, inventory systems → USAGE_EXAMPLES.md
- Complete DDD Guide: Value objects, domain services (10+ patterns) → DDD_INTEGRATION_GUIDE.md
- .NET 8.0 or later
- C# 12 (nullable reference types enabled)
- No external dependencies (uses only .NET built-in APIs)
- ✅ All reference types (
class,record) (see Trimming and Native AOT below) - ✅ All value types (
struct,record struct) - ✅ Generic types with appropriate constraints
- ✅ Collections (IEnumerable, List, Array, ImmutableList, etc.) (see Trimming and Native AOT below)
Old<T>() and EnsureAssignable<T>() have limited support in trimmed and Native AOT applications. Setting DBC_POST=off turns off every postcondition check, these two helpers included. With it, Old<T>() returns default and EnsureAssignable<T>() returns without comparing, so the documented Old + EnsureAssignable pair does nothing.
Old<T>()copies the value field by field with reflection. It needs no JSON setup and no dynamic code. Trimming preserves the fields declared onT. A field that the trimmer removed from reflection keeps its bitwise value, so the object it refers to is shared with the original, and a later change to that object passes silently. This can affect private fields ofT's base classes (for example a base-class auto-property that holds aList<Line>), the fields of the types thatT's fields refer to, and runtime types other thanT. Under Native AOT,EnsureAssignable<T>()does not compare a private field of a base class that is not preserved either (see below). Preserve such types withDynamicDependency; put it onMainor on any method that runs. A caller that forwards its own generic type parameter toOld<T>gets trim warningIL2091until it adds[RequiresUnreferencedCode]or the same[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]to that parameter. SuppressingIL2026does not silence it.EnsureAssignable<T>()compares the members ofTin every kind of build, except whenTis astring, a delegate type, a nullable value type, aFrozenSet<T>or aFrozenDictionary<TKey, TValue>, or an array type orSystem.Array: those are compared as a whole (aFrozenSet<T>or aFrozenDictionary<TKey, TValue>element by element, in enumeration order; two arrays element by element), and nothing needs to be preserved forT=string,int?orDateTime?. For an array type orSystem.ArrayasT, the members of the type of the elements may not be preserved: preserve that type withDynamicDependency. One instance of a type thatOld<T>()shares, and its state can change, passed asactualand asexpectedis not compared either: the method throwsInvalidOperationExceptionin every kind of build, whateverTis,objectincluded (see Types thatOld<T>()shares). A caller that forwards its own generic type parameter toEnsureAssignable<T>gets trim warningIL2091until it adds[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]to that parameter.- Under Native AOT, the private fields declared on a base class are not preserved, so
EnsureAssignable<T>()does not compare them andOld<T>()shares the objects they refer to: a change of such a field of a base class ofTpasses (measured) (#40). To compare them, preserve the base class, for example with[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, typeof(Base))](orDynamicallyAccessedMemberTypes.All) on a method that runs; the change is then reported (measured). The objects that those fields hold are compared by their members, so preserve their types too; otherwise two unequal instances of such a type end inInvalidOperationException("cannot compare", read from the code). This needs the type to have at least one other member visible: the rule for a type with no visible members is decided on its members before the private fields of base classes are added, so a type whose only state is such fields still counts as a type with no visible members. In a trimmed app without Native AOT, these fields may not be preserved either; they are then not compared, and that is not detected (read from the code, not measured). An inherited property with a private getter, and a property whose derived class overrides only the setter, are compared under Native AOT as under the JIT (measured). - Under Native AOT, when it reaches a nested class or collection element whose members are not preserved, it asks that class's
Equalsfirst. IfEqualsreturnstrue, the values are equal. If it returnsfalse, the method throwsInvalidOperationException. This also applies to a nested class that genuinely has no properties or fields (for example an empty marker class): it throws only when itsEqualsreports the values unequal. To resolve it, either list the top-level member as assignable (use the name that the message gives: the member name without its declaring type, and for a backing field the property name it backs; patterns are regular expressions matched against top-level member names only) or preserve the type withDynamicDependency. When another member of the compared type has the same name, the path in the message shows the member with its declaring type (ShelfDerived.ShelfBase.X), but the advice still names it without its declaring type (X), which also excuses the other members of that name. Below a nullable value type asTthere is no member to list: preserve the type, or test both values fornulland pass the unwrapped values (the member can then be listed). Below aFrozenSet<T>or aFrozenDictionary<TKey, TValue>asTthere is no member to list either: preserve the type. Below an array type orSystem.ArrayasTthere is no member to list either: preserve the type that the message names. The path in that message starts at the parameter name:actual[]is an element of the array. Without anEqualsoverride,falseonly means that the two values are different instances, and anOldcopy is always a different instance, so for such a type listing the member is the practical choice. Framework types such asLazy<T>,SemaphoreSlimandCancellationTokenSourceare shared byOld<T>(), as are astringand a delegate. Other shared instances (seeOld<T>()) that a member or element holds are compared by reference; astringis compared withEqualsand a delegate by its methods, also whenTitself is astringor a delegate type. Two instances of aFrozenSet<T>or aFrozenDictionary<TKey, TValue>that a member or element holds are compared element by element, in enumeration order (a dictionary entry by entry, key with key and value with value), also whenTitself is one of the two types; two instances of one other class declared inSystem.Collections.Frozen(two instances of a class of yours declared there) are still compared by reference when a member or element holds them. A value type declared inSystem.Collections.Frozenand an array whose element type is declared inSystem.Collections.Frozenare not shared: they are copied byOld<T>()and compared like any other value type or array. The limits of the comparison of aFrozenSet<T>or aFrozenDictionary<TKey, TValue>, and what to write instead, are in Types thatOld<T>()shares. AsT, two different instances of a shared type other than astring, a delegate type, aFrozenSet<T>and aFrozenDictionary<TKey, TValue>(twoStreams) are still compared member by member (#40), and so is a type that is shared, and fixed (aRegex), also for one instance on both sides. The getters of the public properties ofTthen run, and a getter that throws ends the call inTargetInvocationException: twoMemoryStreams asT=Streamend so, from theReadTimeoutgetter ofStream. List the properties that throw as assignable, or compare withContract.Ensure(see Types thatOld<T>()shares). One instance of a type that is shared, and its state can change, on both sides is not compared: the method throws, as said above. - A property that returns a new instance of its own type on every read (for example
DirectoryInfo.Root) makesEnsureAssignable<T>()grow without bound, where 2.0.0 overflowed the stack. Fields-only comparison would remove it; it was evaluated in #46 and not adopted (ADR-0025). - Native AOT blind spot: a nested class with no visible members is compared by its own
Equals. IfEqualsignores state (for example entity equality by ID), a changed instance compares equal and the change passes silently, where a JIT build reports a violation. Preserve the type withDynamicDependencyto compare it member by member. - Every comparison of a member or element uses the runtime types of the values, so a member declared as
objector as an interface is compared as the type it holds. Two arrays that a member or element holds, or that are passed as an array type orSystem.Array, are equal only when they have the same rank, and the same length and lower bound in every dimension; then they are compared element by element. An array and a sequence that is not an array (aList<T>) are compared by what they enumerate, whatever the rank of the array. Two arrays of pointers or of function pointers are compared by address: neither the type that a pointer points to nor the memory behind an address is compared. Such an array that has elements is different from anything that is not an array of pointers or of function pointers, also from a sequence that is not an array. The limits, and what to write instead, are in Types thatOld<T>()shares, under Two arrays. A member or element of a value type, and a nullable value type asT, is equal when itsEqualssays so; otherwise its visible fields are compared. Under Native AOT a struct whose fields are hidden is decided by itsEqualsalone. For a nullable value type asT, the fields of the underlying type may not be preserved: for a nullable struct that holds a reference, the comparison can then report a difference for an unchanged value or, with anOldsnapshot, miss a change inside the object it refers to. Preserve the struct withDynamicDependency, or test both values fornulland pass the unwrapped values. - Assignable patterns are unanchored regular expressions: listing
Customeralso exempts other top-level members whose names contain it (for exampleCustomerId). Anchor the pattern (^Customer$) to exempt exactly one name. A pattern is matched against the plain name that reflection gives the member and, for the backing field that the compiler generates for an auto-property (<Customer>k__BackingField), also against the property name (Customer) (#46). So^Customer$exempts an auto-propertyCustomerwherever it is compared: one declared on the compared type is compared twice, as the property and as its backing field, and the pattern covers both; an inherited public one is compared once, as the property; one that is not public is compared as its backing field only.^<Customer>k__BackingField$exempts the backing field only. A field written by hand behind a property (_customerbehindCustomer) is matched by its own name only, so it needs its own pattern (for example^(Customer|_customer)$). The backing field of an explicitly implemented interface auto-property is matched by the qualified property name (for exampleNs.IFoo.Id):Id$exempts it,^Id$does not. A violation entry shows the plain name,<Customer>k__BackingFieldfor a backing field. When two members of one compared type have the same name (a private field declared on two levels, a field hidden withnew), each is shown with its declaring type: a violation entry reads_state (declared on StateBase), and the text before the first space is the plain name (the full name of the type is shown when two declaring types have the same name). The declaring type takes no part in matching (StateBase\._stateorBaseexcuses neither): a pattern excuses every member whose plain name, or whose property name, it matches, and one of two members of the same name cannot be excused alone. Members whose names do not collide are shown by their plain name. The patterns do not apply, and are not examined, when exactly one of the two values isnull, whenTis astring, a delegate type, a nullable value type, aFrozenSet<T>or aFrozenDictionary<TKey, TValue>, or an array type orSystem.Array, or when one instance of a type thatOld<T>()shares, and its state can change, is passed asactualand asexpected. - In a trimmed app without Native AOT, a nested type whose property getter was removed by the trimmer makes
EnsureAssignable<T>()throwInvalidOperationExceptionnaming the property ("… no get method is visible through the compared type"). The same two ways out apply: list the top-level member as assignable (not possible below a nullable value type asT, below aFrozenSet<T>or aFrozenDictionary<TKey, TValue>asT, or below an array type orSystem.ArrayasT), or preserve the type withDynamicDependency. - Not detected: a type of which only some members are preserved (the missing members are skipped silently; a struct is compared on its visible fields only) and, in a trimmed app without Native AOT, a nested type whose members were removed entirely (it compares as equal). Detection also depends on the data: a
nullmember, the same instance on both sides, or an empty collection is not inspected.
Preserve types with DynamicDependency. In this example Old<T>() copies a Customer and EnsureAssignable<T>() compares the nested Address, so under Native AOT both types need their members preserved:
using System.Diagnostics.CodeAnalysis;
using uContract;
public sealed record Address(string Street, string City);
public sealed class Customer
{
public string Name { get; set; } = "";
public Address Address { get; set; } = new("1 Main St", "Springfield");
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Customer))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Address))]
public void Rename(string newName)
{
Customer oldState = Contract.Old(() => this)!;
Name = newName;
Contract.EnsureAssignable(this, oldState, nameof(Name));
}
}uContract.NET maintains semantic parity with the Java version while leveraging modern .NET features.
| Feature | Java | C# |
|---|---|---|
| Method naming | require(), old(), imply() |
Require(), Old(), Imply() |
| Lambda syntax | () -> condition |
() => condition |
| Functional types | BooleanSupplier, Supplier<T> |
Func<bool>, Func<T> |
| Null checks | @NonNull annotation |
where T : class constraint |
| Feature | Java | C# |
|---|---|---|
| Parameter validation | ❌ Not validated | ✅ Always validated (even when DBC off), with two exceptions, for EnsureAssignable<T>() and EnsureImmutableCollection<T>(): see Key Behaviors |
| Exception messages | ✅ Unified format | |
| Async support | ❌ ThreadLocal (no async) | ✅ AsyncLocal (async/await safe) |
| Configuration | ✅ Encapsulated ContractConfiguration class | |
| Nullability | ✅ Compiler-enforced nullable reference types | |
Old<T>() overloads |
TypeReference<T> needed for generics) |
✅ Single Old<T>(Func<T>) — reified generics make the type hint unnecessary |
| Description auto-capture | ❌ Annotation mandatory on every call | ✅ Condition-based methods (Require/Ensure/Invariant/Check) have [CallerArgumentExpression] overloads — compiler captures the condition's source text when description is omitted |
Note on description auto-capture: C# 10's
[CallerArgumentExpression]letsContract.Require(() => balance >= 0)work without a manual description — the compiler embeds the condition's source text at compile time, so the exception message shows exactly what failed. Available on the four condition-based methods; value-based null-check methods (RequireNotNull,RequireNotEmpty,EnsureNotNull,InvariantNotNull) do not have CAE overloads due to C# overload-resolution collisions with their existing generic signatures. See ADR-0018 for the full rationale and limitations.
Note on
Old<T>(): Java'sold(Supplier<T>, TypeReference<T>)overload exists to work around type erasure. .NET's reified generics preserve full generic type information at runtime, soOld<T>(Func<T>)already knows the closed-generic typeTand copies each object by its runtime type, with no external hint needed. See ADR-0016 for the full rationale.
Renames to be aware of when porting Java code that uses uContract:
| Java (2.0.1) | uContract.NET | Notes |
|---|---|---|
reject() |
Ignore() |
Renamed for semantic clarity |
ClassInvariantViolationException |
InvariantViolationException |
Shorter name; same role in the exception hierarchy |
old(Supplier<T>, TypeReference<T>) |
(removed) | Use Old<T>(Func<T>) — no type hint needed (ADR-0016) |
Exception messages differ. Java throws with the raw description text
("x positive"); uContract.NET prefixes the contract type
("Precondition violated: x positive"). If your tests or log parsers assert
on exception messages, update them accordingly. The unprefixed description
remains available via the exception's Description property.
Environment variables (DBC, DBC_PRE, DBC_POST, DBC_INV, DBC_CHECK) keep
the same names and semantics as the Java version.
Java:
// Java
require("x positive", () -> x > 0);
var oldBalance = old(() -> balance);
ensure("balance decreased", () -> balance < oldBalance);C#:
// C#
Contract.Require("x positive", () => x > 0);
var oldBalance = Contract.Old(() => balance);
Contract.Ensure("balance decreased", () => balance < oldBalance);-
📖 API_REFERENCE.md - API reference
- Configuration, exception hierarchy, best practices, performance
- Per-method signatures and XML docs available via IDE IntelliSense
-
📚 USAGE_EXAMPLES.md - Real-world examples
- 15+ practical scenarios (banking, e-commerce, inventory, user management)
- Quick DDD examples with links to complete guide
- Configuration examples and testing patterns
-
🏗️ DDD_INTEGRATION_GUIDE.md - DDD integration guide
- 10+ DDD patterns with best practices
- Specifications, repositories
Contributions are welcome!
Before contributing:
- Read the Architecture Decision Records
MIT License — Copyright (c) 2025-2026 uContract.NET Contributors. See LICENSE for details.
This project is a derivative work of the Java uContract library by Teddy Chen and contributors, which is licensed under the Apache License 2.0. See THIRD-PARTY-NOTICES.txt for the required attribution and license text.
This .NET port is based on Java uContract 2.0.1 (GitLab commit: cb1e03f)
- Repository: Java uContract (GitLab)
- Documentation: Javadoc