Search code examples
c#linqlinq-to-entitiesentity-framework-6expression-trees

Generating Dynamic Select Expression for entities with nested entity in it


I have a Class that has A property of Type Another Entity. Something like below:

public class InspectionTopicDataAllViewModel 
{
    public Guid Id { get; set; }
    public Guid? UploadedFileId { get; set; }
    public int InspectionId { get; set; }
    public int InspectionTopicId { get; set; }

    public SparseDataViewModel SparseData { get; set; }
}

public class SparseDataViewModel : SparseData
{        
}

I need To write a Dynamic Select Expression That able to select just columns that I want.

I Write this as below:

private static Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel> GetSparseInitExpression(List<string> columns)
    {
        Expression<Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel>> commonSelector =
           x => new InspectionTopicDataAllViewModel()
           {
               Id = x.Id,
               InspectionId = x.InspectionId,
               InspectionTopicId = x.InspectionTopicId,
               UploadedFileId = x.UploadedFileId,
           };


        // input parameter "x"
        var xParameter = Expression.Parameter(typeof(InspectionTopicDataAll), "x");           

        var xNew = Expression.New(typeof(InspectionTopicDataAllViewModel));

        var sNew = Expression.New(typeof(SparseDataViewModel));

        var bindings = new List<MemberBinding>();

        // create initializers
        foreach (var column in columns)
        {               
            Expression srcBody = xParameter;

            foreach (var member in column.Split('.'))
            {
                srcBody = Expression.PropertyOrField(srcBody, member);                
            }

            var destMember = srcBody as MemberExpression;
            // property "Field1"
            var propInfo = destMember?.Member as PropertyInfo;

            if (propInfo != null)
            {

                bindings.Add(Expression.Bind(propInfo, srcBody));
            }                               
        }

        var sInit = Expression.MemberInit(sNew, bindings);                                  

        var zeroth = ((MemberInitExpression)commonSelector.Body);
        var param = commonSelector.Parameters[0];
        List<MemberBinding> newBindings = new List<MemberBinding>(zeroth.Bindings.OfType<MemberAssignment>());

        var spNestedType = typeof (InspectionTopicDataAllViewModel).GetProperty(nameof(SparseData));

        newBindings.Add(Expression.Bind(spNestedType, sInit));

        var newInit = Expression.MemberInit(xNew, newBindings);

        var childSelector = Expression.Lambda<Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel>>(newInit, xParameter);

        return childSelector.Compile();
}

And Use This Function as Below In my main query:

 var model = worksheetService.GetInspectionTopicData(inspectionTopicId, inspectionId, discrimintor, value).Select(GetSparseInitExpression(columns)).AsQueryable();

With this function I can Generate the desired Expression as Below:

x => new InspectionTopicDataAllViewModel() 
{
    Id = x.Id, 
    InspectionId = x.InspectionId,
    InspectionTopicId = x.InspectionTopicId, 
    UploadedFileId = x.UploadedFileId, 
    SparseData = new SparseDataViewModel() {
        MusteriNo = x.SparseData.MusteriNo, 
        MusteriAdi = x.SparseData.MusteriAdi, 
        CekNo = x.SparseData.CekNo, 
        Banka = x.SparseData.Banka, 
        CekTarihi = x.SparseData.CekTarihi, 
        Meblag = x.SparseData.Meblag, 
        PCN = x.SparseData.PCN, 
        Kesideci = x.SparseData.Kesideci}}}

But when I comes to run expression this error thrown:

variable 'x' of type 'InternalControl.Domain.Entities.InspectionTopicDataAll' referenced from scope '', but it is not defined

I Don't know how can I fix this error...

Update: Thanks for @MBoros for his help, For whom maybe need this type of code later, with above code, even though code works and results can retrieved from database, the select statement is not worked as desired, and all columns fetched from database, so for solving this problem I used the code below: hope to help other people trying to do this sort of thing.

 private static Expression<Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel>> GetCustomSelectExpression(List<string> columns)
    {
        Expression<Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel>> commonSelector =
           x => new InspectionTopicDataAllViewModel()
           {
               Id = x.Id,
               InspectionId = x.InspectionId,
               InspectionTopicId = x.InspectionTopicId,
               UploadedFileId = x.UploadedFileId,
           };


        // input parameter "x"                    
        var xParameter = commonSelector.Parameters[0];

        var xNew = Expression.New(typeof(InspectionTopicDataAllViewModel));

        var sNew = Expression.New(typeof(SparseDataViewModel));

        var bindings = new List<MemberBinding>();

        // create initializers
        foreach (var column in columns)
        {               
            Expression srcBody = xParameter;

            foreach (var member in column.Split('.'))
            {
                srcBody = Expression.PropertyOrField(srcBody, member);                
            }

            var destColName = column.Substring(column.LastIndexOf('.')+1);               
            // property "Field1"
            var propInfo = typeof(SparseDataViewModel).GetProperty(destColName);


            if (propInfo != null)
            {
                bindings.Add(Expression.Bind(propInfo, srcBody));
            }                               
        }

        var sInit = Expression.MemberInit(sNew, bindings);                                  

        var zeroth = ((MemberInitExpression)commonSelector.Body);

        List<MemberBinding> newBindings = new List<MemberBinding>(zeroth.Bindings.OfType<MemberAssignment>());

        var spNestedType = typeof (InspectionTopicDataAllViewModel).GetProperty(nameof(SparseData));

        newBindings.Add(Expression.Bind(spNestedType, sInit));

        var newInit = Expression.MemberInit(xNew, newBindings);

        var childSelector = Expression.Lambda<Func<InspectionTopicDataAll, InspectionTopicDataAllViewModel>>(newInit, xParameter);

        return childSelector;
    } 

And use this in place of select statement as below:

var model =
            worksheetService.GetInspectionTopicData(inspectionTopicId, inspectionId, discrimintor, value)
                .Select(GetCustomSelectExpression(columns));

Solution

  • Your childSelector has no idea about the x parameter of the commonSelector. In expression trees parameters are recognized by instance, not just by name, so the two x parameters are actually different (even if in the debug view they look the same).

    The easiest solution is probably to change your

    // input parameter "x"
    var xParameter = Expression.Parameter(typeof(InspectionTopicDataAll), "x"); 
    

    to

    var xParameter = commonSelector.Parameters[0]; 
    

    That should do the trick :)