Как обернуть результат ExpressionTree в классе контейнера? - PullRequest
0 голосов
/ 19 сентября 2018

Я пытаюсь создать помощника, совместимого с Linq2Sql

Общая идея того, что я пытаюсь сделать, такова:

internal Expression<Func<TSource, Wrapper<TResult>>>
    Wrap<TSource, TResult>(Expression<Func<TSource, TResult>> dataSelector)
    where TSource : IHasOtherProperty
{
    return (TSource data) => new Wrapper<TResult> {
        Entity = dataSelector(data),
        Extra = data.OtherProperty,
    };
}

, чтобы я мог позвонить:

dataStore.Select(Wrap(query))

в местах, которые я сейчас называю

dataStore.Select(query)

Теперь это должно быть совместимо с Linq2Sql, что означает, что это нужно сделать как ExpressionTree.

У меня возникают проблемы с выяснением, как распределить значение dataSelector для Entity дружественным способом EntityFramework

Ниже приведен сломанный прототип:

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.Linq;
using System.Linq.Expressions;

namespace TestLinq
{
    class Program
    {
        static void Main(string[] args)
        {
            var parent = new ParentDomainModel
            {
                ID = Guid.NewGuid(),
            };

            var test = new TestContext { };
            test.Parents.Add(parent);
            test.Metadata.Add(new MetadataDomainModel { ID = Guid.NewGuid(), IsDeleted = false, Key = "test", Value = "value", Parent = parent });
            test.SaveChanges();

            var result = test.Parents
                .WithMetadata<ParentDomainModel, MetadataDomainModel, ParentApiModel>(d => new ParentApiModel { ID = d.ID });

            var materialized = result
                .ToArray();
        }
    }

    public class ParentApiModel : IDescribedEntity
    {
        public Guid ID { get; set; }
        public IDictionary<String, String> Metadata { get; set; }
    }

    public class TestContext : DbContext
    {
        public DbSet<ParentDomainModel> Parents { get; set; }
        public DbSet<MetadataDomainModel> Metadata { get; set; }

        public TestContext() : base()
        {
            this.Database.CommandTimeout = 120;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
            modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();

            base.OnModelCreating(modelBuilder);
        }
    }

    public class ParentDomainModel : IDescribedDomainModel<MetadataDomainModel>
    {
        public Guid ID { get; set; }
        public ICollection<MetadataDomainModel> Metadata { get; set; }
    }

    public class MetadataDomainModel : IMetadata
    {
        public Guid ID { get; set; }
        public ParentDomainModel Parent { get; set; }
        public Guid ParentID { get; set; }
        public Boolean IsDeleted { get; set; }
        public String Key { get; set; }
        public String Value { get; set; }
    }

    public class KeyValuePairApiModel<TKey, TValue>
    {
        [JsonProperty("key")]
        public TKey Key { get; set; }

        [JsonProperty("value")]
        public TKey Value { get; set; }
    }

    public interface IDescribedEntity
    {
        IDictionary<String, String> Metadata { get; set; }
    }

    public interface IMetadata
    {
        Guid ParentID { get; set; }

        Boolean IsDeleted { get; set; }

        String Key { get; set; }

        String Value { get; set; }
    }

    public interface IDescribedDomainModel<TMetadata> where TMetadata : IMetadata
    {
        ICollection<TMetadata> Metadata { get; set; }
    }

    public class MetaWrapper<TEntity> where TEntity : IDescribedEntity
    {
        public TEntity Entity { get; set; }
        public IEnumerable<KeyValuePairApiModel<String, String>> Metadata { get; set; }

        public static implicit operator TEntity(MetaWrapper<TEntity> data)
        {
            if (data.Metadata != null)
            {
                var metadata = new Dictionary<String, String>(StringComparer.InvariantCultureIgnoreCase) { };

                foreach (var kvp in data.Metadata)
                {
                    metadata[kvp.Key] = kvp.Value;
                }

                data.Entity.Metadata = metadata;
            }

            return data.Entity;
        }
    }

