Skip to content

Commit 00ba8f2

Browse files
NH-3693 - refactoring alias to bean setter resolver.
1 parent a84ca9d commit 00ba8f2

File tree

3 files changed

+201
-218
lines changed

3 files changed

+201
-218
lines changed

src/NHibernate/NHibernate.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,6 @@
677677
<Compile Include="Transform\ITupleSubsetResultTransformer.cs" />
678678
<Compile Include="Transform\DistinctRootEntityResultTransformer.cs" />
679679
<Compile Include="Transform\IResultTransformer.cs" />
680-
<Compile Include="Transform\QueryAliasToObjectPropertySetter.cs" />
681680
<Compile Include="Transform\RootEntityResultTransformer.cs" />
682681
<Compile Include="TransientObjectException.cs" />
683682
<Compile Include="Type\AbstractType.cs" />

src/NHibernate/Transform/AliasToBeanResultTransformer.cs

Lines changed: 201 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
35
using System.Reflection;
46

57
namespace NHibernate.Transform
@@ -29,37 +31,46 @@ namespace NHibernate.Transform
2931
/// sorts them by inheritance depth then by visibility from public to private, and takes those ranking first.
3032
/// </remarks>
3133
[Serializable]
32-
public class AliasToBeanResultTransformer : AliasedTupleSubsetResultTransformer
34+
public class AliasToBeanResultTransformer : AliasedTupleSubsetResultTransformer, IEquatable<AliasToBeanResultTransformer>
3335
{
34-
private readonly QueryAliasToObjectPropertySetter _propertySetter;
35-
private const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
36-
private readonly System.Type resultClass;
37-
private readonly ConstructorInfo constructor;
36+
private readonly System.Type _resultClass;
37+
private readonly ConstructorInfo _beanConstructor;
38+
private readonly Dictionary<string, NamedMember<FieldInfo>> _fieldsByNameCaseSensitive;
39+
private readonly Dictionary<string, NamedMember<FieldInfo>> _fieldsByNameCaseInsensitive;
40+
private readonly Dictionary<string, NamedMember<PropertyInfo>> _propertiesByNameCaseSensitive;
41+
private readonly Dictionary<string, NamedMember<PropertyInfo>> _propertiesByNameCaseInsensitive;
3842

3943
public AliasToBeanResultTransformer(System.Type resultClass)
4044
{
41-
this.resultClass = resultClass ?? throw new ArgumentNullException("resultClass");
45+
_resultClass = resultClass ?? throw new ArgumentNullException("resultClass");
4246

43-
constructor = resultClass.GetConstructor(flags, null, System.Type.EmptyTypes, null);
47+
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
48+
_beanConstructor = resultClass.GetConstructor(bindingFlags, null, System.Type.EmptyTypes, null);
4449

4550
// if resultClass is a ValueType (struct), GetConstructor will return null...
4651
// in that case, we'll use Activator.CreateInstance instead of the ConstructorInfo to create instances
47-
if (constructor == null && resultClass.IsClass)
52+
if (_beanConstructor == null && resultClass.IsClass)
4853
{
49-
throw new ArgumentException("The target class of a AliasToBeanResultTransformer need a parameter-less constructor",
50-
"resultClass");
54+
throw new ArgumentException(
55+
"The target class of a AliasToBeanResultTransformer need a parameter-less constructor",
56+
nameof(resultClass));
5157
}
5258

53-
_propertySetter = QueryAliasToObjectPropertySetter.MakeFor(resultClass);
54-
}
59+
var fields = new List<RankedMember<FieldInfo>>();
60+
var properties = new List<RankedMember<PropertyInfo>>();
61+
FetchFieldsAndProperties(fields, properties);
5562

63+
_fieldsByNameCaseSensitive = GetMapByName(fields, StringComparer.Ordinal);
64+
_fieldsByNameCaseInsensitive = GetMapByName(fields, StringComparer.OrdinalIgnoreCase);
65+
_propertiesByNameCaseSensitive = GetMapByName(properties, StringComparer.Ordinal);
66+
_propertiesByNameCaseInsensitive = GetMapByName(properties, StringComparer.OrdinalIgnoreCase);
67+
}
5668

5769
public override bool IsTransformedValueATupleElement(String[] aliases, int tupleLength)
5870
{
5971
return false;
6072
}
6173

