Skip to content

Commit 33090c0

Browse files
mgravelljo-goro
andauthored
Underscore handling (#1947)
* Adds MatchConstructorParametersWithUnderscores option * generalize to single MatchNamesWithUnderscores (pre-existing) * cite 2nd PR --------- Co-authored-by: Jonas Goronczy <goronczy.jonas@gmail.com>
1 parent 19355d5 commit 33090c0

File tree

4 files changed

+123
-12
lines changed

4 files changed

+123
-12
lines changed

Dapper/DefaultTypeMap.cs

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,16 @@ public ConstructorInfo FindConstructor(string[] names, Type[] types)
7474
int i = 0;
7575
for (; i < ctorParameters.Length; i++)
7676
{
77-
if (!string.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase))
77+
if (EqualsCI(ctorParameters[i].Name, names[i]))
78+
{ } // exact match
79+
else if (MatchNamesWithUnderscores && EqualsCIU(ctorParameters[i].Name, names[i]))
80+
{ } // match after applying underscores
81+
else
82+
{
83+
// not a name match
7884
break;
85+
}
86+
7987
if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary)
8088
continue;
8189
var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType;
@@ -119,9 +127,8 @@ public ConstructorInfo FindExplicitConstructor()
119127
/// <returns>Mapping implementation</returns>
120128
public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName)
121129
{
122-
var parameters = constructor.GetParameters();
123-
124-
return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)));
130+
ParameterInfo param = MatchFirstOrDefault(constructor.GetParameters(), columnName, static p => p.Name);
131+
return new SimpleMemberMap(columnName, param);
125132
}
126133

127134
/// <summary>
@@ -131,14 +138,7 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor,
131138
/// <returns>Mapping implementation</returns>
132139
public SqlMapper.IMemberMap GetMember(string columnName)
133140
{
134-
var property = Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal))
135-
?? Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase));
136-
137-
if (property == null && MatchNamesWithUnderscores)
138-
{
139-
property = Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.Ordinal))
140-
?? Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.OrdinalIgnoreCase));
141-
}
141+
var property = MatchFirstOrDefault(Properties, columnName, static p => p.Name);
142142

143143
if (property != null)
144144
return new SimpleMemberMap(columnName, property);
@@ -174,6 +174,54 @@ public SqlMapper.IMemberMap GetMember(string columnName)
174174
/// </summary>
175175
public static bool MatchNamesWithUnderscores { get; set; }
176176

177+
static T MatchFirstOrDefault<T>(IList<T> members, string name, Func<T, string> selector) where T : class
178+
{
179+
if (members is { Count: > 0 })
180+
{
181+
// try exact first
182+
foreach (var member in members)
183+
{
184+
if (string.Equals(name, selector(member), StringComparison.Ordinal))
185+
{
186+
return member;
187+
}
188+
}
189+
// then exact ignoring case
190+
foreach (var member in members)
191+
{
192+
if (string.Equals(name, selector(member), StringComparison.OrdinalIgnoreCase))
193+
{
194+
return member;
195+
}
196+
}
197+
if (MatchNamesWithUnderscores)
198+
{
199+
// same again, minus underscore delta
200+
name = name?.Replace("_", "");
201+
foreach (var member in members)
202+
{
203+
if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.Ordinal))
204+
{
205+
return member;
206+
}
207+
}
208+
foreach (var member in members)
209+
{
210+
if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.OrdinalIgnoreCase))
211+
{
212+
return member;
213+
}
214+
}
215+
}
216+
}
217+
return null;
218+
}
219+
220+
internal static bool EqualsCI(string x, string y)
221+
=> string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
222+
internal static bool EqualsCIU(string x, string y)
223+
=> string.Equals(x?.Replace("_", ""), y?.Replace("_", ""), StringComparison.OrdinalIgnoreCase);
224+
177225
/// <summary>
178226
/// The settable properties for this typemap
179227
/// </summary>

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command
2222

2323
### unreleased
2424

25+
- add underscore handling with constructors (#1786 via @jo-goro, fixes #818; also #1947 via mgravell)
26+
2527
(note: new PRs will not be merged until they add release note wording here)
2628

2729
### 2.0.143

tests/Dapper.Tests/ConstructorTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,5 +220,45 @@ public void TestWithNonPublicConstructor()
220220
var output = connection.Query<WithPrivateConstructor>("select 1 as Foo").First();
221221
Assert.Equal(1, output.Foo);
222222
}
223+
224+
[Fact]
225+
public void CtorWithUnderscores()
226+
{
227+
var obj = connection.QueryFirst<Type_ParamsWithUnderscores>("select 'abc' as FIRST_NAME, 'def' as LAST_NAME");
228+
Assert.NotNull(obj);
229+
Assert.Equal("abc", obj.FirstName);
230+
Assert.Equal("def", obj.LastName);
231+
}
232+
233+
[Fact]
234+
public void CtorWithoutUnderscores()
235+
{
236+
DefaultTypeMap.MatchNamesWithUnderscores = true;
237+
var obj = connection.QueryFirst<Type_ParamsWithoutUnderscores>("select 'abc' as FIRST_NAME, 'def' as LAST_NAME");
238+
Assert.NotNull(obj);
239+
Assert.Equal("abc", obj.FirstName);
240+
Assert.Equal("def", obj.LastName);
241+
}
242+
243+
class Type_ParamsWithUnderscores
244+
{
245+
public string FirstName { get; }
246+
public string LastName { get; }
247+
public Type_ParamsWithUnderscores(string first_name, string last_name)
248+
{
249+
FirstName = first_name;
250+
LastName = last_name;
251+
}
252+
}
253+
class Type_ParamsWithoutUnderscores
254+
{
255+
public string FirstName { get; }
256+
public string LastName { get; }
257+
public Type_ParamsWithoutUnderscores(string firstName, string lastName)
258+
{
259+
FirstName = firstName;
260+
LastName = lastName;
261+
}
262+
}
223263
}
224264
}

tests/Dapper.Tests/MiscTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,5 +1273,26 @@ private class HazGetOnly
12731273
public int Id { get; }
12741274
public string Name { get; } = "abc";
12751275
}
1276+
1277+
[Fact]
1278+
public void TestConstructorParametersWithUnderscoredColumns()
1279+
{
1280+
DefaultTypeMap.MatchNamesWithUnderscores = true;
1281+
var obj = connection.QuerySingle<HazGetOnlyAndCtor>("select 42 as [id_property], 'def' as [name_property];");
1282+
Assert.Equal(42, obj.IdProperty);
1283+
Assert.Equal("def", obj.NameProperty);
1284+
}
1285+
1286+
private class HazGetOnlyAndCtor
1287+
{
1288+
public int IdProperty { get; }
1289+
public string NameProperty { get; }
1290+
1291+
public HazGetOnlyAndCtor(int idProperty, string nameProperty)
1292+
{
1293+
IdProperty = idProperty;
1294+
NameProperty = nameProperty;
1295+
}
1296+
}
12761297
}
12771298
}

0 commit comments

Comments
 (0)