Search code examples
c#.netdapper

Can Dapper Mappings Be Automated with Attributes?


In my webapi dotnet7 application, I utilize Dapper for database operations. Additionally, I employ a solution found on StackOverflow for mapping database columns to class properties using the Column attribute, as shown in the following example:

public class Customer
{
    public int CustomerID { get; set; }

    [Column(Name = "Customer_Name")]
    public string Name{ get; set; }
}

This Column attribute is sourced from the ColumnAttributeTypeMapper.cs file, which contains the custom type mapper logic. Here's an excerpt:

namespace YourNamespace
{
    /// <summary>
    /// Uses the Name value of the <see cref="ColumnAttribute"/> specified to determine
    /// the association between the name of the column in the query results and the member to
    /// which it will be extracted. If no column mapping is present all members are mapped as
    /// usual.
    /// </summary>
    /// <typeparam name="T">The type of the object that this association between the mapper applies to.</typeparam>
    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))
                })
        {
        }
    }
    
        [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
    public class ColumnAttribute : Attribute
    {
        public string Name { get; set; }
    }

    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
                {
                    ConstructorInfo result = mapper.FindConstructor(names, types);
                    if (result != null)
                    {
                        return result;
                    }
                }
                catch (NotImplementedException)
                {
                }
            }
            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)
                {
                }
            }
            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)
                {
                }
            }
            return null;
        }
        
        
        public ConstructorInfo FindExplicitConstructor()
        {
            return _mappers
                .Select(mapper => mapper.FindExplicitConstructor())
                .FirstOrDefault(result => result != null);
        }
    }
  
}

To ensure Dapper recognizes these mappings, I currently add a SetTypeMap call for each model in the Program.cs file, which can lead to merge conflicts. For example:

Dapper.SqlManager.SetTypeMap(typeof(Customer), new ColumnAttributeTypeMapper<Customer>);

Is there a way to automate the registration of these mappings for all models in the project? Perhaps by using an attribute, such as [DapperMapper], to associate each model with Dapper?

Please advise on a more automated approach for registering Dapper mappings.


Solution

  • The short answer currently is "no"; Dapper already does a ton of reflection, and it isn't hugely desirable to add more.

    However; in the upcoming (incomplete, depends at least on net9 for build tool support) DapperAOT era, I do hope to incorporate something like this; in particular:

    • there is already a [DbValue(...)] that allows a Name to be specified (works both both column and parameter names)
    • I intend to add support for the well-known [System.ComponentModel.DataAnnotations.Schema.ColumnAttribute] attribute, (perhaps via a global [UseColumnAttribute] marker, to avoid surprises - perhaps even with a build warning if you have [Column(...)] but don't have an explicit [UseColumnAttribute]/[UseColumnAttribute(false)])

    but: that's at least a few months out (can't release until at least November, and: I haven't even implemented [Column] yet)

    I've logged https://github.com/DapperLib/DapperAOT/issues/36 for tracking