Dapper与属性映射

23

我试图将我的Id字段与列属性进行映射,但出于某种原因,这似乎不起作用,我无法弄清楚为什么。 我设置了一个测试项目来演示我的尝试。

首先,我有两个实体:

实体 Table1

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

在我的数据库中,有两个表,也称为Table1和Table2。这两个表的列名称与实体相同,但Table1除外,它有一个名为Table2Id的列,并且Table1.Table2Id和Table2.Id之间还有一个外键关系。
此外,每个表中都有一个记录,它们都具有Id 2。
接下来我尝试使用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,但我更喜欢使用Id,因为它能让代码更加合乎逻辑,例如Table1.Id而不是Table1.Table1Id。所以我想知道,我想要的是否可能实现,如果可能,应该如何操作呢?

编辑:

我找到了这个主题: 手动将列名称映射到类属性

通过Kaleb Pederson的第一篇帖子中的代码,可以使用FallBackTypeMapper类和ColumnAttributeTypeMapper类在需要时使用属性。所有需要的就是将所需的类添加到typemapping中:

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

但是对于许多实体,这个列表会变得很长。而且你需要手动将每个类添加到列表中,我在想是否可以使用反射自动更加通用地完成这项工作。我找到了一个能够获取所有类型的代码片段:

        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</*???*/>()));

编辑:

经过更多搜索,我找到了解决上一个问题的方法:

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

我也尝试了System.ComponentModel.DataAnnotations属性中的数据注释,但它们也没有起作用。我在想,也许dapper正在使用它们,但我想我错了。那么我的问题是,我应该使用哪些属性呢? - Cornelis
1
如果您正在使用Dapper,应该在查询和类之间进行匹配。因此,不要写成T1.Id as Table1Id。 - Paulo Correia
似乎可以(滥用)Linq to SQL属性。我更新了我的第一篇帖子,展示了如何操作。我只遇到了一个问题,就是获取通用类型。 - Cornelis
1
值得一提的是,关于添加此功能已经有一年的讨论记录:https://github.com/StackExchange/Dapper/issues/722 - Lucas
4个回答

30
为了完成解决方案,我想与感兴趣的人分享我找到并整合的代码。 不要滥用System.Data.Linq.Mapping.ColumnAttribute,创建自己的ColumnAttribute类可能更合理(虽然微软更改linq to sql 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绑定在了一起。更好的映射器似乎不需要你使用任何Dapper特定的属性或元信息来装饰你的POCO。创建自定义映射的想法是为了避免修改POCO以匹配字段名称。 - crush
4
@crush - [Column] 属性不是 Dapper 的一部分,它被其他 ORM(例如 EntityFramework)使用。它也不是 EntityFramework 的一部分,而是 System.ComponentModel.DataAnnotations 的一部分。 - crimbo
1
我遇到了一个问题:'FallBackTypeMapper'没有实现接口成员'SqlMapper.ITypeMap.FindExplicitConstructor()',如果我实现这个无参构造函数,当我尝试查询数据库时会出现NotImplementedException异常。有什么提示吗? - Brugner
我在这里找到了一个解决方案:https://github.com/henkmollema/Dapper-FluentMap/issues/16 - Brugner
我感到困惑,这个问题的答案应该只有一行代码,比如[Column("Name")],但是在这里我们却需要数百行代码来实现一个本应该是开箱即用的功能。我错过了什么?为什么没有像JSON中那样简单的属性,比如JsonProperty("Name")? - PandaWood
显示剩余2条评论

7

Cornelis的答案是正确的,不过我想补充一点。从目前版本的Dapper开始,你还需要实现SqlMapper.ItypeMap.FindExplicitConstructor()接口。我不确定这个改变是何时发生的,但对于其他遇到此问题并缺少解决方法的人来说,这是必要步骤。

FallbackTypeMapper.cs中:

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

此外,您还可以使用位于System.ComponentModel.DataAnnotations.Schema命名空间中的ColumnAttribute类,而不是为内置非数据库/ORM特定版本编写自己的类。

2
我在将.NET框架项目迁移到.NET Core时遇到了一个类似于这个问题的问题。我们在实体上使用列属性(System.ComponentModel.DataAnnotations.Schema),这些属性被移动到了一个公共库中。我正在寻找此帖子中描述的TypeMaps,但我们使用的是Dapper.FluentMapDapper.FluentMap.Dommel,并且这是在应用程序启动时进行配置的。
FluentMapper.Initialize(config =>
{
    ...
    config.ForDommel();
});

config.ForDommel();有中间件,可以将System.ComponentModel.DataAnnotations.Schema列属性映射到实体上。一旦我在.NET Core应用程序中添加了它,一切都正常工作了。希望这可以帮助您,而且它应该比编写自定义解决方案更容易使用。


0
它变得更好了
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))
            })
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接