    internal static class MetadataHelpers
    {
        internal static IEnumerable<TResult> WithMetadata<TSource, TMetadata, TResult>(
            this IQueryable<TSource> data,
            Expression<Func<TSource, TResult>> dataSelector)
            where TMetadata : IMetadata
            where TSource : IDescribedDomainModel<TMetadata>
            where TResult : IDescribedEntity
        {
            var query = data.Select(Wrap<TSource, TMetadata, TResult>(dataSelector));

            return query
                .ToArray()
                .Select(t => (TResult)t);
        }

        internal static Expression<Func<TSource, MetaWrapper<TResult>>> Wrap<TSource, TMetadata, TResult>(
            Expression<Func<TSource, TResult>> dataSelector)
            where TMetadata : IMetadata
            where TSource : IDescribedDomainModel<TMetadata>
            where TResult : IDescribedEntity
        {
            var dataParameter = Expression.Parameter(typeof(TSource), "data");

            Expression<Func<TSource, IEnumerable<KeyValuePairApiModel<String, String>>>> metaSelector = 
                (d) => d.Metadata == null ? null : d.Metadata
                    .Where(m => !m.IsDeleted)
                    .Select(m => new KeyValuePairApiModel<String, String> { Key = m.Key, Value = m.Value });

            var result = Expression.Variable(typeof(MetaWrapper<TResult>));
            var newWrapper = Expression.Assign(result, Expression.New(typeof(MetaWrapper<TResult>)));
            var entityProperty = Expression.Property(result, nameof(MetaWrapper<TResult>.Entity));
            var assignEntity = Expression.Assign(entityProperty, Expression.Invoke(dataSelector, dataParameter));
            var metaProperty = Expression.Property(result, nameof(MetaWrapper<TResult>.Metadata));
            var assignMetadata = Expression.Assign(metaProperty, Expression.Invoke(metaSelector, dataParameter));


            var block = Expression.Lambda<Func<TSource, MetaWrapper<TResult>>>(Expression.Block(new [] { dataParameter }, result, newWrapper, assignEntity, assignMetadata, result), dataParameter);

            return block;
        }
    }
}

1 Ответ

0 голосов
/ 19 сентября 2018

Блочные выражения и выражения вызова не совместимы с переводчиком запросов EF.все, что вам нужно, это Expression.MemberInit.

Но вы можете избежать всех этих сложностей, используя технику, описанную в Отслеживание числа совпадений, сопоставленных до. Список EF Linq Query .По сути, вы создаете лямбда-выражение времени компиляции с дополнительными параметрами, которые используются в качестве заполнителей для замены другим выражением, используя следующий простой вспомогательный метод:

public static class ExpressionExtensions
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node) =>
            node == Source ? Target : base.VisitParameter(node);
    }
}

Применение его к вашему случаю:

internal static Expression<Func<TSource, MetaWrapper<TResult>>> Wrap<TSource, TMetadata, TResult>(
    Expression<Func<TSource, TResult>> dataSelector)
    where TMetadata : class, IMetadata
    where TSource : class, IDescribedDomainModel<TMetadata>
    where TResult : class, IDescribedEntity
{
    Expression<Func<TSource, TResult, MetaWrapper<TResult>>> template = (source, entity) => new MetaWrapper<TResult>
    {
        Entity = entity,
        Metadata = source.Metadata == null ? null : source.Metadata
            .Where(m => !m.IsDeleted)
            .Select(m => new KeyValuePairApiModel<String, String> { Key = m.Key, Value = m.Value }),
    };
    var sourceParameter = template.Parameters[0];
    var entityParameter = template.Parameters[1];
    var entityValue = dataSelector.Body.ReplaceParameter(dataSelector.Parameters[0], sourceParameter);
    var selectorBody = template.Body.ReplaceParameter(entityParameter, entityValue);
    var selector = Expression.Lambda<Func<TSource, MetaWrapper<TResult>>>(selectorBody, sourceParameter);
    return selector;
}
...