Search code examples
c#nrules

NRules: building a rule for complex types


Given the following domain model

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public List<Car> Cars { get; set; }
}
public class Car
{
    public int Year { get; set; }
    public string Make { get; set; }
}

Is it possible to write a rule that performs an action on all cars newer than 2016 owned by people under a the age of 30? I'm doing this while only inserting Person objects as facts.

Person p1 = new Person("Jim", 31);
p1.Cars = GetCars(4);
Person p2 = new Person("Bob", 29);
p2.Cars = GetCars(4);

session.Insert(p1);
session.Insert(p2);

I've tried something like this. I'm guessing I could get it to work if I added a reference in the Car back to the Person that owns it, but my actual use case would make this difficult. I am hoping I am just missing something.

public class CarTest : Rule
{
  public override void Define()
  {
    Person person = null;           
    IEnumerable<Car> cars = null;

    When()
      .Match<Person>(() => person, p => p.Age < 30)
      .Query(() => cars, x => x
         .Match<Car>(c => c == person.Cars.Find(f=> f.Make == c.Make && f.Year == c.Year), c => c.Year > 2016)
         .Collect()
         .Where(p => p.Any()));
    Then()
      .Do(ctx => DoSomethingWithNewCarsThatBelongToYoungPeople(cars));

  }

  private static void DoSomethingWithNewCarsThatBelongToYoungPeople(IEnumerable<Car> cars)
  {
     foreach (var car in cars)
     {
        //Do Something
     }
  }
}

Solution

  • The best way to handle aggregations over complex matches with joins is to break this up into two rules and use forward chaining.

    The first rule matches a given young person with their cars that are considered new. It then yields a new fact that wraps the results.

    public class YoungPersonWithNewCarRule : Rule
    {
        public override void Define()
        {
            Person person = null;
            IEnumerable<Car> cars = null;
    
            When()
                .Match(() => person, p => p.Age < 30)
                .Let(() => cars, () => person.Cars.Where(c => c.Year > 2016))
                .Having(() => cars.Any());
            Then()
                .Yield(ctx => new YoungPersonWithNewCar(person, cars));
        }
    }
    
    public class YoungPersonWithNewCar
    {
        private readonly Car[] _cars;
    
        public Person Person { get; }
        public IEnumerable<Car> Cars => _cars;
    
        public YoungPersonWithNewCar(Person person, IEnumerable<Car> cars)
        {
            _cars = cars.ToArray();
            Person = person;
        }
    }
    

    The second rule matches the facts produced by the first rule and aggregates them into a collection.

    public class YoungPeopleWithNewCarsHandlingRule : Rule
    {
        public override void Define()
        {
            IEnumerable<YoungPersonWithNewCar> youngPeopleWithNewCars = null;
    
            When()
                .Query(() => youngPeopleWithNewCars, q => q
                    .Match<YoungPersonWithNewCar>()
                    .Collect()
                    .Where(c => c.Any()));
            Then()
                .Do(ctx => DoSomethingWithNewCarsThatBelongToYoungPeople(youngPeopleWithNewCars));
        }
    
        private void DoSomethingWithNewCarsThatBelongToYoungPeople(IEnumerable<YoungPersonWithNewCar> youngPeopleWithNewCars)
        {
            //
        }
    }