Mapear manualmente nomes de colunas com propriedades de classe

173

Eu sou novo no Dapper micro ORM. Até agora, posso usá-lo para coisas simples relacionadas ao ORM, mas não consigo mapear os nomes das colunas do banco de dados com as propriedades da classe.

Por exemplo, eu tenho a seguinte tabela de banco de dados:

Table Name: Person
person_id  int
first_name varchar(50)
last_name  varchar(50)

e eu tenho uma classe chamada Person:

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Observe que os nomes das minhas colunas na tabela são diferentes do nome da propriedade da classe para a qual estou tentando mapear os dados que obtive do resultado da consulta.

var sql = @"select top 1 PersonId,FirstName,LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

O código acima não funcionará, pois os nomes das colunas não correspondem às propriedades (Pessoa) do objeto. Nesse cenário, há algo que eu possa fazer no Dapper para mapear manualmente (por exemplo person_id => PersonId) os nomes das colunas com propriedades do objeto?

user1154985
fonte
possível duplicata do Dapper. Mapear a coluna SQL com espaços em nomes de coluna
David McEleney

Respostas:

80

Isso funciona bem:

var sql = @"select top 1 person_id PersonId, first_name FirstName, last_name LastName from Person";
using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(sql).ToList();
    return person;
}

O Dapper não possui nenhum recurso que permita especificar um Atributo de Coluna ; não sou contra a adição de suporte a ele, desde que não extraamos a dependência.

Sam Saffron
fonte
@ Sam Saffron existe alguma maneira de especificar o alias da tabela. Eu tenho uma classe chamada Country, mas no banco de dados a tabela tem um nome muito complicado devido a convenções de nomenclatura.
TheVillageIdiot
64
O atributo da coluna seria útil para mapear os resultados do procedimento armazenado.
Ronnie Overby
2
Os atributos da coluna também seriam úteis para facilitar mais facilmente o acoplamento físico e / ou semântico rígido entre seu domínio e os detalhes de implementação da ferramenta que você está usando para materializar suas entidades. Portanto, não adicione suporte para isso !!!! :)
Derek Greer
Eu não entendo por que a coluna columntribe não está presente quando o tableattribute. Como este exemplo funcionaria com inserções, atualizações e SPs? Eu gostaria de ver o columnattribe, simples, e tornaria a vida muito fácil migrando de outras soluções que implementam algo semelhante, como o agora extinto linq-sql.
Vman
197

O Dapper agora suporta colunas personalizadas para mapeadores de propriedades. Isso é feito através da interface ITypeMap . Uma classe CustomPropertyTypeMap é fornecida pelo Dapper que pode executar a maior parte deste trabalho. Por exemplo:

Dapper.SqlMapper.SetTypeMap(
    typeof(TModel),
    new CustomPropertyTypeMap(
        typeof(TModel),
        (type, columnName) =>
            type.GetProperties().FirstOrDefault(prop =>
                prop.GetCustomAttributes(false)
                    .OfType<ColumnAttribute>()
                    .Any(attr => attr.Name == columnName))));

E o modelo:

public class TModel {
    [Column(Name="my_property")]
    public int MyProperty { get; set; }
}

É importante observar que a implementação do CustomPropertyTypeMap exige que o atributo exista e corresponda a um dos nomes de coluna ou a propriedade não será mapeada. A classe DefaultTypeMap fornece a funcionalidade padrão e pode ser aproveitada para alterar esse comportamento:

public class FallbackTypeMapper : SqlMapper.ITypeMap
{
    private readonly IEnumerable<SqlMapper.ITypeMap> _mappers;

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

    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;
    }
    // implement other interface methods similarly

    // required sometime after version 1.13 of dapper
    public ConstructorInfo FindExplicitConstructor()
    {
        return _mappers
            .Select(mapper => mapper.FindExplicitConstructor())
            .FirstOrDefault(result => result != null);
    }
}

E, com isso em prática, fica fácil criar um mapeador de tipo personalizado que usará automaticamente os atributos se estiverem presentes, mas que retornará ao comportamento padrão:

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(attr => attr.Name == columnName)
                           )
                   ),
                new DefaultTypeMap(typeof(T))
            })
    {
    }
}

Isso significa que agora podemos suportar facilmente tipos que exigem mapa usando atributos:

Dapper.SqlMapper.SetTypeMap(
    typeof(MyModel),
    new ColumnAttributeTypeMapper<MyModel>());

Aqui está uma lista do código fonte completo .

