Dynami c LINQ Запрос к дереву выражений - PullRequest
1 голос
/ 23 января 2020

Я пытаюсь вернуть объект с обновленными свойствами на основе условия в запросе LINQ. Ниже приведен запрос и работает нормально.

    public class SampleDataModel
        {
            public string Name { get; set; }
            public string Group { get; set; }
            public int? Hyper { get; set; }
            public string Sin { get; set; }
            public int? Auto { get; set; }

            public string Comment
            {
                get { return (Auto.HasValue ? "Hyper & Name should be masked" : ""); }
            }

            public override string ToString()
            {
                return $"Group={Group};Name={Name};Hyper={Hyper};Sin={Sin};Auto={Auto};Comment={Comment}";
            }
        }

        public class SampleData
        {
            private static readonly IList<SampleDataModel> Data = new List<SampleDataModel>();

            static SampleData()
            {
                Data.Add(new SampleDataModel()
                    {Name = "Name 1", Group = "Group 11", Hyper = 10, Sin = "S12345986554", Auto = 1});
                Data.Add(new SampleDataModel()
                    {Name = "Name 2", Group = "Group 12", Hyper = 101, Sin = "MS7123452331", Auto = 19});
                Data.Add(new SampleDataModel()
                    {Name = "Name 3", Group = "Group 13", Hyper = 103, Sin = "SJK1234Z5666", Auto = 18});
                Data.Add(new SampleDataModel()
                    {Name = "Name 4", Group = "Group 14", Hyper = 210, Sin = "DFS125349811", Auto = 41});
                Data.Add(new SampleDataModel()
                    {Name = "Name 5", Group = "Group 15", Hyper = 150, Sin = "YUTS12345000", Auto = null});
                Data.Add(new SampleDataModel()
                    {Name = "Name 6", Group = "Group 16", Hyper = 106, Sin = "KTRS12Y345211", Auto = null});
                Data.Add(new SampleDataModel()
                    {Name = "Name 7", Group = "Group 17", Hyper = 510, Sin = "CVS12X342895", Auto = 23});
                Data.Add(new SampleDataModel()
                    {Name = "Name 8", Group = "Group 18", Hyper = 170, Sin = "ZASS12356545", Auto = 76});
                Data.Add(new SampleDataModel()
                    {Name = "Name 9", Group = "Group 19", Hyper = 1210, Sin = "QWES18782345", Auto = 11});
                Data.Add(new SampleDataModel()
                    {Name = "Name 10", Group = "Group 21", Hyper = 1450, Sin = "RTYS989812345", Auto = 91});
                Data.Add(new SampleDataModel()
                    {Name = "Name 11", Group = "Group 31", Hyper = 1067, Sin = "SDF134562345", Auto = null});
                Data.Add(new SampleDataModel()
                    {Name = "Name 12", Group = "Group 41", Hyper = 1087, Sin = "SHGF12376745", Auto = null});
                Data.Add(new SampleDataModel()
                    {Name = "Name 13", Group = "Group 51", Hyper = 10912, Sin = "SKJU00012345", Auto = null});
            }

            public static IEnumerable<SampleDataModel> Get => Data;
        }
    public static class MaskService
        {
            public static IEnumerable<SampleDataModel> Mask(IEnumerable<SampleDataModel> data)
                {
                    List<SampleDataModel> result = new List<SampleDataModel>();

                    var result1 = (from d in data
                        select new SampleDataModel()
                        {
                            Auto = d.Auto, Group = d.Group,
                            Sin = d.Sin,
                            Hyper = (!d.Auto.HasValue) ? d.Hyper : null,
                            Name = (!d.Auto.HasValue) ? d.Name : "XXXXXX"

                        });

                    result = result1.ToList<SampleDataModel>();

                    return result;
                }
        }
    class Program
        {
            static void Main(string[] args)
            {
                var data = SampleData.Get;


                var maskedData = MaskService.Mask(data);
                foreach (var model in maskedData)
                {
                    Console.WriteLine($"{model}");
                }
             }
        }

Каждый раз, когда происходит изменение в столбцах, для которых требуется переопределение, происходит изменение кода либо для переопределения условия, либо для свойства. Чтобы преодолеть это и повторно использовать код, я придумал универсальное решение c, основанное на некоторых предложениях от stackoverflow, сообщениях из поиска Google и примере от Microsoft.

