Part 2How to get it to work against non Linq to object sources

Last week something caught my eyes. How scattered business logic can become if you let your binary expressions be used “here and there” when matching entities against certain rules. Logic for a single entity, e.g Customer can be used all over your codebase. This article will show you a way to get around this and to get a central repository of rules/predicates that can be applied to entities and makes your daily unit test writing easier. I will use simple operator overloading for accomplishing this.

The example code can be downloaded here.

I have put together a testfixture with test going from ugly code to “more” beautiful code. The example is fictive and therefore not the best example for what I’m showing. The problem below might be best solved with simple code as Customer.IsBonusCustomer() which then contains the logic for determening this. But I want to show you a way to handle this if you instead want the logic outside the customer and don’t want the lambdas/linq queries scattered all over your codebase.

In the code I have made use of Func but you can of course use Expression<Func> if you need to parse the expression tree.

The problem

Identify customers that are considered to be bonus customers.

Rules:

  • A customer that has been member shorter than one year and has spent 3000 or more in our store.
  • A customer that has been member for one year or more and where money spent divided by the years is equal to or greater than 5000.

The solution

Lets start with the entity example which of course intentionally have been made simple.

public class Customer
{
    public int NumOfYearsAsMember { get; set; }
    public int CashSpent { get; set; }
}

The ugly ways

First, lets be clear: “No I don’t mean the Linq or Lambda syntax in it self when I say ugly”. I mean ugly as you get “small” (in best case) pieces of logic lying around in your queries.

[Test]
public void TheUglyWayUsingLinq()
{
  var bonusCustomers = 
    from c in _customers
    where (
      c.NumOfYearsAsMember == 0 &&
      c.CashSpent >= 3000
    ) ||
    (
      c.NumOfYearsAsMember > 0 &&
      (c.CashSpent / c.NumOfYearsAsMember) >= 5000
    )
    select c;

  CollectionAssert.AreEqual(_expectedBonusCustomers, bonusCustomers);
}
[Test]
public void TheUglyWayUsingLambdas()
{
  var bonusCustomers = _customers.Where(c =>
    (
      c.NumOfYearsAsMember == 0 &&
      c.CashSpent >= 3000
    ) ||
    (
      c.NumOfYearsAsMember > 0 &&
      (c.CashSpent / c.NumOfYearsAsMember) >= 5000)
    );

  CollectionAssert.AreEqual(_expectedBonusCustomers, bonusCustomers);
}

Both of these examples are rather simple (seen to what you meet in different enterprise applications) but yet rather hard to read. Well of course not if you concentrate on just the algorithm but as a whole it is. First lets make it somewhat nicer. Lets extract the selectors so that we get a name in the Linq and Lambda expressions.

[Test]
public void SomewhatNicerUsingLinq()
{
  Func<Customer, bool> isFirstYearBonusCustomer = c => 
    c.NumOfYearsAsMember == 0 && c.CashSpent >= 3000;

  Func<Customer, bool> isAfterFirstYearBonusCustomer = c => 
    c.NumOfYearsAsMember > 0 &&
    ((c.CashSpent / c.NumOfYearsAsMember) >= 5000);

  var bonusCustomers = from c in _customers
    where isFirstYearBonusCustomer(c) ||
    isAfterFirstYearBonusCustomer(c)
    select c;

  CollectionAssert.AreEqual(_expectedBonusCustomers, bonusCustomers);
}
[Test]
public void SomewhatNicerUsingLambdas()
{
  Func<Customer, bool> isFirstYearBonusCustomer = c =>
    c.NumOfYearsAsMember == 0 && c.CashSpent >= 3000;

  Func<Customer, bool> isAfterFirstYearBonusCustomer = c =>
    c.NumOfYearsAsMember > 0 &&
    ((c.CashSpent / c.NumOfYearsAsMember) >= 5000);

  var bonusCustomers = _customers.Where(c =>
    isFirstYearBonusCustomer(c) ||
    isAfterFirstYearBonusCustomer(c));

  CollectionAssert.AreEqual(_expectedBonusCustomers, bonusCustomers);
}

