From 0bdc2ca14b86b3f7812b29cc2e40e4460e6feaa3 Mon Sep 17 00:00:00 2001 From: Jonas Goronczy Date: Thu, 16 Jun 2022 15:02:29 +0200 Subject: [PATCH 1/3] Adds MatchConstructorParametersWithUnderscores option --- Dapper/DefaultTypeMap.cs | 27 ++++++++++++++++++++++----- tests/Dapper.Tests/MiscTests.cs | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index b278250c..1c46f525 100644 --- a/Dapper/DefaultTypeMap.cs +++ b/Dapper/DefaultTypeMap.cs @@ -74,7 +74,8 @@ 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)) + var denseName = MatchConstructorParametersWithUnderscores ? names[i].Replace("_", "") : names[i]; + if (!string.Equals(ctorParameters[i].Name, denseName, StringComparison.OrdinalIgnoreCase)) break; if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary) continue; @@ -120,8 +121,9 @@ public ConstructorInfo FindExplicitConstructor() public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) { var parameters = constructor.GetParameters(); + var denseColumnName = MatchConstructorParametersWithUnderscores ? columnName.Replace("_", "") : columnName; - return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase))); + return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, denseColumnName, StringComparison.OrdinalIgnoreCase))); } /// @@ -134,7 +136,7 @@ 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) + if (property == null && MatchFieldsAndPropertiesWithUnderscores) { property = Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.Ordinal)) ?? Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.OrdinalIgnoreCase)); @@ -153,7 +155,7 @@ public SqlMapper.IMemberMap GetMember(string columnName) ?? _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); - if (field == null && MatchNamesWithUnderscores) + if (field == null && MatchFieldsAndPropertiesWithUnderscores) { var effectiveColumnName = columnName.Replace("_", ""); backingFieldName = "<" + effectiveColumnName + ">k__BackingField"; @@ -172,7 +174,22 @@ public SqlMapper.IMemberMap GetMember(string columnName) /// /// Should column names like User_Id be allowed to match properties/fields like UserId ? /// - public static bool MatchNamesWithUnderscores { get; set; } + [Obsolete("Use MatchFieldsAndPropertiesWithUnderscores for clarity")] + public static bool MatchNamesWithUnderscores + { + get { return MatchFieldsAndPropertiesWithUnderscores; } + set { MatchFieldsAndPropertiesWithUnderscores = value; } + } + + /// + /// Should column names like User_Id be allowed to match properties/fields like UserId ? + /// + public static bool MatchFieldsAndPropertiesWithUnderscores { get; set; } + + /// + /// Should column names like User_Id be allowed to match constructor arguments like userId ? + /// + public static bool MatchConstructorParametersWithUnderscores { get; set; } /// /// The settable properties for this typemap diff --git a/tests/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs index 4d5dd26c..9cff3b04 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -1273,5 +1273,27 @@ private class HazGetOnly public int Id { get; } public string Name { get; } = "abc"; } + + [Fact] + public void TestConstructorParametersWithUnderscoredColumns() + { + DefaultTypeMap.MatchConstructorParametersWithUnderscores = true; + DefaultTypeMap.MatchFieldsAndPropertiesWithUnderscores = 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; + } + } } } From fbdb6097df21c25fa57f2f679fb743b16732f776 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 17 Aug 2023 16:27:59 +0100 Subject: [PATCH 2/3] generalize to single MatchNamesWithUnderscores (pre-existing) --- Dapper/DefaultTypeMap.cs | 87 +++++++++++++++++--------- docs/index.md | 2 + tests/Dapper.Tests/ConstructorTests.cs | 40 ++++++++++++ tests/Dapper.Tests/MiscTests.cs | 3 +- 4 files changed, 102 insertions(+), 30 deletions(-) diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index 1c46f525..12e43152 100644 --- a/Dapper/DefaultTypeMap.cs +++ b/Dapper/DefaultTypeMap.cs @@ -74,9 +74,16 @@ public ConstructorInfo FindConstructor(string[] names, Type[] types) int i = 0; for (; i < ctorParameters.Length; i++) { - var denseName = MatchConstructorParametersWithUnderscores ? names[i].Replace("_", "") : names[i]; - if (!string.Equals(ctorParameters[i].Name, denseName, 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; @@ -120,10 +127,8 @@ public ConstructorInfo FindExplicitConstructor() /// Mapping implementation public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) { - var parameters = constructor.GetParameters(); - var denseColumnName = MatchConstructorParametersWithUnderscores ? columnName.Replace("_", "") : columnName; - - return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, denseColumnName, StringComparison.OrdinalIgnoreCase))); + ParameterInfo param = MatchFirstOrDefault(constructor.GetParameters(), columnName, static p => p.Name); + return new SimpleMemberMap(columnName, param); } /// @@ -133,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 && MatchFieldsAndPropertiesWithUnderscores) - { - 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); @@ -155,7 +153,7 @@ public SqlMapper.IMemberMap GetMember(string columnName) ?? _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); - if (field == null && MatchFieldsAndPropertiesWithUnderscores) + if (field == null && MatchNamesWithUnderscores) { var effectiveColumnName = columnName.Replace("_", ""); backingFieldName = "<" + effectiveColumnName + ">k__BackingField"; @@ -174,22 +172,55 @@ public SqlMapper.IMemberMap GetMember(string columnName) /// /// Should column names like User_Id be allowed to match properties/fields like UserId ? /// - [Obsolete("Use MatchFieldsAndPropertiesWithUnderscores for clarity")] - public static bool MatchNamesWithUnderscores + public static bool MatchNamesWithUnderscores { get; set; } + + static T MatchFirstOrDefault(IList members, string name, Func selector) where T : class { - get { return MatchFieldsAndPropertiesWithUnderscores; } - set { MatchFieldsAndPropertiesWithUnderscores = value; } + 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; } - /// - /// Should column names like User_Id be allowed to match properties/fields like UserId ? - /// - public static bool MatchFieldsAndPropertiesWithUnderscores { get; set; } - - /// - /// Should column names like User_Id be allowed to match constructor arguments like userId ? - /// - public static bool MatchConstructorParametersWithUnderscores { get; set; } + 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..1d3cc3ee 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) + (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 9cff3b04..608607a0 100644 --- a/tests/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -1277,8 +1277,7 @@ private class HazGetOnly [Fact] public void TestConstructorParametersWithUnderscoredColumns() { - DefaultTypeMap.MatchConstructorParametersWithUnderscores = true; - DefaultTypeMap.MatchFieldsAndPropertiesWithUnderscores = true; + 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); From e5044bb4dfcf943777feddbe77698e02cd611d38 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Thu, 17 Aug 2023 16:29:38 +0100 Subject: [PATCH 3/3] cite 2nd PR --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1d3cc3ee..326c30cc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ 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) +- 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)