diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs
new file mode 100644
index 00000000000..77229193cbf
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3290/Fixture.cs
@@ -0,0 +1,147 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by AsyncGenerator.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+using System.Collections.Generic;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Transform;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3290
+{
+ using System.Threading.Tasks;
+ [TestFixture(true)]
+ [TestFixture(false)]
+ public class FixtureAsync : TestCaseMappingByCode
+ {
+ private readonly bool _detectFetchLoops;
+
+ public FixtureAsync(bool detectFetchLoops)
+ {
+ _detectFetchLoops = detectFetchLoops;
+ }
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));
+
+ rc.Property(
+ x => x.Name
+ );
+
+ rc.Set(
+ x => x.Children,
+ v =>
+ {
+ v.Table("EntityToEntity");
+ v.Cascade(Mapping.ByCode.Cascade.None);
+ v.Inverse(true);
+ v.Key(x =>
+ {
+ x.Column("ParentId");
+ x.NotNullable(true);
+ });
+ v.Lazy(CollectionLazy.Lazy);
+ v.Fetch(CollectionFetchMode.Join);
+ },
+ h => h.ManyToMany(m => m.Column("ChildId"))
+ );
+
+ rc.Set(
+ x => x.Parents,
+ v =>
+ {
+ v.Table("EntityToEntity");
+ v.Cascade(Mapping.ByCode.Cascade.All);
+
+ v.Key(x =>
+ {
+ x.Column("ChildId");
+ x.NotNullable(true);
+ });
+ v.Lazy(CollectionLazy.Lazy);
+ v.Fetch(CollectionFetchMode.Join);
+ },
+ h => h.ManyToMany(m => m.Column("ParentId"))
+ );
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
+ configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
+ }
+
+ protected override void OnSetUp()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var person = new Entity
+ {
+ Name = "pers",
+ Parents = new HashSet()
+ };
+ session.Save(person);
+ var job = new Entity
+ {
+ Name = "job",
+ Children = new HashSet()
+ };
+ session.Save(job);
+
+ job.Children.Add(person);
+ person.Parents.Add(job);
+
+ transaction.Commit();
+ }
+
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public async Task QueryWithFetchAsync()
+ {
+ using var session = OpenSession();
+ using var _ = session.BeginTransaction();
+
+ var all = await (session
+ .QueryOver()
+ .Fetch(SelectMode.Fetch, x => x.Children)
+ .Fetch(SelectMode.Fetch, x => x.Parents)
+ .TransformUsing(Transformers.DistinctRootEntity)
+ .ListAsync());
+
+ foreach (var entity in all)
+ {
+ var isPerson = entity.Name == "pers";
+ if (isPerson)
+ Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
+ else
+ Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
+ }
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs
index 0e917134d11..9f85c760a14 100644
--- a/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs
+++ b/src/NHibernate.Test/NHSpecificTest/GH3288/Entity.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace NHibernate.Test.NHSpecificTest.GH3288
{
@@ -10,6 +11,7 @@ class TopEntity
class MiddleEntity
{
public virtual int Id { get; set; }
+ public virtual string Name { get; set; }
public virtual ISet Components { get; set; } = new HashSet();
}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml
index 94ca5e12a95..b9cd68ded6f 100644
--- a/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml
+++ b/src/NHibernate.Test/NHSpecificTest/GH3288/Mappings.hbm.xml
@@ -13,6 +13,7 @@
+
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs
new file mode 100644
index 00000000000..0ef176af019
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3290/Entity.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+
+namespace NHibernate.Test.NHSpecificTest.GH3290
+{
+ class Entity
+ {
+ public virtual Guid Id { get; set; }
+ public virtual string Name { get; set; }
+ public virtual ISet Parents { get; set; }
+ public virtual ISet Children { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs
new file mode 100644
index 00000000000..f7c6fe8dbe0
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3290/Fixture.cs
@@ -0,0 +1,136 @@
+using System.Collections.Generic;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Transform;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3290
+{
+ [TestFixture(true)]
+ [TestFixture(false)]
+ public class Fixture : TestCaseMappingByCode
+ {
+ private readonly bool _detectFetchLoops;
+
+ public Fixture(bool detectFetchLoops)
+ {
+ _detectFetchLoops = detectFetchLoops;
+ }
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, map => map.Generator(Generators.GuidComb));
+
+ rc.Property(
+ x => x.Name
+ );
+
+ rc.Set(
+ x => x.Children,
+ v =>
+ {
+ v.Table("EntityToEntity");
+ v.Cascade(Mapping.ByCode.Cascade.None);
+ v.Inverse(true);
+ v.Key(x =>
+ {
+ x.Column("ParentId");
+ x.NotNullable(true);
+ });
+ v.Lazy(CollectionLazy.Lazy);
+ v.Fetch(CollectionFetchMode.Join);
+ },
+ h => h.ManyToMany(m => m.Column("ChildId"))
+ );
+
+ rc.Set(
+ x => x.Parents,
+ v =>
+ {
+ v.Table("EntityToEntity");
+ v.Cascade(Mapping.ByCode.Cascade.All);
+
+ v.Key(x =>
+ {
+ x.Column("ChildId");
+ x.NotNullable(true);
+ });
+ v.Lazy(CollectionLazy.Lazy);
+ v.Fetch(CollectionFetchMode.Join);
+ },
+ h => h.ManyToMany(m => m.Column("ParentId"))
+ );
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty(Environment.DetectFetchLoops, _detectFetchLoops ? "true" : "false");
+ configuration.SetProperty(Environment.MaxFetchDepth, _detectFetchLoops ? "-1" : "2");
+ }
+
+ protected override void OnSetUp()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ var person = new Entity
+ {
+ Name = "pers",
+ Parents = new HashSet()
+ };
+ session.Save(person);
+ var job = new Entity
+ {
+ Name = "job",
+ Children = new HashSet()
+ };
+ session.Save(job);
+
+ job.Children.Add(person);
+ person.Parents.Add(job);
+
+ transaction.Commit();
+ }
+
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+
+ session.CreateSQLQuery("delete from EntityToEntity").ExecuteUpdate();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public void QueryWithFetch()
+ {
+ using var session = OpenSession();
+ using var _ = session.BeginTransaction();
+
+ var all = session
+ .QueryOver()
+ .Fetch(SelectMode.Fetch, x => x.Children)
+ .Fetch(SelectMode.Fetch, x => x.Parents)
+ .TransformUsing(Transformers.DistinctRootEntity)
+ .List();
+
+ foreach (var entity in all)
+ {
+ var isPerson = entity.Name == "pers";
+ if (isPerson)
+ Assert.That(entity.Parents, Has.Count.EqualTo(1), "Person's job not found or non-unique.");
+ else
+ Assert.That(entity.Children, Has.Count.EqualTo(1), "Job's employee not found or non-unique.");
+ }
+ }
+ }
+}
diff --git a/src/NHibernate/Loader/JoinWalker.cs b/src/NHibernate/Loader/JoinWalker.cs
index 731ee2cc670..65d7594395c 100644
--- a/src/NHibernate/Loader/JoinWalker.cs
+++ b/src/NHibernate/Loader/JoinWalker.cs
@@ -192,7 +192,15 @@ private void AddAssociationToJoinTree(IAssociationType type, string[] aliasedLhs
if (qc != null)
{
- _joinQueue.Enqueue(new CollectionJoinQueueEntry(qc, subalias, path, pathAlias));
+ var collection = new CollectionJoinQueueEntry(qc, subalias, path, pathAlias);
+ // Many-to-Many element entity join needs to be added right after collection bridge table
+ // (see IsManyToManyWith, ManyToManySelectFragment, IsManyToManyRoot usages)
+ if (qc.IsManyToMany)
+ {
+ collection.Walk(this);
+ return;
+ }
+ _joinQueue.Enqueue(collection);
}
else if (joinable is IOuterJoinLoadable jl)
{