NRules: building a rule for complex types

970 Views Asked by At

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
     }
  }
}
1

There are 1 best solutions below

0
Sergiy Nikolayev On BEST ANSWER

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)
    {
        //
    }
}