Видоизменение дерева выражений предиката для предназначения для другого типа

Введение

В приложении I 'm в настоящее время продолжающий работать, существует два вида каждого бизнес-объекта: вид "ActiveRecord" и вид "DataContract". Так, например, был бы:

namespace ActiveRecord {
    class Widget {
        public int Id { get; set; }
    }
}

namespace DataContract {
    class Widget {
        public int Id { get; set; }
    }
}

Слой доступа к базе данных заботится о переводе между семействами: можно сказать этому обновлять a DataContract.Widget и это волшебно создаст ActiveRecord.Widget с теми же значениями свойств и сохраняют это вместо этого.

Проблема появилась при попытке осуществить рефакторинг этот слой доступа к базе данных.

Проблема

Я хочу добавить методы как следующее к слою доступа к базе данных:

// Widget is DataContract.Widget

interface IDbAccessLayer {
    IEnumerable GetMany(Expression> predicate);
}

Вышеупомянутое является простым общим использованием, "получают" метод с пользовательским предикатом. Единственное интересное место - то, что я являюсь передающим в дереве выражений вместо лямбды потому что внутри IDbAccessLayer Я запрашиваю IQueryable; чтобы сделать это эффективно (думают LINQ к SQL), я должен передать в дереве выражений, таким образом, этот метод просит просто это.

Препятствие: параметр должен быть волшебно преобразован от Expression> к Expression>.

Предпринятое решение

Что мне 'd нравится делать внутри GetMany :

IEnumerable GetMany(
    Expression> predicate)
{
    var lambda = Expression.Lambda>(
        predicate.Body,
        predicate.Parameters);

    // use lambda to query ActiveRecord.Widget and return some value
}

Это не будет работать потому что в типичном сценарии, например, если:

predicate == w => w.Id == 0;

... дерево выражений содержит a MemberAccessExpression экземпляр, который имеет свойство типа MemberInfo это описывает DataContract.Widget.Id. Существуют также ParameterExpression экземпляры и в дереве выражений и в его наборе параметра (predicate.Parameters) это описывает DataContract.Widget; все это приведет к ошибкам, так как queryable тело не содержит тот тип виджета, а скорее ActiveRecord.Widget.

После поиска немного, я нашел System.Linq.Expressions.ExpressionVisitor (его источник может быть найден здесь в контексте практического руководства), который предлагает удобный способ изменить дерево выражений. В.NET 4, этот класс включен из поля.

Вооруженный этим, я реализовал посетителя. Этот простой посетитель только заботится об изменении типов в членском доступе и выражениях параметра, но это - достаточно функциональности для работы с предикатом w => w.Id == 0.

internal class Visitor : ExpressionVisitor
{
    private readonly Func typeConverter;

    public Visitor(Func typeConverter)
    {
        this.typeConverter = typeConverter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        var dataContractType = node.Member.ReflectedType;
        var activeRecordType = this.typeConverter(dataContractType);

        var converted = Expression.MakeMemberAccess(
            base.Visit(node.Expression),
            activeRecordType.GetProperty(node.Member.Name));

        return converted;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        var dataContractType = node.Type;
        var activeRecordType = this.typeConverter(dataContractType);

        return Expression.Parameter(activeRecordType, node.Name);
    }
}

С этим посетителем, GetMany становится:

