diff --git a/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs b/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs index 9bf9afe7dd4..f7327666edf 100644 --- a/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs +++ b/src/NHibernate.Test/Async/TransformTests/AliasToBeanResultTransformerFixture.cs @@ -8,7 +8,8 @@ //------------------------------------------------------------------------------ -using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using NHibernate.Transform; using NHibernate.Util; @@ -260,6 +261,39 @@ public async Task SerializationAsync() await (AssertSerializationAsync()); } + enum TestEnum + { Value0, Value1 } + + class TestDto + { + private TestDto() + { } + + public TestDto(bool bogus) { } + + public string StringProp { get; set; } + public int IntProp { get; set; } + public int IntPropNull { get; set; } + public int? IntPropNullNullable { get; set; } + public TestEnum EnumProp { get; set; } + } + + struct TestDtoAsStruct + { + public string StringProp { get; set; } + public int IntProp { get; set; } + public int IntPropNull { get; set; } + public int? IntPropNullNullable { get; set; } + public TestEnum EnumProp { get; set; } + } + + class NoDefCtorDto + { + public NoDefCtorDto(bool bogus) + { + } + } + private async Task AssertCardinalityNameAndIdAsync(IResultTransformer transformer = null, CancellationToken cancellationToken = default(CancellationToken)) { using (var s = OpenSession()) @@ -299,7 +333,7 @@ public async Task SerializationAsync() { var transformer = Transformers.AliasToBean(); var bytes = SerializationHelper.Serialize(transformer); - transformer = (IResultTransformer)SerializationHelper.Deserialize(bytes); + transformer = (IResultTransformer) SerializationHelper.Deserialize(bytes); return AssertCardinalityNameAndIdAsync(transformer: transformer, cancellationToken: cancellationToken); } catch (System.Exception ex) diff --git a/src/NHibernate.Test/TransformTests/AliasToBeanResultTransformerFixture.cs b/src/NHibernate.Test/TransformTests/AliasToBeanResultTransformerFixture.cs index c3e7f5b750a..86daf35a79a 100644 --- a/src/NHibernate.Test/TransformTests/AliasToBeanResultTransformerFixture.cs +++ b/src/NHibernate.Test/TransformTests/AliasToBeanResultTransformerFixture.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using NHibernate.Transform; using NHibernate.Util; @@ -248,6 +249,99 @@ public void Serialization() AssertSerialization(); } + enum TestEnum + { Value0, Value1 } + + class TestDto + { + private TestDto() + { } + + public TestDto(bool bogus) { } + + public string StringProp { get; set; } + public int IntProp { get; set; } + public int IntPropNull { get; set; } + public int? IntPropNullNullable { get; set; } + public TestEnum EnumProp { get; set; } + } + + struct TestDtoAsStruct + { + public string StringProp { get; set; } + public int IntProp { get; set; } + public int IntPropNull { get; set; } + public int? IntPropNullNullable { get; set; } + public TestEnum EnumProp { get; set; } + } + + [Test] + public void TupleConversion() + { + var o = new TestDto(true) + { + IntProp = 1, + IntPropNull = 0, + StringProp = "hello", + IntPropNullNullable = null, + EnumProp = TestEnum.Value1, + }; + string nullMarker = "NULL"; + var testData = new Dictionary + { + {nameof(o.IntProp), o.IntProp}, + {nullMarker, decimal.MaxValue}, + {nameof(o.IntPropNull).ToLowerInvariant(), null}, + {string.Empty, new object()}, + {nameof(o.IntPropNullNullable).ToLowerInvariant(), null}, + {nameof(o.EnumProp), 1}, + {nameof(o.StringProp), o.StringProp}, + }; + var aliases = testData.Keys.Select(k => k == nullMarker ? null : k).ToArray(); + + var tuple = testData.Values.ToArray(); + + var actual = (TestDto) Transformers.AliasToBean().TransformTuple(tuple, aliases); + var actualStruct = (TestDtoAsStruct) Transformers.AliasToBean().TransformTuple(tuple, aliases); + Assert.That(actual.IntProp, Is.EqualTo(o.IntProp)); + Assert.That(actual.IntPropNull, Is.EqualTo(o.IntPropNull)); + Assert.That(actual.StringProp, Is.EqualTo(o.StringProp)); + Assert.That(actual.IntPropNullNullable, Is.EqualTo(o.IntPropNullNullable)); + Assert.That(actual.EnumProp, Is.EqualTo(o.EnumProp)); + } + + [Test] + public void ThrowUserFriendlyException() + { + var o = new TestDto(true) { }; + + string nullMarker = "NULL"; + var testData = new Dictionary + { + {nameof(o.IntProp), "hello"}, + }; + var aliases = testData.Keys.Select(k => k == nullMarker ? null : k).ToArray(); + var tuple = testData.Values.ToArray(); + + var ex = Assert.Throws(() => Transformers.AliasToBean().TransformTuple(tuple, aliases)); + Assert.That(ex, Has.Message.Contains(nameof(o.IntProp))); + var ex2 = Assert.Throws(() => Transformers.AliasToBean().TransformTuple(tuple, aliases)); + Assert.That(ex2, Has.Message.Contains(nameof(o.IntProp))); + } + + class NoDefCtorDto + { + public NoDefCtorDto(bool bogus) + { + } + } + + [Test] + public void ThrowsForClassWithoutDefaultCtor() + { + Assert.That(() => Transformers.AliasToBean().TransformTuple(new object[0], new string[0]), Throws.ArgumentException); + } + private void AssertCardinalityNameAndId(IResultTransformer transformer = null) { using (var s = OpenSession()) @@ -285,7 +379,7 @@ private void AssertSerialization() { var transformer = Transformers.AliasToBean(); var bytes = SerializationHelper.Serialize(transformer); - transformer = (IResultTransformer)SerializationHelper.Deserialize(bytes); + transformer = (IResultTransformer) SerializationHelper.Deserialize(bytes); AssertCardinalityNameAndId(transformer: transformer); } } diff --git a/src/NHibernate/Transform/AliasToBeanResultTransformer.cs b/src/NHibernate/Transform/AliasToBeanResultTransformer.cs index 7e2cd704974..b6d93f53c71 100644 --- a/src/NHibernate/Transform/AliasToBeanResultTransformer.cs +++ b/src/NHibernate/Transform/AliasToBeanResultTransformer.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Runtime.Serialization; using NHibernate.Util; @@ -12,6 +13,7 @@ namespace NHibernate.Transform /// Result transformer that allows to transform a result to /// a user specified class which will be populated via setter /// methods or fields matching the alias names. + /// NOTE: This transformer can't be reused by different queries as it caches query aliases on first transformation /// /// /// @@ -36,31 +38,23 @@ namespace NHibernate.Transform public class AliasToBeanResultTransformer : AliasedTupleSubsetResultTransformer, IEquatable { private readonly System.Type _resultClass; - private readonly ConstructorInfo _beanConstructor; private readonly Dictionary> _fieldsByNameCaseSensitive; private readonly Dictionary> _fieldsByNameCaseInsensitive; private readonly Dictionary> _propertiesByNameCaseSensitive; private readonly Dictionary> _propertiesByNameCaseInsensitive; + [NonSerialized] + Func _transformer; + + public System.Type ResultClass => _resultClass; + public AliasToBeanResultTransformer(System.Type resultClass) { _resultClass = resultClass ?? throw new ArgumentNullException("resultClass"); - const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - _beanConstructor = resultClass.GetConstructor(bindingFlags, null, System.Type.EmptyTypes, null); - - // if resultClass is a ValueType (struct), GetConstructor will return null... - // in that case, we'll use Activator.CreateInstance instead of the ConstructorInfo to create instances - if (_beanConstructor == null && resultClass.IsClass) - { - throw new ArgumentException( - "The target class of a AliasToBeanResultTransformer need a parameter-less constructor", - nameof(resultClass)); - } - var fields = new List>(); var properties = new List>(); - FetchFieldsAndProperties(fields, properties); + FetchFieldsAndProperties(resultClass, fields, properties); _fieldsByNameCaseSensitive = GetMapByName(fields, StringComparer.Ordinal); _fieldsByNameCaseInsensitive = GetMapByName(fields, StringComparer.OrdinalIgnoreCase); @@ -73,35 +67,10 @@ public override bool IsTransformedValueATupleElement(String[] aliases, int tuple return false; } - public override object TransformTuple(object[] tuple, String[] aliases) + public override object TransformTuple(object[] tuple, string[] aliases) { - if (aliases == null) - { - throw new ArgumentNullException("aliases"); - } - object result; - - try - { - result = _resultClass.IsClass - ? _beanConstructor.Invoke(null) - : Activator.CreateInstance(_resultClass, true); - - for (int i = 0; i < aliases.Length; i++) - { - SetProperty(aliases[i], tuple[i], result); - } - } - catch (InstantiationException e) - { - throw new HibernateException("Could not instantiate result class: " + _resultClass.FullName, e); - } - catch (MethodAccessException e) - { - throw new HibernateException("Could not instantiate result class: " + _resultClass.FullName, e); - } - - return result; + var transformer = _transformer ?? (_transformer = CreateTransformerDelegate(aliases)); + return transformer(tuple); } public override IList TransformList(IList collection) @@ -116,53 +85,31 @@ protected virtual void OnPropertyNotFound(string propertyName) #region Setter resolution - /// - /// Set the value of a property or field matching an alias. - /// - /// The alias for which resolving the property or field. - /// The value to which the property or field should be set. - /// The object on which to set the property or field. It must be of the type for which - /// this instance has been built. - /// Thrown if no matching property or field can be found. - /// Thrown if many matching properties or fields are found, having the - /// same visibility and inheritance depth. - private void SetProperty(string alias, object value, object resultObj) + protected MemberInfo GetMemberInfo(string alias) { - if (alias == null) - // Grouping properties in criteria are selected without alias, just ignore them. - return; - - if (TrySet(alias, value, resultObj, _propertiesByNameCaseSensitive)) - return; - if (TrySet(alias, value, resultObj, _fieldsByNameCaseSensitive)) - return; - if (TrySet(alias, value, resultObj, _propertiesByNameCaseInsensitive)) - return; - if (TrySet(alias, value, resultObj, _fieldsByNameCaseInsensitive)) - return; - + if (TryGetMemberInfo(alias, _propertiesByNameCaseSensitive, out var property)) + return property; + if (TryGetMemberInfo(alias, _fieldsByNameCaseSensitive, out var field)) + return field; + if (TryGetMemberInfo(alias, _propertiesByNameCaseInsensitive, out property)) + return property; + if (TryGetMemberInfo(alias, _fieldsByNameCaseInsensitive, out field)) + return field; OnPropertyNotFound(alias); + return null; } - private bool TrySet(string alias, object value, object resultObj, Dictionary> fieldsMap) - { - if (fieldsMap.TryGetValue(alias, out var field)) - { - CheckMember(field, alias); - field.Member.SetValue(resultObj, value); - return true; - } - return false; - } - - private bool TrySet(string alias, object value, object resultObj, Dictionary> propertiesMap) + private bool TryGetMemberInfo(string alias, Dictionary> propertiesMap, out TMemberInfo member) + where TMemberInfo : MemberInfo { if (propertiesMap.TryGetValue(alias, out var property)) { CheckMember(property, alias); - property.Member.SetValue(resultObj, value); + member = property.Member; return true; } + + member = null; return false; } @@ -173,7 +120,7 @@ private void CheckMember(NamedMember member, string alias) where T : Membe if (member.AmbiguousMembers == null || member.AmbiguousMembers.Length < 2) { - // Should never happen, check NamedMember instanciations. + // Should never happen, check NamedMember instantiations. throw new InvalidOperationException( $"{nameof(NamedMember.Member)} missing and {nameof(NamedMember.AmbiguousMembers)} invalid."); } @@ -184,10 +131,93 @@ private void CheckMember(NamedMember member, string alias) where T : Membe $"{string.Join(", ", member.AmbiguousMembers.Select(m => m.Name))}"); } - private void FetchFieldsAndProperties(List> fields, List> properties) + protected Func CreateTransformerDelegate(string[] aliases) + { + if (aliases == null) + { + throw new ArgumentNullException("aliases"); + } + + Expression> getException = (name, obj) => new InvalidCastException($"Failed to init member '{name}' with value of type '{obj.GetType()}'"); + bool wrapInVariables = ShouldWrapInVariables(); + var bindings = new List(aliases.Length); + var tupleParam = Expression.Parameter(typeof(object[]), "tuple"); + var variableAndAssigmentDic = wrapInVariables + ? new Dictionary(aliases.Length) + : null; + for (int i = 0; i < aliases.Length; i++) + { + string alias = aliases[i]; + if (string.IsNullOrEmpty(alias)) + continue; + + var memberInfo = GetMemberInfo(alias); + var valueExpr = Expression.ArrayAccess(tupleParam, Expression.Constant(i)); + var valueToAssign = GetTyped(memberInfo, valueExpr, getException); + if (wrapInVariables) + { + var variable = Expression.Variable(valueToAssign.Type); + variableAndAssigmentDic[variable] = Expression.Assign(variable, valueToAssign); + valueToAssign = variable; + } + bindings.Add(Expression.Bind(memberInfo, valueToAssign)); + } + Expression initExpr = Expression.MemberInit(Expression.New(_resultClass), bindings); + if (!ResultClass.IsClass) + initExpr = Expression.Convert(initExpr, typeof(object)); + + if (wrapInVariables) + { + initExpr = Expression.Block(variableAndAssigmentDic.Keys, variableAndAssigmentDic.Values.Concat(new[] { initExpr })); + } + return (Func) Expression.Lambda(initExpr, tupleParam).Compile(); + } + + private bool ShouldWrapInVariables() + { +#if NETFX + //On .net461 throws if DTO is struct: TryExpression is not supported as a child expression when accessing a member on type '[TYPE]' because it is a value type. + if (!_resultClass.IsClass) + return true; +#endif + return false; + } + + private static Expression GetTyped(MemberInfo memberInfo, Expression expr, Expression> getEx) + { + var type = GetMemberType(memberInfo); + if (type == typeof(object)) + return expr; + + var originalValue = expr; + if (!type.IsClass) + { + expr = Expression.Coalesce(expr, Expression.Default(type)); + } + + return Expression.TryCatch( + Expression.Convert(expr, type), + Expression.Catch( + typeof(InvalidCastException), + Expression.Throw(Expression.Invoke(getEx, Expression.Constant(memberInfo.ToString()), originalValue), type) + )); + } + + private static System.Type GetMemberType(MemberInfo memberInfo) + { + if (memberInfo is PropertyInfo prop) + return prop.PropertyType; + + if (memberInfo is FieldInfo field) + return field.FieldType; + + throw new NotSupportedException($"Member type {memberInfo} is not supported"); + } + + private static void FetchFieldsAndProperties(System.Type resultClass, List> fields, List> properties) { const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; - var currentType = _resultClass; + var currentType = resultClass; var rank = 1; // For grasping private members, we need to manually walk the hierarchy. while (currentType != null && currentType != typeof(object)) @@ -206,7 +236,7 @@ private void FetchFieldsAndProperties(List> fields, List } } - private int GetFieldVisibilityRank(FieldInfo field) + private static int GetFieldVisibilityRank(FieldInfo field) { if (field.IsPublic) return 1; @@ -219,7 +249,7 @@ private int GetFieldVisibilityRank(FieldInfo field) return 5; } - private int GetPropertyVisibilityRank(PropertyInfo property) + private static int GetPropertyVisibilityRank(PropertyInfo property) { var setter = property.SetMethod; if (setter.IsPublic) @@ -233,7 +263,7 @@ private int GetPropertyVisibilityRank(PropertyInfo property) return 5; } - private Dictionary> GetMapByName(IEnumerable> members, StringComparer comparer) where T : MemberInfo + private static Dictionary> GetMapByName(IEnumerable> members, StringComparer comparer) where T : MemberInfo { return members .GroupBy(m => m.Member.Name, @@ -315,6 +345,8 @@ public bool Equals(AliasToBeanResultTransformer other) { return true; } + if (GetType() != other.GetType()) + return false; return Equals(other._resultClass, _resultClass); } diff --git a/src/NHibernate/Transform/Transformers.cs b/src/NHibernate/Transform/Transformers.cs index e139c709e8f..ec62e5cc9d8 100644 --- a/src/NHibernate/Transform/Transformers.cs +++ b/src/NHibernate/Transform/Transformers.cs @@ -15,6 +15,7 @@ public static class Transformers /// /// Creates a result transformer that will inject aliased values into instances /// of via property methods or fields. + /// NOTE: This transformer can't be reused by different queries as it caches query aliases on first transformation /// /// The type of the instances to build. /// A result transformer for supplied type. @@ -31,6 +32,7 @@ public static IResultTransformer AliasToBean(System.Type target) /// /// Creates a result transformer that will inject aliased values into instances /// of via property methods or fields. + /// NOTE: This transformer can't be reused by different queries as it caches query aliases on first transformation /// /// The type of the instances to build. /// A result transformer for supplied type.