Skip to content

Commit e53c78f

Browse files
NH-3693 - case sensitive resolver with fallback to case insensitive.
1 parent e77130f commit e53c78f

File tree

2 files changed

+211
-30
lines changed

2 files changed

+211
-30
lines changed

src/NHibernate.Test/TransformTests/AliasToBeanResultTransformerFixture.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections;
22
using System.Collections.Generic;
3+
using System.Reflection;
34
using NHibernate.Transform;
45
using NUnit.Framework;
56

@@ -11,7 +12,7 @@ public class AliasToBeanResultTransformerFixture : TestCase
1112
public class WithOutPublicParameterLessCtor
1213
{
1314
private string something;
14-
protected WithOutPublicParameterLessCtor() {}
15+
protected WithOutPublicParameterLessCtor() { }
1516

1617
public WithOutPublicParameterLessCtor(string something)
1718
{
@@ -77,11 +78,18 @@ public class PrivateInheritedFieldsSimpleDTO : BasePrivateFieldSimpleDTO
7778
public string Name { get { return name; } }
7879
}
7980

81+
public class PropertiesInsensitivelyDuplicated
82+
{
83+
public object Id { get; set; }
84+
public string NaMe { get; set; }
85+
public string NamE { get; set; }
86+
}
87+
8088
#region Overrides of TestCase
8189

8290
protected override IList Mappings
8391
{
84-
get { return new[] {"TransformTests.Simple.hbm.xml"}; }
92+
get { return new[] { "TransformTests.Simple.hbm.xml" }; }
8593
}
8694

8795
protected override string MappingsAssembly
@@ -109,7 +117,7 @@ public void WorkWithOutPublicParameterLessCtor()
109117
}
110118
finally
111119
{
112-
Cleanup();
120+
Cleanup();
113121
}
114122
}
115123

@@ -133,7 +141,7 @@ public void ToPublicProperties_WithoutAnyProjections()
133141
finally
134142
{
135143
Cleanup();
136-
}
144+
}
137145
}
138146

139147
[Test]
@@ -247,6 +255,30 @@ public void WorksWithStruct()
247255
}
248256
}
249257

