diff --git a/Tools/packages.config b/Tools/packages.config
index e54c238eca4..2a94e5aa85e 100644
--- a/Tools/packages.config
+++ b/Tools/packages.config
@@ -7,5 +7,5 @@
-
+
diff --git a/doc/reference/modules/query_linq.xml b/doc/reference/modules/query_linq.xml
index 06f4f4cf3ec..3e994729866 100644
--- a/doc/reference/modules/query_linq.xml
+++ b/doc/reference/modules/query_linq.xml
@@ -42,6 +42,28 @@ using NHibernate.Linq;]]>
.Where(c => c.Name == "Max")
.ToList();]]>
+
+ Starting with NHibernate 5.0, queries can also be created from an entity collection, with the standard
+ Linq extension AsQueryable available from System.Linq namespace.
+
+ whiteKittens =
+ cat.Kittens.AsQueryable()
+ .Where(k => k.Color == "white")
+ .ToList();]]>
+
+ This will be executed as a query on that cat's kittens without loading the
+ entire collection.
+
+
+ If the collection is a map, call AsQueryable on its Values
+ property.
+
+ whiteKittens =
+ cat.Kittens.Values.AsQueryable()
+ .Where(k => k.Color == "white")
+ .ToList();]]>
+
+
A client timeout for the query can be defined.
@@ -789,4 +811,4 @@ cfg.LinqToHqlGeneratorsRegistry();
-
\ No newline at end of file
+
diff --git a/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs
index aa3c2db5793..10bccd2305f 100644
--- a/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs
+++ b/src/NHibernate.Test/Async/NHSpecificTest/Logs/LogsFixture.cs
@@ -72,6 +72,7 @@ public class TextLogSpy : IDisposable
private readonly TextWriterAppender appender;
private readonly Logger loggerImpl;
private readonly StringBuilder stringBuilder;
+ private readonly Level previousLevel;
public TextLogSpy(string loggerName, string pattern)
{
@@ -84,6 +85,7 @@ public TextLogSpy(string loggerName, string pattern)
};
loggerImpl = (Logger)LogManager.GetLogger(typeof(LogsFixtureAsync).Assembly, loggerName).Logger;
loggerImpl.AddAppender(appender);
+ previousLevel = loggerImpl.Level;
loggerImpl.Level = Level.All;
}
@@ -98,9 +100,10 @@ public string[] Events
public void Dispose()
{
loggerImpl.RemoveAppender(appender);
+ loggerImpl.Level = previousLevel;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
index 03286bacbc9..39732200737 100644
--- a/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
+++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
@@ -51,8 +51,8 @@ public async Task LoadCompositeElementsWithWithSimpleHbmAliasInjectionAsync()
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithSimpleHbmAliasInjection"));
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
await (CleanupWithPersonsAsync());
}
@@ -63,8 +63,8 @@ public async Task LoadCompositeElementsWithWithComplexHbmAliasInjectionAsync()
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithComplexHbmAliasInjection"));
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
await (CleanupWithPersonsAsync());
}
@@ -75,8 +75,8 @@ public async Task LoadCompositeElementsWithWithCustomAliasesAsync()
Country country = await (LoadCountryWithNativeSQLAsync(CreateCountry(stats), "LoadAreaStatisticsWithCustomAliases"));
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
await (CleanupWithPersonsAsync());
}
@@ -201,8 +201,8 @@ public async Task LoadCompositeElementCollectionWithCustomLoaderAsync()
{
var a = await (session.GetAsync(country.Code));
Assert.That(a, Is.Not.Null, "area");
- Assert.That((ICollection) a.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "area.Keys");
- Assert.That((ICollection) a.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "area.Elements");
+ Assert.That(a.Statistics.Keys, Is.EquivalentTo(stats.Keys), "area.Keys");
+ Assert.That(a.Statistics.Values, Is.EquivalentTo(stats.Values), "area.Elements");
}
await (CleanupWithPersonsAsync());
}
@@ -428,4 +428,4 @@ private static IDictionary CreateStatistics()
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs
new file mode 100644
index 00000000000..68dc36629f3
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/Fixture.cs
@@ -0,0 +1,741 @@
+//------------------------------------------------------------------------------
+//
+// 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;
+using System.Linq;
+using System.Reflection;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Collection;
+using NHibernate.Engine.Query;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Util;
+using NUnit.Framework;
+using NHibernate.Linq;
+
+namespace NHibernate.Test.NHSpecificTest.NH2319
+{
+ using System.Threading.Tasks;
+ using System.Threading;
+ [TestFixture]
+ public abstract class FixtureBaseAsync : TestCaseMappingByCode
+ {
+ private Guid _parent1Id;
+ private Guid _child1Id;
+ private Guid _parent2Id;
+ private Guid _child3Id;
+
+ [Test]
+ public Task ShouldBeAbleToFindChildrenByNameAsync()
+ {
+ return FindChildrenByNameAsync(_parent1Id, _child1Id);
+ }
+
+ private async Task FindChildrenByNameAsync(Guid parentId, Guid childId, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(parentId, cancellationToken));
+
+ Assert.That(parent, Is.Not.Null);
+
+ var filtered = await (parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync(cancellationToken));
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(filtered[0].Id, Is.EqualTo(childId));
+ }
+ }
+
+ [Test]
+ public async Task ShouldBeAbleToPerformComplexFilteringAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+
+ Assert.NotNull(parent);
+
+ var filtered = await (parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Piter")
+ .SelectMany(x => x.GrandChildren)
+ .Select(x => x.Id)
+ .CountAsync());
+
+ Assert.That(filtered, Is.EqualTo(2));
+ }
+ }
+
+ [Test]
+ public async Task ShouldBeAbleToReuseQueryPlanAsync()
+ {
+ await (ShouldBeAbleToFindChildrenByNameAsync());
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ Assert.That(ShouldBeAbleToFindChildrenByNameAsync, Throws.Nothing);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public async Task ShouldNotMixResultsAsync()
+ {
+ await (FindChildrenByNameAsync(_parent1Id, _child1Id));
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ await (FindChildrenByNameAsync(_parent2Id, _child3Id));
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public async Task ShouldNotInitializeCollectionWhenPerformingQueryAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var persistentCollection = (IPersistentCollection) parent.Children;
+
+ var filtered = await (parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync());
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(persistentCollection.WasInitialized, Is.False);
+ }
+ }
+
+ [Test]
+ public async Task ShouldPerformSqlQueryEvenIfCollectionAlreadyInitializedAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var loaded = parent.Children.ToList();
+ Assert.That(loaded, Has.Count.EqualTo(2));
+
+ var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ var filtered = await (parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync());
+
+ var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1));
+ }
+ }
+
+ [Test]
+ public async Task TestFilterAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var children = await ((await (session.CreateFilterAsync(parent.Children, "where this.Name = 'Jack'")))
+ .ListAsync());
+
+ Assert.That(children, Has.Count.EqualTo(1));
+ }
+ }
+
+ [Test]
+ public async Task TestPlanCacheMissAsync()
+ {
+ var internalPlanCache = typeof(QueryPlanCache)
+ .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache;
+ Assert.That(internalPlanCache, Is.Not.Null,
+ $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation.");
+
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ internalPlanCache.Clear();
+ await (ShouldBeAbleToFindChildrenByNameAsync());
+ AssertFilterPlanCacheMiss(spy);
+ }
+ }
+
+ private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache";
+
+ private static void AssertFilterPlanCacheHit(LogSpy spy) =>
+ // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed
+ // rather than only asserting it has been hit.
+ Assert.That(spy.GetWholeLog(),
+ Contains.Substring("located collection-filter query plan in cache (")
+ .And.Not.Contains(_filterPlanCacheMissLog));
+
+ private static void AssertFilterPlanCacheMiss(LogSpy spy) =>
+ Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog));
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty("show_sql", "true");
+ configuration.SetProperty("generate_statistics", "true");
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var parent1 = new Parent { Name = "Bob" };
+ _parent1Id = (Guid) session.Save(parent1);
+
+ var parent2 = new Parent { Name = "Martin" };
+ _parent2Id = (Guid) session.Save(parent2);
+
+ var child1 = new Child
+ {
+ Name = "Jack",
+ Parent = parent1
+ };
+ parent1.Children.Add(child1);
+ child1.Parents.Add(parent1);
+ _child1Id = (Guid) session.Save(child1);
+
+ var child2 = new Child
+ {
+ Name = "Piter",
+ Parent = parent1
+ };
+ parent1.Children.Add(child2);
+ child2.Parents.Add(parent1);
+ session.Save(child2);
+
+ var grandChild1 = new GrandChild
+ {
+ Name = "Kate",
+ Child = child2
+ };
+ child2.GrandChildren.Add(grandChild1);
+ grandChild1.ParentChidren.Add(child2);
+ session.Save(grandChild1);
+
+ var grandChild2 = new GrandChild
+ {
+ Name = "Mary",
+ Child = child2
+ };
+ child2.GrandChildren.Add(grandChild2);
+ grandChild2.ParentChidren.Add(child2);
+ session.Save(grandChild2);
+
+ var child3 = new Child
+ {
+ Name = "Jack",
+ Parent = parent2
+ };
+ parent2.Children.Add(child3);
+ child3.Parents.Add(parent2);
+ _child3Id = (Guid) session.Save(child3);
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+ }
+
+ [TestFixture]
+ public class BagFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Parent);
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("child_id"));
+ map.Inverse(true);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Child, x => x.Column("child_id"));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class SetFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Parent);
+ rc.Set(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("child_id"));
+ map.Inverse(true);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Child, x => x.Column("child_id"));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class ManyToManyBagFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map =>
+ {
+ map.Key(k => k.Column("Parent"));
+ map.Table("ParentChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Parents,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ParentChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Parent")));
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Table("ChildGrandChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("GrandChild")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.ParentChidren,
+ map =>
+ {
+ map.Key(k => k.Column("GrandChild"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ChildGrandChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class ManyToManySetFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map =>
+ {
+ map.Key(k => k.Column("Parent"));
+ map.Table("ParentChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Parents,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ParentChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Parent")));
+ rc.Set(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Table("ChildGrandChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("GrandChild")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.ParentChidren,
+ map =>
+ {
+ map.Key(k => k.Column("GrandChild"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ChildGrandChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ // No bidi list/idbag: mapping not supported unless not marking inverse the parent side with list, which is not
+ // normal for a bidi. This is a general limitation, not a limitation of the feature tested here.
+
+ [TestFixture]
+ public class UnidiBagFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiListFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Key(k => k.Column("child_id"));
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ c => c.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiSetFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyBagFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyIdBagFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.IdBag(
+ x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.IdBag(
+ c => c.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyListFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Key(k => k.Column("child_id"));
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ c => c.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManySetFixtureAsync : FixtureBaseAsync
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs
new file mode 100644
index 00000000000..e93c7d652f1
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2319/MapFixture.cs
@@ -0,0 +1,299 @@
+//------------------------------------------------------------------------------
+//
+// 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;
+using System.Linq;
+using System.Reflection;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Collection;
+using NHibernate.Engine.Query;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Util;
+using NUnit.Framework;
+using NHibernate.Linq;
+
+namespace NHibernate.Test.NHSpecificTest.NH2319
+{
+ using System.Threading.Tasks;
+ using System.Threading;
+ [TestFixture]
+ public class MapFixtureAsync : TestCaseMappingByCode
+ {
+ private Guid _parent1Id;
+ private Guid _child1Id;
+ private Guid _parent2Id;
+ private Guid _child3Id;
+
+ [Test]
+ public Task ShouldBeAbleToFindChildrenByNameAsync()
+ {
+ return FindChildrenByNameAsync(_parent1Id, _child1Id);
+ }
+
+ private async Task FindChildrenByNameAsync(Guid parentId, Guid childId, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(parentId, cancellationToken));
+
+ Assert.That(parent, Is.Not.Null);
+
+ var filtered = await (parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync(cancellationToken));
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(filtered[0].Id, Is.EqualTo(childId));
+ }
+ }
+
+ [Test]
+ public async Task ShouldBeAbleToPerformComplexFilteringAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+
+ Assert.NotNull(parent);
+
+ var filtered = await (parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Piter")
+ .SelectMany(x => x.GrandChildren)
+ .Select(x => x.Id)
+ .CountAsync());
+
+ Assert.That(filtered, Is.EqualTo(2));
+ }
+ }
+
+ [Test]
+ public async Task ShouldBeAbleToReuseQueryPlanAsync()
+ {
+ await (ShouldBeAbleToFindChildrenByNameAsync());
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ Assert.That(ShouldBeAbleToFindChildrenByNameAsync, Throws.Nothing);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public async Task ShouldNotMixResultsAsync()
+ {
+ await (FindChildrenByNameAsync(_parent1Id, _child1Id));
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ await (FindChildrenByNameAsync(_parent2Id, _child3Id));
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public async Task ShouldNotInitializeCollectionWhenPerformingQueryAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var persistentCollection = (IPersistentCollection) parent.ChildrenMap;
+
+ var filtered = await (parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync());
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(persistentCollection.WasInitialized, Is.False);
+ }
+ }
+
+ [Test]
+ public async Task ShouldPerformSqlQueryEvenIfCollectionAlreadyInitializedAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var loaded = parent.ChildrenMap.ToList();
+ Assert.That(loaded, Has.Count.EqualTo(2));
+
+ var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ var filtered = await (parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToListAsync());
+
+ var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1));
+ }
+ }
+
+ [Test]
+ public async Task TestFilterAsync()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = await (session.GetAsync(_parent1Id));
+ Assert.That(parent, Is.Not.Null);
+
+ var children = await ((await (session.CreateFilterAsync(parent.ChildrenMap, "where this.Name = 'Jack'")))
+ .ListAsync());
+
+ Assert.That(children, Has.Count.EqualTo(1));
+ }
+ }
+
+ [Test]
+ public async Task TestPlanCacheMissAsync()
+ {
+ var internalPlanCache = typeof(QueryPlanCache)
+ .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache;
+ Assert.That(internalPlanCache, Is.Not.Null,
+ $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation.");
+
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ internalPlanCache.Clear();
+ await (ShouldBeAbleToFindChildrenByNameAsync());
+ AssertFilterPlanCacheMiss(spy);
+ }
+ }
+
+ private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache";
+
+ private static void AssertFilterPlanCacheHit(LogSpy spy) =>
+ // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed
+ // rather than only asserting it has been hit.
+ Assert.That(spy.GetWholeLog(),
+ Contains.Substring("located collection-filter query plan in cache (")
+ .And.Not.Contains(_filterPlanCacheMissLog));
+
+ private static void AssertFilterPlanCacheMiss(LogSpy spy) =>
+ Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog));
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty("show_sql", "true");
+ configuration.SetProperty("generate_statistics", "true");
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var parent1 = new Parent { Name = "Bob" };
+ _parent1Id = (Guid) session.Save(parent1);
+
+ var parent2 = new Parent { Name = "Martin" };
+ _parent2Id = (Guid) session.Save(parent2);
+
+ var child1 = new Child
+ {
+ Name = "Jack",
+ Parent = parent1
+ };
+ _child1Id = (Guid) session.Save(child1);
+ parent1.ChildrenMap.Add(child1.Id, child1);
+
+ var child2 = new Child
+ {
+ Name = "Piter",
+ Parent = parent1
+ };
+ session.Save(child2);
+ parent1.ChildrenMap.Add(child2.Id, child2);
+
+ var grandChild1 = new GrandChild
+ {
+ Name = "Kate",
+ Child = child2
+ };
+ session.Save(grandChild1);
+ child2.GrandChildren.Add(grandChild1);
+
+ var grandChild2 = new GrandChild
+ {
+ Name = "Mary",
+ Child = child2
+ };
+ session.Save(grandChild2);
+ child2.GrandChildren.Add(grandChild2);
+
+ var child3 = new Child
+ {
+ Name = "Jack",
+ Parent = parent2
+ };
+ _child3Id = (Guid) session.Save(child3);
+ parent2.ChildrenMap.Add(child1.Id, child3);
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Map(x => x.ChildrenMap,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs b/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs
index 5b5f1ac4b12..2ffc692763b 100644
--- a/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs
+++ b/src/NHibernate.Test/NHSpecificTest/Logs/LogsFixture.cs
@@ -61,6 +61,7 @@ public class TextLogSpy : IDisposable
private readonly TextWriterAppender appender;
private readonly Logger loggerImpl;
private readonly StringBuilder stringBuilder;
+ private readonly Level previousLevel;
public TextLogSpy(string loggerName, string pattern)
{
@@ -73,6 +74,7 @@ public TextLogSpy(string loggerName, string pattern)
};
loggerImpl = (Logger)LogManager.GetLogger(typeof(LogsFixture).Assembly, loggerName).Logger;
loggerImpl.AddAppender(appender);
+ previousLevel = loggerImpl.Level;
loggerImpl.Level = Level.All;
}
@@ -87,9 +89,10 @@ public string[] Events
public void Dispose()
{
loggerImpl.RemoveAppender(appender);
+ loggerImpl.Level = previousLevel;
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs b/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs
index d92cb3c9ef7..b9b5cca8398 100644
--- a/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs
+++ b/src/NHibernate.Test/NHSpecificTest/NH1136/PersistentMilestoneCollection.cs
@@ -20,9 +20,9 @@ public PersistentMilestoneCollection(ISessionImplementor session) : base(session
public TValue FindValueFor(TKey key)
{
Read();
- return ((IMilestoneCollection) WrappedMap).FindValueFor(key);
+ return ((IMilestoneCollection) Entries(null)).FindValueFor(key);
}
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
index b5c6910a8ce..7c4c618284a 100644
--- a/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
+++ b/src/NHibernate.Test/NHSpecificTest/NH1612/NativeSqlCollectionLoaderFixture.cs
@@ -39,8 +39,8 @@ public void LoadCompositeElementsWithWithSimpleHbmAliasInjection()
Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithSimpleHbmAliasInjection");
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
CleanupWithPersons();
}
@@ -51,8 +51,8 @@ public void LoadCompositeElementsWithWithComplexHbmAliasInjection()
Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithComplexHbmAliasInjection");
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
CleanupWithPersons();
}
@@ -63,8 +63,8 @@ public void LoadCompositeElementsWithWithCustomAliases()
Country country = LoadCountryWithNativeSQL(CreateCountry(stats), "LoadAreaStatisticsWithCustomAliases");
Assert.That(country, Is.Not.Null);
- Assert.That((ICollection) country.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "Keys");
- Assert.That((ICollection) country.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "Elements");
+ Assert.That(country.Statistics.Keys, Is.EquivalentTo(stats.Keys), "Keys");
+ Assert.That(country.Statistics.Values, Is.EquivalentTo(stats.Values), "Elements");
CleanupWithPersons();
}
@@ -189,8 +189,8 @@ public void LoadCompositeElementCollectionWithCustomLoader()
{
var a = session.Get(country.Code);
Assert.That(a, Is.Not.Null, "area");
- Assert.That((ICollection) a.Statistics.Keys, Is.EquivalentTo((ICollection) stats.Keys), "area.Keys");
- Assert.That((ICollection) a.Statistics.Values, Is.EquivalentTo((ICollection) stats.Values), "area.Elements");
+ Assert.That(a.Statistics.Keys, Is.EquivalentTo(stats.Keys), "area.Keys");
+ Assert.That(a.Statistics.Values, Is.EquivalentTo(stats.Values), "area.Elements");
}
CleanupWithPersons();
}
@@ -416,4 +416,4 @@ private static IDictionary CreateStatistics()
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs
new file mode 100644
index 00000000000..0f83b28e91b
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH2319/Fixture.cs
@@ -0,0 +1,728 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Collection;
+using NHibernate.Engine.Query;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Util;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.NH2319
+{
+ [TestFixture]
+ public abstract class FixtureBase : TestCaseMappingByCode
+ {
+ private Guid _parent1Id;
+ private Guid _child1Id;
+ private Guid _parent2Id;
+ private Guid _child3Id;
+
+ [Test]
+ public void ShouldBeAbleToFindChildrenByName()
+ {
+ FindChildrenByName(_parent1Id, _child1Id);
+ }
+
+ private void FindChildrenByName(Guid parentId, Guid childId)
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(parentId);
+
+ Assert.That(parent, Is.Not.Null);
+
+ var filtered = parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(filtered[0].Id, Is.EqualTo(childId));
+ }
+ }
+
+ [Test]
+ public void ShouldBeAbleToPerformComplexFiltering()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+
+ Assert.NotNull(parent);
+
+ var filtered = parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Piter")
+ .SelectMany(x => x.GrandChildren)
+ .Select(x => x.Id)
+ .Count();
+
+ Assert.That(filtered, Is.EqualTo(2));
+ }
+ }
+
+ [Test]
+ public void ShouldBeAbleToReuseQueryPlan()
+ {
+ ShouldBeAbleToFindChildrenByName();
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ Assert.That(ShouldBeAbleToFindChildrenByName, Throws.Nothing);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public void ShouldNotMixResults()
+ {
+ FindChildrenByName(_parent1Id, _child1Id);
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ FindChildrenByName(_parent2Id, _child3Id);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public void ShouldNotInitializeCollectionWhenPerformingQuery()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var persistentCollection = (IPersistentCollection) parent.Children;
+
+ var filtered = parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(persistentCollection.WasInitialized, Is.False);
+ }
+ }
+
+ [Test]
+ public void ShouldPerformSqlQueryEvenIfCollectionAlreadyInitialized()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var loaded = parent.Children.ToList();
+ Assert.That(loaded, Has.Count.EqualTo(2));
+
+ var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ var filtered = parent.Children
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1));
+ }
+ }
+
+ [Test]
+ public void TestFilter()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var children = session.CreateFilter(parent.Children, "where this.Name = 'Jack'")
+ .List();
+
+ Assert.That(children, Has.Count.EqualTo(1));
+ }
+ }
+
+ [Test]
+ public void TestPlanCacheMiss()
+ {
+ var internalPlanCache = typeof(QueryPlanCache)
+ .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache;
+ Assert.That(internalPlanCache, Is.Not.Null,
+ $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation.");
+
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ internalPlanCache.Clear();
+ ShouldBeAbleToFindChildrenByName();
+ AssertFilterPlanCacheMiss(spy);
+ }
+ }
+
+ private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache";
+
+ private static void AssertFilterPlanCacheHit(LogSpy spy) =>
+ // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed
+ // rather than only asserting it has been hit.
+ Assert.That(spy.GetWholeLog(),
+ Contains.Substring("located collection-filter query plan in cache (")
+ .And.Not.Contains(_filterPlanCacheMissLog));
+
+ private static void AssertFilterPlanCacheMiss(LogSpy spy) =>
+ Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog));
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty("show_sql", "true");
+ configuration.SetProperty("generate_statistics", "true");
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var parent1 = new Parent { Name = "Bob" };
+ _parent1Id = (Guid) session.Save(parent1);
+
+ var parent2 = new Parent { Name = "Martin" };
+ _parent2Id = (Guid) session.Save(parent2);
+
+ var child1 = new Child
+ {
+ Name = "Jack",
+ Parent = parent1
+ };
+ parent1.Children.Add(child1);
+ child1.Parents.Add(parent1);
+ _child1Id = (Guid) session.Save(child1);
+
+ var child2 = new Child
+ {
+ Name = "Piter",
+ Parent = parent1
+ };
+ parent1.Children.Add(child2);
+ child2.Parents.Add(parent1);
+ session.Save(child2);
+
+ var grandChild1 = new GrandChild
+ {
+ Name = "Kate",
+ Child = child2
+ };
+ child2.GrandChildren.Add(grandChild1);
+ grandChild1.ParentChidren.Add(child2);
+ session.Save(grandChild1);
+
+ var grandChild2 = new GrandChild
+ {
+ Name = "Mary",
+ Child = child2
+ };
+ child2.GrandChildren.Add(grandChild2);
+ grandChild2.ParentChidren.Add(child2);
+ session.Save(grandChild2);
+
+ var child3 = new Child
+ {
+ Name = "Jack",
+ Parent = parent2
+ };
+ parent2.Children.Add(child3);
+ child3.Parents.Add(parent2);
+ _child3Id = (Guid) session.Save(child3);
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+ }
+
+ [TestFixture]
+ public class BagFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Parent);
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("child_id"));
+ map.Inverse(true);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Child, x => x.Column("child_id"));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class SetFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children, map => map.Inverse(true), rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Parent);
+ rc.Set(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("child_id"));
+ map.Inverse(true);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.ManyToOne(x => x.Child, x => x.Column("child_id"));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class ManyToManyBagFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map =>
+ {
+ map.Key(k => k.Column("Parent"));
+ map.Table("ParentChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Parents,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ParentChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Parent")));
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Table("ChildGrandChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("GrandChild")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.ParentChidren,
+ map =>
+ {
+ map.Key(k => k.Column("GrandChild"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ChildGrandChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class ManyToManySetFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map =>
+ {
+ map.Key(k => k.Column("Parent"));
+ map.Table("ParentChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Parents,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ParentChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Parent")));
+ rc.Set(x => x.GrandChildren,
+ map =>
+ {
+ map.Key(k => k.Column("Child"));
+ map.Table("ChildGrandChild");
+ map.Inverse(true);
+ },
+ rel => rel.ManyToMany(map => map.Column("GrandChild")));
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.ParentChidren,
+ map =>
+ {
+ map.Key(k => k.Column("GrandChild"));
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Table("ChildGrandChild");
+ },
+ rel => rel.ManyToMany(map => map.Column("Child")));
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ // No bidi list/idbag: mapping not supported unless not marking inverse the parent side with list, which is not
+ // normal for a bidi. This is a general limitation, not a limitation of the feature tested here.
+
+ [TestFixture]
+ public class UnidiBagFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiListFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Key(k => k.Column("child_id"));
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ c => c.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiSetFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyBagFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Bag(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyIdBagFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.IdBag(
+ x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.IdBag(
+ c => c.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManyListFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ x => x.Children,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Key(k => k.Column("child_id"));
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.List(
+ c => c.GrandChildren,
+ map =>
+ {
+ map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans);
+ map.Index(i => i.Column("i"));
+ },
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+
+ [TestFixture]
+ public class UnidiManyToManySetFixture : FixtureBase
+ {
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.Children,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.ManyToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs
new file mode 100644
index 00000000000..6aa9b1c8e86
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH2319/MapFixture.cs
@@ -0,0 +1,286 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Collection;
+using NHibernate.Engine.Query;
+using NHibernate.Mapping.ByCode;
+using NHibernate.Util;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.NH2319
+{
+ [TestFixture]
+ public class MapFixture : TestCaseMappingByCode
+ {
+ private Guid _parent1Id;
+ private Guid _child1Id;
+ private Guid _parent2Id;
+ private Guid _child3Id;
+
+ [Test]
+ public void ShouldBeAbleToFindChildrenByName()
+ {
+ FindChildrenByName(_parent1Id, _child1Id);
+ }
+
+ private void FindChildrenByName(Guid parentId, Guid childId)
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(parentId);
+
+ Assert.That(parent, Is.Not.Null);
+
+ var filtered = parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(filtered[0].Id, Is.EqualTo(childId));
+ }
+ }
+
+ [Test]
+ public void ShouldBeAbleToPerformComplexFiltering()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+
+ Assert.NotNull(parent);
+
+ var filtered = parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Piter")
+ .SelectMany(x => x.GrandChildren)
+ .Select(x => x.Id)
+ .Count();
+
+ Assert.That(filtered, Is.EqualTo(2));
+ }
+ }
+
+ [Test]
+ public void ShouldBeAbleToReuseQueryPlan()
+ {
+ ShouldBeAbleToFindChildrenByName();
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ Assert.That(ShouldBeAbleToFindChildrenByName, Throws.Nothing);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public void ShouldNotMixResults()
+ {
+ FindChildrenByName(_parent1Id, _child1Id);
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ FindChildrenByName(_parent2Id, _child3Id);
+ AssertFilterPlanCacheHit(spy);
+ }
+ }
+
+ [Test]
+ public void ShouldNotInitializeCollectionWhenPerformingQuery()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var persistentCollection = (IPersistentCollection) parent.ChildrenMap;
+
+ var filtered = parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(persistentCollection.WasInitialized, Is.False);
+ }
+ }
+
+ [Test]
+ public void ShouldPerformSqlQueryEvenIfCollectionAlreadyInitialized()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var loaded = parent.ChildrenMap.ToList();
+ Assert.That(loaded, Has.Count.EqualTo(2));
+
+ var countBeforeFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ var filtered = parent.ChildrenMap.Values
+ .AsQueryable()
+ .Where(x => x.Name == "Jack")
+ .ToList();
+
+ var countAfterFiltering = session.SessionFactory.Statistics.QueryExecutionCount;
+
+ Assert.That(filtered, Has.Count.EqualTo(1));
+ Assert.That(countAfterFiltering, Is.EqualTo(countBeforeFiltering + 1));
+ }
+ }
+
+ [Test]
+ public void TestFilter()
+ {
+ using (var session = OpenSession())
+ using (session.BeginTransaction())
+ {
+ var parent = session.Get(_parent1Id);
+ Assert.That(parent, Is.Not.Null);
+
+ var children = session.CreateFilter(parent.ChildrenMap, "where this.Name = 'Jack'")
+ .List();
+
+ Assert.That(children, Has.Count.EqualTo(1));
+ }
+ }
+
+ [Test]
+ public void TestPlanCacheMiss()
+ {
+ var internalPlanCache = typeof(QueryPlanCache)
+ .GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(Sfi.QueryPlanCache) as SoftLimitMRUCache;
+ Assert.That(internalPlanCache, Is.Not.Null,
+ $"Unable to find the internal query plan cache for clearing it, please adapt code to current {nameof(QueryPlanCache)} implementation.");
+
+ using (var spy = new LogSpy(typeof(QueryPlanCache)))
+ {
+ internalPlanCache.Clear();
+ ShouldBeAbleToFindChildrenByName();
+ AssertFilterPlanCacheMiss(spy);
+ }
+ }
+
+ private const string _filterPlanCacheMissLog = "unable to locate collection-filter query plan in cache";
+
+ private static void AssertFilterPlanCacheHit(LogSpy spy) =>
+ // Each query currently ask the cache two times, so asserting reuse requires to check cache has not been missed
+ // rather than only asserting it has been hit.
+ Assert.That(spy.GetWholeLog(),
+ Contains.Substring("located collection-filter query plan in cache (")
+ .And.Not.Contains(_filterPlanCacheMissLog));
+
+ private static void AssertFilterPlanCacheMiss(LogSpy spy) =>
+ Assert.That(spy.GetWholeLog(), Contains.Substring(_filterPlanCacheMissLog));
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.SetProperty("show_sql", "true");
+ configuration.SetProperty("generate_statistics", "true");
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var parent1 = new Parent { Name = "Bob" };
+ _parent1Id = (Guid) session.Save(parent1);
+
+ var parent2 = new Parent { Name = "Martin" };
+ _parent2Id = (Guid) session.Save(parent2);
+
+ var child1 = new Child
+ {
+ Name = "Jack",
+ Parent = parent1
+ };
+ _child1Id = (Guid) session.Save(child1);
+ parent1.ChildrenMap.Add(child1.Id, child1);
+
+ var child2 = new Child
+ {
+ Name = "Piter",
+ Parent = parent1
+ };
+ session.Save(child2);
+ parent1.ChildrenMap.Add(child2.Id, child2);
+
+ var grandChild1 = new GrandChild
+ {
+ Name = "Kate",
+ Child = child2
+ };
+ session.Save(grandChild1);
+ child2.GrandChildren.Add(grandChild1);
+
+ var grandChild2 = new GrandChild
+ {
+ Name = "Mary",
+ Child = child2
+ };
+ session.Save(grandChild2);
+ child2.GrandChildren.Add(grandChild2);
+
+ var child3 = new Child
+ {
+ Name = "Jack",
+ Parent = parent2
+ };
+ _child3Id = (Guid) session.Save(child3);
+ parent2.ChildrenMap.Add(child1.Id, child3);
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Map(x => x.ChildrenMap,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ rc.Set(x => x.GrandChildren,
+ map => map.Cascade(Mapping.ByCode.Cascade.All | Mapping.ByCode.Cascade.DeleteOrphans),
+ rel => rel.OneToMany());
+ });
+
+ mapper.Class(rc =>
+ {
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs
new file mode 100644
index 00000000000..ba5ef9b1368
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/NH2319/Model.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+
+namespace NHibernate.Test.NHSpecificTest.NH2319
+{
+ class Parent
+ {
+ public virtual Guid Id { get; set; }
+ public virtual string Name { get; set; }
+ public virtual ICollection Children { get; set; } = new List();
+ public virtual IDictionary ChildrenMap { get; set; } = new Dictionary();
+ }
+
+ class Child
+ {
+ public virtual Parent Parent { get; set; }
+ public virtual Guid Id { get; set; }
+ public virtual string Name { get; set; }
+ public virtual ICollection GrandChildren { get; set; } = new List();
+ public virtual ICollection Parents { get; set; } = new List();
+ }
+ class GrandChild
+ {
+ public virtual Child Child { get; set; }
+ public virtual Guid Id { get; set; }
+ public virtual string Name { get; set; }
+ public virtual ICollection ParentChidren { get; set; } = new List();
+ }
+}
diff --git a/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs b/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs
index 4852e0315aa..bf0f8338457 100644
--- a/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs
+++ b/src/NHibernate.Test/SystemTransactions/ResourceManagerFixture.cs
@@ -684,7 +684,6 @@ void Clone_TransactionCompleted(object sender, TransactionEventArgs e)
[OneTimeSetUp]
public void TestFixtureSetUp()
{
- ((Logger)_log.Logger).Level = log4net.Core.Level.Info;
_spy = new LogSpy(_log);
_spy.Appender.Layout = new PatternLayout("%d{ABSOLUTE} [%t] - %m%n");
}
diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs
index f681e4940b9..4275faabc27 100644
--- a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs
+++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs
@@ -13,8 +13,11 @@
using System.Collections.Generic;
using System.Data.Common;
using System.Diagnostics;
+using System.Linq;
+using System.Linq.Expressions;
using NHibernate.DebugHelpers;
using NHibernate.Engine;
+using NHibernate.Linq;
using NHibernate.Loader;
using NHibernate.Persister.Collection;
using NHibernate.Type;
@@ -27,7 +30,7 @@ namespace NHibernate.Collection.Generic
///
/// Contains generated async methods
///
- public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList
+ public partial class PersistentGenericBag : AbstractPersistentCollection, IList, IList, IQueryable
{
public override async Task