1

I'm trying to cast a property to object, then to string (Please don't ask why. :D) and then call to a method but it doesn't work for a very simple reason.

Let's take a look at the code:

var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
Expression<Func<EmployeeDto, object>> parameter = s => s.BirthDate;
var convert = Expression.Convert(Expression.Convert(parameter.Body, typeof(object)), typeof(string));
var condition = Expression.Call(
    convert,
    containsMethod!,
    Expression.Constant("bla bla", typeof(string))
);

I've omitted some unnecessary codes. Basically, I'm trying to cast any type of property into string then call to the Contains method. But the expression generates the following expression:

(string)(object) s.BirthDate.Contains("bla bla")

which is definitely incorrect syntax. I would like to put s.BirthDate in parentheses. Like this:

((string)(object)s.BirthDate).Contains("bla bla")
Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Iris
  • 1,436
  • 3
  • 14
  • 29
  • What's the type of `BirthDate`? Also, why do you think you have to do this? – Camilo Terevinto Nov 09 '21 at 08:45
  • That is DateTime. I'm trying to convert any property to string which is impossible in EF core. So I've found a workaround for that. Basically, you can cast something into an object and then into string that works fine. – Iris Nov 09 '21 at 08:46
  • Can you add the rest of the code to see how you're using this? It makes it easier to debug and update – Camilo Terevinto Nov 09 '21 at 09:00
  • How do you know it produces that expression with "incorrect syntax"? – Evk Nov 09 '21 at 09:15
  • 1
    You can cast a `DateTime` as an object no problem, but you cant then just magically cast that to a string. https://dotnetfiddle.net/nJ8yle - you could call`ToString()` on the object but thats a completely different thing (and doesnt need the cast to an `object`) – Jamiec Nov 09 '21 at 09:20
  • Also re: "Please don't ask why." - anyone not asking why is doing you a disservice in trying to help you! See: [XY Problem](http://xyproblem.info) – Jamiec Nov 09 '21 at 09:22
  • @Jamiec OP builds expression tree which is not the same. There you can do that, and depending on how that tree is handled - it might even make sense. – Evk Nov 09 '21 at 09:24
  • @Evk Really? Whatever way you cut this code `((string)(object)s.BirthDate).Contains("bla bla")` it will only work if `s.BirthDate` is castable to a string - and the OP confirmed that object is of type `DateTime` above in comments. Id be amazed if it worked! – Jamiec Nov 09 '21 at 09:27
  • @Jamiec expression tree is not necessary executed. For example if you use something like Entity Framework to convert expression into sql query - provider will not execute it, it will analyze it and convert to SQL. It might not matter that `DateTime` is not castable to `string` for that process. – Evk Nov 09 '21 at 09:34
  • @Jamiec here is an example I remember: https://stackoverflow.com/q/47902821/5311735 – Evk Nov 09 '21 at 09:50
  • @Evk Im still not convinced you're comparing apples to apples! And the given answer seems to agree. – Jamiec Nov 09 '21 at 09:57
  • @Jamiec I've added my own answer. You can test that yourself if you are curious. – Evk Nov 09 '21 at 10:53

2 Answers2

2

This is really not going to do what you think it will do. Expression.Convert is a type cast operation, and there is no type cast between DateTime and string. In fact very few types have a defined cast to string.

Try this:

var str = (string)((object)DateTime.Now);

You'll get an immediate error: "Unable to cast an object of type 'System.DateTime' to type 'System.String'". For your own types you can define an implicit or explicit conversion and it'll work, but not standard types like System.DateTime.

If you want a string value, every object in C# has a ToString method. Whether or not it returns something useful is up to the implementation, but it's the most reliable method for getting the string representation of any arbitrary object.

var toString = typeof(object).GetMethod("ToString");
var contains = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var selector = (Expression<Func<EmployeeDto, object>>)(e => e.BirthDate);

var testStringParam = Expression.Parameter(typeof(string), "s");

var condition = Expression.Lambda<Func<EmployeeDto, string, bool>>
(
    Expression.Call
    (
        Expression.Call
        (
            selector.Body,
            toString        
        ),
        contains,
        new[] { testStringParam}
    ),
    selector.Parameters[0], testStringParam
);

That's using a string parameter for the portion to find, but you already know how to do it with a constant.


Incidentally, you can do some fun things with expression visitors to make this a tiny bit simpler. I use the following extensively:

sealed class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression _from;
    private readonly Expression _to;
    
    private ReplaceVisitor(Expression from, Expression to)
    {
        _from = from;
        _to = to;
    }

    public override Expression Visit(Expression e)
    {
        if (ReferenceEquals(e, _from))
            return _to;
        return base.Visit(e);
    }
    
    public static T Execute<T>(T expression, Expression from, Expression to)
        where T : Expression
    {
        var replacer = new ReplaceVisitor(from, to);
        return (T)replacer.Visit(expression);
    }
}

(Abbreviated for simplicity - there are a few type checks and such in the Execute method that make it less prone to error.)

With that you can do:

Expression<Func<object, string, bool>> template =
    (o, s) => o == null ? null : o.ToString().Contains(s);
Expression<Func<EmployeeDto, object>> selector = e => e.BirthDate;

var pEmployee = selector.Parameters[0];
var pString = template.Parameters[1];

var condition = Expression.Lambda<Func<EmployeeDto, string, object>>
(
    ReplaceVisitor.Execute
    (
        template.Body,
        template.Parameters[0],
        selector.Body
    ),
    pEmployee, pString
);

In this case the outcome is mostly the same (added null checking) but you can leverage the compiler to check your template for correctness rather than having to rely on figuring things out at runtime. The compiler will let you know when you mess this up.

Corey
  • 15,524
  • 2
  • 35
  • 68
2

Your approach looks correct but you need to use expression parameter in a different way:

var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
// this is parameter we pass to lambda
// x => ...
var parameter = Expression.Parameter(typeof(EmployeeDto), "x");                
var convert = Expression.Convert(Expression.Convert(Expression.Property(parameter, "BirthDate"), typeof(object)), typeof(string));
var condition = Expression.Call(
    convert,
    containsMethod!,
    Expression.Constant("bla bla", typeof(string))
);
// x => ((string) (object) x).Contains("bla bla");
var lambda = Expression.Lambda<Func<EmployeeDto, bool>>(condition, parameter);

Now you can pass that lambda to EF Where clause.

To those who say that DateTime is not castable to string - it does not matter in this case. This expression is not going to be compiled and executed - it's going to be passed to Entity Framework to analyze and convert to SQL. What does matter is whether Entity Framework is able to convert such expression to SQL - and it does (at least EF version 6). Suppose we have table named "Picture" with column of type datetime named "CreationTime". Then, this query:

// c.CreationTime is of .NET DateTime type here
var picts = await ctx.Pictures.Where(c => ((string)(object)c.CreationTime).Contains("12")).ToArrayAsync();

Does work just fine and produces sql query in the form of:

select * from Picture where CAST(CreationTime as nvarchar(max)) LIKE '%12%'

And returns matching entities without problems (whether it's good idea to make such queries in terms of efficiency is another story).

Evk
  • 98,527
  • 8
  • 141
  • 191