Querying on methods

46 views
Skip to first unread message

Ben

unread,
Jul 4, 2012, 7:35:06 PM7/4/12
to rav...@googlegroups.com
When we first started using RavenDB we changed most of our readonly properties to methods so that they weren't persisted. Often these were calculated properties using variables that may change (such as the date). Persisting the calculated result would be of no use.

The downside of this approach is that we don't seem to be able to query on these methods, which is a pain because many of these methods represent business assertions that we then have to replicate again in our queries.

For example:

public bool IsHighProfile()
{
return Salary.To > 100000;
}

This is a business rule that states, all vacancies with salaries greater than 100,000 are high profile.

To query for high profile salaries we can't do:

session.Query<Vacancy>().Where(v => v.IsHighProfile()).ToList();

Instead we have to duplicate this rule into a query extension:

public static IQueryable<Vacancy> ThatAreHighProfile(this IQueryable<Vacancy> items)
{
return items.Where(v => v.Salary.To > 100000);
}

This just smells.

So what is the recommended approach? I thought about perhaps using the specification pattern so that I'm not duplicating the business rules but am curious to see what others are doing.

Itamar Syn-Hershko

unread,
Jul 4, 2012, 7:40:46 PM7/4/12
to rav...@googlegroups.com
Define this in your index, indexes can contain business logic, and they can be strongly typed in your poject

Something like

Map = docs => from doc in docs
select new { Salary = doc.Salary.To, IsHighProfile = doc.Salary.To > 1000 }

But note that updating this index definition will reset that index, and if you have a lot of data it can take it some while to catch up. You could either take this tadeoff, or split this specific functionality to a separate, smaller index 

Ben

unread,
Jul 4, 2012, 7:50:13 PM7/4/12
to rav...@googlegroups.com
Hi Itamar,

I can't see how this helps me, since I am still duplicating this business rule that belongs in my domain model (not in an index), and I can't use methods as criteria in indexes either (i.e. I can't pass in a specification).

So where does this leave us? 

The sample I gave is pretty simple. Take a real life situation where I have the following on my domain model:

        public virtual bool IsRunning()
        {
            return Status == CampaignStatus.Active && CampaignPeriod.IsCurrent();
        }

which calls:

        public bool IsCurrent()
        {
            return StartDate.Date <= DateTime.UtcNow.Date && EndDate >= DateTime.UtcNow;
        }

How can I query on these same rules (all Running campaigns) without copy pasting the code into an index/query criteria?

Kijana Woodard

unread,
Jul 4, 2012, 7:51:53 PM7/4/12
to rav...@googlegroups.com
It sounds like you were better off with the persisted ReadOnly properties. Can you use those for all/most/some of these cases?

Itamar Syn-Hershko

unread,
Jul 4, 2012, 7:55:07 PM7/4/12
to rav...@googlegroups.com
The problem is you can't query on data that is not indexed, and methods are not even data.

You _could_ define those as properties, and they will get indexed. The problem with that approach, and the reason why I didn't suggest it before, is it isn't really enforced. Whenever some business rule changes, you will have to go and load and re-store all those documents for the properties to be updated. And that is bad.

Using extension methods as you proposed earlier looks the smoothest way to do this. If you'll use constants it won't smell that much.

Ben

unread,
Jul 4, 2012, 8:11:08 PM7/4/12
to rav...@googlegroups.com
Is this a limitation of the linq provider?

It seems it is not possible to use an external method for criteria other than when using simple operators, for example I can't do "v.Salary.IsHighSalary()" but I can do "v.Salary > GetHighSalary()".

Ben

unread,
Jul 4, 2012, 8:13:45 PM7/4/12
to rav...@googlegroups.com
Read-only properties aren't really a good solution either because we use variables like dates or if the business rules change (we would need re-evaluate every document).

Oren Eini (Ayende Rahien)

unread,
Jul 5, 2012, 4:45:28 AM7/5/12
to rav...@googlegroups.com
It is a limitation in general, you can't deconstruct a method to figure out what it is doing. n

Matt Warren

unread,
Jul 6, 2012, 10:04:20 AM7/6/12
to rav...@googlegroups.com
If you're happy to have all the business logic at query time, rather than at indexing time, you can do something like this (full gist at  https://gist.github.com/3060300)

    public class Salary
    {
        public static Expression<Func<Salarybool>> IsHighSalary
        {
            get { return salary => salary.Amount > 20000; }
        }
 
        public static Expression<Func<Salarybool>> IsRunning
        {           
            get
            {
                return salary => salary.Status == SalaryStatus.Active &&
                                 (salary.StartDate.Date <= DateTime.UtcNow.Date && salary.EndDate >= DateTime.UtcNow);
            }
        }
 
        public static Expression<Func<Salarybool>> IsCurrent
        {
            get { return salary => salary.StartDate.Date <= DateTime.UtcNow.Date && salary.EndDate >= DateTime.UtcNow; }
        }
 
        public Decimal Amount { getset; }
        public DateTime StartDate { getset; }
        public DateTime EndDate { getset; }
        public SalaryStatus Status { getset; }
 
        public enum SalaryStatus
        {
            Active,
            InActive,
            Suspended
        }
    }

Then you can write queries like this:
    var query = session.Query<Salary>()
                      .Where(Salary.IsHighSalary) 
                      .ToList();


I've not worked out a way to combine 2 (that's why there's repeated logic in IsRunning and IsCurrent), plus I don't know how you can use the same logic inside an index (without resorting to defining indexes as strings)
Reply all
Reply to author
Forward
0 new messages