diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index b278250c..12e43152 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 e2092eff..326c30cc 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 4c72894e..af8ae7af 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 4d5dd26c..608607a0 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; + } + } } }