Kaleb Pederson
fonte
Eu tenho lutado com esse mesmo problema ... e isso parece o caminho que eu deveria seguir ... Estou bastante confuso sobre o local onde esse código seria chamado "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), novo ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Você deve chamá-lo uma vez antes de fazer qualquer pergunta. Você pode fazer isso em um construtor estático, por exemplo, pois ele precisa ser chamado apenas uma vez.
Kaleb Pederson
7
Recomende que seja a resposta oficial - esse recurso do Dapper é extremamente útil.
killthrush
3
A solução de mapeamento postada por @Oliver ( stackoverflow.com/a/34856158/364568 ) funciona e requer menos código.
Riga
4
Eu amo como a palavra "fácil" é jogado em torno tão facilmente: P
Jonathan B.
80

Por algum tempo, o seguinte deve funcionar:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
fonte
6
Embora essa não seja realmente a resposta para a pergunta " Mapear manualmente os nomes das colunas com propriedades de classe", para mim é muito melhor do que ter que mapear manualmente (infelizmente no PostgreSQL é melhor usar sublinhados nos nomes das colunas). Por favor, não remova a opção MatchNamesWithUnderscores nas próximas versões! Obrigado!!!
victorvartan
5
@victorvartan não há planos para remover a MatchNamesWithUnderscoresopção. Na melhor das hipóteses , se refatorássemos a API de configuração, eu deixaria o MatchNamesWithUnderscoresmembro no lugar (que ainda funciona, idealmente) e adicionaria um [Obsolete]marcador para apontar as pessoas para a nova API.
Marc Gravell
4
@ MarcGravell As palavras "Por algum tempo" no início de sua resposta me preocuparam com a possibilidade de removê-lo em uma versão futura, obrigado por esclarecer! E muito obrigado por Dapper, um micro ORM maravilhoso que eu comecei a usar em um projeto minúsculo junto com o Npgsql no ASP.NET Core!
Victorvartan 11/08
2
Esta é facilmente a melhor resposta. Encontrei montes de pilhas de trabalho, mas finalmente encontrei isso. Facilmente a melhor resposta, mas menos anunciada.
teaMonkeyFruit
29

Aqui está uma solução simples que não requer atributos, permitindo que você mantenha o código de infraestrutura fora dos seus POCOs.

Esta é uma classe para lidar com os mapeamentos. Um dicionário funcionaria se você mapeasse todas as colunas, mas essa classe permite especificar apenas as diferenças. Além disso, inclui mapas reversos para que você possa obter o campo da coluna e a coluna do campo, o que pode ser útil ao fazer coisas como gerar instruções sql.

public class ColumnMap
{
    private readonly Dictionary<string, string> forward = new Dictionary<string, string>();
    private readonly Dictionary<string, string> reverse = new Dictionary<string, string>();

    public void Add(string t1, string t2)
    {
        forward.Add(t1, t2);
        reverse.Add(t2, t1);
    }

    public string this[string index]
    {
        get
        {
            // Check for a custom column map.
            if (forward.ContainsKey(index))
                return forward[index];
            if (reverse.ContainsKey(index))
                return reverse[index];

            // If no custom mapping exists, return the value passed in.
            return index;
        }
    }
}

Configure o objeto ColumnMap e diga ao Dapper para usar o mapeamento.

var columnMap = new ColumnMap();
columnMap.Add("Field1", "Column1");
columnMap.Add("Field2", "Column2");
columnMap.Add("Field3", "Column3");

SqlMapper.SetTypeMap(typeof (MyClass), new CustomPropertyTypeMap(typeof (MyClass), (type, columnName) => type.GetProperty(columnMap[columnName])));
Randall Sutton
fonte
Essa é uma boa solução quando você basicamente tem uma incompatibilidade de propriedades no POCO em relação à qual seu banco de dados está retornando, por exemplo, um procedimento armazenado.
esmagar
1
Eu meio que gosto da concisão que o uso de um atributo fornece, mas conceitualmente esse método é mais limpo - ele não combina seu POCO com os detalhes do banco de dados.
Bruno Brant
Se eu entendi o Dapper corretamente, ele não possui um método Insert () específico, apenas um Execute () ... essa abordagem de mapeamento funcionaria para inserções? Ou atualizações? Obrigado
UuDdLrLrSs 26/09/18
29

Eu faço o seguinte usando dinâmico e LINQ:

    var sql = @"select top 1 person_id, first_name, last_name from Person";
    using (var conn = ConnectionFactory.GetConnection())
    {
        List<Person> person = conn.Query<dynamic>(sql)
                                  .Select(item => new Person()
                                  {
                                      PersonId = item.person_id,
                                      FirstName = item.first_name,
                                      LastName = item.last_name
                                  }
                                  .ToList();

        return person;
    }
liorafar
fonte
12

Uma maneira fácil de conseguir isso é usar aliases nas colunas da sua consulta. Se a coluna do banco de dados estiver PERSON_IDe a propriedade do seu objeto for, IDvocê pode fazer isso select PERSON_ID as Id ...na sua consulta e o Dapper irá buscá-la conforme o esperado.

Brad Westness
fonte
12

Retirado dos testes do Dapper, atualmente no Dapper 1.42.

// custom mapping
var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), 
                                    (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName));
