Can EF Core invoke methods in queries against properties which have conversions?

Viewed 538

Suppose I want to enhance a model like

public class Person
{
    public string Name { get; set; }
    public string Address { get; set; }
}

so that I use a complex object for Address:

public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

EF Core is quite nice in allowing this with the HasConversion feature:

modelBuilder.Entity<Person>
            .Property(p => p.Address)
            .HasConversion(addr => addr.ToString(), str => Address.FromString(str));

I even tested and this works in queries with == operator: The following will successfully convert to SQL

var whiteHouse = Address.Parse("1600 Pennsylvania Avenue NW");
var matches = from person in people 
              where person.Address == whiteHouse 
              select person;

However, suppose I want to string.Contains on the string version of Address, something like

var search = "1600";
var matches = from person in people 
              where person.Address.ToString().Contains(search) 
              select person;

This will fail to convert. Is there any feature of EF Core to map the ToString() method or otherwise map a complex object that converts to a string / VARCHAR so that I can write a query like this?

2 Answers

The problem with EF Core value converters and LINQ queries is that the LINQ query is against the CLR entity property, hence the CLR type rather than the provider type. This is partially mentioned under currently Limitations of the value conversion system section of the EF Core documentation:

Use of value conversions may impact the ability of EF Core to translate expressions to SQL. A warning will be logged for such cases. Removal of these limitations is being considered for a future release.

So having query expression against the CLR type combined with the inability to translate custom methods is causing your issue. Technically it's possible to add custom method/property translation, but it's quite complicated because requires a lot of non user friendly infrastructure plumbing code, which makes practically unusable in real life application development.

In this particular case though, you know that the provider type is string, and the database table values are generated by ToString method. So you just need to let the query use the provider type. And you can do that by using cast operator.

Normally C# compiler won't allow you to cast known object type to another known object type if there is no conversion between them. But you can trick it by using the "double cast" technique by first casting to object and then to the desired type. Fortunately EF Core translator supports such casts and properly (sort of) translates them to SQL. By sort of I mean it emits unnecessary (redundant) CAST inside the query, but at least it translates and executes server side.

With that being said, the solution for your example is

where ((string)(object)person.Address).Contains(search)

As a default behavior, EF Core use Server-side evaluation, EF Core try to translate your expression to standard DB provider T-SQL code (based on selected DB provider)

you expression can't translate to T-SQL code and the DB provider can't handle it (Because the logic you write in the overridden version of ToString() is in your C# code and is unknown to the database provider)

You should force EF Core to use client-side evaluation by fetching all data to memory and then query on the loaded entities, something like this:

 var search = "1600";
var matches = from person in people.ToList() 
              where person.Address.ToString().Contains(search) 
              select person;
  • Note that fetching all data to memory in huge databases have performance impacts and use client-side evaluation carefully.
Related