Asigne manualmente nombres de columna con propiedades de clase

173

Soy nuevo en Dapper micro ORM. Hasta ahora, puedo usarlo para cosas simples relacionadas con ORM, pero no puedo asignar los nombres de columna de la base de datos con las propiedades de la clase.

Por ejemplo, tengo la siguiente tabla de base de datos:

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

y tengo una clase llamada Persona:

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

Tenga en cuenta que los nombres de mis columnas en la tabla son diferentes del nombre de la propiedad de la clase a la que estoy tratando de asignar los datos que obtuve del resultado de la 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;
}

El código anterior no funcionará ya que los nombres de las columnas no coinciden con las propiedades del objeto (Persona). En este escenario, ¿hay algo que pueda hacer en Dapper para asignar manualmente (por ejemplo person_id => PersonId) los nombres de columna con propiedades de objeto?

usuario1154985
fuente

Respuestas:

80

Esto funciona bien:

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;
}

Dapper no tiene ninguna facilidad que le permita especificar un Atributo de columna , no estoy en contra de agregarle soporte, siempre que no obtengamos la dependencia.

Sam Azafrán
fuente
@ Sam Saffron, ¿hay alguna forma de especificar el alias de la tabla? Tengo una clase llamada Country pero en la base de datos la tabla tiene un nombre muy complicado debido a las convenciones de nomenclatura archic.
TheVillageIdiot
64
La columna Attribue sería útil para mapear los resultados del procedimiento almacenado.
Ronnie Overby
2
Los atributos de columna también serían útiles para facilitar más fácilmente el acoplamiento físico y / o semántico entre su dominio y los detalles de implementación de la herramienta que está utilizando para materializar sus entidades. Por lo tanto, no agregue soporte para esto !!!! :)
Derek Greer
No entiendo por qué columnattribe no está allí cuando tableattribute. ¿Cómo funcionaría este ejemplo con inserciones, actualizaciones y SP? Me gustaría ver columnattribe, es muy simple y facilitaría mucho la migración de otras soluciones que implementan algo similar como el ahora desaparecido linq-sql.
Vman
197

Dapper ahora admite columnas personalizadas para mapeadores de propiedades. Lo hace a través de la interfaz ITypeMap . Dapper proporciona una clase CustomPropertyTypeMap que puede hacer la mayor parte de este trabajo. Por ejemplo:

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))));

Y el modelo:

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

Es importante tener en cuenta que la implementación de CustomPropertyTypeMap requiere que el atributo exista y coincida con uno de los nombres de columna o la propiedad no se asignará. La clase DefaultTypeMap proporciona la funcionalidad estándar y se puede aprovechar para cambiar este comportamiento:

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);
    }
}

Y con eso en su lugar, se vuelve fácil crear un mapeador de tipos personalizado que usará automáticamente los atributos si están presentes, pero de lo contrario volverá al comportamiento estándar:

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))
            })
    {
    }
}

Eso significa que ahora podemos admitir fácilmente los tipos que requieren un mapa usando atributos:

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

Aquí hay un resumen del código fuente completo .

Kaleb Pederson
fuente
He estado luchando con este mismo problema ... y esta parece ser la ruta por la que debería ir ... Estoy bastante confundido en cuanto a dónde se llamaría este código "Dapper.SqlMapper.SetTypeMap (typeof (MyModel), nuevo ColumnAttributeTypeMapper <MyModel> ()); " stackoverflow.com/questions/14814972/…
Rohan Büchner
Deberá llamarlo una vez antes de realizar cualquier consulta. Podría hacerlo en un constructor estático, por ejemplo, ya que solo necesita llamarse una vez.
Kaleb Pederson
77
Recomiendo que esta sea la respuesta oficial: esta característica de Dapper es extremadamente útil.
killthrush
3
La solución de mapeo publicada por @Oliver ( stackoverflow.com/a/34856158/364568 ) funciona y requiere menos código.
Riga
44
Me encanta cómo se lanza la palabra "fácilmente" tan fácilmente: P
Jonathan B.
80

