Как сделать динамический маппинг полей класса через рефлексию средствами Dapper?
Делаю рефакторинг проекта, поступила задача упростить маппинг. Проект использует большое количество хранимых процедур, поэтому для введения новых сущностей каждый раз необходимо заводить маппинг к колонкам вывода SQL-результатов. Я хочу оптимизировать этот процесс, написав динамический маппинг.
Есть класс:
public class DocumentsQnt
{
[JsonProperty("draftsCount")]
[MapColumn("drafts_count")]
public int DraftsCount { get; set; }
[JsonProperty("notStartedCount")]
[MapColumn("not_started_count")]
public int NotStartedCount { get; set; }
[JsonProperty("notFinishedCount")]
[MapColumn("not_finished_count")]
public int NotFinishedCount { get; set; }
}
В атрибуте ColumnTo указана колонка, к которой мапим.
Далее, в классе маппинга:
internal class DocumentsQntMap : EntityMap<DocumentsQnt>
{
internal DocumentsQntMap()
{
DocumentsQnt dq = new DocumentsQnt();
MappingFactory mpf = new MappingFactory(dq);
/*
Map(m => m.DraftsCount).ToColumn("drafts_count");
Map(m => m.NotStartedCount).ToColumn("not_started_count");
Map(m => m.NotFinishedCount).ToColumn("not_finished_count");
*/
}
}
В комментариях то, от чего избавимся.
Класс MappingFactory:
public class MappingFactory : EntityMap<Type>
{
public MappingFactory(object mappingType)
{
/* Что происходит:
* 1) Передали сущность
* 2) Определили тип
* 3) Замапили поле типа (которое сейчас выбрано в цикле и имеет непустой атрибут MapColumn) к строке поля, указанного в аттрибуте
*/
var _entityType = mappingType.GetType();
foreach (PropertyInfo? prop in _entityType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ))
{
var attr = prop.GetCustomAttribute<MapColumnAttribute>();
// защита от полей без аттрибута!
if (attr == null ||
string.IsNullOrEmpty(attr.ColumnTo))
continue;
Map(t => t.GetField(prop.Name)).ToColumn(attr.ColumnTo);
}
}
}
Проблема: ошибка "Object reference not set to an instance of an object" при компиляции.
Скажите, как мне улучшить код. Возможно, есть альтернативные способы решения, но я их сейчас не вижу.
Ответы (1 шт):
В конце концов придумал интересный способ справиться с маппингом динамически через рефлексию средствами самого Dapper. Для этого создал класс
/// <summary>
/// Универсальный класс для маппинга
/// </summary>
public static class DapperMappingHelper
{
/// <summary>
/// Добавит маппинг к полям переданной сущности
/// </summary>
/// <typeparam name="T"></typeparam>
public static void AddTypeMap<T>()
{
SqlMapper.SetTypeMap(typeof(T), new CustomPropertyTypeMap(
typeof(T),
(type, columnName) =>
{
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<MapColumnAttribute>()
.Any(attr => attr.ColumnTo == columnName));
}
));
}
/// <summary>
/// Добавит маппинг к полям переданных сущностей
/// </summary>
/// <param name="types">Массив сущностей для маппинга</param>
public static void AddTypeMaps(params Type[] types)
{
foreach (var type in types)
{
var method = typeof(DapperMappingHelper).GetMethod(nameof(AddTypeMap)).MakeGenericMethod(type);
method.Invoke(null, null);
}
}
}
Этот класс будет принимать все поля класса, отмеченные атрибутом MapColumnAttribute, в котором указано название колонки, к которой мапим.
Далее, будем использовать это следующим образом:
public static void MapData(FluentMapConfiguration config)
{
DapperMappingHelper.AddTypeMaps(typeof(DocumentsQnt),
typeof(SomeClass1),
typeof(SomeClass2)
);
}
Данный способ избавит нас от необходимости создавать дополнительные файлы с явным указанием мапов полей класса к колонкам хранимой процедуры. Т.е. при проектировании новой сущности нам нужно лишь указать над нужными полями атрибут с указанием названия колонки, к которой мапим. После этого просто добавляем этот класс в качестве аргумента для AddMapTypes.
Громоздкие конструкции маппинга больше не нужны!
//Нам это больше не пригодится
Map(m => m.DraftsCount).ToColumn("drafts_count");
Map(m => m.NotStartedCount).ToColumn("not_started_count");
Map(m => m.NotFinishedCount).ToColumn("not_finished_count");