简体   繁体   中英

Insert expression into LINQ statement for Linq-To-Sql

I have a semi-complicated function that I use in a LINQ expression. The function is called DatePeriodOverlap() and goes like this:

 bool DatePeriodOverlap(DateTime startDate, DateTime endDate, DateTime filterStartDate, DateTime filterEndDate)
 {
    return ((startDate >= filterStartDate && startDate <= filterEndDate)
            ||
            (endDate <= filterEndDate && endDate >= filterStartDate)
            ||
            (startDate <= filterStartDate && endDate >= filterEndDate));
}

Now, I want to use this in a LINQ to SQL scenario (nHibernate). That means, I can't just use the function itself, but I have to create an Expression and use that. So I created this function to do that:

Expression<Func<Domain.Course, bool>> GetDatePeriodOverlapForCourseExpression(DateTime filterStartDate, DateTime filterEndDate)
{
    return x => (x.StartDate >= filterStartDate && x.StartDate <= filterEndDate)
                ||
                (x.EndDate <= filterEndDate && x.EndDate >= filterStartDate)
                ||
                (x.StartDate <= filterStartDate && x.EndDate >= filterEndDate);
}

I can now use it in my query without issue. However, I have to rewrite the exact same function for every SQL object that has a date range. Wouldn't it be easier to just do something like this:

Expression<Func<TObject, bool>> GetDatePeriodOverlapExpression<TObject>(Expression<Func<TObject, DateTime>> StartDate, Expression<Func<TObject, DateTime>> EndDate, DateTime filterStartDate, DateTime filterEndDate)
{
    return x => (StartDate(x) >= filterStartDate && StartDate(x) <= filterEndDate)
                ||
                (EndDate(x)<= filterEndDate && EndDate(x) >= filterStartDate)
                ||
                (StartDate(x) <= filterStartDate && EndDate(x) >= filterEndDate);

}

Then I could call it like this:

var query = GetDatePeriodOverlapExpression<Domain.Course>((x => x.StartDate), (y => y.EndDate), filterStartDate, filterEndDate);

That seems like an awesome idea. Except that it doesn't work. "Method, delegate or event is expected" for every call to StartDate(x) and EndDate(x) . As near as I can tell, I have to create the expression tree by scratch and can't use LINQ to create it. True, I would only need to do it once but that seems like a lot of work to do what LINQ should do naturally.

I've tried to use a call to StartDate.Compile()(x) instead and that compiles. But when I run it, it seems to compare function pointers instead of the results of the functions.

Is there a way to insert an Expression into a LINQ statement?

You can build expression tree dynamically like that:

Expression<Func<TObject, bool>> GetDatePeriodOverlapExpression<TObject>(Expression<Func<TObject, DateTime>> StartDate, Expression<Func<TObject, DateTime>> EndDate, DateTime filterStartDate, DateTime filterEndDate)
{
    var parameter = Expression.Parameter(typeof(TObject));


    var e1 = Expression.GreaterThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, StartDate.Body), Expression.Constant(filterStartDate)); // StartDate(x) >= filterStartDate
    var e2 = Expression.LessThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, StartDate.Body), Expression.Constant(filterEndDate)); // StartDate(x) <= filterEndDate

    var e3 = Expression.AndAlso(e1, e2); // (StartDate(x) >= filterStartDate && StartDate(x) <= filterEndDate)

    var e4 = Expression.LessThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, EndDate.Body), Expression.Constant(filterEndDate)); // EndDate(x) <= filterEndDate
    var e5 = Expression.GreaterThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, EndDate.Body), Expression.Constant(filterStartDate)); // EndDate(x) >= filterStartDate

    var e6 = Expression.AndAlso(e4, e5); // (EndDate(x) <= filterEndDate && EndDate(x) >= filterStartDate)

    var e7 = Expression.LessThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, StartDate.Body), Expression.Constant(filterStartDate)); // StartDate(x) <= filterStartDate
    var e8 = Expression.GreaterThanOrEqual(ParameterRebinder.ReplaceParameters(parameter, EndDate.Body), Expression.Constant(filterEndDate)); // EndDate(x) >= filterEndDate

    var e9 = Expression.AndAlso(e7, e8); // (StartDate(x) <= filterStartDate && EndDate(x) >= filterEndDate)

    var e10 = Expression.OrElse(Expression.OrElse(e3, e6), e9);

    return Expression.Lambda<Func<TObject, bool>>(e10, parameter);
}  

public class ParameterRebinder : ExpressionVisitor 
{ 
    private readonly ParameterExpression parameter; 


    public ParameterRebinder(ParameterExpression parameter) 
    { 
        this.parameter = parameter; 
    } 

    public static Expression ReplaceParameters(ParameterExpression parameter, Expression exp) 
    { 
        return new ParameterRebinder(parameter).Visit(exp); 
    } 

    protected override Expression VisitParameter(ParameterExpression p) 
    { 
        return base.VisitParameter(parameter); 
    } 
}

EDIT : When I ran your initial code, I got an error "An item with the same key has already been added." I have updated the code with that fixed, and fixed a copy-paste issue within the logic. - @ErikAllen

You could create an interface:

public interface IDateSearchable
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

Then make implement the interface in all the classes you want to use your expression on. Your expression function would become:

Expression<Func<IDateSearchable, bool>> GetDatePeriodOverlapForCourseExpression(DateTime filterStartDate, DateTime filterEndDate)
{
    return x => (x.StartDate >= filterStartDate && x.StartDate <= filterEndDate)
                ||
                (x.EndDate <= filterEndDate && x.EndDate >= filterStartDate)
                ||
                (x.StartDate <= filterStartDate && x.EndDate >= filterEndDate);
}

Because your Getperiodoverlap... method returns a func that accepts an IDateSearchable as a parameter, it will work on any class that implements the interface.

Then your class would be

public class SomeClassName : IDateSearchable
{
    public DateTime StartDate { get; set; } 
    public DateTime EndDate { get; set; }

    // all your other stuff
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM