EntityFrameworkCore.Projectables
EntityFrameworkCore.Projectables copied to clipboard
Interface Expression not translated
My Entity QualityDataPart
implements an interface IQualityDataPart
which has a property Type
(string).
In order to save storage this text is stored in a different Entity QualityDataPartInfo
Now I want to map the property from the different Entity:
public class QualityDataPart : IQualityDataPart
{
...some other props...
public virtual QualityDataPartInfo PartInfo { get; init; } = new QualityDataPartInfo();
[Projectable] public string Type => PartInfo.Type;
}
The user should be able to query an expression via the exposed interface IQualityDataPart
Hovever this will not transalte the Type
property to PartInfo.Type
.
This won't work:
Expression<Func<IQualityDataPart, bool>> test = p => p.Type == "Test";
return await qualityDataParts.Where(test).ToListAsync();
This is working as expected:
Expression<Func<QualityDataPart, bool>> test = p => p.Type == "Test";
return await qualityDataParts.Where(test).ToListAsync();
Might this be a bug or am I doing something wrong?
Having a projectable property on the concrete entity type as you have in QualityDataPart is supported. Your sample code is therefore supported and when I run this:
using System;
using EntityFrameworkCore.Projectables;
public class QualityDataPartInfo { public string Type => ""Test""; }
public interface IQualityDataPart { string Type {get;} }
public class QualityDataPart : IQualityDataPart
{
public virtual QualityDataPartInfo PartInfo { get; set; } = new QualityDataPartInfo();
[Projectable] public string Type => PartInfo.Type;
}
I get the following generated companion expression:
// <auto-generated/>
#nullable disable
using System;
using EntityFrameworkCore.Projectables;
namespace EntityFrameworkCore.Projectables.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
static class _QualityDataPart_Type
{
static global::System.Linq.Expressions.Expression<global::System.Func<global::QualityDataPart, string>> Expression()
{
return (global::QualityDataPart @this) => @this.PartInfo.Type;
}
}
}
Can you share the actual generated code that is failing?
Thanks for your response. I didn't know that there will be auto generated code. Where do I find it?
Ok I have found the generated sources (maybe you should add a hint to the readme that they are visible in Dependencies > Analyzers > EntityFrameworkCore.Projectables.Generator)
Here is the auto generated code:
// <auto-generated/>
#nullable disable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using QualityData.Types;
using EntityFrameworkCore.Projectables;
using DataContext.Modules.EntityFramework.Database.Tables;
namespace EntityFrameworkCore.Projectables.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
static class DataContext_Modules_EntityFramework_Database_Tables_QualityDataPart_Type
{
static global::System.Linq.Expressions.Expression<global::System.Func<global::DataContext.Modules.EntityFramework.Database.Tables.QualityDataPart, string>> Expression()
{
return (global::DataContext.Modules.EntityFramework.Database.Tables.QualityDataPart @this) => @this.PartInfo.Type;
}
}
}
This seems correct to me. But I think the expession from the interface is just not translated to the concrete type.
When I build the expression with the concrete type everything works as expected. But when I use the interface the auto-generated expression is not used.
//working as expected
Expression<Func<QualityDataPart, bool>> test = p => p.Type == "Test";
var testResult = await qualityDataParts.Where(test).ToListAsync();
//generated expression not used -> "Translation of member 'Type' on entity type 'QualityDataPart' failed"
Expression<Func<IQualityDataPart, bool>> test2 = p => p.Type == "Test";
var testResult2 = await qualityDataParts.Where(test2).ToListAsync();
This seems to be an issue that will hard to solve.
Expression<Func<IQualityDataPart, bool>> test = p => p.Type == "Test";
doesn't inform this library what concrete type IQualityDataPart
actually is and therefore, this library would not know where to get the generated expression from. I can see some options here:
- Support for default interface implementation so that you can mark an interface member as Projectable and give it a default implementation for which we can generate an expression and use it at runtime.
- Enhance the projectable attribute to refer to an different type when specifying:
UseMemberBody
so that we can teach an interface where the expression of that interface lives.
I have not tested this with your code, but we had a similar situation with a slightly stupid - but working - solution. Maybe this helps others:
Not working (from your code):
Expression<Func<IQualityDataPart, bool>> test2 = p => p.Type == "Test";
var testResult2 = await qualityDataParts.Where(test2).ToListAsync();
Working (in theory):
public string GetData(IQualityDataPart input)
{
return input.Type
}
Expression<Func<IQualityDataPart, bool>> test2 = p => GetData(p) == "Test";
var testResult2 = await qualityDataParts.Where(test2).ToListAsync();
My understanding of this solution: The main issue with Interfaces is, that the library tries to find a class when it only got information to find an interface - which is why it fails originally. What we do, is give it a method whithout having to touch the interface. The method is outside the generated expression. So it actually knows how to deal with the interface.
I do not have a 100% grasp of this problem, so our case might be different than this. If so, I apologize. But nevertheless, this issue helped solve our problem, so thank you :D
@JodliDev This will not work. In your example, The implementation of GetData
is still a blackbox for EF and since the method is not marked as a Projectable
, there are no companion expression trees that this library can take to tell EF how it is implemented. Even if that methods was marked as a Projectable, we would still run into this particular issue as we do not know in advance the concrete type of input and therefore we can't swap out a call to Type with a concrete expression.
Hm. But I think it works if GetData()
is static, no?
Here is some code that actually works for us (notice IDataContextForIdHelper
that is "translated" by GetTable
into an IQueryable
):
namespace BlueDanubeCrmModel
{
public interface IDataContextForIdHelper
{
IQueryable<T> GetIQueryable<T>() where T : class;
}
class SlModel {
public static string BuildIdHelperString(RobotModelIdHelperData robotData, string category, string slName, string flag, int p)
{
return RobotModel.BuildIdHelperString(robotData) + BuildSlModelString(category, slName, flag, p);
}
[Projectable]
public string IdHelperJoined(IDataContextForIdHelper dataContext) =>
BuildIdHelperString(
GetTable(dataContext)
.Where(x => x.Id == RobotModelId)
.Select(x => new RobotModelIdHelperData
{
RobMan = x.RobMan,
RobMod1 = x.RobMod1,
RobMod2 = x.RobMod2,
RobMod3 = x.RobMod3
})
.FirstOrDefault(),
Category
);
}
public static IQueryable<RobotModel> GetTable(IDataContextForIdHelper dataContext)
{
return dataContext.GetIQueryable<RobotModel>();
}
}
}
This generates the following code:
namespace EntityFrameworkCore.Projectables.Generated
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
static class BlueDanubeCrmModel_SlModel_IdHelperJoined
{
static global::System.Linq.Expressions.Expression<global::System.Func<global::BlueDanubeCrmModel.SlModel, global::DataAccessNfStandard.DataExtender.IDataContextForIdHelper, string>> Expression()
{
return (global::BlueDanubeCrmModel.SlModel @this, global::DataAccessNfStandard.DataExtender.IDataContextForIdHelper dataContext) => global::BlueDanubeCrmModel.SlModel.BuildIdHelperString(global::System.Linq.Queryable.FirstOrDefault(global::System.Linq.Queryable.Select(global::System.Linq.Queryable.Where(global::BlueDanubeCrmModel.SlModel.GetTable(dataContext), x => x.Id == @this.RobotModelId), x => new global::BlueDanubeCrmModel.RobotModel.RobotModelIdHelperData { RobMan = x.RobMan, RobMod1 = x.RobMod1, RobMod2 = x.RobMod2, RobMod3 = x.RobMod3 })), @this.Category, @this.SlName, @this.Flag, @this.P);
}
}
}
Not the most pretty, and we are still testing things, but so far it seems to work and seems to produce SQL queries that do what we want