Fluent Rules DSL

The standard way to define rules in NRules is with the internal DSL using fluent C# API.

A rule is a .NET class that inherits from Rule. Rule class overrides Define method where the actual conditions (left-hand side, or LHS) and actions (right-hand side, or RHS) parts are specified.

Within the Define method, LHS is specified by fluently chaining conditions to the When() method; and RHS by fluently chaining actions to Then() method.

Warning

Make sure rule classes are public, otherwise the engine won't find them.

See Fluent Rules Loading on how to load rules defined with the fluent rules DSL.

Rule class can also optionally be decorated with the following custom attributes to associate additional metadata with the rule.

Attribute Allow Multiple? Inherited? Description
Name No No Specifies rule name. Default is the fully qualified rule class name.
Description No No Specifies rule description. Default is an empty string.
Tag Yes Yes Associates arbitrary tag with the rule (can later be used to group or filter rules).
Priority No Yes Sets rule priority. If multiple rules get activated at the same time, rules with higher priority (larger number) get executed first. Priority can be positive or negative. Default is zero.
Repeatability No No Sets rule's repeatability, that is, how it behaves when it is activated with the same set of facts multiple times, which is important for recursion control. Repeatability can be set to repeatable - the rule is activated if any of the matched facts are updated, or non-repeatable - the rule is only activated once for a given set of matched facts (unless the match is retracted, before being re-asserted again). Default is Repeatable.
[Name("MyRule"), Description("Test rule that demonstrates metadata usage")]
[Tag("Test"), Tag("Metadata")]
[Priority(10)]
[Repeatability(RuleRepeatability.Repeatable)]
public class RuleWithMetadata : Rule
{
    public override void Define()
    {
        When()
            .Match<Customer>(c => c.Name == "John Do");
        Then()
            .Do(ctx => DoSomething());
    }
}

While fluent rules DSL uses C#, the rules have to be defined using declarative approach. There should be no imperative C# code used anywhere in the rule definition, except within condition expressions, action expressions and methods called from those expressions.

If a rule pattern is bound to a variable (see below), that variable should only be used in subsequent condition and action expressions directly. The purpose of the binding variable is to serve as a token (that has a name and a type) that instructs the engine to link corresponding conditions and actions. Don't write any code that manipulates the binding variables outside of the condition/action expressions.

Matching Facts with Patterns

Rule's left hand side is a set of patterns that match facts of a given type. A pattern is defined using a Match method. A pattern can have zero, one or many conditions that must all be true in order for the pattern to match a given fact.

Pattern matching is also polymorphic, which means it matches all facts of a given type and any derived type. Given a class hierarchy of Fruit, Apple and Pear, Match<Fruit> will match both Apples and Pears. Consequently, Match<object> will match all facts in the engine's working memory.

If a given pattern matches multiple facts in the engine’s working memory, each match will result in a separate firing of the rule.

Optionally, a pattern can be bound to a variable, in which case that variable can be used in subsequent patterns to specify inter-fact conditions. Also, the variable can be used inside actions to update or retract the corresponding fact, or use it in the expression. Do not use or otherwise manipulate the binding variable anywhere outside of the condition/action expressions.

public class PreferredCustomerActiveAccountsRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;
        Account account = default!;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .Match<Account>(() => account, a => a.Owner == customer, a => a.IsActive);

        Then()
            .Do(ctx => customer.DoSomething());
    }
}

Existential Rules

Existential rules test for presence of facts that match a particular set of conditions. An existential quantifier is defined using Exists method.

An existential quantifier cannot be bound to a variable, since it does not match any single fact.

public class PreferredCustomerActiveAccountsRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .Exists<Account>(a => a.Owner == customer, a => a.IsActive);

        Then()
            .Do(ctx => customer.DoSomething());
    }
}

Negative Rules

Opposite to existential rules, negative rules test for absence of facts that match a particular set of conditions. A negative existential quantifier is defined using Not method.

A negative existential quantifier cannot be bound to a variable, since it does not match any single fact.

public class PreferredCustomerNotDelinquentRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .Not<Account>(a => a.Owner == customer, a => a.IsDelinquent);

        Then()
            .Do(ctx => customer.DoSomething());
    }
}

Universal Quantifier

Universal quantifier ensures that all facts that match a particular condition also match all subsequent conditions defined by the quantifier. A universal quantifier is defined using All method.

A universal quantifier cannot be bound to a variable, since it does not match any single fact.

public class PreferredCustomerAllAccountsActiveRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .All<Account>(a => a.Owner == customer, a => a.IsActive);

        Then()
            .Do(ctx => customer.DoSomething());
    }
}

Grouping Patterns

By default all patterns on the left-hand side of the rule are connected using AND operator. This means that all patterns must match for the rule to activate.

Patterns can also be connected using OR operator, as well as combined into nested groups.

public class PreferredCustomerOrHasLargeOrderRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;

        When()
            .Or(x => x
                .Match<Customer>(() => customer, c => c.IsPreferred)
                .And(xx => xx
                    .Match<Customer>(() => customer, c => !c.IsPreferred)
                    .Exists<Order>(o => o.Customer == customer, o => o.Price >= 1000.00)));

        Then()
            .Do(ctx => customer.DoSomething());
    }
}

Rules with Complex Logic

In complex rules it is usually required to aggregate or project facts, calculate derived values and correlate different matched facts. The rules engine provides several different DSL operators to express such logic.

Rules can match and transform sets of facts using Query syntax, which enables rules authors to apply LINQ operators to matched facts; see [[Reactive LINQ Queries]] for more details.

A Let operator binds an expression to a variable, so that the results of intermediate calculations can be used in subsequent rule conditions and actions.

Also, Having operator can add new conditions to previously defined patterns, including Let expressions, improving rules expressiveness and composability.

public class LargeTotalRule : Rule
{
    public override void Define()
    {
        Customer customer = default!;
        IEnumerable<Order> orders = default!;
        double total = 0;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .Query(() => orders, x => x
                .Match<Order>(
                    o => o.Customer == customer,
                    o => o.IsOpen)
                .Collect())
            .Let(() => total, () => orders.Sum(x => x.Amount))
            .Having(() => total > 100);

        Then()
            .Do(ctx => customer.DoSomething(total));
    }
}