EntityFrameworkCore.Projectables icon indicating copy to clipboard operation
EntityFrameworkCore.Projectables copied to clipboard

Interface Expression not translated

Open DG4ever opened this issue 1 year ago • 7 comments

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?

DG4ever avatar Oct 26 '23 19:10 DG4ever

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?

koenbeuk avatar Oct 31 '23 01:10 koenbeuk

Thanks for your response. I didn't know that there will be auto generated code. Where do I find it?

DG4ever avatar Oct 31 '23 06:10 DG4ever

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();

DG4ever avatar Nov 13 '23 09:11 DG4ever

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:

  1. 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.
  2. 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.

koenbeuk avatar Nov 19 '23 02:11 koenbeuk

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 avatar Mar 08 '24 10:03 JodliDev

@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.

koenbeuk avatar Mar 12 '24 00:03 koenbeuk

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

JodliDev avatar Mar 12 '24 09:03 JodliDev