Dapper.SqlMapper.SetTypeMap(typeof(TypeWithMapping), map);

Classe auxiliar para obter o nome do atributo Descrição (eu pessoalmente usei a coluna como exemplo @kalebs)

static string GetDescriptionFromAttribute(MemberInfo member)
{
   if (member == null) return null;

   var attrib = (DescriptionAttribute)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false);
   return attrib == null ? null : attrib.Description;
}

Classe

public class TypeWithMapping
{
   [Description("B")]
   public string A { get; set; }

   [Description("A")]
   public string B { get; set; }
}
Oliver
fonte
2
Para que ele funcione mesmo para propriedades onde a descrição não é definido, eu mudei o retorno de GetDescriptionFromAttributea return (attrib?.Description ?? member.Name).ToLower();e acrescentou .ToLower()que columnNameno mapa não deve ser maiúsculas de minúsculas.
Sam White
11

Mexer com o mapeamento é limítrofe, movendo-se para o terreno ORM real. Em vez de lutar com ele e manter o Dapper em sua verdadeira forma simples (rápida), apenas modifique seu SQL da seguinte maneira:

var sql = @"select top 1 person_id as PersonId,FirstName,LastName from Person";
mxmissile
fonte
8

Antes de abrir a conexão com o banco de dados, execute este código para cada uma de suas classes poco:

// Section
SqlMapper.SetTypeMap(typeof(Section), new CustomPropertyTypeMap(
    typeof(Section), (type, columnName) => type.GetProperties().FirstOrDefault(prop =>
    prop.GetCustomAttributes(false).OfType<ColumnAttribute>().Any(attr => attr.Name == columnName))));

Em seguida, adicione as anotações de dados às suas classes poco da seguinte maneira:

public class Section
{
    [Column("db_column_name1")] // Side note: if you create aliases, then they would match this.
    public int Id { get; set; }
    [Column("db_column_name2")]
    public string Title { get; set; }
}

Depois disso, está tudo pronto. Basta fazer uma chamada de consulta, algo como:

using (var sqlConnection = new SqlConnection("your_connection_string"))
{
    var sqlStatement = "SELECT " +
                "db_column_name1, " +
                "db_column_name2 " +
                "FROM your_table";

    return sqlConnection.Query<Section>(sqlStatement).AsList();
}
Tadej
fonte
1
Ele precisa de todas as propriedades para ter o atributo Coluna. Existe alguma maneira de mapear com propriedade caso o mapeador não esteja disponível?
Sandeep.gosavi
5

Se você estiver usando o .NET 4.5.1 ou o checkout Dapper.FluentColumnMapping para mapear o estilo LINQ. Permite separar completamente o mapeamento db do seu modelo (sem necessidade de anotações)

mamuesstack
fonte
5
Eu sou o autor de Dapper.FluentColumnMapping. Separar os mapeamentos dos modelos era um dos principais objetivos do projeto. Eu queria isolar o acesso aos dados principais (ou seja, interfaces de repositório, objetos de modelo etc.) das implementações concretas específicas do banco de dados para uma separação clara das preocupações. Obrigado pela menção e fico feliz que você tenha achado útil! :-)
Alexander
O github.com/henkmollema/Dapper-FluentMap é semelhante. Mas você não precisa mais de um pacote de terceiros. Dapper adicionou Dapper.SqlMapper. Veja minha resposta para mais detalhes, se você estiver interessado.
Tadej
4

Isso é um retrocesso de outras respostas. É apenas um pensamento que eu tinha para gerenciar as cadeias de consulta.

Person.cs

public class Person 
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public static string Select() 
    {
        return $"select top 1 person_id {nameof(PersonId)}, first_name {nameof(FirstName)}, last_name {nameof(LastName)}from Person";
    }
}

Método API

using (var conn = ConnectionFactory.GetConnection())
{
    var person = conn.Query<Person>(Person.Select()).ToList();
    return person;
}
christo8989
fonte
1

