Skip to content

Commit

Permalink
generalize to single MatchNamesWithUnderscores (pre-existing)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgravell committed Aug 17, 2023
1 parent 3f8eb8b commit fbdb609
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 30 deletions.
87 changes: 59 additions & 28 deletions Dapper/DefaultTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,10 +127,8 @@ public ConstructorInfo FindExplicitConstructor()
/// <returns>Mapping implementation</returns>
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);
}

/// <summary>
Expand All @@ -133,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 && 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);
Expand All @@ -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";
Expand All @@ -174,22 +172,55 @@ public SqlMapper.IMemberMap GetMember(string columnName)
/// <summary>
/// Should column names like User_Id be allowed to match properties/fields like UserId ?
/// </summary>
[Obsolete("Use MatchFieldsAndPropertiesWithUnderscores for clarity")]
public static bool MatchNamesWithUnderscores
public static bool MatchNamesWithUnderscores { get; set; }

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

/// <summary>
/// Should column names like User_Id be allowed to match properties/fields like UserId ?
/// </summary>
public static bool MatchFieldsAndPropertiesWithUnderscores { get; set; }

/// <summary>
/// Should column names like User_Id be allowed to match constructor arguments like userId ?
/// </summary>
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);

/// <summary>
/// The settable properties for this typemap
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)

(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;
}
}
}
}
3 changes: 1 addition & 2 deletions tests/Dapper.Tests/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1277,8 +1277,7 @@ private class HazGetOnly
[Fact]
public void TestConstructorParametersWithUnderscoredColumns()
{
DefaultTypeMap.MatchConstructorParametersWithUnderscores = true;
DefaultTypeMap.MatchFieldsAndPropertiesWithUnderscores = true;
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);
Expand Down

0 comments on commit fbdb609

Please sign in to comment.