IEnumerable GetMany(
    Expression> predicate)
{
    var visitor = new Visitor(...);
    var lambda = Expression.Lambda>(
        visitor.Visit(predicate.Body),
        predicate.Parameters.Select(p => visitor.Visit(p));

    var widgets = ActiveRecord.Widget.Repository().Where(lambda);

    // This is just for reference, see below
    Expression> referenceLambda = 
        w => w.Id == 0;

    // Here we 'd convert the widgets to instances of DataContract.Widget and
    // return them -- this has nothing to do with the question though.
}

Результаты

Хорошие новости - это lambda создается очень хорошо. Плохие новости - то, что это не работает; это аварийно завершается на мне, когда я пытаюсь использовать его, и сообщения об исключениях действительно не полезны вообще.

Я исследовал лямбду, которую мой код производит и hardcoded лямбда с тем же выражением; они смотрят точно то же. Я провел часы в отладчике, пытающемся найти некоторое различие, но я не могу.

Когда предикат w => w.Id == 0, lambda взгляды точно как referenceLambda. Но последние работы с, например. IQueryable.Where, в то время как первый не делает; я попробовал это в непосредственном окне отладчика.

Я должен также упомянуть это, когда предикат w => true, все хорошо работает. Поэтому я предполагаю, что я 'm не делающий достаточно работы в посетителе, но не могу найти, больше ведет для следования.

Конечное решение

После принятия во внимание корректных ответов на проблему (два из них ниже; одно короткое, один с кодом), проблема была решена; я поместил код наряду с несколькими важными примечаниями в отдельном ответе для препятствия этого долгого вопроса стать еще длиннее.

Благодаря всем для Ваших ответов и комментариев!

50
задан Community 23 May 2017 в 01:47
поделиться

4 ответа

Кажется, вы генерируете выражение параметра дважды, в VisitMember () здесь:

var converted = Expression.MakeMemberAccess(
    base.Visit(node.Expression),
    activeRecordType.GetProperty(node.Member.Name));

... поскольку base.Visit () окажется в VisitParameter, как я полагаю, и в самом GetMany ():

var lambda = Expression.Lambda<Func<ActiveRecord.Widget, bool>>(
    visitor.Visit(predicate.Body),
    predicate.Parameters.Select(p => visitor.Visit(p));

Если вы используете ParameterExpression в теле, это должен быть тот же экземпляр (а не только тот же тип и имя), что и тот, который объявлен для Lambda. Раньше у меня были проблемы с таким сценарием, хотя я думаю, что в результате я просто не смог создать выражение, оно просто выбросило бы исключение. В любом случае вы можете попробовать повторно использовать экземпляр параметра и посмотреть, поможет ли это.

15
ответ дан 7 November 2019 в 11:08
поделиться

Я думаю, что Linq-To-Sql создаст желаемый SQL, если вы правильно сделаете свои запросы. В данном случае, используя IQueryable и отложенное выполнение, можно избежать возврата всех записей ActiveRecord.Widget.

IEnumerable<DataContract.Widget> GetMany( 
    Func<DataContract.Widget, bool> predicate) 
{ 
    // get Widgets
    IQueryable<DataContract.Widget> qry = dc.Widgets.Select(w => TODO: CONVERT_TO_DataContract.Widget);

    return qry.Where(predicate);
}
-4
ответ дан 7 November 2019 в 11:08
поделиться

Я пробовал простую (не полную) реализацию для изменения выражения p => p.Id == 15 (код ниже). Есть один класс с именем «CrossMapping», который определяет соответствие между исходным и «новым» типами и членами типа.

Для каждого типа выражения существует несколько методов с именем Mutate_XY_Expression , которые создают новое измененное выражение. Для входных данных метода требуется исходное выражение ( MemberExpression originalExpression ) в качестве модели выражения, список или выражение параметров ( IList parameterExpressions ), которые определяются параметрами «родительским» выражением и должен использоваться "родительским" телом и объектом отображения ( Отображение CrossMapping ), который определяет отображение между типами и членами.

Для полной реализации вам может потребоваться больше информации из родительского выражения, чем из параметров. Но узор должен быть таким же.

Образец не реализует шаблон Visitor, как вы знаете - это из-за простоты. Но преград для перехода на них нет.

Надеюсь, это поможет.

Код (C # 4.0):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;

namespace ConsoleApplication1 {
    public class Product1 {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Weight { get; set; }
    }

    public class Product2 {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Weight { get; set; }
    }

    class Program {
        static void Main( string[] args ) {
            // list of products typed as Product1
            var lst1 = new List<Product1> {
                new Product1{ Id = 1, Name = "One" },
                new Product1{ Id = 15, Name = "Fifteen" },
                new Product1{ Id = 9, Name = "Nine" }
            };

            // the expression for filtering products
            // typed as Product1
            Expression<Func<Product1, bool>> q1;
            q1 = p => p.Id == 15;

            // list of products typed as Product2
            var lst2 = new List<Product2> {
                new Product2{ Id = 1, Name = "One" },
                new Product2{ Id = 15, Name = "Fifteen" },
                new Product2{ Id = 9, Name = "Nine" }
            };

            // type of Product1
            var tp1 = typeof( Product1 );
            // property info of "Id" property from type Product1
            var tp1Id = tp1.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
            // delegate type for predicating for Product1
            var tp1FuncBool = typeof( Func<,> ).MakeGenericType( tp1, typeof( bool ) );

            // type of Product2
            var tp2 = typeof( Product2 );
            // property info of "Id" property from type Product2
            var tp21Id = tp2.GetProperty( "Id", BindingFlags.Public | BindingFlags.Instance );
            // delegate type for predicating for Product2
            var tp2FuncBool = typeof( Func<,> ).MakeGenericType( tp2, typeof( bool ) );

            // mapping object for types and type members
            var cm1 = new CrossMapping {
                TypeMapping = {
                    // Product1 -> Product2
                    { tp1, tp2 },
                    // Func<Product1, bool> -> Func<Product2, bool>
                    { tp1FuncBool, tp2FuncBool }
                },
                MemberMapping = {
                    // Product1.Id -> Product2.Id
                    { tp1Id, tp21Id }
                }
            };

            // mutate express from Product1's "enviroment" to Product2's "enviroment"
            var cq1_2 = MutateExpression( q1, cm1 );

            // compile lambda to delegate
            var dlg1_2 = ((LambdaExpression)cq1_2).Compile();

            // executing delegate
            var rslt1_2 = lst2.Where( (Func<Product2, bool>)dlg1_2 ).ToList();

            return;
        }

        class CrossMapping {
            public IDictionary<Type, Type> TypeMapping { get; private set; }
            public IDictionary<MemberInfo, MemberInfo> MemberMapping { get; private set; }

            public CrossMapping() {
                this.TypeMapping = new Dictionary<Type, Type>();
                this.MemberMapping = new Dictionary<MemberInfo, MemberInfo>();
            }
        }
        static Expression MutateExpression( Expression originalExpression, CrossMapping mapping ) {
            var ret = MutateExpression(
                originalExpression: originalExpression,
                parameterExpressions: null,
                mapping: mapping
            );

            return ret;
        }
        static Expression MutateExpression( Expression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            Expression ret;

            if ( null == originalExpression ) {
                ret = null;
            }
            else if ( originalExpression is LambdaExpression ) {
                ret = MutateLambdaExpression( (LambdaExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is BinaryExpression ) {
                ret = MutateBinaryExpression( (BinaryExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is ParameterExpression ) {
                ret = MutateParameterExpression( (ParameterExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is MemberExpression ) {
                ret = MutateMemberExpression( (MemberExpression)originalExpression, parameterExpressions, mapping );
            }
            else if ( originalExpression is ConstantExpression ) {
                ret = MutateConstantExpression( (ConstantExpression)originalExpression, parameterExpressions, mapping );
            }
            else {
                throw new NotImplementedException();
            }

            return ret;
        }

        static Type MutateType( Type originalType, IDictionary<Type, Type> typeMapping ) {
            if ( null == originalType ) { return null; }

            Type ret;
            typeMapping.TryGetValue( originalType, out ret );
            if ( null == ret ) { ret = originalType; }

            return ret;
        }
        static MemberInfo MutateMember( MemberInfo originalMember, IDictionary<MemberInfo, MemberInfo> memberMapping ) {
            if ( null == originalMember ) { return null; }

            MemberInfo ret;
            memberMapping.TryGetValue( originalMember, out ret );
            if ( null == ret ) { ret = originalMember; }

            return ret;
        }
        static LambdaExpression MutateLambdaExpression( LambdaExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newParameters = (from p in originalExpression.Parameters
                                 let np = MutateParameterExpression( p, parameterExpressions, mapping )
                                 select np).ToArray();

            var newBody = MutateExpression( originalExpression.Body, newParameters, mapping );

            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );

            var ret = Expression.Lambda(
                delegateType: newType,
                body: newBody,
                name: originalExpression.Name,
                tailCall: originalExpression.TailCall,
                parameters: newParameters
            );

            return ret;
        }
        static BinaryExpression MutateBinaryExpression( BinaryExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newExprConversion = MutateExpression( originalExpression.Conversion, parameterExpressions, mapping );
            var newExprLambdaConversion = (LambdaExpression)newExprConversion;
            var newExprLeft = MutateExpression( originalExpression.Left, parameterExpressions, mapping );
            var newExprRigth = MutateExpression( originalExpression.Right, parameterExpressions, mapping );
            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
            var newMember = MutateMember( originalExpression.Method, mapping.MemberMapping);
            var newMethod = (MethodInfo)newMember;

            var ret = Expression.MakeBinary(
                binaryType: originalExpression.NodeType,
                left: newExprLeft,
                right: newExprRigth,
                liftToNull: originalExpression.IsLiftedToNull,
                method: newMethod,
                conversion: newExprLambdaConversion
            );

            return ret;
        }
        static ParameterExpression MutateParameterExpression( ParameterExpression originalExpresion, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpresion ) { return null; }

            ParameterExpression ret = null;
            if ( null != parameterExpressions ) {
                ret = (from p in parameterExpressions
                       where p.Name == originalExpresion.Name
                       select p).FirstOrDefault();
            }

            if ( null == ret ) {
                var newType = MutateType( originalExpresion.Type, mapping.TypeMapping );

                ret = Expression.Parameter( newType, originalExpresion.Name );
            }

            return ret;
        }
        static MemberExpression MutateMemberExpression( MemberExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newExpression = MutateExpression( originalExpression.Expression, parameterExpressions, mapping );
            var newMember = MutateMember( originalExpression.Member, mapping.MemberMapping );

            var ret = Expression.MakeMemberAccess(
                expression: newExpression,
                member: newMember
            );

            return ret;
        }
        static ConstantExpression MutateConstantExpression( ConstantExpression originalExpression, IList<ParameterExpression> parameterExpressions, CrossMapping mapping ) {
            if ( null == originalExpression ) { return null; }

            var newType = MutateType( originalExpression.Type, mapping.TypeMapping );
            var newValue = originalExpression.Value;

            var ret = Expression.Constant(
                value: newValue,
                type: newType
            );

            return ret;
        }
    }
}
6
ответ дан 7 November 2019 в 11:08
поделиться

Разве ExecuteTypedList не выполняет то, что вы хотите? SubSonic заполнит ваши DTO / POCO. Из блога Роба Коннери:

ExecuteTypedList <> пытается сопоставить имена возвращаемых столбцов с именами свойств переданного типа . В этом примере они точно совпадают - и это не полностью реальный мир. Вы можете решить эту проблему, применив псевдонимы для столбцов - точно так же, как вы бы использовали псевдоним для вызова SQL :

return Northwind.DB.Select("ProductID as 'ID'", "ProductName as 'Name'", "UnitPrice as 'Price'")
            .From<Northwind.Product>().ExecuteTypedList<Product>();

Вот ссылка на Writing Decoupled Роба , Тестируемый код с помощью SubSonic 2.1

-2
ответ дан 7 November 2019 в 11:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: