#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 для резервного копирования
Комментарии:
1. Я хочу, чтобы решение было как можно более общим. Использование вашего решения будет работать только для TranslatableClass, который я использую только для проверки моего метода расширения. Я действительно хочу использовать этот метод расширений для 50 других объектов, и я не буду писать пользовательский распознаватель для каждого из них ^^.