Dapper с отображением атрибутов

Я пытаюсь сопоставить свои поля Id с атрибутами столбца, но по какой-то причине это не работает, и я не могу понять, почему. Я создал тестовый проект, чтобы продемонстрировать, что я пытаюсь.

Во-первых, я получил 2 объекта:

Таблица сущностей1

using System.Data.Linq.Mapping;

namespace DapperTestProj
{
    public class Table1
    {
        [Column(Name = "Table1Id")]
        public int Id { get; set; }

        public string Column1 { get; set; }

        public string Column2 { get; set; }

        public Table2 Table2 { get; set; }

        public Table1()
        {
            Table2 = new Table2();
        }
    }
}

и сущность Table2

using System.Data.Linq.Mapping;

namespace DapperTestProj
{
    public class Table2
    {
        [Column(Name = "Table2Id")]
        public int Id { get; set; }

        public string Column3 { get; set; }

        public string Column4 { get; set; }
    }
}

В моей базе данных я получил 2 таблицы, также называемые Table1 и Table2. Обе таблицы получили свои столбцы с именем, равным сущности, за исключением того, что Table1 имеет столбец с именем Table2Id, а также есть внешний ключ между Table1.Table2Id и Table2.Id.

Кроме того, в обеих таблицах есть по 1 записи, и у них есть оба идентификатора.

Что я пытаюсь сделать, так это выполнить запрос с dapper и он должен вернуть объект типа Table1. Это работает, но и свойство Table1.Id и Table1.Table2.Id остается 0 (по умолчанию целое число). Я ожидаю, что атрибуты столбца будут отображать поля Id, но это явно не так.

Это запрос и сопоставление, выполняемые в коде:

private Table1 TestMethod(IDbConnection connection)
{
    var result = connection.Query<Table1, Table2, Table1>(
        @"SELECT 
             T1.Id as Table1Id, 
             T1.Column1 as Column1,
             T1.Column2 as Column2,
             T2.Id as Table2Id,
             T2.Column3 as Column3,
             T2.Column4 as Column4
          FROM Table1 T1 
          INNER JOIN Table2 T2 ON T1.Table2Id = T2.Id",
        (table1, table2) =>
            {
                table1.Table2 = table2;
                return table1;
            },
        splitOn: "Table2Id"
        ).SingleOrDefault();

    return result;
}

Теперь я мог переименовать оба свойства свойства Id в сущности в Table1Id и Table2Id, но вместо этого я предпочитаю идентификатор вместо более логического кода, например Table1.Id вместо Table1.Table1Id. Поэтому мне было интересно, возможно ли, что я хочу здесь, и если да, то как?

Изменить:

Я нашел эту тему: Вручную сопоставить имена столбцов с свойствами класса

И с кодом в первом посте Kaleb Pederson можно использовать атрибуты, когда это необходимо, с классом FallBackTypeMapper и классом ColumnAttributeTypeMapper. Все, что необходимо, - это добавить необходимые классы в typemapping с помощью:

SqlMapper.SetTypeMap(typeof(Table1), new ColumnAttributeTypeMapper<Table1>());
SqlMapper.SetTypeMap(typeof(Table2), new ColumnAttributeTypeMapper<Table2>());

Но со многими сущностями этот список будет расти долго. Также вам нужно добавить каждый класс вручную в список, и мне было интересно, можно ли это сделать автоматически с более общим с Reflection. Я нашел фрагмент кода, способный получить все типы:

        const string @namespace = "DapperTestProj.Entities";

        var types = from type in Assembly.GetExecutingAssembly().GetTypes()
                    where type.IsClass && type.Namespace == @namespace
                    select type;

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

        typeList.ToList().ForEach(type => SqlMapper.SetTypeMap(type, 
                               new ColumnAttributeTypeMapper</*???*/>()));

Edit:

После большего поиска я нашел решение для моей последней проблемы:

        typeList.ToList().ForEach(type =>
            {
                var mapper = (SqlMapper.ITypeMap)Activator.CreateInstance(
                    typeof(ColumnAttributeTypeMapper<>)
                        .MakeGenericType(type));
                SqlMapper.SetTypeMap(type, mapper);
            });

Ответ 1

Для завершения решения я хочу поделиться кодом, который я нашел, и собрать его вместе с теми, кто заинтересован.

