Getting Started

snikolayev edited this page Jan 3, 2018 · 3 revisions

This guide shows step by step how to install and use NRules rules engine.

Installing NRules

Create a new Visual studio project and install NRules package via Package Manager Console

PM> Install-Package NRules

Creating Domain Model

NRules is geared towards writing rules against a domain model, so we start by creating a simple one, which describes customers and orders. In this example we create this model in a separate Visual Studio project called Domain.

public class Customer
{
    public string Name { get; private set; }
    public bool IsPreferred { get; set; }

    public Customer(string name)
    {
        Name = name;
    }

    public void NotifyAboutDiscount()
    {
        Console.WriteLine("Customer {0} was notified about a discount", Name);
    }
}

public class Order
{
    public int Id { get; private set; }
    public Customer Customer { get; private set; }
    public int Quantity { get; private set; }
    public double UnitPrice { get; private set; }
    public double PercentDiscount { get; private set; }
    public bool IsDiscounted { get { return PercentDiscount > 0; } }

    public double Price
    {
        get { return UnitPrice*Quantity*(1.0 - PercentDiscount/100.0); }
    }

    public Order(int id, Customer customer, int quantity, double unitPrice)
    {
        Id = id;
        Customer = customer;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }

    public void ApplyDiscount(double percentDiscount)
    {
        PercentDiscount = percentDiscount;
    }
}

Creating Rules

When using NRules internal DSL, a rule is a class that inherits from NRules.Fluent.Dsl.Rule. A rule consists of a set of conditions (patterns that match facts in the rules engine's memory) and a set of actions executed by the engine should the rule fire. In this sample, we create our rules in a separate Visual Studio project called Rules, which depends on the Domain model project.

Let's look at the first rule. We want to find all preferred customers, and for every matching customer we want to collect all orders and apply a discount of 10%. Each pattern in the When part of the rule is bound to a variable via an expression, and then can be used in the Then part of the rule. Also note that if there is more than one pattern in the rule, the patterns must be joined to avoid a Cartesian Product between the matching facts. In this example the orders are joined with the customer.

public class PreferredCustomerDiscountRule : Rule
{
    public override void Define()
    {
        Customer customer = null;
        IEnumerable<Order> orders = null;

        When()
            .Match<Customer>(() => customer, c => c.IsPreferred)
            .Query(() => orders, x => x
                .Match<Order>(
                    o => o.Customer == customer,
                    o => o.IsOpen,
                    o => !o.IsDiscounted)
                .Collect()
                .Where(c => c.Any()));

        Then()
            .Do(ctx => ApplyDiscount(orders, 10.0))
            .Do(ctx => ctx.UpdateAll(orders));
    }

    private static void ApplyDiscount(IEnumerable<Order> orders, double discount)
    {
        foreach (var order in orders)
        {
            order.ApplyDiscount(discount);
        }
    }
}

The second rule will find all customers that have orders with discounts and will notify them of the discount. It's interesting that this rule relies on the first rule to have fired. In other words, the first rule fires and updates the rules engine's memory, triggering the second rule. This is forward chaining in action.

public class DicsountNotificationRule : Rule
{
    public override void Define()
    {
        Customer customer = null;

        When()
            .Match<Customer>(() => customer)
            .Exists<Order>(o => o.Customer == customer, o => o.PercentDiscount > 0.0);

        Then()
            .Do(_ => customer.NotifyAboutDiscount());
    }
}

Running Rules

NRules is an inference engine. It means there is no predefined order in which rules are executed, and it runs a match/resolve/act cycle to figure it out. It first matches facts (instances of domain entities in our case) with the rules and determines which rules can fire. It then resolves the conflict by choosing a single rule that will actually fire. And, finally, it fires the chosen rule by executing it's actions. The cycle is repeated until there are no more rules to fire. We need to do several things for the engine to enter the match/resolve/act cycle. First, we need to load the rules and compile them into an internal structure (Rete network), so that the engine knows what the rules are and can efficiently match facts. We do this by creating a rules repository and letting it scan an assembly to find the rule classes. Then we compile the rules into a session factory. Next we need to create a working session with the engine and insert facts into the engine's memory. Finally we tell the engine to start the match/resolve/act cycle.

//Load rules
var repository = new RuleRepository();
repository.Load(x => x.From(typeof(PreferredCustomerDiscountRule).Assembly));

//Compile rules
var factory = repository.Compile();

//Create a working session
var session = factory.CreateSession();
            
//Load domain model
var customer = new Customer("John Doe") {IsPreferred = true};
var order1 = new Order(123456, customer, 2, 25.0);
var order2 = new Order(123457, customer, 1, 100.0);

//Insert facts into rules engine's memory
session.Insert(customer);
session.Insert(order1);
session.Insert(order2);

//Start match/resolve/act cycle
session.Fire();
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.