3

When using:

DateTime.ToString().Contains("2016")

Entity Framework produces:

CAST(DateValue AS nvarchar(max)) LIKE '%2016%'

This uses the default date-format "mon dd yyyy hh:miAM (or PM)"

I would like to user "yyyy-mm-dd hh:mi:ss (24h)" which is obtainable with something like:

CONVERT(VARCHAR(max), DateValue, 20) LIKE '%2016%'

I need help implementing this format to an existing generic method.

static Expression<Func<T, TResult>> Expr<T, TResult>(Expression<Func<T, TResult>> source) { return source; }
static MethodInfo GetMethod(this LambdaExpression source) { return ((MethodCallExpression)source.Body).Method; }
static readonly MethodInfo Object_ToString = Expr((object x) => x.ToString()).GetMethod();
static readonly MethodInfo String_Contains = Expr((string x) => x.Contains("y")).GetMethod();

public static IQueryable<T> Filter<T>(this IQueryable<T> query, List<SearchFilterDto> filters)
 where T : BaseEntity
{
    if (filters != null && filters.Count > 0 && !filters.Any(f => string.IsNullOrEmpty(f.Filter)))
    {
        var item = Expression.Parameter(query.ElementType, "item");
        var body = filters.Select(f =>
        {
            var value = f.Column.Split('.').Aggregate((Expression)item, Expression.PropertyOrField);
            if (value.Type != typeof(string))
            {
                value = Expression.Call(value, Object_ToString);
            }

            return (Expression)Expression.Call(value, String_Contains, Expression.Constant(f.Filter));
        })
        .Where(r => r != null)
        .Aggregate(Expression.AndAlso);

        var predicate = Expression.Lambda(body, item);
        MethodInfo whereCall = (typeof(Queryable).GetMethods().First(mi => mi.Name == "Where" && mi.GetParameters().Length == 2).MakeGenericMethod(query.ElementType));
        MethodCallExpression call = Expression.Call(whereCall, new Expression[] { query.Expression, predicate });
        query = query.Provider.CreateQuery<T>(call);
    }
    return query;
}

Please note, this is an example - it will not always be "2016" and not always be a year. The user may type the time, or "01" to recall all records either on the 1st day of the month, January or in 2001. It's a very flexible filter.

I also understand that many people will not like this situation, but I am really looking for a solution here and not be told "don't do this"

The solution also needs to cater for LINQ to Entities, so I cant simply .ToString("MMM d yyyy H:mm tt") as this will result in:

"LINQ to Entities does not recognize the method 'System.String ToString(System.String)' method, and this method cannot be translated into a store expression."

The code works with the default date-format. The reason for my question is to change the date-format at SQL level, by manipulating the query in Entity Framework.

Sean Thorburn
  • 1,728
  • 17
  • 31
  • I don't think using LIKE with dates is such a good idea. You could probably get a more efficient query by not converting the date into a string. What is it that you are actually trying to achieve, i.e. what would you use in place of "xyz" in practice? – Matti Virkkunen Feb 13 '16 at 20:52
  • @MattiVirkkunen, I have replaced xyz with a better example - 2016. The text is coming from an open text box. If you have a better way of working with dates, I'm all for it! It just needs to be flexible and accurate no matter what the string/filter contains. – Sean Thorburn Feb 13 '16 at 20:56
  • You should not mix code and data like that. You probably should parse text field into integer (to get year). SQL has DATEPART function, probably EF will translate DateTime.Year == value to DATEPART. – Oleh Nechytailo Feb 13 '16 at 20:59
  • @OlehNechytailo, I have added more text to the end of the question. – Sean Thorburn Feb 13 '16 at 21:17

2 Answers2

1

If you are trying to determine if the date is within the year from your input value why not:

DateTime.Year == 2016 //or your variable

Maybe there is more to your need than I am seeing, though.

