Вопрос по entity-framework, linq, expression-trees – Перевод дерева выражений из типа в другой тип со сложными отображениями

5

вдохновленныйэтот ответ Я пытаюсь сопоставить свойство класса модели с выражением, основанным на фактической сущности. Это два участвующих класса:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Id { get; set; }
    public DateTime? BirthDate { get; set; }
    public int CustomerTypeId { get; set; }
}

public class CustomerModel
{
    ...
    public bool HasEvenId { get; set; }
}

Пример возможного выражения, которое я хотел бы преобразовать:

Expression<Func<CustomerModel, bool>> from = model => model.HasEvenId;
Expression<Func<Customer, bool>> to = entity => ((entity.Id % 2) == 0);

Проблема заключается в том, что мне нужно предоставить конечную точку OData через ASP.NET WebAPI, но мне нужно выполнить некоторые операции над объектами, прежде чем я смогу их получить, поэтому мне нужен класс модели и необходимость переводить выражение на основе модели, которая Я мог получить запрос OData в выражении, основанном на сущности, который я использовал бы для запроса EF4.

Вот где я дошел до сих пор:

private static readonly Dictionary<Expression, Expression> Mappings = GetMappings();

private static Dictionary<Expression, Expression> GetMappings()
{
    var mappings = new Dictionary<Expression, Expression>();

    var mapping = GetMappingFor((CustomerModel model) => model.HasEvenId, (Customer customer) => (customer.Id%2) == 0);
    mappings.Add(mapping.Item1, mapping.Item2);

    return mappings;
}

private static Tuple<Expression, Expression> GetMappingFor<TFrom, TTo, TValue>(Expression<Func<TFrom, TValue>> fromExpression, Expression<Func<TTo, TValue>> toExpression)
{
    MemberExpression fromMemberExpression = (MemberExpression) fromExpression.Body;
    return Tuple.Create<Expression, Expression>(fromMemberExpression, toExpression);
}

public static Expression<Func<TTo, bool>> Translate<TFrom, TTo>(Expression<Func<TFrom, bool>> expression, Dictionary<Expression, Expression> mappings = null)
{
    if (expression == null)
        return null;

    string parameterName = expression.Parameters[0].Name;

    parameterName = string.IsNullOrWhiteSpace(parameterName) ? "p" : parameterName;

    var param = Expression.Parameter(typeof(TTo), parameterName);
    var subst = new Dictionary<Expression, Expression> { { expression.Parameters[0], param } };
    ParameterChangeVisitor parameterChange = new ParameterChangeVisitor(parameterName);
    if (mappings != null)
        foreach (var mapp in mappings)
            subst.Add(mapp.Key, parameterChange.Visit(mapp.Value));

    var visitor = new TypeChangeVisitor(typeof(TFrom), typeof(TTo), subst);
    return Expression.Lambda<Func<TTo, bool>>(visitor.Visit(expression.Body), param);
}

public IQueryable<CustomerModel> Get()
{
    var filterExtractor = new ODataFilterExtractor<CustomerModel>();
    Expression<Func<CustomerModel, bool>> expression = filterExtractor.Extract(Request);
    Expression<Func<Customer, bool>> translatedExpression = Translate<CustomerModel, Customer>(expression, Mappings);

    IQueryable<Customer> query = _context.Customers;

    if (translatedExpression != null)
        query = query.Where(translatedExpression);

    var finalQuery = from item in query.AsEnumerable() 
                     select new CustomerModel()
                        {
                            FirstName = item.FirstName, 
                            LastName = item.LastName, 
                            Id = item.Id, 
                            BirthDate = item.BirthDate, 
                            CustomerTypeId = item.CustomerTypeId,
                            HasEvenId = (item.Id % 2 ) == 0
                        };

    return finalQuery.AsQueryable();
}

где:

ODataFilterExtractor is a class that extract the $filter expression from the RequestMessage we receive; ParameterChangeVisitor just changes all the ParameterExpression to a new one having the provided string as parameter name;

Кроме того, я изменил метод VisitMember ответа, связанного выше, таким образом:

protected override Expression VisitMember(MemberExpression node)
{
    // if we see x.Name on the old type, substitute for new type
    if (node.Member.DeclaringType == _from)
    {
        MemberInfo toMember = _to.GetMember(node.Member.Name, node.Member.MemberType, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).SingleOrDefault();
        if (toMember != null)
        {
            return Expression.MakeMemberAccess(Visit(node.Expression), toMember);
        }
        else
        {
            if (_substitutions.Select(kvp => kvp.Key).OfType<MemberExpression>().Any(me => me.Member.Equals(node.Member)))
            {
                MemberExpression key = _substitutions.Select(kvp => kvp.Key).OfType<MemberExpression>().Single(me => me.Member.Equals(node.Member));
                Expression value = _substitutions[key];

                // What to return here?
                return Expression.Invoke(value);
            }
        }
    }
    return base.VisitMember(node);
}

Спасибо за помощь.