62-
6374
public override object TransformTuple(object[] tuple, String[] aliases)
6475
{
6576
if (aliases == null)
@@ -70,22 +81,22 @@ public override object TransformTuple(object[] tuple, String[] aliases)
7081

7182
try
7283
{
73-
result = resultClass.IsClass
74-
? constructor.Invoke(null)
75-
: Cfg.Environment.BytecodeProvider.ObjectsFactory.CreateInstance(resultClass, true);
84+
result = _resultClass.IsClass
85+
? _beanConstructor.Invoke(null)
86+
: Cfg.Environment.BytecodeProvider.ObjectsFactory.CreateInstance(_resultClass, true);
7687

7788
for (int i = 0; i < aliases.Length; i++)
7889
{
79-
_propertySetter.SetProperty(aliases[i], tuple[i], result);
90+
SetProperty(aliases[i], tuple[i], result);
8091
}
8192
}
8293
catch (InstantiationException e)
8394
{
84-
throw new HibernateException("Could not instantiate result class: " + resultClass.FullName, e);
95+
throw new HibernateException("Could not instantiate result class: " + _resultClass.FullName, e);
8596
}
8697
catch (MethodAccessException e)
8798
{
88-
throw new HibernateException("Could not instantiate result class: " + resultClass.FullName, e);
99+
throw new HibernateException("Could not instantiate result class: " + _resultClass.FullName, e);
89100
}
90101

91102
return result;
@@ -96,6 +107,173 @@ public override IList TransformList(IList collection)
96107
return collection;
97108
}
98109