Nick
  • 7,103
  • 2
  • 21
  • 43
  • Please see updated question. I have added text explaining why this will not help. – Sean Thorburn Feb 13 '16 at 21:09
  • How will you know if what is being passed is a month, day, or year? – Nick Feb 13 '16 at 21:11
  • You don't, and don't need to. The code works with the default date-format. The reason for my question is to change the date-format at the SQL level by manipulating the query in Entity Framework. – Sean Thorburn Feb 13 '16 at 21:15
  • Sorry if I am missing something, but if you get a value of 15, which was meant as the date number - wouldn't a 2015 date match always (based on the pattern match)? If so, that doesn't seem like a very accurate and consistent way to handle this. Again, correct me if I am misunderstanding something here with your program. – Nick Feb 13 '16 at 21:19
  • You are correct, it should match - and so then the user must be more specific with the value - to limit the filtered results. – Sean Thorburn Feb 13 '16 at 21:21
  • So you are basically allowing the user to provide a pattern to match on the date? – Nick Feb 13 '16 at 21:24
  • Yes, in the form of an empty text box. if they type something stupid or non-specific - they are not going to get good results. – Sean Thorburn Feb 13 '16 at 21:26
  • Gotcha. Based on this: 'I would like to user "yyyy-mm-dd hh:mi:ss (24h)"', the user could match on hours, minutes, and seconds. If that is the intended affect (bad user experience?) then it sounds like you need to use regular expressions. – Nick Feb 13 '16 at 21:29
  • You are correct, I will obviously adjust it if users complain about the filtering - but for now I would like to include everything. Give it a try with regular expressions, see if you can solve it :) – Sean Thorburn Feb 13 '16 at 21:32
  • With all due respect, this is not my challenge - it is yours. I could certainly "give it a try", but shouldn't you? I'm not here to do your work for you - just to help point you into the direction of a possible solution. – Nick Feb 13 '16 at 21:34
  • Sure, but I have no cooking clue how you are going to use regular expressions to manipulate the SQL generated by Entity Framework to use the CONVERT function over CAST, so I'm all-eyes on a basic example :) – Sean Thorburn Feb 13 '16 at 21:38
1

The only way I found to produce the desired result is manually building it with expression like this

Expression<Func<DateTime, string>> Date_ToString = date =>
    DbFunctions.Right("000" + date.Year.ToString(), 4) + "-" +
    DbFunctions.Right("0" + date.Month.ToString(), 2) + "-" +
    DbFunctions.Right("0" + date.Day.ToString(), 2) + " " +
    DbFunctions.Right("0" + date.Hour.ToString(), 2) + ":" +
    DbFunctions.Right("0" + date.Minute.ToString(), 2) + ":" +
    DbFunctions.Right("0" + date.Second.ToString(), 2);

Ugly, I know. And frankly you don't want to see the EF generated SQL from the above expression - a huge monster compared to the desired CONVERT(...). But at least it works.

Here is the code. One could build the above expression using System.Linq.Expressions, but I'm too lazy for that and used a simple parameter replacer.

The modified part:

if (value.Type != typeof(string))
{
    if (value.Type == typeof(DateTime))
        value = value.ToDateString();
    else if (value.Type == typeof(DateTime?))
        value = Expression.Condition(
            Expression.NotEqual(value, Expression.Constant(null, typeof(DateTime?))),
            Expression.Property(value, "Value").ToDateString(),
            Expression.Constant(""));
    else
        value = Expression.Call(value, Object_ToString);
}

and the used helpers:

static readonly Expression<Func<DateTime, string>> Date_ToString = date =>
    DbFunctions.Right("000" + date.Year.ToString(), 4) + "-" +
    DbFunctions.Right("0" + date.Month.ToString(), 2) + "-" +
    DbFunctions.Right("0" + date.Day.ToString(), 2) + " " +
    DbFunctions.Right("0" + date.Hour.ToString(), 2) + ":" +
    DbFunctions.Right("0" + date.Minute.ToString(), 2) + ":" +
    DbFunctions.Right("0" + date.Second.ToString(), 2);

static Expression ToDateString(this Expression source)
{
    return Date_ToString.ReplaceParameter(source);
}

static Expression ReplaceParameter(this LambdaExpression expression, Expression target)
{
    return new ParameterReplacer { Source = expression.Parameters[0], Target = target }.Visit(expression.Body);
}

class ParameterReplacer : ExpressionVisitor
{
    public ParameterExpression Source;
    public Expression Target;
    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node == Source ? Target : base.VisitParameter(node);
    }
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Absolutely perfect! I was half way with a monster datepart-concat abomination when I reached a SQL "nested to level 10" exception. I need to go through this code in detail, I will learn a lot from it. Thank you for your time and effort, I am extremely grateful! – Sean Thorburn Feb 14 '16 at 01:04