Как насчет создания представления SQL? Guillaume86
Ясно, что вопрос: как получить выражение, основанное на определенном классе, как я могу получить эквивалентное выражение, работающее со вторым классом с более сложными переводами, чем простое отображение по имени? Я надеюсь, что это может помочь. Kralizek
Ну, ты действительно смелый! Внедряя провайдера Linq, будь собой ... Aliostad
я думаю, что посетитель может сделать работу Proviste
Не могли бы вы преобразовать свое неясное предложениеThe problem is (...) to query EF4. на четкий, лаконичный вопрос? Возможно, отметьте кусок кода, где вам нужна помощь? Gert Arnold

Ваш Ответ

2   ответа
3

AutoMapper отобразить сложные типы и изменить полученный запрос выражения с помощью ExpressionTransformer до его выполнения. Я постараюсь объяснить с полным примером:

Model classes

Некоторые классы POCO только для хранения данных.

public enum CostUnitType
{
    None = 0,
    StockUnit = 1,
    MachineUnit = 2,
    MaintenanceUnit = 3
}

public class CostUnit
{
    public string CostUnitId { get; set; }
    public string WorkplaceId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    public CostUnitType Type { get; set; }

    public override string ToString()
    {
        return CostUnitId + " " + Name + " " + Type;
    }
}

public class StockUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MachineUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MaintenanceUnit
{
    public string Id { get; set; }
    public string Name { get; set; }
}

The ExpressionTransformer class

Большую часть времени можно использовать статический класс Mapper, но иногда вам нужно сопоставить одни и те же типы с разными конфигурациями, поэтому вам нужно явно использовать IMappingEngine, например:

var configuration = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
var engine = new MappingEngine(configuration);

Способ создания MappingEngine совсем не очевиден. Мне пришлось копаться в исходном коде, чтобы найти, как это было сделано.

public static class ExpressionTransformer
{
    private static readonly MappingEngine Mapper;

    /// <summary>
    /// Initializes the <see cref="ExpressionTransformer"/> class.
    /// Creates an instance of AutoMapper. Initializes mappings.
    /// </summary>
    static ExpressionTransformer()
    {
        ConfigurationStore configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);

        // Create mapping
        // Maps Id from StockUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<StockUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));
        // Maps Id from MachineUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<MachineUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));
        // Maps Id from MaintenanceUnit to CostUnitId from CostUnit
        configurationStore.CreateMap<MaintenanceUnit, CostUnit>()
            .ForMember(m => m.CostUnitId, opt => opt.MapFrom(src => src.Id));

        // Create instance of AutoMapper
        Mapper = new MappingEngine(configurationStore);
    }

    public static Expression<Func<TDestination, bool>> Tranform<TSource, TDestination>(Expression<Func<TSource, bool>> sourceExpression)
    {
        // Resolve mappings by AutoMapper for given types.
        var map = Mapper.ConfigurationProvider.FindTypeMapFor(typeof(TSource), typeof(TDestination));

        if (map == null)
        {
            throw new AutoMapperMappingException(string.Format("No Mapping found for {0} --> {1}.", typeof(TSource).Name, typeof(TDestination).Name));
        }

        // Transform from TSource to TDestination with specified mappings
        var visitor = new ParameterTypeVisitor<TSource, TDestination>(sourceExpression, map.GetPropertyMaps());
        var expression = visitor.Transform();

        return expression;
    }

    private class ParameterTypeVisitor<TSource, TDestination> : ExpressionVisitor
    {
        private readonly Dictionary<string, ParameterExpression> _parameters;
        private readonly Expression<Func<TSource, bool>> _expression;
        private readonly IEnumerable<PropertyMap> _maps;

        public ParameterTypeVisitor(Expression<Func<TSource, bool>> expression, IEnumerable<PropertyMap> maps)
        {
            _parameters = expression.Parameters
                .ToDictionary(p => p.Name, p => Expression.Parameter(typeof(TDestination), p.Name));

            _expression = expression;

            _maps = maps;
        }

        public Expression<Func<TDestination, bool>> Transform()
        {
            return (Expression<Func<TDestination, bool>>) Visit(_expression);
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Member.DeclaringType == typeof(TSource))
            {
                var memberName = node.Member.Name;
                var member = _maps.FirstOrDefault(p => typeof(TSource) == node.Expression.Type
                                                        && p.SourceMember.Name == memberName);
                if (member != null)
                {
                    // Return Property from TDestination
                    var expression = Visit(node.Expression);
                    return Expression.MakeMemberAccess(expression, member.DestinationProperty.MemberInfo);
                }
            }

            return base.VisitMember(node);
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var parameter = _parameters[node.Name];
       ,     return parameter;
        }

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            var expression = Visit(node.Body);
            return Expression.Lambda(expression, _parameters.Select(x => x.Value));
        }
    }
}

Usage