para todos que usam o Dapper 1.12, veja o que você precisa fazer para fazer isso:

  • Adicione uma nova classe de atributo da coluna:

      [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property]
    
      public class ColumnAttribute : Attribute
      {
    
        public string Name { get; set; }
    
        public ColumnAttribute(string name)
        {
          this.Name = name;
        }
      }

  • Procure esta linha:

    map = new DefaultTypeMap(type);

    e comente.

  • Escreva isto:

            map = new CustomPropertyTypeMap(type, (t, columnName) =>
            {
              PropertyInfo pi = t.GetProperties().FirstOrDefault(prop =>
                                prop.GetCustomAttributes(false)
                                    .OfType<ColumnAttribute>()
                                    .Any(attr => attr.Name == columnName));
    
              return pi != null ? pi : t.GetProperties().FirstOrDefault(prop => prop.Name == columnName);
            });

  • Uri Abramson
    fonte
    Não sei se entendi - você está recomendando que os usuários alterem o Dapper para possibilitar o mapeamento de atributos por colunas? Nesse caso, é possível usar o código que eu postei acima sem fazer alterações no Dapper.
    Kaleb Pederson
    1
    Mas então você terá que chamar a função de mapeamento para todos e cada um dos seus Tipos de modelo, não é? Estou interessado em uma solução genérica para que todos os meus tipos possam usar o atributo sem precisar chamar o mapeamento para cada tipo.
    precisa saber é o seguinte
    2
    Gostaria que o DefaultTypeMap fosse implementado usando um padrão de estratégia que possa ser substituído pelo motivo que @UriAbramson menciona. Consulte code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette,
    1

    A solução de Kaleb Pederson funcionou para mim. Atualizei o ColumnAttributeTypeMapper para permitir um atributo personalizado (tinha requisito para dois mapeamentos diferentes no mesmo objeto de domínio) e atualizei as propriedades para permitir setters privados nos casos em que um campo precisava ser derivado e os tipos diferentes.

    public class ColumnAttributeTypeMapper<T,A> : FallbackTypeMapper where A : ColumnAttribute
    {
        public ColumnAttributeTypeMapper()
            : base(new SqlMapper.ITypeMap[]
                {
                    new CustomPropertyTypeMap(
                       typeof(T),
                       (type, columnName) =>
                           type.GetProperties( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(prop =>
                               prop.GetCustomAttributes(true)
                                   .OfType<A>()
                                   .Any(attr => attr.Name == columnName)
                               )
                       ),
                    new DefaultTypeMap(typeof(T))
                })
        {
            //
        }
    }
    GameSalutes
    fonte
    1

    Sei que esse é um tópico relativamente antigo, mas pensei em lançar o que fiz por aí.

    Eu queria que o mapeamento de atributos funcionasse globalmente. Você corresponde ao nome da propriedade (também conhecido como padrão) ou corresponde a um atributo de coluna na propriedade da classe. Eu também não queria configurar isso para todas as classes para as quais estava mapeando. Como tal, criei uma classe DapperStart que invoco no início do aplicativo:

    public static class DapperStart
    {
        public static void Bootstrap()
        {
            Dapper.SqlMapper.TypeMapProvider = type =>
            {
                return new CustomPropertyTypeMap(typeof(CreateChatRequestResponse),
                    (t, columnName) => t.GetProperties().FirstOrDefault(prop =>
                        {
                            return prop.Name == columnName || prop.GetCustomAttributes(false).OfType<ColumnAttribute>()
                                       .Any(attr => attr.Name == columnName);
                        }
                    ));
            };
        }
    }

    Bem simples. Não tenho certeza de quais problemas encontrarei ainda quando acabei de escrever isso, mas funciona.

    Matt M
    fonte
    Como é o CreateChatRequestResponse? Além disso, como você o está chamando na inicialização?
    Glen F.
    1
    @GlenF. o ponto é que não importa a aparência de CreateChatRequestResponse. pode ser qualquer POCO. isso é invocado na sua inicialização. Você pode invocá-lo apenas no início do aplicativo, no StartUp.cs ou no Global.asax.
    Matt M
    Talvez eu esteja completamente errado, mas a menos que CreateChatRequestResponseseja substituído por Tcomo isso iteraria em todos os objetos da Entidade. Por favor corrija-me se eu estiver errado.
    Fwd079 31/01/19
    0

    A solução simples para o problema que Kaleb está tentando resolver é aceitar o nome da propriedade se o atributo da coluna não existir:

    Dapper.SqlMapper.SetTypeMap(
        typeof(T),
        new Dapper.CustomPropertyTypeMap(
            typeof(T),
            (type, columnName) =>
                type.GetProperties().FirstOrDefault(prop =>
                    prop.GetCustomAttributes(false)
                        .OfType<ColumnAttribute>()
                        .Any(attr => attr.Name == columnName) || prop.Name == columnName)));
    
    Stewart Cunningham
    fonte