Slightly better and if we put the selectors in a class they can be reused.

The more beautiful way

Now lets write a test in a way I would like to read it.

[Test]
public void MaybeMoreBeautifulUsingRules()
{
  var bonusCustomers = _customers.Where(new IsBonusCustomer());

  CollectionAssert.AreEqual(_expectedBonusCustomers, bonusCustomers);
}

How can I accomplish this?

Well it’s easy. Create a class that implements a implicit operator overloading for Func which then allows you to pass instances of this class into the IEnumerable.Where function.

public class ExpressionRule<T> 
  : IRule<T> where T : class
{
  public Func<T, bool> Expression { get; private set; }

  public ExpressionRule(Func<T, bool> expression)
  {
    Expression = expression;
  }

  public static implicit operator Func<T, bool>(ExpressionRule<T> item)
  {
    return item.Expression;
  }

  public bool Evaluate(T item)
  {
    return Expression(item);
  }
}

The next step is to create one representing the rule at hand IsBonusCustomer.

public class IsBonusCustomer 
  : ExpressionRule<Customer>, IIsBonusCustomer
{
  public IsBonusCustomer() : base(c =>
    (c.NumOfYearsAsMember == 0 && c.CashSpent >= 3000) ||
    (c.NumOfYearsAsMember > 0 && (c.CashSpent / c.NumOfYearsAsMember) >= 5000))
  { }
}

Again, if you need to parse the expression tree the ExpressionRule class would look something like this.

public class ExpressionRule<T>
  : IRule<T> where T : class
{
  public Func<T, bool> Expression { get; private set; }

  public ExpressionRule(Expression<Func<T, bool>> expression)
  {
    //Do something with the lambda
    //...
    //...
    Expression = expression.Compile();
  }

  public static implicit operator Func<T, bool>(ExpressionRule<T> item)
  {
    return item.Expression;
  }

  public bool Evaluate(T item)
  {
    return Expression(item);
  }
}

Easier to test

By having the solution above you open up your code to be easy to test. Lets say we have a PriceLocator that uses this rule to determine the price for a customer. We want to test the discount algorithm and be sure that the rule for determining if a customer is a bonus customer, always evaluates to true.

public class PriceCalculator
{
  public IIsBonusCustomer IsBonusCustomer;

  public PriceCalculator()
  {
    IsBonusCustomer = new IsBonusCustomer();
  }

  public decimal Calculate(decimal basePrice, Customer customer)
  {
    return IsBonusCustomer.Evaluate(customer)
      ? basePrice * 0.8M
      : basePrice;
  }
}

Now I can put up a fake and inject it to the price locator, so that the code under test becomes isolated.

[Test]
public void CanBeEasilyBeFaked()
{
  var alwaysBonusCustomerRuleFake = new Mock<IIsBonusCustomer>();
  alwaysBonusCustomerRuleFake
    .Setup(f => f.Evaluate(It.IsAny<Customer>()))
    .Returns(true);

  var priceCalculator = new PriceCalculator
  {
    IsBonusCustomer = alwaysBonusCustomerRuleFake.Object
  };
  var priceWithDiscount = priceCalculator.Calculate(100, new Customer());

  Assert.AreEqual(80, priceWithDiscount);
}

I can now of course also test the IsBonusCustomer rule.

[Test]
public void CanBeTestedInIsolation()
{
  var firstYearBonusCustomer = new Customer
  {
    CashSpent = 3000,
    NumOfYearsAsMember = 0
  };

  var rule = new IsBonusCustomer();

  ExpressionAssert.EvalAsTrue(rule, firstYearBonusCustomer);
}

That’s it. Again the examples above might be overkill since you could have placed methods directly on the Customer, but….

Why this solution at all?

What this gives me:

  • A name to my rule.
  • I can have interfaces represent it and have a factory or IoC returning variations of the rule.
  • I can isolate it for testing.