Вместо (ab) использования System.Data.Linq.Mapping.ColumnAttribute может быть больше логики (и, возможно, сохранения, хотя вероятность того, что Microsoft изменит linq на sql ColumnAttribute, будет очень мала, чтобы создать собственный ColumnAttribute класс:

ColumnAttribute.cs

using System;

namespace DapperTestProj.DapperAttributeMapper //Maybe a better namespace here
{
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class ColumnAttribute : Attribute
    {
        public string Name { get; set; }

        public ColumnAttribute(string name)
        {
            Name = name;
        }
    }
}

Находясь в теме, о которой я упоминал ранее, классы FallBackTypeMapper и ColumnAttributeTypeMapper:

FallBackTypeMapper.cs

using System;
using System.Collections.Generic;
using System.Reflection;
using Dapper;

namespace DapperTestProj.DapperAttributeMapper
{
    public class FallBackTypeMapper : SqlMapper.ITypeMap
    {
        private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;

        public FallBackTypeMapper(IEnumerable<SqlMapper.ITypeMap> mappers)
        {
            _mappers = mappers;
        }

        public ConstructorInfo FindConstructor(string[] names, Type[] types)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    var result = mapper.FindConstructor(names, types);

                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException nix)
                {
                    // the CustomPropertyTypeMap only supports a no-args
                    // constructor and throws a not implemented exception.
                    // to work around that, catch and ignore.
                }
            }
            return null;
        }

        public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    var result = mapper.GetConstructorParameter(constructor, columnName);

                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException nix)
                {
                    // the CustomPropertyTypeMap only supports a no-args
                    // constructor and throws a not implemented exception.
                    // to work around that, catch and ignore.
                }
            }
            return null;
        }

        public SqlMapper.IMemberMap GetMember(string columnName)
        {
            foreach (var mapper in _mappers)
            {
                try
                {
                    var result = mapper.GetMember(columnName);

                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException nix)
                {
                    // the CustomPropertyTypeMap only supports a no-args
                    // constructor and throws a not implemented exception.
                    // to work around that, catch and ignore.
                }
            }
            return null;
        }
    }
}

ColumnAttributeTypeMapper.cs

using System.Linq;
using Dapper;

namespace DapperTestProj.DapperAttributeMapper
{
    public class ColumnAttributeTypeMapper<T> : FallBackTypeMapper
    {
        public ColumnAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
                    {
                        new CustomPropertyTypeMap(typeof(T),
                            (type, columnName) =>
                                type.GetProperties().FirstOrDefault(prop =>
                                    prop.GetCustomAttributes(false)
                                        .OfType<ColumnAttribute>()
                                        .Any(attribute => attribute.Name == columnName)
                            )
                        ),
                        new DefaultTypeMap(typeof(T)) 
                    })
        {
        }
    }
}

и, наконец, TypeMapper.cs для инициализации сопоставления.

using System;
using System.Linq;
using System.Reflection;
using Dapper;

namespace DapperTestProj.DapperAttributeMapper
{
    public static class TypeMapper
    {
        public static void Initialize(string @namespace)
        {
            var types = from assem in AppDomain.CurrentDomain.GetAssemblies().ToList()
                    from type in assem.GetTypes()
                    where type.IsClass && type.Namespace == @namespace
                    select type;

            types.ToList().ForEach(type =>
            {
                var mapper = (SqlMapper.ITypeMap)Activator
                    .CreateInstance(typeof(ColumnAttributeTypeMapper<>)
                                    .MakeGenericType(type));
                SqlMapper.SetTypeMap(type, mapper);
            });
        }
    }
}

При запуске необходимо вызвать TypeMapper.Initialize:

TypeMapper.Initialize("DapperTestProj.Entities");

И вы можете начать использовать атрибуты для свойств объекта

using DapperTestProj.DapperAttributeMapper;

namespace DapperTestProj.Entities
{
    public class Table1
    {
        [Column("Table1Id")]
        public int Id { get; set; }

        public string Column1 { get; set; }

        public string Column2 { get; set; }

        public Table2 Table2 { get; set; }

        public Table1()
        {
            Table2 = new Table2();
        }
    }
}

Ответ 2

Ответ Корнелиса правильный, однако я хотел добавить к нему обновление. Начиная с текущей версии Dapper вам также нужно реализовать SqlMapper.ItypeMap.FindExplicitConstructor(). Я не уверен, когда это изменение было сделано, но это для всех, кто наткнулся на этот вопрос и не хватает той части решения.

Внутри FallbackTypeMapper.cs

public ConstructorInfo FindExplicitConstructor()
{
    return _mappers.Select(m => m.FindExplicitConstructor())
        .FirstOrDefault(result => result != null);
}

Также вы можете использовать класс ColumnAttribute, расположенный в пространстве имен System.ComponentModel.DataAnnotations.Schema, вместо того, чтобы сворачивать свою собственную для встроенной версии, отличной от базы данных/orm.

Ответ 3

это становится еще лучше

public class ColumnOrForeignKeyAttributeTypeMapper<T> : FallBackTypeMapper
    {
        public ColumnOrForeignKeyAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
                    {
                        new CustomPropertyTypeMap(typeof(T),
                            (type, columnName) =>
                                type.GetProperties().FirstOrDefault(prop =>
                                    prop.GetCustomAttributes(false)
                                        .Where(a=>a is ColumnAttribute || a is ForeignKeyAttribute)
                                        .Any(attribute => attribute.GetType() == typeof(ColumnAttribute) ? 
                                            ((ColumnAttribute)attribute).Name == columnName : ((ForeignKeyAttribute)attribute).Name == columnName)
                            )
                        ),
                        new DefaultTypeMap(typeof(T))
                    })
        {
        }
    }