258+
[Test]
259+
public void ToPropertiesInsensitivelyDuplicated_WithoutAnyProjections()
260+
{
261+
try
262+
{
263+
Setup();
264+
265+
using (ISession s = OpenSession())
266+
{
267+
var transformer = Transformers.AliasToBean<PropertiesInsensitivelyDuplicated>();
268+
Assert.Throws<AmbiguousMatchException>(() =>
269+
{
270+
s.CreateSQLQuery("select * from Simple")
271+
.SetResultTransformer(transformer)
272+
.List<PropertiesInsensitivelyDuplicated>();
273+
});
274+
}
275+
}
276+
finally
277+
{
278+
Cleanup();
279+
}
280+
}
281+
250282
private void AssertAreWorking(string queryString)
251283
{
252284
using (ISession s = OpenSession())
@@ -277,8 +309,8 @@ private void Setup()
277309
{
278310
using (s.BeginTransaction())
279311
{
280-
s.Save(new Simple {Name = "Name1"});
281-
s.Save(new Simple {Name = "Name2"});
312+
s.Save(new Simple { Name = "Name1" });
313+
s.Save(new Simple { Name = "Name2" });
282314
s.Transaction.Commit();
283315
}
284316
}

src/NHibernate/Transform/QueryAliasToObjectPropertySetter.cs

Lines changed: 173 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,193 @@
55

66
namespace NHibernate.Transform
77
{
8+
/// <summary>
9+
/// Resolves setter for an alias with a heuristic: search among properties then fields for matching name and case, then,
10+
/// if no matching property or field was found, retry with a case insensitive match. For members having the same name, it
11+
/// sorts them by inheritance depth then by visibility from public to private, and takes those ranking first.
12+
/// </summary>
813
[Serializable]
914
public class QueryAliasToObjectPropertySetter
1015
{
11-
private readonly IEnumerable<FieldInfo> _fields;
12-
private readonly IEnumerable<PropertyInfo> _properties;
16+
private readonly IDictionary<string, NamedMember<FieldInfo>> _fieldsByNameCaseSensitive;
17+
private readonly IDictionary<string, NamedMember<FieldInfo>> _fieldsByNameCaseInsensitive;
18+
private readonly IDictionary<string, NamedMember<PropertyInfo>> _propertiesByNameCaseSensitive;
19+
private readonly IDictionary<string, NamedMember<PropertyInfo>> _propertiesByNameCaseInsensitive;
1320

14-
private QueryAliasToObjectPropertySetter(FieldInfo[] fields, PropertyInfo[] properties)
21+
private QueryAliasToObjectPropertySetter(IDictionary<string, NamedMember<FieldInfo>> fieldsByNameCaseSensitive,
22+
IDictionary<string, NamedMember<FieldInfo>> fieldsByNameCaseInsensitive,
23+
IDictionary<string, NamedMember<PropertyInfo>> propertiesByNameCaseSensitive,
24+
IDictionary<string, NamedMember<PropertyInfo>> propertiesByNameCaseInsensitive)
1525
{
16-
_fields = fields;
17-
_properties = properties;
26+
_fieldsByNameCaseSensitive = fieldsByNameCaseSensitive;
27+
_fieldsByNameCaseInsensitive = fieldsByNameCaseInsensitive;
28+
_propertiesByNameCaseSensitive = propertiesByNameCaseSensitive;
29+
_propertiesByNameCaseInsensitive = propertiesByNameCaseInsensitive;
1830
}
1931

32+
/// <summary>
33+
/// Set the value of a property or field matching an alias.
34+
/// </summary>
35+
/// <param name="alias">The alias for which resolving the property or field.</param>
36+
/// <param name="value">The value to which the property or field should be set.</param>
37+
/// <param name="resultObj">The object on which to set the property or field. It must be of the type for which
38+
/// this instance has been built.</param>
39+
/// <exception cref="PropertyNotFoundException">Thrown if no matching property or field can be found.</exception>
40+
/// <exception cref="AmbiguousMatchException">Thrown if many matching properties or fields are found, having the
41+
/// same visibility and inheritance depth.</exception>
42+
public void SetProperty(string alias, object value, object resultObj)
43+
{
44+
if (_propertiesByNameCaseSensitive.TryGetValue(alias, out var property))
45+
{
46+
checkMember(property);
47+
property.Member.SetValue(resultObj, value, new object[0]);
48+
return;
49+
}
50+
if (_fieldsByNameCaseSensitive.TryGetValue(alias, out var field))
51+
{
52+
checkMember(field);
53+
field.Member.SetValue(resultObj, value);
54+
return;
55+
}
56+
if (_propertiesByNameCaseInsensitive.TryGetValue(alias, out property))
57+
{
58+
checkMember(property);
59+
property.Member.SetValue(resultObj, value, new object[0]);
60+
return;
61+
}
62+
if (_fieldsByNameCaseInsensitive.TryGetValue(alias, out field))
63+
{
64+
checkMember(field);
65+
field.Member.SetValue(resultObj, value);
66+
return;
67+
}
68+
69+
throw new PropertyNotFoundException(resultObj.GetType(), alias, "setter");
70+
71+
void checkMember<T>(NamedMember<T> member) where T : MemberInfo
72+
{
73+
if (member.Member != null)
74+
return;
75+
76+
if (member.AmbiguousMembers == null || member.AmbiguousMembers.Length < 2)
77+
throw new InvalidOperationException($"{nameof(NamedMember<T>.Member)} missing and {nameof(NamedMember<T>.AmbiguousMembers)} invalid.");
78+
79+
throw new AmbiguousMatchException(
80+
$"Unable to find adequate property or field to set on '{member.AmbiguousMembers[0].DeclaringType.Name}' for alias '{alias}', " +
81+
$"many {(member.AmbiguousMembers[0] is PropertyInfo ? "properties" : "fields")} matches: {string.Join(", ", member.AmbiguousMembers.Select(m => m.Name))}");
82+
}
83+
}
84+
85+
/// <summary>
86+
/// Build a <c>QueryAliasToObjectPropertySetter</c> for a supplied type.
87+
/// </summary>
88+
/// <param name="objType">The type.</param>
89+
/// <returns>A <c>QueryAliasToObjectPropertySetter</c>.</returns>
2090
public static QueryAliasToObjectPropertySetter MakeFor(System.Type objType)
2191
{
22-
var bindingFlags = BindingFlags.Instance |
23-
BindingFlags.Public |
24-
BindingFlags.NonPublic |
25-
BindingFlags.IgnoreCase;
26-
var fields = objType.GetFields(bindingFlags);
27-
var properties = objType.GetProperties(bindingFlags);
28-
29-
return new QueryAliasToObjectPropertySetter(fields, properties);
92+
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
93+
var fields = new List<RankedMember<FieldInfo>>();
94+
var properties = new List<RankedMember<PropertyInfo>>();
95+
var currentType = objType;
96+
var rank = 1;
97+
// For grasping private members, we need to manually walk the hierarchy.
98+
while (currentType != null && currentType != typeof(object))
99+
{
100+
fields.AddRange(
101+
currentType
102+
.GetFields(bindingFlags)
103+
.Select(f => new RankedMember<FieldInfo> { Member = f, VisibilityRank = getFieldVisibilityRank(f), HierarchyRank = rank }));
104+
properties.AddRange(
105+
currentType
106+
.GetProperties(bindingFlags)
107+
.Where(p => p.CanWrite)
108+
.Select(p => new RankedMember<PropertyInfo> { Member = p, VisibilityRank = getPropertyVisibilityRank(p), HierarchyRank = rank }));
109+
currentType = currentType.BaseType;
110+
rank++;
111+
}
112+
113+
var fieldsByNameCaseSensitive = getMapByName(fields, StringComparer.Ordinal);
114+
var fieldsByNameCaseInsensitive = getMapByName(fields, StringComparer.OrdinalIgnoreCase);
115+
var propertiesByNameCaseSensitive = getMapByName(properties, StringComparer.Ordinal);
116+
var propertiesByNameCaseInsensitive = getMapByName(properties, StringComparer.OrdinalIgnoreCase);
117+
118+
return new QueryAliasToObjectPropertySetter(fieldsByNameCaseSensitive, fieldsByNameCaseInsensitive, propertiesByNameCaseSensitive, propertiesByNameCaseInsensitive);
119+
120+
int getFieldVisibilityRank(MemberInfo member)
121+
{
122+
var field = (FieldInfo)member;
123+
if (field.IsPublic)
124+
return 1;
125+
if (field.IsFamilyOrAssembly)
126+
return 2;
127+
if (field.IsFamily)
128+
return 3;
129+
if (field.IsPrivate)
130+
return 4;
131+
return 5;
132+
}
133+
134+
int getPropertyVisibilityRank(MemberInfo member)
135+
{
136+
var setter = ((PropertyInfo)member).SetMethod;
137+
if (setter.IsPublic)
138+
return 1;
139+
if (setter.IsFamilyOrAssembly)
140+
return 2;
141+
if (setter.IsFamily)
142+
return 3;
143+
if (setter.IsPrivate)
144+
return 4;
145+
return 5;
146+
}
147+
148+
Dictionary<string, NamedMember<T>> getMapByName<T>(IEnumerable<RankedMember<T>> members, StringComparer comparer) where T : MemberInfo
149+
{
150+
return members
151+
.GroupBy(m => m.Member.Name,
152+
(k, g) =>
153+
new NamedMember<T>(k,
154+
g
155+
.GroupBy(m => new { m.HierarchyRank, m.VisibilityRank })
156+
.OrderBy(subg => subg.Key.HierarchyRank).ThenBy(subg => subg.Key.VisibilityRank)
157+
.First()
158+
.Select(m => m.Member)
159+
.ToArray()),
160+
comparer)
161+
.ToDictionary(f => f.Name, comparer);
162+
}
30163
}
31164

32-
public void SetProperty(string alias, object value, object resultObj)
165+
private struct RankedMember<T> where T : MemberInfo
33166
{
34-
var property = _properties.SingleOrDefault(prop => string.Equals(prop.Name, alias, StringComparison.OrdinalIgnoreCase));
35-
var field = _fields.SingleOrDefault(prop => string.Equals(prop.Name, alias, StringComparison.OrdinalIgnoreCase));
36-
if (field == null && property == null)
37-
throw new PropertyNotFoundException(resultObj.GetType(), alias, "setter");
167+
public T Member;
168+
public int HierarchyRank;
169+
public int VisibilityRank;
170+
}
38171

39-
if (field != null)
172+
[Serializable]
173+
private struct NamedMember<T> where T : MemberInfo
174+
{
175+
public NamedMember(string name, T[] members)
40176
{
41-
field.SetValue(resultObj, value);
42-
return;
177+
if (members == null)
178+
throw new ArgumentNullException(nameof(members));
179+
Name = name;
180+
if (members.Length == 1)
181+
{
182+
Member = members[0];
183+
AmbiguousMembers = null;
184+
}
185+
else
186+
{
187+
Member = null;
188+
AmbiguousMembers = members;
189+
}
43190
}
44-
if (property != null && property.CanWrite)
45-
property.SetValue(resultObj, value, new object[0]);
191+
192+
public string Name;
193+
public T Member;
194+
public T[] AmbiguousMembers;
46195
}
47196
}
48-
}
197+
}

0 commit comments

Comments
 (0)