Skip to content

Commit

Permalink
Underscore handling (#1947)
Browse files Browse the repository at this point in the history
* Adds MatchConstructorParametersWithUnderscores option

* generalize to single MatchNamesWithUnderscores (pre-existing)

* cite 2nd PR

---------

Co-authored-by: Jonas Goronczy <[email protected]>
  • Loading branch information
mgravell and jo-goro authored Aug 17, 2023
1 parent 19355d5 commit 33090c0
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 12 deletions.
72 changes: 60 additions & 12 deletions Dapper/DefaultTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,9 +127,8 @@ public ConstructorInfo FindExplicitConstructor()
/// <returns>Mapping implementation</returns>
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);
}

/// <summary>
Expand All @@ -131,14 +138,7 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor,
/// <returns>Mapping implementation</returns>
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);
Expand Down Expand Up @@ -174,6 +174,54 @@ public SqlMapper.IMemberMap GetMember(string columnName)
/// </summary>
public static bool MatchNamesWithUnderscores { get; set; }

static T MatchFirstOrDefault<T>(IList<T> members, string name, Func<T, string> 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);

/// <summary>
/// The settable properties for this typemap
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions tests/Dapper.Tests/ConstructorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,45 @@ public void TestWithNonPublicConstructor()
var output = connection.Query<WithPrivateConstructor>("select 1 as Foo").First();
Assert.Equal(1, output.Foo);
}

[Fact]
public void CtorWithUnderscores()
{
var obj = connection.QueryFirst<Type_ParamsWithUnderscores>("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<Type_ParamsWithoutUnderscores>("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;
}
}
}
}
21 changes: 21 additions & 0 deletions tests/Dapper.Tests/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HazGetOnlyAndCtor>("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;
}
}
}
}

0 comments on commit 33090c0

Please sign in to comment.