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.