110+
#region Setter resolution
111+
112+
/// <summary>
113+
/// Set the value of a property or field matching an alias.
114+
/// </summary>
115+
/// <param name="alias">The alias for which resolving the property or field.</param>
116+
/// <param name="value">The value to which the property or field should be set.</param>
117+
/// <param name="resultObj">The object on which to set the property or field. It must be of the type for which
118+
/// this instance has been built.</param>
119+
/// <exception cref="PropertyNotFoundException">Thrown if no matching property or field can be found.</exception>
120+
/// <exception cref="AmbiguousMatchException">Thrown if many matching properties or fields are found, having the
121+
/// same visibility and inheritance depth.</exception>
122+
private void SetProperty(string alias, object value, object resultObj)
123+
{
124+
if (TrySet(alias, value, resultObj, _propertiesByNameCaseSensitive))
125+
return;
126+
if (TrySet(alias, value, resultObj, _fieldsByNameCaseSensitive))
127+
return;
128+
if (TrySet(alias, value, resultObj, _propertiesByNameCaseInsensitive))
129+
return;
130+
if (TrySet(alias, value, resultObj, _fieldsByNameCaseInsensitive))
131+
return;
132+
133+
throw new PropertyNotFoundException(resultObj.GetType(), alias, "setter");
134+
}
135+
136+
private bool TrySet(string alias, object value, object resultObj, Dictionary<string, NamedMember<FieldInfo>> fieldsMap)
137+
{
138+
if (fieldsMap.TryGetValue(alias, out var field))
139+
{
140+
CheckMember(field, alias);
141+
field.Member.SetValue(resultObj, value);
142+
return true;
143+
}
144+
return false;
145+
}
146+
147+
private bool TrySet(string alias, object value, object resultObj, Dictionary<string, NamedMember<PropertyInfo>> propertiesMap)
148+
{
149+
if (propertiesMap.TryGetValue(alias, out var property))
150+
{
151+
CheckMember(property, alias);
152+
property.Member.SetValue(resultObj, value);
153+
return true;
154+
}
155+
return false;
156+
}
157+
158+
private void CheckMember<T>(NamedMember<T> member, string alias) where T : MemberInfo
159+
{
160+
if (member.Member != null)
161+
return;
162+
163+
if (member.AmbiguousMembers == null || member.AmbiguousMembers.Length < 2)
164+
{
165+
// Should never happen, check NamedMember instanciations.
166+
throw new InvalidOperationException(
167+
$"{nameof(NamedMember<T>.Member)} missing and {nameof(NamedMember<T>.AmbiguousMembers)} invalid.");
168+
}
169+
170+
throw new AmbiguousMatchException(
171+
$"Unable to find adequate property or field to set on '{member.AmbiguousMembers[0].DeclaringType.Name}' for alias '{alias}', " +
172+
$"many {(member.AmbiguousMembers[0] is PropertyInfo ? "properties" : "fields")} matches: " +
173+
$"{string.Join(", ", member.AmbiguousMembers.Select(m => m.Name))}");
174+
}
175+
176+
private void FetchFieldsAndProperties(List<RankedMember<FieldInfo>> fields, List<RankedMember<PropertyInfo>> properties)
177+
{
178+
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
179+
var currentType = _resultClass;
180+
var rank = 1;
181+
// For grasping private members, we need to manually walk the hierarchy.
182+
while (currentType != null && currentType != typeof(object))
183+
{
184+
fields.AddRange(
185+
currentType
186+
.GetFields(bindingFlags)
187+
.Select(f => new RankedMember<FieldInfo> { Member = f, VisibilityRank = GetFieldVisibilityRank(f), HierarchyRank = rank }));
188+
properties.AddRange(
189+
currentType
190+
.GetProperties(bindingFlags)
191+
.Where(p => p.CanWrite && p.GetIndexParameters().Length == 0)
192+
.Select(p => new RankedMember<PropertyInfo> { Member = p, VisibilityRank = GetPropertyVisibilityRank(p), HierarchyRank = rank }));
193+
currentType = currentType.BaseType;
194+
rank++;
195+
}
196+
}
197+
198+
private int GetFieldVisibilityRank(FieldInfo field)
199+
{
200+
if (field.IsPublic)
201+
return 1;
202+
if (field.IsFamilyOrAssembly)
203+
return 2;
204+
if (field.IsFamily)
205+
return 3;
206+
if (field.IsPrivate)
207+
return 4;
208+
return 5;
209+
}
210+
211+
private int GetPropertyVisibilityRank(PropertyInfo property)
212+
{
213+
var setter = property.SetMethod;
214+
if (setter.IsPublic)
215+
return 1;
216+
if (setter.IsFamilyOrAssembly)
217+
return 2;
218+
if (setter.IsFamily)
219+
return 3;
220+
if (setter.IsPrivate)
221+
return 4;
222+
return 5;
223+
}
224+
225+
private Dictionary<string, NamedMember<T>> GetMapByName<T>(IEnumerable<RankedMember<T>> members, StringComparer comparer) where T : MemberInfo
226+
{
227+
return members
228+
.GroupBy(m => m.Member.Name,
229+
(k, g) =>
230+
new NamedMember<T>(k,
231+
g
232+
.GroupBy(m => new { m.HierarchyRank, m.VisibilityRank })
233+
.OrderBy(subg => subg.Key.HierarchyRank).ThenBy(subg => subg.Key.VisibilityRank)
234+
.First()
235+
.Select(m => m.Member)
236+
.ToArray()),
237+
comparer)
238+
.ToDictionary(f => f.Name, comparer);
239+
}
240+
241+
private struct RankedMember<T> where T : MemberInfo
242+
{
243+
public T Member;
244+
public int HierarchyRank;
245+
public int VisibilityRank;
246+
}
247+
248+
[Serializable]
249+
private struct NamedMember<T> where T : MemberInfo
250+
{
251+
public NamedMember(string name, T[] members)
252+
{
253+
if (members == null)
254+
throw new ArgumentNullException(nameof(members));
255+
Name = name;
256+
if (members.Length == 1)
257+
{
258+
Member = members[0];
259+
AmbiguousMembers = null;
260+
}
261+
else
262+
{
263+
Member = null;
264+
AmbiguousMembers = members;
265+
}
266+
}
267+
268+
public string Name;
269+
public T Member;
270+
public T[] AmbiguousMembers;
271+
}
272+
273+
#endregion
274+
275+
#region Equality & hash-code
276+
99277
public override bool Equals(object obj)
100278
{
101279
return Equals(obj as AliasToBeanResultTransformer);
@@ -111,12 +289,14 @@ public bool Equals(AliasToBeanResultTransformer other)
111289
{
112290
return true;
113291
}
114-
return Equals(other.resultClass, resultClass);
292+
return Equals(other._resultClass, _resultClass);
115293
}
116294

117295
public override int GetHashCode()
118296
{
119-
return resultClass.GetHashCode();
297+
return _resultClass.GetHashCode();
120298
}
299+
300+
#endregion
121301
}
122302
}

0 commit comments

Comments
 (0)