Skip to content

Custom Rules

“All domain classes must have a Create factory method”, “Classes with a Service suffix are prohibited” — the framework does not provide these team-specific rules by default. But what if you could create them yourself?

In this chapter, you will learn how to define and compose project-specific custom rules using DelegateArchRule, CompositeArchRule, and the Apply() method. When built-in rules are not enough, infinite extensibility becomes possible.

“A good architecture test framework is not one with rich built-in rules, but one that is easily extensible when built-in rules fall short.”

  1. Writing lambda-based custom rules with DelegateArchRule<T>

    • Constructor pattern that takes a rule description and validation function
    • How to report violations by returning RuleViolation
  2. AND composition of multiple rules with CompositeArchRule<T>

    • Pattern for combining individual rules into compound rules
    • How all violations from all rules are collected
  3. Integrating custom rules into existing verification chains with Apply()

    • Freely mix with built-in rules (RequireSealed(), RequireImmutable())
    • Apply both built-in and custom rules in a single verification chain
  • Factory method rule: Verify that all domain classes have a static Create method
  • Service suffix prohibition rule: Verify that domain class names do not end with Service
  • Composite rule composition: Combine two custom rules with AND and apply at once
public sealed class Invoice
{
public string InvoiceNo { get; }
public decimal Amount { get; }
private Invoice(string invoiceNo, decimal amount)
{
InvoiceNo = invoiceNo;
Amount = amount;
}
public static Invoice Create(string invoiceNo, decimal amount)
=> new(invoiceNo, amount);
}
public sealed class Payment
{
public string PaymentId { get; }
public decimal Amount { get; }
public string Method { get; }
private Payment(string paymentId, decimal amount, string method)
{
PaymentId = paymentId;
Amount = amount;
Method = method;
}
public static Payment Create(string paymentId, decimal amount, string method)
=> new(paymentId, amount, method);
}

DelegateArchRule - Lambda-Based Custom Rules

Section titled “DelegateArchRule - Lambda-Based Custom Rules”

DelegateArchRule<T> defines rules with lambda functions. The constructor takes a rule description and validation function.

private static readonly DelegateArchRule<Class> s_factoryMethodRule = new(
"All domain classes must have a static Create factory method",
(target, _) =>
{
var hasCreate = target.Members
.OfType<MethodMember>()
.Any(m => m.Name.StartsWith("Create(") && m.IsStatic == true);
return hasCreate
? []
: [new RuleViolation(target.FullName, "FactoryMethodRequired",
$"Class '{target.Name}' must have a static Create method.")];
});

The validation function takes (TType target, Architecture architecture) parameters and returns IReadOnlyList<RuleViolation>. If there are no violations it returns an empty list; if there are violations it returns a list of RuleViolation instances.

CompositeArchRule<T> composes multiple IArchRule<T> instances with AND. It collects violations from all rules.

private static readonly CompositeArchRule<Class> s_domainClassRule = new(
s_factoryMethodRule,
s_noServiceSuffixRule);

Custom rules are integrated into the verification chain with the Apply() method.

[Fact]
public void DomainClasses_ShouldSatisfy_CompositeRule()
{
ArchRuleDefinition.Classes()
.That()
.ResideInNamespace(DomainNamespace)
.ValidateAllClasses(Architecture, @class => @class
.Apply(s_domainClassRule),
verbose: true)
.ThrowIfAnyFailures("Domain Composite Rule");
}

Built-in methods like RequireSealed() and RequireImmutable() can be freely chained with Apply().

[Fact]
public void DomainClasses_ShouldSatisfy_CompositeRuleWithBuiltIn()
{
ArchRuleDefinition.Classes()
.That()
.ResideInNamespace(DomainNamespace)
.ValidateAllClasses(Architecture, @class => @class
.RequireSealed()
.RequireImmutable()
.Apply(s_domainClassRule),
verbose: true)
.ThrowIfAnyFailures("Domain Full Composite Rule");
}

The following table summarizes the core types used for custom rule authoring.

TypeRoleUsage
IArchRule<T>Interface for custom rulesDefines Description and Validate()
DelegateArchRule<T>Define rules with lambda functionsnew DelegateArchRule<Class>("description", (target, arch) => ...)
CompositeArchRule<T>AND composition of multiple rulesnew CompositeArchRule<Class>(rule1, rule2)
RuleViolationSealed record containing violation information(TargetName, RuleName, Description)
Apply(rule)Integrates custom rules into verification chain.Apply(s_domainClassRule)

The following table compares the roles of built-in and custom rules.

AspectBuilt-In RulesCustom Rules
Definition methodRequireXxx() method callsDelegateArchRule or IArchRule implementation
Application methodDirect chainingApply(rule)
CompositionAutomatic AND through chainingExplicit AND with CompositeArchRule
ReusabilityProvided by frameworkShareable within project

Q1: What is the difference between DelegateArchRule and directly implementing IArchRule?

Section titled “Q1: What is the difference between DelegateArchRule and directly implementing IArchRule?”

A: DelegateArchRule is suitable for quickly defining simple rules with lambdas. When rule logic is complex, state (fields) is needed, or the rule must be reused in multiple places, creating a class that directly implements the IArchRule<T> interface is more appropriate.

Q2: Does CompositeArchRule also support OR composition?

Section titled “Q2: Does CompositeArchRule also support OR composition?”

A: No. CompositeArchRule only supports AND composition — it collects and returns violations from all rules. If OR composition is needed, you must implement the OR logic directly inside a DelegateArchRule.

Q3: When is the Architecture parameter used in custom rules?

Section titled “Q3: When is the Architecture parameter used in custom rules?”

A: The Architecture parameter is used when accessing type information across the entire project. For example, it is needed when analyzing relationships between types, such as “does this class depend on another class that implements a specific interface?” For simple member inspection, it can be ignored with _.

A: Yes, you can apply multiple custom rules sequentially like .Apply(rule1).Apply(rule2). This has the same effect as bundling them with CompositeArchRule, but can be expressed more readably in chaining style.


Being able to write custom rules means the framework’s limits do not become the project’s limits. Now that you have learned all the advanced verification techniques in Part 3, the next Part 4 applies all these techniques to real-world layer-by-layer architecture rules.

-> Part 4: Real-World Patterns