koenbeuk / EntityFrameworkCore.Projectables

Project over properties and functions in your linq queries
MIT License
297 stars 20 forks source link

Interface Expression not translated #89

Open DG4ever opened 11 months ago

DG4ever commented 11 months ago

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?

koenbeuk commented 11 months ago

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?

DG4ever commented 11 months ago

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

DG4ever commented 10 months ago

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();
koenbeuk commented 10 months ago

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.
JodliDev commented 7 months ago

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

koenbeuk commented 6 months ago

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

JodliDev commented 6 months ago

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