Generi c Подход:

    public static class MaskService
    {
        public static bool Contains<T>(this IEnumerable<T> source, string property) where T:IMaskCriteria
        {
            if (source == null || string.IsNullOrWhiteSpace(property))
                return false;

            foreach (var maskCriteria in source)
            {
                return maskCriteria.Contains(property);
            }

            return false;
        }
        public static IEnumerable<MaskCondition> Get<T>(this IEnumerable<T> source, string property) where T : IMaskCriteria
        {
            IEnumerable<MaskCondition> result = null;
            if (source == null || string.IsNullOrWhiteSpace(property))
                return result;

            foreach (var maskCriteria in source)
            {
                if (maskCriteria.Contains(property))
                {
                    result = maskCriteria.Conditions;
                    break;
                }
            }

            return result;
        }

        public static PropertyInfo Get(this PropertyInfo[] source, string property)
        {
            if (source == null || string.IsNullOrWhiteSpace(property))
                return null;
            property = property.ToLower();
            foreach (var prop in source)
            {
                if (prop.Name.ToLower().Equals(property))
                {
                    return prop;
                }
            }

            return null;
        }
        public static IEnumerable<T> Mask<T>(this IEnumerable<T> source, IEnumerable<IMaskCriteria> maskProperties) where T : new()
        {
            var result = source.Select(MaskFun<T>(maskProperties));
            return result;
        }

        public static Func<T, T> MaskFun<T>(IEnumerable<IMaskCriteria> maskProperties) where T : new()
        {
            if (maskProperties == null)
                return null;

            Type typeofT = typeof(T);
            var model = Expression.New(typeofT);
            var inputLinqVar = Expression.Parameter(typeofT, "d");//input variable in linq query
            object GetDefault(Type type) => Nullable.GetUnderlyingType(type) != null ? null : type.GetTypeInfo().IsValueType ? Activator.CreateInstance(type) : "XXXXXX";
            var maskCriterias = maskProperties as List<IMaskCriteria> ?? maskProperties.ToList();

            PropertyInfo[] props = typeofT.GetProperties();
            List<MemberAssignment> memberAssignments = new List<MemberAssignment>();
            foreach (var propertyInfo in props)
            {
                if (!propertyInfo.CanWrite)
                    continue;

                bool hasToBeMasked = maskCriterias.Contains(propertyInfo.Name);
                var valueFromLinqVariable = Expression.Property(inputLinqVar, propertyInfo); //d.{propertyName} eg. d.Id
                MemberAssignment memberAssignment = Expression.Bind(propertyInfo, valueFromLinqVariable);//{propertyName} = d.{propertyName} eg. Id = d.Id
                if (hasToBeMasked)
                {
                    List<MaskCondition> maskConditions = maskCriterias.Get(propertyInfo.Name) as List<MaskCondition>;
                    List<Expression> binConditions = new List<Expression>();
                    if (maskConditions != null)
                        foreach (var maskCondition in maskConditions)
                        {
                            var condPropInfo = props.Get(maskCondition.PropertyName);
                            Expression left = Expression.Property(inputLinqVar, condPropInfo);
                            Expression right = Expression.Constant(maskCondition.PropertyValue);
                            Expression e1 = null;
                            switch (maskCondition.Comparer)
                            {
                                case MaskConditionComparer.Equals:
                                {
                                    e1 = Expression.Equal(left, right);
                                    break;
                                }

                                case MaskConditionComparer.NotEquals:
                                {
                                    e1 = Expression.NotEqual(left, right);
                                    break;
                                }

                                case MaskConditionComparer.GreaterThan:
                                {
                                    e1 = Expression.GreaterThan(left, right);
                                    break;
                                }

                                case MaskConditionComparer.GreaterThanOrEquals:
                                {
                                    e1 = Expression.GreaterThanOrEqual(left, right);
                                    break;
                                }

                                case MaskConditionComparer.LessThan:
                                {
                                    e1 = Expression.LessThan(left, right);
                                    break;
                                }

                                case MaskConditionComparer.LessThanOrEquals:
                                {
                                    e1 = Expression.LessThanOrEqual(left, right);
                                    break;
                                }
                            }

                            binConditions.Add(e1);
                        }

                    ConstantExpression maskedValue = Expression.Constant(GetDefault(propertyInfo.PropertyType),
                        propertyInfo.PropertyType);
                    ConditionalExpression assignMaskValueIfMaskConditionTrue = null;
                    if (binConditions.Count == 1)
                    {
                        assignMaskValueIfMaskConditionTrue =
                            Expression.Condition(binConditions[0], maskedValue, valueFromLinqVariable);
                    }
                    else if (binConditions.Count > 1)
                    {
                        Expression andExpression = binConditions[0];
                        //Expression getExp(Expression e1, Expression e2) => Expression.AndAlso(e1, e2);
                        for (var i = 1; i < binConditions.Count; i++)
                        {
                            andExpression = Expression.AndAlso(andExpression, binConditions[i]);
                        }

                        assignMaskValueIfMaskConditionTrue =
                            Expression.Condition(andExpression, maskedValue, valueFromLinqVariable);
                    }
                    assignMaskValueIfMaskConditionTrue =
                    (ConditionalExpression)new ParameterReplacer(inputLinqVar).Visit(
                        assignMaskValueIfMaskConditionTrue);
                    var condExp = Expression.Lambda<Func<T, Object>>(assignMaskValueIfMaskConditionTrue, inputLinqVar);

                    memberAssignment = Expression.Bind(propertyInfo, condExp);
                }

                memberAssignments.Add(memberAssignment);
                var message = hasToBeMasked ? "will be masked" : "no masking";
                Console.WriteLine($"{propertyInfo.Name}->{propertyInfo.PropertyType.Name}==>{message};{Nullable.GetUnderlyingType(propertyInfo.PropertyType)?.FullName}");
            }

            var modelInit = Expression.MemberInit(model, memberAssignments);// new T() { propertyName = d.propertyName} eg. new Emp() { Name = d.Name }
            var lambda = Expression.Lambda<Func<T, T>>(modelInit, inputLinqVar);

            return lambda.Compile();
        }
    }
    public class ParameterReplacer : ExpressionVisitor
    {
        private readonly ParameterExpression _parameter;

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return base.VisitParameter(_parameter);
        }

        public ParameterReplacer(ParameterExpression parameter)
        {
            _parameter = parameter;
        }
    }

    public class PropertyComparer : IEqualityComparer<string>
    {
        public bool Equals(string x, string y)
        {
            if (string.IsNullOrWhiteSpace(x) || string.IsNullOrWhiteSpace(y))
                return false;
            return (x.ToLower() == (y.ToLower()));
        }

        public int GetHashCode(string obj)
        {
            return obj.GetHashCode();
        }
    }

    public interface IMaskCriteria
    {
        List<string> ToBeMasked { get; set; }

        List<MaskCondition> Conditions { get; set; }

        bool Contains(string property);
    }
    public class MaskCriteria : IMaskCriteria
    {
        private  static readonly PropertyComparer comparer = new PropertyComparer();

        public MaskCriteria()
        {
            ToBeMasked = new List<string>();
            Conditions = new List<MaskCondition>();
        }
        public List<string> ToBeMasked { get; set; }

        public List<MaskCondition> Conditions { get; set; }

        public bool Contains(string property)
        {
            if (ToBeMasked == null)
                return false;

            if (string.IsNullOrWhiteSpace(property))
                return false;

            return ToBeMasked.Contains(property, comparer);
        }

    }

    public class MaskCondition
    {
        public string PropertyName { get; set; }
        public object PropertyValue { get; set; }
        public MaskConditionComparer Comparer { get; set; }
    }

    public enum MaskConditionComparer
    {
        Equals,
        NotEquals,
        GreaterThan,
        GreaterThanOrEquals,
        LessThan,
        LessThanOrEquals
    }
    class Program
    {
        static void Main(string[] args)
        {
            var data = SampleData.Get;

            Console.WriteLine("******************Start Masking Using Expression Trees In LINQ***********************");
            var mc = new MaskCriteria();
            mc.ToBeMasked.AddRange(new List<string>() { "name", "hyper" });
            mc.Conditions.Add(new MaskCondition(){ PropertyValue = null,PropertyName = "Auto", Comparer = MaskConditionComparer.Equals});

            maskedData = data.Mask<SampleDataModel>(new List<MaskCriteria>() {mc});
            foreach (var model in maskedData)
            {
                Console.WriteLine($"{model}");
            }
            Console.WriteLine("******************End Masking Using Expression Trees In LINQ***********************");
            Console.ReadLine();
        }
    }

Когда я запускаю код, я получаю следующее исключение в memberAssignment = Expression.Bind(propertyInfo, condExp);

Система .ArgumentException: 'Типы аргументов не совпадают'

Если я удалю регистр переключателя, то свойства Name и Hyper получат XXXXX или ноль соответственно.

Я пробовал несколько способов без удачи и разных исключений. Я никогда не использовал деревья выражений раньше. Так может кто-нибудь подсказать, как поступить с условным заданием?

1 Ответ

0 голосов
/ 23 января 2020

Я нашел решение после дальнейшего прочтения и этот пост № 8 помог мне. Спасибо Рубену.

Вот изменение.

До

var condExp = Expression.Lambda<Func<T, object>>(assignMaskValueIfMaskConditionTrue, new ParameterExpression[] { inputLinqVar });

После

var condExp = Expression.Lambda(assignMaskValueIfMaskConditionTrue, new ParameterExpression[] { inputLinqVar });

...