I'm working with Microsoft's Entity Framework Core and tried to utilize ValueConverters to allow for custom types in my database model entities. The point is to have my own type which I can customize and which shields the rest of the code from a type actually used in database.
(Sadly, the legacy code access the model entities directly with no interface, so this is what I am left with unless I do a significant overhaul.)
It mostly works, but my problem is that Entity Framework is not able to convert my type to database type for a where clause (possibly others, but this is what I have encountered) and instead does a client-side evaluation, which obviously is a performace issue, as all candidates are queried.
So, I wonder if anyone has encountered this and if there is a solution, or if I have to try something different.
If you want some code, there it is. I tried to trim it down, so the implementation is a bit odd, but it still fails in the same way.
Let's call my custom struct type ItemId
, make it hold a string and allow it to be created from either long or string:
public struct ItemId
{
public string Data;
public ItemId(long data)
{
Data = data.ToString();
}
public ItemId(string data)
{
Data = data;
}
public override bool Equals(object obj)
{
return obj is ItemId itemId && Data == itemId.Data;
}
public override int GetHashCode()
{
return HashCode.Combine(Data);
}
public static bool operator ==(ItemId id1, ItemId id2)
{
return id1.Data == id1.Data;
}
public static bool operator !=(ItemId id1, ItemId id2)
{
return !(id1== id1);
}
}
Then, there is my converter for a database which stores 64-bit numeric Ids. I strongly suspect that hand-written Expressions are unnecessary, as the build-in converters often don't use them and they seem to work fine, but I've added them in attempt to fix my problem:
public class ItemIdToLongConverter : ValueConverter<ItemId, long>
{
public ItemIdToLongConverter(ConverterMappingHints mappingHints = null)
: base(ToLong(), ToItemId(), mappingHints)
{ }
protected static Expression<Func<ItemId, long>> ToLong()
{
var data = typeof(ItemId).GetField(nameof(ItemId.StringData));
var tryParseMethod = typeof(long).GetMethod(
nameof(long.TryParse),
new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), typeof(long).MakeByRefType() });
var param = Expression.Parameter(typeof(ItemId));
var parsedVariable = Expression.Variable(typeof(long));
return Expression.Lambda<Func<ItemId, long>>(
Expression.Block(
typeof(long),
new[] { parsedVariable },
Expression.Condition(
Expression.Call(
tryParseMethod,
Expression.Field(param, data),
Expression.Constant(NumberStyles.Any),
Expression.Constant(CultureInfo.InvariantCulture, typeof(IFormatProvider)),
parsedVariable),
parsedVariable,
Expression.Constant(default(long), typeof(long)))),
param);
}
protected static Expression<Func<long, ItemId>> ToItemId()
{
var ctor = typeof(ItemId).GetConstructor(new[] { typeof(long) });
var param = Expression.Parameter(typeof(long));
return Expression.Lambda<Func<long, ItemId>>(
Expression.Block(
typeof(ItemId),
Expression.New(ctor, param)
),
param);
}
}
I register my converter in the model in this fashion:
modelBuilder.Entity<MyTable>(entity =>
{
...
entity.Property(e => e.ItemId).HasConversion(new ItemIdToLongConverter()).ValueGeneratedNever();
...
});
And here is a query which gets client-evaluated because it can not convert id
to database type:
var id = new ItemId(100);
dbContext.MyTable.FirstOrDefault(x => x.ItemId == id);
Curiously, this oddly structured one gets translated fine:
var ids = Enumerable.Repeat(new ItemId(100), 1);
dbContext.MyTable.FirstOrDefault(x => ids.Contains(x.ItemId));
==
operator herex => x.ItemId == id
coming from? It doesn't compile with the current code. – DisenfranchiseEquals
etc, nothing worked. So my first comment applies. Apparently this is one of the current cases that are not translated to SQL. You can post in their GitHub issue tracker if you wish, but until (and if) they fix it, there is nothing you can do (except using the strange hack :)) – DisenfranchiseString <-> TNumber
converter works for translations and it seems equivalent to mine, and the hack works, so seemingly it can translate ItemId sometimes. But at this point I'm quite willing to believe it's just a new feature with strange quirks, so maybe I won't avoid reworking some old code after all :( Thanks for suggestions anyway. – Precis