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.
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:
[DbValue(...)]
that allows a Name
to be specified (works both both column and parameter names)[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