web-dev-qa-db-fra.com

Dapper avec mappage d'attributs

J'essaie de mapper mes champs d'ID avec les attributs de colonne, mais pour une raison quelconque, cela ne semble pas fonctionner et je ne peux pas comprendre pourquoi. J'ai mis en place un projet de test pour démontrer ce que j'essaie.

Tout d'abord, j'ai obtenu mes 2 entités:

Table d'entité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();
        }
    }
}

et entité 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; }
    }
}

Dans ma base de données, j'ai obtenu 2 tables, également nommées Table1 et Table2. Les deux tables ont obtenu leurs colonnes nommées égales aux entités à l'exception que Table1 a une colonne nommée Table2Id et il existe également une clé étrangère entre Table1.Table2Id et Table2.Id.

De plus, il y a 1 enregistrement dans les deux tableaux et ceux-ci ont tous deux l'ID 2.

Ce que j'essaye ensuite, c'est d'exécuter une requête avec dapper et cela devrait retourner un objet de type Table1. Cela fonctionne, mais la propriété Table1.Id et Table1.Table2.Id reste à la fois 0 (entier par défaut). Je m'attends à ce que les attributs de colonne mappent les champs d'identification, mais ce n'est clairement pas le cas.

Voici la requête et le mappage que j'exécute dans le code:

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

Maintenant, je pouvais renommer les deux champs de propriété Id dans les entités en Table1Id et Table2Id mais je préfère Id à la place à cause du code plus logique comme Table1.Id au lieu de Table1.Table1Id. Je me demandais donc, est-ce possible ce que je veux ici et si oui, comment?

Éditer:

J'ai trouvé ce sujet: Mapper manuellement les noms de colonne avec les propriétés de classe

Et avec le code dans le premier article de Kaleb Pederson, il est possible d'utiliser des attributs en cas de besoin avec la classe FallBackTypeMapper et la classe ColumnAttributeTypeMapper. Il suffit d'ajouter les classes requises au typemapping avec:

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

Mais avec de nombreuses entités, cette liste s'allongera. Vous devez également ajouter chaque classe manuellement à la liste et je me demandais si cela pouvait être fait automatiquement et plus générique avec Reflection. J'ai trouvé un fragment de code capable d'obtenir tous les types:

        const string @namespace = "DapperTestProj.Entities";

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

Et en parcourant tous les types, je peux le faire, le seul problème que j'ai maintenant est de savoir quel fragment de code dois-je avoir ou mettre à l'endroit où se trouvent les points d'interrogation en ce moment?

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

Éditer:

Après plus de recherches, j'ai trouvé la solution à mon dernier problème:

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

Pour l'achèvement de la solution, je veux partager le code que j'ai trouvé et assemblé avec ceux qui sont intéressés.

Au lieu d'utiliser (ab) la System.Data.Linq.Mapping.ColumnAttribute, il pourrait être plus logique (et probablement d'économiser, bien que la chance soit très faible que Microsoft change la classe linq en sql ColumnAttribute) pour créer notre propre ColumnAttribute classe:

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

Trouvé dans le sujet que j'ai mentionné plus tôt, les classes FallBackTypeMapper et 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)) 
                    })
        {
        }
    }
}

et enfin, le TypeMapper.cs pour initialiser le mappage.

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 type in Assembly.GetExecutingAssembly().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);
            });
        }
    }
}

Au démarrage, TypeMapper.Initialize doit être appelé:

TypeMapper.Initialize("DapperTestProj.Entities");

Et vous pouvez commencer à utiliser des attributs pour les propriétés de l'entité

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();
        }
    }
}
19
Cornelis

La réponse de Cornelis est correcte, mais je voulais ajouter une mise à jour à cela. À partir de la version actuelle de Dapper, vous devez également implémenter SqlMapper.ItypeMap.FindExplicitConstructor(). Je ne sais pas quand cette modification a été apportée, mais ceci pour quiconque trébuche sur cette question et manque cette partie de la solution.

Dans FallbackTypeMapper.cs

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

Vous pouvez également utiliser la classe ColumnAttribute située dans le System.ComponentModel.DataAnnotations.Schema espace de noms au lieu de lancer le vôtre pour une version non spécifique à la base de données/orm intégrée.

2
JNYRanger

c'est encore mieux

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))
                    })
        {
        }
    }
0
Al-Hanash Moataz