Dapper with Attributes mapping
Asked Answered
N

4

24

I try to map my Id fields with the Column Attributes but for some reason this doesn't seem to work and I can't figure out why. I set up a test project to demonstrate what I am trying.

First, I got my 2 entities:

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

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

In my database I got 2 tables, also named Table1 and Table2. Both tables got their columns named equal to the entities with the exception that Table1 has a column named Table2Id and there is also a foreign key between Table1.Table2Id and Table2.Id.

Also there is 1 record each in both tables and those got both the Id 2.

What I try next is to execute a query with dapper and it should return a object of type Table1. This works, but both the property Table1.Id and Table1.Table2.Id remains 0 (default integer). I expect the column attributes would map the Id fields but clearly this isn't happing.

This is the query and mapping I am executing in 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;
}

Now I could rename the both Id property fields in the entities to Table1Id and Table2Id but I prefer Id instead cause of the more logic code like Table1.Id instead of Table1.Table1Id. So I was wondering, is it possible what I want here and if so, how?

Edit:

I found this topic: Manually Map column names with class properties

And with the code in the first post of Kaleb Pederson it is possible to use attributes when needed with the FallBackTypeMapper class and the ColumnAttributeTypeMapper class. All that is needed is to add the required classes to the typemapping with:

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

But with many entities this list will grow long. Also you need to add every class manually to the list and I was wondering if this could be done automaticly en more generic with Reflection. I found a code fragment that is able to get all the types:

        const string @namespace = "DapperTestProj.Entities";

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

And looping through all the types, I can do this, only problem I have now is what code fragment do I need to have or need to put on the place where the questionmarks are right now?

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

Edit:

After more searching, I found the solution for my last problem:

        typeList.ToList().ForEach(type =>
            {
                var mapper = (SqlMapper.ITypeMap)Activator.CreateInstance(
                    typeof(ColumnAttributeTypeMapper<>)
                        .MakeGenericType(type));
                SqlMapper.SetTypeMap(type, mapper);
            });
Newt answered 6/1, 2014 at 13:56 Comment(4)
Well, I also tried the dataannotations from System.ComponentModel.DataAnnotations attributes but those weren't working either. And I was thinking, maybe dapper is using those, but I guess I was wrong. Then my question is, which attributes should I use then?Newt
If you are using Dapper, you should have a match between the query and the class. So instead of T1.Id as Table1IdGamo
It seems possible to (ab)use the Linq to SQL attributes. I updated my first post which shows how. I only got one problem with getting the the generic type.Newt
For the record, there is a year old discussion about adding this feature: github.com/StackExchange/Dapper/issues/722Bernie
N
31

For the completion of the solution, I want to share the code I found and put together with those who are interested.

Instead of (ab)using the System.Data.Linq.Mapping.ColumnAttribute, it might be more logic (and probably save, although the chance will be very small that Microsoft will change the linq to sql ColumnAttribute class) to create our own ColumnAttribute class:

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

Found in the topic I mentioned earlier, the FallBackTypeMapper and the ColumnAttributeTypeMapper classes:

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

and finally, the TypeMapper.cs to initialize the mapping.

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

At start up, TypeMapper.Initialize needs to be called:

TypeMapper.Initialize("DapperTestProj.Entities");

And you can start using attributes for the entity properties

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();
        }
    }
}
Newt answered 7/1, 2014 at 10:41 Comment(7)
The only problem I have with using attributes is that it ties your class to Dapper. It seems a better mapper wouldn't require you to decorate your POCO with any Dapper specific attributes or meta information. The idea of creating a custom mapping is so that you don't have to modify the POCO to match the field names.Pegram
@Pegram - The [Column] attribute is not part of dapper. It's used by other ORMs, eg EntityFramework. It's also not part of EntityFramework - it's part of System.ComponentModel.DataAnnotations.Splenic
@Newt - I'm just looking into dapper (a little late I know) and had the same question about the mapper as the tables/columns in the db I'm using are all names weird with underscores and capitals etc (arrrh) so my mapper would have 100's of lines for the entities - Like the reflection solution you have. Thanks!Mycosis
I'm getting 'FallBackTypeMapper' does not implement interface member 'SqlMapper.ITypeMap.FindExplicitConstructor()' and if I implement this parameterless constructor, when I'm trying to query the database I get a NotImplementedException. Any hints?Hone
I found a solution here: github.com/henkmollema/Dapper-FluentMap/issues/16Hone
I am confused, the answer to this question should be 1 line of code eg something like [Column("Name"] yet here we have hundreds of lines of code to achieve something that should be out of the box. What am I missing? How is there not just a simple attribute, like in JSON, JsonProperty("Name")Dehnel
What you are missing is the fact that dapper and Newtonsoft are 3rd parties creating libraries to make some jobs easier. Dapper though didn't implement the code in the answer (yet) so I had to do it myself. Many years later, they might have implemented or not, I don't know. But if it isn't yet, this code can be used. Also, someone is always free to make an extension package on the dapper NuGet package, containing this code. And lastly, there is propably many lines of code behind the JsonProperty attribute as well, but you don't see it, it is "hidden" inside the package.Newt
M
7

Cornelis's answer is correct, however I wanted to add an update to this. As of the current version of Dapper you also need to implement SqlMapper.ItypeMap.FindExplicitConstructor(). I'm not sure when this change was made, but this for anyone else that stumbles upon this question and is missing that part of the solution.

Within FallbackTypeMapper.cs

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

Also you can use the ColumnAttribute class located within the System.ComponentModel.DataAnnotations.Schema namespace instead of rolling your own for build-in non-database/orm specific version.

Medick answered 17/8, 2017 at 13:10 Comment(0)
G
3

I was having an issue, during a .NET framework project migration to .NET Core, that is similar to this issue. We were using Column Attributes (System.ComponentModel.DataAnnotations.Schema) on our entities, which got moved to a common library. I was looking for the TypeMaps described in this post but, we were using Dapper.FluentMap and Dapper.FluentMap.Dommel and this was in the app startup.

FluentMapper.Initialize(config =>
{
    ...
    config.ForDommel();
});

The config.ForDommel(); has middleware that maps the System.ComponentModel.DataAnnotations.Schema Column Attributes on the entities and once I added that to the .NET Core app, everything was working correctly. Hope this helps and it should be easier to use than rolling up a custom solution.

Gigue answered 18/12, 2021 at 21:5 Comment(0)
C
0

it gets even better

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))
            })
        }
    }
}
Curable answered 6/10, 2019 at 7:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.