//Daniel

Category:
Architecture, Design, C#, Clean code
Tags:
, ,

Join the conversation! 13 Comments

  1. C#, Clean up your Linq-queries and lambda expressions « Daniel Wertheim…

    Thank you for submitting this cool story – Trackback from DotNetShoutout…

    Reply
  2. [...] C#, Clean up your Linq-queries and lambda expressions – Daniel Wertheim [...]

    Reply
  3. By doing this however your LINQ query is now only usable in LINQ to Objects – it will no longer work for LINQ to SQL, LINQ to Entities etc.

    [)amien

    Reply
  4. [...] a comment » This is an update to my post “C#, Clean up your Linq-queries and lambda expressions”, where I got a comment that it will not work other than against Linq to objects. That is somewhat [...]

    Reply
  5. would you mind explaining your use of the custom operator and why it is necessary? Thanks!

    Reply
    • I make use of the implicit operator so that I don’t have to call a method on my object representing the query. Of course I can have the expressions lying as a member in a class somewhere with a nice name, but I want one class representing the “rule” of my business logic, which I can fake, inject, test, reuse.

      //Daniel

      Reply
  6. Hi Daniel, really good article. I used your ExpressionRule as a basis for my own version, which I have written about here if you are interested: http://www.stormbase.net/expression-rules-version-2

    Reply
  7. I think it’s pretty much simplier to put that where clause in an extension method and call:

    var bonusCustomers = _customers.ConsiderToBeBonusCostumers();

    public static class CustomersExtensions {
    public static IQueryable ConsiderToBeBonusCostumers(this IQueryable self) {
    return self.Where(c =>
    5
    (c.NumOfYearsAsMember == 0 && c.CashSpent >= 3000) ||
    6
    (c.NumOfYearsAsMember > 0 && (c.CashSpent / c.NumOfYearsAsMember) >= 5000));
    }
    }

    It’s easy, it doesn’t have a new object model for it and it’s readable.

    Reply
    • Hi,
      Yes this is one approach and I have covered it in a previous post. I had troubles isolating the algorithms in tests and I wanted to use the selector/predicate in other source than IQueryable and IEnumerable. I would rather have named Func or Expressions as props/fields in a class which then could be substituted for testing purposes.

      //Daniel

      Reply
  8. I realize I’m responding to a post that’s more than three years old, but given that you’re pulling this information from a database table, it seems to me the most logical place to put this calculation is in the database table as a calculated column, or if you can’t do that in a view. If you’re using SQL Server, you can create a calculated column using code like what’s shown at the end of this post.

    If you go the route suggested in the blog post, then only code that goes through your .NET layer can make use of this information. Someone may need to write a stored procedure that knows what a “bonus customer” is.

    If your client has committed to a particular database management system–and in my experience, most do–there are enormous advantages to centralizing core business logic in the database. Anyone who tells you that business logic doesn’t belong in the database because that’s the “data layer” ought to be asked to explain the difference between a layer and a tier, because they’re confusing the two.

    ALTER TABLE dbo.Customers
    ADD COLUMN IsBonusCustomer AS
    Convert(
    bit,
    CASE
    WHEN NumOfYearsAsMember = 0 AND CashSpent >= 3000 THEN 1
    WHEN NumOfYearsAsMember > 0 AND CashSpent / NumOfYearsAsMember > 5000 THEN 1
    ELSE 0
    END
    )
    PERSISTED NOT NULL

    Reply
    • I can understand logic in DBs if it’s part of an ETL process piping lots of data. But when it comes to “one-and-one” business transactions where logic is being kept in the database, that is for me just wrong. Especially if you plan to use that as some sort of shared logic between bounded contexts since this gives you coupling in logic between to contexts that might force one or the other to change. In code this can be handled a lot differently and more importantly versioned. Testing, well, I have been setting up integration and regression tests against SPs, sure I can ensure that the process works, but the tests becomes to abstract/high level making them harder to understand, especially when they fail.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: