Automapper — Доступ к функции карты по умолчанию в MapFrom для резервного копирования

#c# #.net #automapper

Вопрос:

Я пишу метод расширения, чтобы выполнить перевод с помощью Automapper.

У меня есть несколько занятий :

 public class TranslatableClass : ITranslatable<TranslationClass>
{
    public string Id { get; set; }
    public string Label { get; set; }
    public string Description { get; set; }
    public List<TranslationClass> Translations { get; set; }
    public string OtherEntityId { get; set; }
    public string OtherEntityLabel { get; set; }
    public List<OtherEntityTranslation> OtherEntityTranslations { get; set; }
}

public class TranslationClass : ITranslation
{
    public Guid LanguageId { get; set; }
    public string Label { get; set; }
    public string Description { get; set; }
}

public class TranslatedClass
{
    public string Id { get; set; }
    public string Label { get; set; }
    public string Description { get; set; }
    public string OtherEntityLabel { get; set; }
}

public class OtherEntityTranslation : ITranslation
{
    public string Label { get; set; }

    public Guid LanguageId { get; set; }
}
 

Я хотел бы получить метод расширения, подобный этому :

 cfg.CreateMap<TranslatableClass, TranslatedClass>()
    .ForMember(t => t.OtherEntityLabel, opt => opt.MapFromTranslation(t => t.OtherEntityTranslations, oet => oet.Label));
 

И мой метод расширения выглядит следующим образом

 public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Func<TSource, IEnumerable<TTranslation>> getTranslations, Func<TTranslation, string> getValue)
    where TTranslation : ITranslation
{
    opt.MapFrom((src, _, _, context) =>
    {
        string result = null; // here is the pain point ; I'd like to get the value as if I was automapper 

        if (context.Options.Items.TryGetValue(LANGUAGE, out object contextLanguage) amp;amp; contextLanguage is Guid languageId)
        {
            var translations = getTranslations(src);
            var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);

            if (translation != null)
            {
                result = getValue(translation);
            }
        }

        return resu<
    });
}
 

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

Я пытаюсь поставить предварительное условие перед MapFrom, но это не сопоставляет свойство, поэтому я тоже получаю null.

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

 public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Func<TSource, IEnumerable<TTranslation>> getTranslations, Func<TTranslation, string> getValue)
    where TTranslation : ITranslation
{
    var destinationMember = opt.DestinationMember as PropertyInfo;

    var source = typeof(TSource);
    var sourceProperty = source.GetProperty(destinationMember.Name);

    if (sourceProperty != null)
    {
        opt.MapFrom((src, _, _, context) =>
        {
            string result = sourceProperty.GetValue(src) as string; // Get value from source as if it was the mapper 

            if (context.Options.Items.TryGetValue(LANGUAGE, out object contextLanguage) amp;amp; contextLanguage is Guid languageId)
            {
                var translations = getTranslations(src);

                if (translations != null)
                {
                    var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);

                    if (translation != null)
                    {
                        var value = getValue(translation);
                        if (!String.IsNullOrWhiteSpace(value))
                        {
                            result = value;
                        }
                    }
                }
            }

            return resu<
        });
    }
    else
    {
        throw new Exception($"Can't map property {opt.DestinationMember.Name} from {source.Name}");
    }
}
 

Комментарии:

1. Вам нужно наследование отображения.

2. Как это работает ?

3. Проверьте документы. Есть также много примеров, доступных в Интернете.

Ответ №1:

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

 cfg.CreateMap<TranslatableClass, TranslatedClass>()
    .ForMember(dest => dest.OtherEntityLabel, opt => opt.MapFrom<CustomResolver>();
 

Реализующий IValueResolver<TranslatableClass, TranslatedClass, string> интерфейс:

 public class CustomResolver: IValueResolver<TranslatableClass, TranslatedClass, string>
{
    public string Resolve(TranslatableClass source, TranslatedClass destination, string member, ResolutionContext context)
    {
        
        string result = source.Label;  /* needed effect! */ 

        /* can we simplify this condition? */
        if (context.Options.Items.TryGetValue(source.OtherEntityLabel, out object contextLanguage) 
            amp;amp; contextLanguage is Guid languageId)
        {
            var translations = source.OtherEntityTranslations;
            var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);

            if (translation != null)
            {
                result = translation.Label;
            };
        }
        
        return resu<

    }

}
 

Отсюда вытекает та же логика из
MapFromTranslation<TSource, TDestination, TMember, ... метод расширения, приведенный ниже, давайте правильно сформулируем эту логику — мы сопоставляем TSource as TranslatableClass с TDestination as TranslatedClass .

Кроме того, я считаю, что if (context.Options.Items.TryGetValue(...)) это тоже следует удалить для простоты (мы пытаемся добраться languageId сюда?)

Таким образом, используя Custom Value Resolvers эту функцию, мы можем упростить конфигурацию и рефакторинг картографа для обеспечения тестового покрытия или отладки.

Обновить

Я действительно хочу использовать этот метод расширений для 50 других объектов, и я не буду писать пользовательский распознаватель для каждого из них

Использование выражений вместо отражения должно помочь реализовать «универсальное решение». Решение состоит в том, чтобы определить кэш mapping expressions для доступа к свойствам TSource и TDestination.

 public static class MappingCache<TFirst, TSecond>
    {
        static MappingCache()
        {
            var first = Expression.Parameter(typeof(TFirst), "first");
            var second = Expression.Parameter(typeof(TSecond), "second");

            var secondSetExpression = MappingCache.GetSetExpression(second, first);

            var blockExpression = Expression.Block(first, second, secondSetExpression, first);

            Map = Expression.Lambda<Func<TFirst, TSecond, TFirst>>(blockExpression, first, second).Compile();
        }

        public static Func<TFirst, TSecond, TFirst> Map { get; private set; }
    }
    

 

Далее давайте попробуем определить универсальные лямбда-выражения для обоих
Func<TTranslation, string> getValue и getTranslations(...

напр.:

         public static Expression GetSetExpression(ParameterExpression sourceExpression, params ParameterExpression[] destinationExpressions)
        {
         /** AutoMapper also can be used here */

         /* compile here all (source)=>(destination) expressions */

            var destination = destinationExpressions
                .Select(parameter => new
                {
                    Parameter = parameter,
                    Property =  parameter.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                        .FirstOrDefault(property => IsWritable(property) amp;amp; IsOfType(property, sourceExpression.Type))
                })
                .FirstOrDefault(parameter => parameter.Property != null);

            if (destination == null)
            {
                throw new InvalidOperationException(string.Format("No writable property of type {0} found in types {1}.", sourceExpression.Type.FullName, string.Join(", ", destinationExpressions.Select(parameter => parameter.Type.FullName))));
            }

         /* Here is the generic version of mapping code! */

            return Expression.IfThen(
                Expression.Not(Expression.Equal(destination.Parameter, Expression.Constant(null))),
                Expression.Call(destination.Parameter, destination.Property.GetSetMethod(), sourceExpression));
        }
 

Далее идет IsWritable(PropertyInfo property) то, что используется для проверки правильности свойств, попробуйте реализовать фильтрацию свойств на основе соглашений (имена, атрибуты и т.д.) Здесь

         public static bool IsWritable(PropertyInfo property)
        {
/*  eliminating reflection code from extension method */
            return property.CanWrite amp;amp; !property.GetIndexParameters().Any();

        }
 

Далее IsOfType(PropertyInfo... и IsSubclassOf методы, — определите простые правила правильных TSource->TDestination способов отображения…

         public static bool IsOfType(PropertyInfo property, Type type)
        {
         /* here AutoMapper could be used too, making filtering needed destination entities by following some convention */ 
            return property.PropertyType == type || IsSubclassOf(type, property.PropertyType) || property.PropertyType.IsAssignableFrom(type);
        }

        public static bool IsSubclassOf(Type type, Type otherType)
        {
            return type.IsSubclassOf(otherType);
        }
    }
 

Попытка реализовать подход к составлению карт на основе конвенций:

 public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Expression<Func<TSource, TDestination, TMember, TTranslation>> mapping )
    where TTranslation : ITranslation
 

Проводка вокруг Expression<Func<TSource,TDestination,TMember, TTranslation> mapping и MappingCache<TSource,TDestination,TMember, TTranslation>.Map это следующий шаг. Наше лямбда-выражение представляет намерение преобразования свойств в общем виде (сопоставление,преобразование,проверка,навигация и т. Д.), И когда скомпилированная лямбда вызывается с переданными параметрами, мы получаем результат такого преобразования.

Выражение:

 MappingCache<TSource,TDestination,TMember, TTranslation>.GetSetExpression(first, second, third, proxy...
 

Функция:

 var result = MappingCache<TSource,TDestination,TMember, TTranslation>.Map(first,second,third,...
 

Сохраняя статически скомпилированные абстракции лямбда-делегатов открытыми, мы можем охватить все необходимые аспекты сопоставления с помощью надлежащих тестов, — похоже, что общий подход, который можно было бы использовать для решения этого вопроса

Доступ к функции карты по умолчанию в MapFrom для резервного копирования

(c) Щеголь.Картограф Тесты

Комментарии:

1. Я хочу, чтобы решение было как можно более общим. Использование вашего решения будет работать только для TranslatableClass, который я использую только для проверки моего метода расширения. Я действительно хочу использовать этот метод расширений для 50 других объектов, и я не буду писать пользовательский распознаватель для каждого из них ^^.