Home > OS >  C# Put expression in parentheses
C# Put expression in parentheses

Time:11-09

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")

CodePudding user response:

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.

CodePudding user response:

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 '%'

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

  • Related