diff --git a/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs b/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs index 03c76d428af..21fac97d4ae 100644 --- a/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs +++ b/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs @@ -12,7 +12,9 @@ public class Animal public virtual Animal Father { get; set; } public virtual IList Children { get; set; } public virtual string SerialNumber { get; set; } - } + + public virtual Animal FatherOrMother => Father ?? Mother; + } public abstract class Reptile : Animal { @@ -30,4 +32,4 @@ public abstract class Mammal : Animal public class Dog : Mammal { } public class Cat : Mammal { } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/Async/Linq/SelectionTests.cs b/src/NHibernate.Test/Async/Linq/SelectionTests.cs index cf065e6bf5d..2404abae275 100644 --- a/src/NHibernate.Test/Async/Linq/SelectionTests.cs +++ b/src/NHibernate.Test/Async/Linq/SelectionTests.cs @@ -289,6 +289,14 @@ public async Task CanSelectFirstElementFromChildCollectionAsync() } } + [Test] + public async Task CanSelectNotMappedEntityPropertyAsync() + { + var list = await (db.Animals.Where(o => o.Mother != null).Select(o => o.FatherOrMother.SerialNumber).ToListAsync()); + + Assert.That(list, Has.Count.GreaterThan(0)); + } + [Test] public async Task CanProjectWithCastAsync() { diff --git a/src/NHibernate.Test/Linq/SelectionTests.cs b/src/NHibernate.Test/Linq/SelectionTests.cs index 7aac7edc2da..e68c87c5cac 100644 --- a/src/NHibernate.Test/Linq/SelectionTests.cs +++ b/src/NHibernate.Test/Linq/SelectionTests.cs @@ -328,6 +328,14 @@ public void CanSelectWrappedType() Assert.IsTrue(query.ToArray().Length > 0); } + [Test] + public void CanSelectNotMappedEntityProperty() + { + var list = db.Animals.Where(o => o.Mother != null).Select(o => o.FatherOrMother.SerialNumber).ToList(); + + Assert.That(list, Has.Count.GreaterThan(0)); + } + [Test] public void CanProjectWithCast() { diff --git a/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs b/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs index d022e1ffc88..dc8f1952516 100644 --- a/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs +++ b/src/NHibernate/Linq/ReWriters/AddJoinsReWriter.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Specialized; using System.Linq; +using System.Linq.Expressions; using NHibernate.Engine; using NHibernate.Linq.Clauses; using NHibernate.Linq.Visitors; +using NHibernate.Util; using Remotion.Linq; using Remotion.Linq.Clauses; @@ -11,8 +13,7 @@ namespace NHibernate.Linq.ReWriters { internal interface IIsEntityDecider { - bool IsEntity(System.Type type); - bool IsIdentifier(System.Type type, string propertyName); + bool IsEntity(MemberExpression expression, out bool isIdentifier); } public class AddJoinsReWriter : NhQueryModelVisitorBase, IIsEntityDecider @@ -77,11 +78,15 @@ public override void VisitJoinClause(JoinClause joinClause, QueryModel queryMode _currentJoin = null; } + // Since v5.3 + [Obsolete("This method has no usages and will be removed in a future version")] public bool IsEntity(System.Type type) { return _sessionFactory.GetImplementors(type.FullName).Any(); } + // Since v5.3 + [Obsolete("This method has no usages and will be removed in a future version")] public bool IsIdentifier(System.Type type, string propertyName) { var metadata = _sessionFactory.GetClassMetadata(type); @@ -99,5 +104,14 @@ private void AddJoin(QueryModel queryModel, NhJoinClause joinClause) queryModel.BodyClauses.Add(joinClause); } + + bool IIsEntityDecider.IsEntity(MemberExpression expression, out bool isIdentifier) + { + isIdentifier = + ExpressionsHelper.TryGetMappedType(_sessionFactory, expression, out var mappedType, out var entityPersister, out _, out var memberPath) + && entityPersister?.IdentifierPropertyName == memberPath; + + return mappedType?.IsEntityType == true; + } } } diff --git a/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs b/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs index f333fc5e61d..580ba3cf00c 100644 --- a/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs +++ b/src/NHibernate/Linq/Visitors/MemberExpressionJoinDetector.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; +using NHibernate.Engine; using NHibernate.Linq.Expressions; using NHibernate.Linq.ReWriters; using Remotion.Linq.Clauses; @@ -39,7 +40,7 @@ protected override Expression VisitMember(MemberExpression expression) return base.VisitMember(expression); } - var isIdentifier = _isEntityDecider.IsIdentifier(expression.Expression.Type, expression.Member.Name); + var isEntity = _isEntityDecider.IsEntity(expression, out var isIdentifier); if (isIdentifier) _hasIdentifier = true; if (!isIdentifier) @@ -50,7 +51,7 @@ protected override Expression VisitMember(MemberExpression expression) if (!isIdentifier) _memberExpressionDepth--; - if (_isEntityDecider.IsEntity(expression.Type) && + if (isEntity && ((_requiresJoinForNonIdentifier && !_hasIdentifier) || _memberExpressionDepth > 0) && _joiner.CanAddJoin(expression)) { diff --git a/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs b/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs index 9205b4f1b05..10d7bd07d3c 100644 --- a/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs +++ b/src/NHibernate/Linq/Visitors/SelectClauseNominator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using NHibernate.Engine; using NHibernate.Linq.Functions; using NHibernate.Linq.Expressions; using NHibernate.Util; @@ -15,6 +16,7 @@ namespace NHibernate.Linq.Visitors class SelectClauseHqlNominator : RelinqExpressionVisitor { private readonly ILinqToHqlGeneratorsRegistry _functionRegistry; + private readonly ISessionFactoryImplementor _sessionFactory; /// /// The expression parts that can be converted to pure HQL. @@ -35,6 +37,7 @@ class SelectClauseHqlNominator : RelinqExpressionVisitor public SelectClauseHqlNominator(VisitorParameters parameters) { _functionRegistry = parameters.SessionFactory.Settings.LinqToHqlGeneratorsRegistry; + _sessionFactory = parameters.SessionFactory; } internal Expression Nominate(Expression expression) @@ -168,8 +171,10 @@ private bool CanBeEvaluatedInHqlSelectStatement(Expression expression, bool proj return projectConstantsInHql; } - // Assume all is good - return true; + return !(expression is MemberExpression memberExpression) || // Assume all is good + // Nominate only expressions that represent a mapped property or a translatable method call + ExpressionsHelper.TryGetMappedType(_sessionFactory, expression, out _, out _, out _, out _) || + _functionRegistry.TryGetGenerator(memberExpression.Member, out _); } private static bool CanBeEvaluatedInHqlStatementShortcut(Expression expression) diff --git a/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs b/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs index 4be0b8d2af2..886d4e0e2b1 100644 --- a/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs +++ b/src/NHibernate/Linq/Visitors/WhereJoinDetector.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using NHibernate.Engine; using NHibernate.Linq.ReWriters; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; @@ -314,10 +315,7 @@ protected override Expression VisitMember(MemberExpression expression) return base.VisitMember(expression); } - var isIdentifier = _isEntityDecider.IsIdentifier( - expression.Expression.Type, - expression.Member.Name); - + var isEntity = _isEntityDecider.IsEntity(expression, out var isIdentifier); if (!isIdentifier) _memberExpressionDepth++; @@ -327,7 +325,7 @@ protected override Expression VisitMember(MemberExpression expression) _memberExpressionDepth--; ExpressionValues values = _values.Pop().Operation(pvs => pvs.MemberAccess(expression.Type)); - if (_isEntityDecider.IsEntity(expression.Type)) + if (isEntity) { // Don't add joins for things like a.B == a.C where B and C are entities. // We only need to join B when there's something like a.B.D.