diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs
index b278250cb..12e431522 100644
--- a/Dapper/DefaultTypeMap.cs
+++ b/Dapper/DefaultTypeMap.cs
@@ -74,8 +74,16 @@ public ConstructorInfo FindConstructor(string[] names, Type[] types)
int i = 0;
for (; i < ctorParameters.Length; i++)
{
- if (!string.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase))
+ if (EqualsCI(ctorParameters[i].Name, names[i]))
+ { } // exact match
+ else if (MatchNamesWithUnderscores && EqualsCIU(ctorParameters[i].Name, names[i]))
+ { } // match after applying underscores
+ else
+ {
+ // not a name match
break;
+ }
+
if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary)
continue;
var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType;
@@ -119,9 +127,8 @@ public ConstructorInfo FindExplicitConstructor()
/// Mapping implementation
public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
{
- var parameters = constructor.GetParameters();
-
- return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)));
+ ParameterInfo param = MatchFirstOrDefault(constructor.GetParameters(), columnName, static p => p.Name);
+ return new SimpleMemberMap(columnName, param);
}
///
@@ -131,14 +138,7 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor,
/// Mapping implementation
public SqlMapper.IMemberMap GetMember(string columnName)
{
- var property = Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal))
- ?? Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase));
-
- if (property == null && MatchNamesWithUnderscores)
- {
- property = Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.Ordinal))
- ?? Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.OrdinalIgnoreCase));
- }
+ var property = MatchFirstOrDefault(Properties, columnName, static p => p.Name);
if (property != null)
return new SimpleMemberMap(columnName, property);
@@ -174,6 +174,54 @@ public SqlMapper.IMemberMap GetMember(string columnName)
///
public static bool MatchNamesWithUnderscores { get; set; }
+ static T MatchFirstOrDefault(IList members, string name, Func selector) where T : class
+ {
+ if (members is { Count: > 0 })
+ {
+ // try exact first
+ foreach (var member in members)
+ {
+ if (string.Equals(name, selector(member), StringComparison.Ordinal))
+ {
+ return member;
+ }
+ }
+ // then exact ignoring case
+ foreach (var member in members)
+ {
+ if (string.Equals(name, selector(member), StringComparison.OrdinalIgnoreCase))
+ {
+ return member;
+ }
+ }
+ if (MatchNamesWithUnderscores)
+ {
+ // same again, minus underscore delta
+ name = name?.Replace("_", "");
+ foreach (var member in members)
+ {
+ if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.Ordinal))
+ {
+ return member;
+ }
+ }
+ foreach (var member in members)
+ {
+ if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.OrdinalIgnoreCase))
+ {
+ return member;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ internal static bool EqualsCI(string x, string y)
+ => string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
+ internal static bool EqualsCIU(string x, string y)
+ => string.Equals(x?.Replace("_", ""), y?.Replace("_", ""), StringComparison.OrdinalIgnoreCase);
+
///
/// The settable properties for this typemap
///
diff --git a/docs/index.md b/docs/index.md
index e2092effc..326c30cc8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -22,6 +22,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command
### unreleased
+- add underscore handling with constructors (#1786 via @jo-goro, fixes #818; also #1947 via mgravell)
+
(note: new PRs will not be merged until they add release note wording here)
### 2.0.143
diff --git a/tests/Dapper.Tests/ConstructorTests.cs b/tests/Dapper.Tests/ConstructorTests.cs
index 4c72894ee..af8ae7afa 100644
--- a/tests/Dapper.Tests/ConstructorTests.cs
+++ b/tests/Dapper.Tests/ConstructorTests.cs
@@ -220,5 +220,45 @@ public void TestWithNonPublicConstructor()
var output = connection.Query("select 1 as Foo").First();
Assert.Equal(1, output.Foo);
}
+
+ [Fact]
+ public void CtorWithUnderscores()
+ {
+ var obj = connection.QueryFirst("select 'abc' as FIRST_NAME, 'def' as LAST_NAME");
+ Assert.NotNull(obj);
+ Assert.Equal("abc", obj.FirstName);
+ Assert.Equal("def", obj.LastName);
+ }
+
+ [Fact]
+ public void CtorWithoutUnderscores()
+ {
+ DefaultTypeMap.MatchNamesWithUnderscores = true;
+ var obj = connection.QueryFirst("select 'abc' as FIRST_NAME, 'def' as LAST_NAME");
+ Assert.NotNull(obj);
+ Assert.Equal("abc", obj.FirstName);
+ Assert.Equal("def", obj.LastName);
+ }
+
+ class Type_ParamsWithUnderscores
+ {
+ public string FirstName { get; }
+ public string LastName { get; }
+ public Type_ParamsWithUnderscores(string first_name, string last_name)
+ {
+ FirstName = first_name;
+ LastName = last_name;
+ }
+ }
+ class Type_ParamsWithoutUnderscores
+ {
+ public string FirstName { get; }
+ public string LastName { get; }
+ public Type_ParamsWithoutUnderscores(string firstName, string lastName)
+ {
+ FirstName = firstName;
+ LastName = lastName;
+ }
+ }
}
}
diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs
index 4d5dd26c4..608607a0d 100644
--- a/tests/Dapper.Tests/MiscTests.cs
+++ b/tests/Dapper.Tests/MiscTests.cs
@@ -1273,5 +1273,26 @@ private class HazGetOnly
public int Id { get; }
public string Name { get; } = "abc";
}
+
+ [Fact]
+ public void TestConstructorParametersWithUnderscoredColumns()
+ {
+ DefaultTypeMap.MatchNamesWithUnderscores = true;
+ var obj = connection.QuerySingle("select 42 as [id_property], 'def' as [name_property];");
+ Assert.Equal(42, obj.IdProperty);
+ Assert.Equal("def", obj.NameProperty);
+ }
+
+ private class HazGetOnlyAndCtor
+ {
+ public int IdProperty { get; }
+ public string NameProperty { get; }
+
+ public HazGetOnlyAndCtor(int idProperty, string nameProperty)
+ {
+ IdProperty = idProperty;
+ NameProperty = nameProperty;
+ }
+ }
}
}