Por algún tiempo, lo siguiente debería funcionar:

Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Marc Gravell
fuente
66
Aunque esta no es realmente la respuesta a la pregunta " Asignar manualmente nombres de columna con propiedades de clase", para mí es mucho mejor que tener que asignar manualmente (desafortunadamente en PostgreSQL es mejor usar guiones bajos en los nombres de columna). ¡No elimine la opción MatchNamesWithUnderscores en las próximas versiones! ¡¡¡Gracias!!!
victorvartan
55
@victorvartan no hay planes para eliminar la MatchNamesWithUnderscoresopción. En el mejor de los casos , si refactorizamos la API de configuración, dejaría al MatchNamesWithUnderscoresmiembro en su lugar (que todavía funciona, idealmente) y agregaría un [Obsolete]marcador para señalar a las personas a la nueva API.
Marc Gravell
44
@MarcGravell las palabras "Por algún tiempo" al comienzo de tu respuesta me preocuparon de que pudieras eliminarlo en una versión futura, ¡gracias por aclarar! ¡Y muchas gracias por Dapper, un maravilloso micro ORM que recién comencé a usar para un pequeño proyecto junto con Npgsql en ASP.NET Core!
victorvartan
2
Esta es fácilmente la mejor respuesta. He encontrado montones y montones de soluciones, pero finalmente me topé con esto. Fácilmente la mejor respuesta, pero menos anunciada.
teaMonkeyFruit
29

Aquí hay una solución simple que no requiere atributos que le permitan mantener el código de infraestructura fuera de sus POCO.

Esta es una clase para tratar con las asignaciones. Un diccionario funcionaría si asignara todas las columnas, pero esta clase le permite especificar solo las diferencias. Además, incluye mapas inversos para que pueda obtener el campo de la columna y la columna del campo, lo que puede ser útil al hacer cosas como generar sentencias 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 el objeto ColumnMap y dígale a Dapper que use la asignación.

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
fuente
Esta es una buena solución cuando básicamente tiene una falta de coincidencia de propiedades en su POCO con lo que su base de datos está devolviendo, por ejemplo, de un procedimiento almacenado.
aplastar
1
Me gusta un poco la concisión que proporciona el uso de un atributo, pero conceptualmente este método es más limpio: no combina su POCO con los detalles de la base de datos.
Bruno Brant
Si entiendo Dapper correctamente, no tiene un método Insert () específico, solo un Execute () ... ¿funcionaría este enfoque de mapeo para las inserciones? O actualizaciones? Gracias
UuDdLrLrSs
29

Hago lo siguiente usando dynamic y 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
fuente
12

Una manera fácil de lograr esto es usar solo alias en las columnas de su consulta. Si la columna de su base de datos es PERSON_IDy la propiedad de su objeto es lo IDque puede hacer select PERSON_ID as Id ...en su consulta y Dapper lo recogerá como se esperaba.

Brad Westness
fuente
12

Tomado de las pruebas Dapper que actualmente se encuentra en 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);

Clase auxiliar para obtener el nombre del atributo Descripción (personalmente he usado Columna como ejemplo @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;
}

Clase

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

   [Description("A")]
   public string B { get; set; }
}
Oliver
fuente
2
Con el fin de que funcione incluso para las propiedades donde se define ninguna descripción, he cambiado el regreso de GetDescriptionFromAttributea return (attrib?.Description ?? member.Name).ToLower();y añadió .ToLower()que columnNameen el mapa no debería ser entre mayúsculas y minúsculas.
Sam White
11

Jugar con el mapeo es moverse al límite en tierra ORM real. En lugar de luchar con él y mantener a Dapper en su verdadera forma simple (rápida), simplemente modifique su SQL ligeramente de la siguiente manera:

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

Antes de abrir la conexión a su base de datos, ejecute este código para cada una de sus clases de 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))));

Luego agregue las anotaciones de datos a sus clases de poco como esta:

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; }
}

Después de eso, ya está todo listo. Simplemente haga una llamada 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
fuente
1
Necesita todas las propiedades para tener el atributo Column. ¿Hay alguna forma de mapear con propiedad en caso de que el mapeador no esté disponible?
sandeep.gosavi
5