Для преобразования выражения нам просто нужно вызвать метод Transform класса ExpressionTransformer.

            Expression<Func<StockUnit, bool>> stockQuery = unit => unit.Id == "0815" && unit.Name == "ABC";

            // Call Transform<TSource, TDestination> method.
            Expression<Func<CostUnit, bool>> costUnitQuery = ExpressionTransformer.Tranform<StockUnit, CostUnit>(stockQuery);

Result

Resulting expressions

4

но это делает свое дело,

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Id { get; set; }
    public DateTime? BirthDate { get; set; }
    public int CustomerTypeId { get; set; }
}

public class CustomerModel
{
    public string FullName { get; set; }
    public bool HasEvenId { get; set; }
}

sealed class AToBConverter<TA, TB> : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> _parameters = new Dictionary<ParameterExpression, ParameterExpression>();
    private readonly Dictionary<MemberInfo, LambdaExpression> _mappings;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (node.Type == typeof(TA))
        {
            ParameterExpression parameter;
            if (!this._parameters.TryGetValue(node, out parameter))
            {
                this._parameters.Add(node, parameter = Expression.Parameter(typeof(TB), node.Name));
            }
            return parameter;
        }
        return node;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == null || node.Expression.Type != typeof(TA))
        {
            return base.VisitMember(node);
        }
        Expression expression = this.Visit(node.Expression);
        if (expression.Type != typeof(TB))
        {
            throw new Exception("Whoops");
        }
        LambdaExpression lambdaExpression;
        if (this._mappings.TryGetValue(node.Member, out lambdaExpression))
        {
            return new SimpleExpressionReplacer(lambdaExpression.Parameters.Single(), expression).Visit(lambdaExpression.Body);
        }
        return Expression.Property(
            expression,
            node.Member.Name
        );
    }

    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        return Expression.Lambda(
            this.Visit(node.Body),
            node.Parameters.Select(this.Visit).Cast<ParameterExpression>()
        );
    }

    public AToBConverter(Dictionary<MemberInfo, LambdaExpression> mappings)
    {
        this._mappings = mappings;
    }
}

sealed class SimpleExpressionReplacer : ExpressionVisitor
{
    private readonly Expression _replacement;
    private readonly Expression _toFind;

    public override Expression Visit(Expression node)
    {
        return node == this._toFind ? this._replacement : base.Visit(node);
    }

    public SimpleExpressionReplacer(Expression toFind, Expression replacement)
    {
        this._toFind = toFind;
        this._replacement = replacement;
    }
}

class Program 
{
    private static Dictionary<MemberInfo, LambdaExpression> GetMappings()
    {
        var mappings = new Dictionary<MemberInfo, LambdaExpression>();
        var mapping = GetMappingFor(model => model.HasEvenId, customer => (customer.Id % 2) == 0);
        mappings.Add(mapping.Item1, mapping.Item2);
        mapping = GetMappingFor(model => model.FullName, customer => customer.FirstName + " " + customer.LastName);
        mappings.Add(mapping.Item1, mapping.Item2);
        return mappings;
    }

    private static Tuple<MemberInfo, LambdaExpression> GetMappingFor<TValue>(Expression<Func<CustomerModel, TValue>> fromExpression, Expression<Func<Customer, TValue>> toExpression)
    {
        return Tuple.Create(((MemberExpression)fromExpression.Body).Member, (LambdaExpression)toExpression);
    }

    static void Main()
    {
        Expression<Func<CustomerModel, bool>> source = model => model.HasEvenId && model.FullName == "John Smith";
        Expression<Func<Customer, bool>> desiredResult = model => (model.Id % 2) == 0 && (model.FirstName + " " + model.LastName) == "John Smith";
        Expression output = new AToBConverter<CustomerModel, Customer>(GetMappings()).Visit(source);
        Console.WriteLine("The two expressions do {0}match.", desiredResult.ToString() == output.ToString() ? null : "not ");
        Console.ReadLine();
    }
}
Я попробую код в эти выходные :) Kralizek
Как вы можете видеть в коде, я добавил свойство FullName к классу модели, чтобы еще больше проверить отображение отношений. Я также взял на себя смелость посетителя кодировать его так, что любое другое свойство будет существовать в обоих (т. Е. Если вы добавите свойство Id в CustomerModel, то любой запрос к свойству CustomerModel Id будет просто переведен как запрос по идентификатору клиента, поскольку специально для этого свойства нет карт)
Любой совет о том, как генерировать входные литералы? То есть, в примере с полным именем, что если вы не знаете, что такое «Джон Смит»? будет, но может быть любое строковое значение?
Большое спасибо за это. Комбинируя ваш код с кодом изstackoverflow.com/questions/18365764/…Я смог предоставить слой OData поверх массивной уже существующей базы данных. Потребовалась небольшая дополнительная работа для перевода объектов IQueryable, созданных из классов шаблонов репозитория, и передачи их в OData, но это работает. Используя традиционную проекцию и / или.ToList() вызовы привели к ошибкам OOM. Ваш подход допускает очень позднее связывание, поэтому объекты транслируются только тогда, когда OData запрашивает их специально.

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