Si está utilizando .NET 4.5.1 o superior, compruebe Dapper.FluentColumnMapping para mapear el estilo LINQ. Le permite separar completamente el mapeo db de su modelo (sin necesidad de anotaciones)

mamuesstack
fuente
55
Soy el autor de Dapper.FluentColumnMapping. Separar las asignaciones de los modelos fue uno de los principales objetivos de diseño. Quería aislar el acceso a los datos centrales (es decir, interfaces de repositorio, objetos modelo, etc.) de las implementaciones concretas específicas de la base de datos para una separación clara de las preocupaciones. Gracias por la mención y me alegro de que te haya resultado útil. :-)
Alexander
github.com/henkmollema/Dapper-FluentMap es similar. Pero ya no necesita un paquete de terceros. Dapper agregó Dapper.SqlMapper. Vea mi respuesta para más detalles si está interesado.
Tadej
4

Esto es el respaldo de otras respuestas. Es solo un pensamiento que tenía para administrar las cadenas 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
fuente
1

Para todos ustedes que usan Dapper 1.12, esto es lo que deben hacer para lograrlo:

  • Agregue una nueva clase de atributo de columna:

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

  • Busque esta línea:

    map = new DefaultTypeMap(type);

    y comentarlo

  • Escribe esto en su lugar:

            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
    fuente
    No estoy seguro de entenderlo: ¿recomienda que los usuarios cambien Dapper para hacer posible la asignación de atributos por columnas? Si es así, es posible usar el código que publiqué anteriormente sin hacer cambios en Dapper.
    Kaleb Pederson el
    1
    Pero entonces tendrá que llamar a la función de mapeo para todos y cada uno de sus Tipos de modelo, ¿no es así? Estoy interesado en una solución genérica para que todos mis tipos puedan usar el atributo sin tener que llamar a la asignación de cada tipo.
    Uri Abramson
    2
    Me gustaría ver que DefaultTypeMap se implemente utilizando un patrón de estrategia tal que pueda reemplazarse por la razón que menciona @UriAbramson. Ver code.google.com/p/dapper-dot-net/issues/detail?id=140
    Richard Collette
    1

    La solución de Kaleb Pederson funcionó para mí. Actualicé el ColumnAttributeTypeMapper para permitir un atributo personalizado (tenía requisitos para dos asignaciones diferentes en el mismo objeto de dominio) y propiedades actualizadas para permitir a los establecedores privados en los casos en que se necesitaba derivar un campo y los tipos diferían.

    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
    fuente
    1

    Sé que este es un hilo relativamente antiguo, pero pensé en tirar lo que hice allí.

    Quería que la asignación de atributos funcionara globalmente. O coincide con el nombre de la propiedad (también conocido como predeterminado) o con un atributo de columna en la propiedad de la clase. Tampoco quería tener que configurar esto para cada clase a la que estaba asignando. Como tal, creé una clase DapperStart que invoco al inicio de la aplicación:

    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);
                        }
                    ));
            };
        }
    }

    Bastante simple. No estoy seguro de qué problemas me encontraré todavía, ya que acabo de escribir esto, pero funciona.

    Matt M
    fuente
    ¿Cómo se ve CreateChatRequestResponse? Además, ¿cómo lo invocas en el inicio?
    Glen F.
    1
    @GlenF. el punto es que no importa cómo se ve CreateChatRequestResponse. Puede ser cualquier POCO. esto se invoca en su inicio. Puede invocarlo en el inicio de su aplicación, ya sea en su StartUp.cs o su Global.asax.
    Matt M
    Quizás estoy completamente equivocado, pero a menos que CreateChatRequestResponsesea ​​reemplazado por Tcómo iteraría esto a través de todos los objetos de la Entidad. Por favor, corríjame si estoy equivocado.
    Fwd079
    0

    La solución simple al problema que Kaleb está tratando de resolver es simplemente aceptar el nombre de la propiedad si el atributo de columna no existe:

    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
    fuente