diff --git a/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
index ad8af003134..5d12747ec68 100644
--- a/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
+++ b/src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
@@ -29,7 +29,7 @@ public async Task TestSimpleCacheAsync()
private CacheKey CreateCacheKey(string text)
{
- return new CacheKey(text, NHibernateUtil.String, "Foo", null);
+ return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
}
public async Task DoTestCacheAsync(ICacheProvider cacheProvider, CancellationToken cancellationToken = default(CancellationToken))
diff --git a/src/NHibernate.Test/Async/ConnectionStringTest/NamedConnectionStringFixture.cs b/src/NHibernate.Test/Async/ConnectionStringTest/NamedConnectionStringFixture.cs
index 8d3b82709d0..48a945bba1c 100644
--- a/src/NHibernate.Test/Async/ConnectionStringTest/NamedConnectionStringFixture.cs
+++ b/src/NHibernate.Test/Async/ConnectionStringTest/NamedConnectionStringFixture.cs
@@ -26,9 +26,8 @@ public partial class MockConnectionProvider : ConnectionProvider
///
/// Get an open .
///
- /// A cancellation token that can be used to cancel the work
/// An open .
- public override Task GetConnectionAsync(CancellationToken cancellationToken)
+ public override Task GetConnectionAsync(string connectionString, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
diff --git a/src/NHibernate.Test/Async/DebugSessionFactory.cs b/src/NHibernate.Test/Async/DebugSessionFactory.cs
index 54b9e7ac6ad..5c24cd724c5 100644
--- a/src/NHibernate.Test/Async/DebugSessionFactory.cs
+++ b/src/NHibernate.Test/Async/DebugSessionFactory.cs
@@ -26,6 +26,7 @@
using NHibernate.Id;
using NHibernate.Impl;
using NHibernate.Metadata;
+using NHibernate.MultiTenancy;
using NHibernate.Persister.Collection;
using NHibernate.Persister.Entity;
using NHibernate.Proxy;
diff --git a/src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs b/src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs
index 96d43ef72d0..9dd89c37955 100644
--- a/src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs
+++ b/src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs
@@ -45,7 +45,7 @@ public async Task SecondLevelCachedCollectionsFilteringAsync()
var persister = Sfi
.GetCollectionPersister(typeof(Salesperson).FullName + ".Orders");
var cacheKey =
- new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi);
+ new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi, null);
CollectionCacheEntry cachedData;
using (var session = OpenSession())
diff --git a/src/NHibernate.Test/Async/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs b/src/NHibernate.Test/Async/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs
new file mode 100644
index 00000000000..3fc06fb5a44
--- /dev/null
+++ b/src/NHibernate.Test/Async/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs
@@ -0,0 +1,265 @@
+//------------------------------------------------------------------------------
+//
+// 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.Data.Common;
+using System.Data.SqlClient;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization.Formatters.Binary;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Dialect;
+using NHibernate.Driver;
+using NHibernate.Engine;
+using NHibernate.Linq;
+using NHibernate.Mapping.ByCode;
+using NHibernate.MultiTenancy;
+using NHibernate.Util;
+using NUnit.Framework;
+
+namespace NHibernate.Test.MultiTenancy
+{
+ using System.Threading.Tasks;
+ [TestFixture]
+ public class DatabaseStrategyNoDbSpecificFixtureAsync : TestCaseMappingByCode
+ {
+ private Guid _id;
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.DataBaseIntegration(
+ x =>
+ {
+ x.MultiTenancy = MultiTenancyStrategy.Database;
+ x.MultiTenancyConnectionProvider();
+ });
+ configuration.Properties[Cfg.Environment.GenerateStatistics] = "true";
+ base.Configure(configuration);
+ }
+
+ private static void ValidateSqlServerConnectionAppName(ISession s, string tenantId)
+ {
+ var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
+ Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
+ }
+
+ private static void ValidateSqlServerConnectionAppName(IStatelessSession s, string tenantId)
+ {
+ var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
+ Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
+ }
+
+ [Test]
+ public async Task SecondLevelCacheReusedForSameTenantAsync()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen1.GetAsync(_id));
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen2.GetAsync(_id));
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task SecondLevelCacheSeparationPerTenantAsync()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen1.GetAsync(_id));
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant2"))
+ {
+ var entity = await (sesTen2.GetAsync(_id));
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(0));
+ }
+
+ [Test]
+ public async Task QueryCacheReusedForSameTenantAsync()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen2.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public async Task QueryCacheSeparationPerTenantAsync()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant2"))
+ {
+ var entity = await (sesTen2.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
+ Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0));
+ }
+
+ [Test]
+ public async Task TenantSessionIsSerializableAndCanBeReconnectedAsync()
+ {
+ ISession deserializedSession = null;
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = await (sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
+ sesTen1.Disconnect();
+ deserializedSession = SpoofSerialization(sesTen1);
+ }
+
+ Sfi.Statistics.Clear();
+ using (deserializedSession)
+ {
+ deserializedSession.Reconnect();
+
+ //Expect session cache hit
+ var entity = await (deserializedSession.GetAsync(_id));
+ if (IsSqlServerDialect)
+ ValidateSqlServerConnectionAppName(deserializedSession, "tenant1");
+ deserializedSession.Clear();
+
+ //Expect second level cache hit
+ await (deserializedSession.GetAsync(_id));
+ Assert.That(GetTenantId(deserializedSession), Is.EqualTo("tenant1"));
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
+ }
+
+ private static string GetTenantId(ISession session)
+ {
+ return session.GetSessionImplementation().GetTenantIdentifier();
+ }
+
+ private static string GetTenantId(IStatelessSession session)
+ {
+ return session.GetSessionImplementation().GetTenantIdentifier();
+ }
+
+ private T SpoofSerialization(T session)
+ {
+ var formatter = new BinaryFormatter
+ {
+#if !NETFX
+ SurrogateSelector = new SerializationHelper.SurrogateSelector()
+#endif
+ };
+ var stream = new MemoryStream();
+ formatter.Serialize(stream, session);
+
+ stream.Position = 0;
+
+ return (T) formatter.Deserialize(stream);
+ }
+
+ private ISession OpenTenantSession(string tenantId)
+ {
+ return Sfi.WithOptions().Tenant(GetTenantConfig(tenantId)).OpenSession();
+ }
+
+ private IStatelessSession OpenTenantStatelessSession(string tenantId)
+ {
+ return Sfi.WithStatelessOptions().Tenant(GetTenantConfig(tenantId)).OpenStatelessSession();
+ }
+
+ private TenantConfiguration GetTenantConfig(string tenantId)
+ {
+ return new TestTenantConfiguration(tenantId, IsSqlServerDialect);
+ }
+
+ private bool IsSqlServerDialect => Sfi.Dialect is MsSql2000Dialect && !(Sfi.ConnectionProvider.Driver is OdbcDriver);
+
+ #region Test Setup
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+
+ mapper.Class(
+ rc =>
+ {
+ rc.Cache(m => m.Usage(CacheUsage.NonstrictReadWrite));
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+
+ protected override DbConnection OpenConnectionForSchemaExport()
+ {
+ return Sfi.Settings.MultiTenancyConnectionProvider
+ .GetConnectionAccess(GetTenantConfig("defaultTenant"), Sfi).GetConnection();
+ }
+
+ protected override ISession OpenSession()
+ {
+ return OpenTenantSession("defaultTenant");
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var e1 = new Entity {Name = "Bob"};
+ session.Save(e1);
+
+ var e2 = new Entity {Name = "Sally"};
+ session.Save(e2);
+
+ session.Flush();
+ transaction.Commit();
+ _id = e1.Id;
+ }
+ }
+
+ #endregion Test Setup
+ }
+}
diff --git a/src/NHibernate.Test/CacheTest/CacheFixture.cs b/src/NHibernate.Test/CacheTest/CacheFixture.cs
index 2c96ba341dd..5c86f6ced46 100644
--- a/src/NHibernate.Test/CacheTest/CacheFixture.cs
+++ b/src/NHibernate.Test/CacheTest/CacheFixture.cs
@@ -18,7 +18,7 @@ public void TestSimpleCache()
private CacheKey CreateCacheKey(string text)
{
- return new CacheKey(text, NHibernateUtil.String, "Foo", null);
+ return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
}
public void DoTestCache(ICacheProvider cacheProvider)
diff --git a/src/NHibernate.Test/CacheTest/QueryKeyFixture.cs b/src/NHibernate.Test/CacheTest/QueryKeyFixture.cs
index dd62799eb28..6ed9fd8a584 100644
--- a/src/NHibernate.Test/CacheTest/QueryKeyFixture.cs
+++ b/src/NHibernate.Test/CacheTest/QueryKeyFixture.cs
@@ -35,13 +35,13 @@ private void QueryKeyFilterDescLikeToCompare(out QueryKey qk, out QueryKey qk1,
f.SetParameter("pLike", "so%");
var fk = new FilterKey(f);
ISet fks = new HashSet { fk };
- qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
var f1 = new FilterImpl(Sfi.GetFilterDefinition(filterName));
f1.SetParameter("pLike", sameValue ? "so%" : "%ing");
var fk1 = new FilterKey(f1);
fks = new HashSet { fk1 };
- qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
}
private void QueryKeyFilterDescValueToCompare(out QueryKey qk, out QueryKey qk1, bool sameValue)
@@ -52,13 +52,13 @@ private void QueryKeyFilterDescValueToCompare(out QueryKey qk, out QueryKey qk1,
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
var fk = new FilterKey(f);
ISet fks = new HashSet { fk };
- qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
var f1 = new FilterImpl(Sfi.GetFilterDefinition(filterName));
f1.SetParameter("pDesc", "something").SetParameter("pValue", sameValue ? 10 : 11);
var fk1 = new FilterKey(f1);
fks = new HashSet { fk1 };
- qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
}
[Test]
@@ -122,7 +122,7 @@ public void ToStringWithFilters()
f.SetParameter("pLike", "so%");
var fk = new FilterKey(f);
ISet fks = new HashSet { fk };
- var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}']"), "Like");
filterName = "DescriptionEqualAndValueGT";
@@ -130,7 +130,7 @@ public void ToStringWithFilters()
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
fk = new FilterKey(f);
fks = new HashSet { fk };
- qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}']"), "Value");
}
@@ -148,7 +148,7 @@ public void ToStringWithMoreFilters()
var fvk = new FilterKey(fv);
ISet fks = new HashSet { fk, fvk };
- var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
+ var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}', '{fvk}']"));
}
}
diff --git a/src/NHibernate.Test/CfgTest/Loquacious/LambdaConfigurationFixture.cs b/src/NHibernate.Test/CfgTest/Loquacious/LambdaConfigurationFixture.cs
index fcf841757a0..373543bc103 100644
--- a/src/NHibernate.Test/CfgTest/Loquacious/LambdaConfigurationFixture.cs
+++ b/src/NHibernate.Test/CfgTest/Loquacious/LambdaConfigurationFixture.cs
@@ -3,7 +3,6 @@
using NHibernate.Bytecode;
using NHibernate.Cache;
using NHibernate.Cfg;
-using NHibernate.Cfg.Loquacious;
using NHibernate.Dialect;
using NHibernate.Driver;
using NHibernate.Hql.Ast.ANTLR;
@@ -11,6 +10,7 @@
using NHibernate.Type;
using NUnit.Framework;
using NHibernate.Exceptions;
+using NHibernate.MultiTenancy;
namespace NHibernate.Test.CfgTest.Loquacious
{
@@ -64,6 +64,7 @@ public void FullConfiguration()
db.HqlToSqlSubstitutions = "true 1, false 0, yes 'Y', no 'N'";
db.SchemaAction = SchemaAutoAction.Validate;
db.ThrowOnSchemaUpdate = true;
+ db.MultiTenancy = MultiTenancyStrategy.Database;
});
Assert.That(configure.Properties[Environment.SessionFactoryName], Is.EqualTo("SomeName"));
@@ -109,7 +110,8 @@ public void FullConfiguration()
Assert.That(configure.Properties[Environment.Hbm2ddlAuto], Is.EqualTo("validate"));
Assert.That(configure.Properties[Environment.Hbm2ddlThrowOnUpdate], Is.EqualTo("true"));
Assert.That(configure.Properties[Environment.LinqToHqlGeneratorsRegistry], Is.EqualTo(typeof(DefaultLinqToHqlGeneratorsRegistry).AssemblyQualifiedName));
-
+ Assert.That(configure.Properties[Environment.MultiTenancy], Is.EqualTo(nameof(MultiTenancyStrategy.Database)));
+
// Keywords import and auto-validation require a valid connection string, disable them before checking
// the session factory can be built.
configure.SetProperty(Environment.Hbm2ddlKeyWords, "none");
diff --git a/src/NHibernate.Test/ConnectionStringTest/NamedConnectionStringFixture.cs b/src/NHibernate.Test/ConnectionStringTest/NamedConnectionStringFixture.cs
index 5030067e5e8..5933d5bb65d 100644
--- a/src/NHibernate.Test/ConnectionStringTest/NamedConnectionStringFixture.cs
+++ b/src/NHibernate.Test/ConnectionStringTest/NamedConnectionStringFixture.cs
@@ -58,7 +58,7 @@ public string PublicConnectionString
/// Get an open .
///
/// An open .
- public override DbConnection GetConnection()
+ public override DbConnection GetConnection(string connectionString)
{
throw new NotImplementedException();
}
diff --git a/src/NHibernate.Test/DebugSessionFactory.cs b/src/NHibernate.Test/DebugSessionFactory.cs
index eb43b825eee..7643924119f 100644
--- a/src/NHibernate.Test/DebugSessionFactory.cs
+++ b/src/NHibernate.Test/DebugSessionFactory.cs
@@ -16,6 +16,7 @@
using NHibernate.Id;
using NHibernate.Impl;
using NHibernate.Metadata;
+using NHibernate.MultiTenancy;
using NHibernate.Persister.Collection;
using NHibernate.Persister.Entity;
using NHibernate.Proxy;
@@ -400,7 +401,9 @@ public static ISessionCreationOptions GetCreationOptions(IStatelessSessionBuilde
(ISessionCreationOptions)sessionBuilder;
}
- internal class SessionBuilder : ISessionBuilder
+ internal class SessionBuilder : ISessionBuilder,
+ //TODO 6.0: Remove interface with implementation (will be replaced TenantConfiguration ISessionBuilder method)
+ ISessionCreationOptionsWithMultiTenancy
{
private readonly ISessionBuilder _actualBuilder;
private readonly DebugSessionFactory _debugFactory;
@@ -465,9 +468,17 @@ ISessionBuilder ISessionBuilder.FlushMode(FlushMode flushMode)
}
#endregion
+
+ TenantConfiguration ISessionCreationOptionsWithMultiTenancy.TenantConfiguration
+ {
+ get => (_actualBuilder as ISessionCreationOptionsWithMultiTenancy)?.TenantConfiguration;
+ set => _actualBuilder.Tenant(value);
+ }
}
- internal class StatelessSessionBuilder : IStatelessSessionBuilder
+ internal class StatelessSessionBuilder : IStatelessSessionBuilder,
+ //TODO 6.0: Remove interface with implementation (will be replaced TenantConfiguration IStatelessSessionBuilder method)
+ ISessionCreationOptionsWithMultiTenancy
{
private readonly IStatelessSessionBuilder _actualBuilder;
private readonly DebugSessionFactory _debugFactory;
@@ -501,6 +512,12 @@ IStatelessSessionBuilder IStatelessSessionBuilder.AutoJoinTransaction(bool autoJ
return this;
}
+ TenantConfiguration ISessionCreationOptionsWithMultiTenancy.TenantConfiguration
+ {
+ get => (_actualBuilder as ISessionCreationOptionsWithMultiTenancy)?.TenantConfiguration;
+ set => _actualBuilder.Tenant(value);
+ }
+
#endregion
}
}
diff --git a/src/NHibernate.Test/FilterTest/DynamicFilterTest.cs b/src/NHibernate.Test/FilterTest/DynamicFilterTest.cs
index b345b642ab8..5830894d526 100644
--- a/src/NHibernate.Test/FilterTest/DynamicFilterTest.cs
+++ b/src/NHibernate.Test/FilterTest/DynamicFilterTest.cs
@@ -33,7 +33,7 @@ public void SecondLevelCachedCollectionsFiltering()
var persister = Sfi
.GetCollectionPersister(typeof(Salesperson).FullName + ".Orders");
var cacheKey =
- new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi);
+ new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi, null);
CollectionCacheEntry cachedData;
using (var session = OpenSession())
diff --git a/src/NHibernate.Test/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs b/src/NHibernate.Test/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs
new file mode 100644
index 00000000000..2a37052266b
--- /dev/null
+++ b/src/NHibernate.Test/MultiTenancy/DatabaseStrategyNoDbSpecificFixture.cs
@@ -0,0 +1,334 @@
+using System;
+using System.Data.Common;
+using System.Data.SqlClient;
+using System.IO;
+using System.Linq;
+using System.Runtime.Serialization.Formatters.Binary;
+using NHibernate.Cfg;
+using NHibernate.Cfg.MappingSchema;
+using NHibernate.Dialect;
+using NHibernate.Driver;
+using NHibernate.Engine;
+using NHibernate.Linq;
+using NHibernate.Mapping.ByCode;
+using NHibernate.MultiTenancy;
+using NHibernate.Util;
+using NUnit.Framework;
+
+namespace NHibernate.Test.MultiTenancy
+{
+ [TestFixture]
+ public class DatabaseStrategyNoDbSpecificFixture : TestCaseMappingByCode
+ {
+ private Guid _id;
+
+ protected override void Configure(Configuration configuration)
+ {
+ configuration.DataBaseIntegration(
+ x =>
+ {
+ x.MultiTenancy = MultiTenancyStrategy.Database;
+ x.MultiTenancyConnectionProvider();
+ });
+ configuration.Properties[Cfg.Environment.GenerateStatistics] = "true";
+ base.Configure(configuration);
+ }
+
+ [Test]
+ public void ShouldThrowWithNoTenantIdentifier()
+ {
+ Assert.Throws(() => Sfi.WithOptions().Tenant(new TenantConfiguration(null)));
+ }
+
+ [Test]
+ public void DifferentConnectionStringForDifferentTenants()
+ {
+ if (!IsSqlServerDialect)
+ Assert.Ignore("MSSqlServer specific test");
+
+ using (var session1 = OpenTenantSession("tenant1"))
+ using (var session2 = OpenTenantSession("tenant2"))
+ {
+ Assert.That(session1.Connection.ConnectionString, Is.Not.EqualTo(session2.Connection.ConnectionString));
+ ValidateSqlServerConnectionAppName(session1, "tenant1");
+ ValidateSqlServerConnectionAppName(session2, "tenant2");
+ Assert.That(GetTenantId(session1), Is.EqualTo("tenant1"));
+ Assert.That(GetTenantId(session2), Is.EqualTo("tenant2"));
+ }
+ }
+
+ [Test]
+ public void StatelessSessionShouldThrowWithNoTenantIdentifier()
+ {
+ Assert.Throws(() => Sfi.WithStatelessOptions().Tenant(new TenantConfiguration(null)));
+ }
+
+ [Test]
+ public void StatelessSessionDifferentConnectionStringForDifferentTenants()
+ {
+ if (!IsSqlServerDialect)
+ Assert.Ignore("MSSqlServer specific test");
+
+ using (var session1 = OpenTenantStatelessSession("tenant1"))
+ using (var session2 = OpenTenantStatelessSession("tenant2"))
+ {
+ Assert.That(session1.Connection.ConnectionString, Is.Not.EqualTo(session2.Connection.ConnectionString));
+ ValidateSqlServerConnectionAppName(session1, "tenant1");
+ ValidateSqlServerConnectionAppName(session2, "tenant2");
+ Assert.That(GetTenantId(session1), Is.EqualTo("tenant1"));
+ Assert.That(GetTenantId(session2), Is.EqualTo("tenant2"));
+ }
+ }
+
+ [Test]
+ public void SharedSessionSameConnectionString()
+ {
+ using (var session1 = OpenTenantSession("tenant1"))
+ using (var session2 = session1.SessionWithOptions().OpenSession())
+ {
+ Assert.That(session1.Connection, Is.Not.EqualTo(session2.Connection));
+ Assert.That(session1.Connection.ConnectionString, Is.EqualTo(session2.Connection.ConnectionString));
+ Assert.That(session2.GetSessionImplementation().GetTenantIdentifier(), Is.EqualTo("tenant1"));
+ }
+ }
+
+ [Test]
+ public void SharedSessionSameConnection()
+ {
+ using (var session1 = OpenTenantSession("tenant1"))
+ using (var session2 = session1.SessionWithOptions().Connection().OpenSession())
+ {
+ Assert.That(session1.Connection, Is.EqualTo(session2.Connection));
+ Assert.That(GetTenantId(session2), Is.EqualTo("tenant1"));
+ }
+ }
+
+ [Test]
+ public void SharedStatelessSessionSameConnectionString()
+ {
+ using (var session1 = OpenTenantSession("tenant1"))
+ using (var session2 = session1.StatelessSessionWithOptions().OpenStatelessSession())
+ {
+ Assert.That(session1.Connection.ConnectionString, Is.EqualTo(session2.Connection.ConnectionString));
+ Assert.That(GetTenantId(session2), Is.EqualTo("tenant1"));
+ }
+ }
+
+ private static void ValidateSqlServerConnectionAppName(ISession s, string tenantId)
+ {
+ var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
+ Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
+ }
+
+ private static void ValidateSqlServerConnectionAppName(IStatelessSession s, string tenantId)
+ {
+ var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
+ Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
+ }
+
+ [Test]
+ public void SecondLevelCacheReusedForSameTenant()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen1.Get(_id);
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen2.Get(_id);
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void SecondLevelCacheSeparationPerTenant()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen1.Get(_id);
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant2"))
+ {
+ var entity = sesTen2.Get(_id);
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void QueryCacheReusedForSameTenant()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefault();
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen2.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefault();
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void QueryCacheSeparationPerTenant()
+ {
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefault();
+ }
+
+ Sfi.Statistics.Clear();
+ using (var sesTen2 = OpenTenantSession("tenant2"))
+ {
+ var entity = sesTen2.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefault();
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
+ Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void TenantSessionIsSerializableAndCanBeReconnected()
+ {
+ ISession deserializedSession = null;
+ using (var sesTen1 = OpenTenantSession("tenant1"))
+ {
+ var entity = sesTen1.Query().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefault();
+ sesTen1.Disconnect();
+ deserializedSession = SpoofSerialization(sesTen1);
+ }
+
+ Sfi.Statistics.Clear();
+ using (deserializedSession)
+ {
+ deserializedSession.Reconnect();
+
+ //Expect session cache hit
+ var entity = deserializedSession.Get(_id);
+ if (IsSqlServerDialect)
+ ValidateSqlServerConnectionAppName(deserializedSession, "tenant1");
+ deserializedSession.Clear();
+
+ //Expect second level cache hit
+ deserializedSession.Get(_id);
+ Assert.That(GetTenantId(deserializedSession), Is.EqualTo("tenant1"));
+ }
+
+ Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
+ Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
+ }
+
+ private static string GetTenantId(ISession session)
+ {
+ return session.GetSessionImplementation().GetTenantIdentifier();
+ }
+
+ private static string GetTenantId(IStatelessSession session)
+ {
+ return session.GetSessionImplementation().GetTenantIdentifier();
+ }
+
+ private T SpoofSerialization(T session)
+ {
+ var formatter = new BinaryFormatter
+ {
+#if !NETFX
+ SurrogateSelector = new SerializationHelper.SurrogateSelector()
+#endif
+ };
+ var stream = new MemoryStream();
+ formatter.Serialize(stream, session);
+
+ stream.Position = 0;
+
+ return (T) formatter.Deserialize(stream);
+ }
+
+ private ISession OpenTenantSession(string tenantId)
+ {
+ return Sfi.WithOptions().Tenant(GetTenantConfig(tenantId)).OpenSession();
+ }
+
+ private IStatelessSession OpenTenantStatelessSession(string tenantId)
+ {
+ return Sfi.WithStatelessOptions().Tenant(GetTenantConfig(tenantId)).OpenStatelessSession();
+ }
+
+ private TenantConfiguration GetTenantConfig(string tenantId)
+ {
+ return new TestTenantConfiguration(tenantId, IsSqlServerDialect);
+ }
+
+ private bool IsSqlServerDialect => Sfi.Dialect is MsSql2000Dialect && !(Sfi.ConnectionProvider.Driver is OdbcDriver);
+
+ #region Test Setup
+
+ protected override HbmMapping GetMappings()
+ {
+ var mapper = new ModelMapper();
+
+ mapper.Class(
+ rc =>
+ {
+ rc.Cache(m => m.Usage(CacheUsage.NonstrictReadWrite));
+ rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
+ rc.Property(x => x.Name);
+ });
+
+ return mapper.CompileMappingForAllExplicitlyAddedEntities();
+ }
+
+ protected override DbConnection OpenConnectionForSchemaExport()
+ {
+ return Sfi.Settings.MultiTenancyConnectionProvider
+ .GetConnectionAccess(GetTenantConfig("defaultTenant"), Sfi).GetConnection();
+ }
+
+ protected override ISession OpenSession()
+ {
+ return OpenTenantSession("defaultTenant");
+ }
+
+ protected override void OnTearDown()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ session.Delete("from System.Object");
+
+ session.Flush();
+ transaction.Commit();
+ }
+ }
+
+ protected override void OnSetUp()
+ {
+ using (var session = OpenSession())
+ using (var transaction = session.BeginTransaction())
+ {
+ var e1 = new Entity {Name = "Bob"};
+ session.Save(e1);
+
+ var e2 = new Entity {Name = "Sally"};
+ session.Save(e2);
+
+ session.Flush();
+ transaction.Commit();
+ _id = e1.Id;
+ }
+ }
+
+ #endregion Test Setup
+ }
+}
diff --git a/src/NHibernate.Test/MultiTenancy/Entity.cs b/src/NHibernate.Test/MultiTenancy/Entity.cs
new file mode 100644
index 00000000000..04636a58484
--- /dev/null
+++ b/src/NHibernate.Test/MultiTenancy/Entity.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace NHibernate.Test.MultiTenancy
+{
+ [Serializable]
+ class Entity
+ {
+ public virtual Guid Id { get; set; }
+ public virtual string Name { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/MultiTenancy/TestMultiTenancyConnectionProvider.cs b/src/NHibernate.Test/MultiTenancy/TestMultiTenancyConnectionProvider.cs
new file mode 100644
index 00000000000..b690b78f14c
--- /dev/null
+++ b/src/NHibernate.Test/MultiTenancy/TestMultiTenancyConnectionProvider.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Data.SqlClient;
+using NHibernate.Connection;
+using NHibernate.Engine;
+using NHibernate.MultiTenancy;
+
+namespace NHibernate.Test.MultiTenancy
+{
+ [Serializable]
+ public class TestMultiTenancyConnectionProvider : AbstractMultiTenancyConnectionProvider
+ {
+ protected override string GetTenantConnectionString(TenantConfiguration tenantConfiguration, ISessionFactoryImplementor sessionFactory)
+ {
+ return tenantConfiguration is TestTenantConfiguration tenant && tenant.IsSqlServerDialect
+ ? new SqlConnectionStringBuilder(sessionFactory.ConnectionProvider.GetConnectionString()) {ApplicationName = tenantConfiguration.TenantIdentifier}.ToString()
+ : sessionFactory.ConnectionProvider.GetConnectionString();
+ }
+ }
+}
diff --git a/src/NHibernate.Test/MultiTenancy/TestTenantConfiguration.cs b/src/NHibernate.Test/MultiTenancy/TestTenantConfiguration.cs
new file mode 100644
index 00000000000..b743cc95ae0
--- /dev/null
+++ b/src/NHibernate.Test/MultiTenancy/TestTenantConfiguration.cs
@@ -0,0 +1,16 @@
+using System;
+using NHibernate.MultiTenancy;
+
+namespace NHibernate.Test.MultiTenancy
+{
+ [Serializable]
+ public class TestTenantConfiguration : TenantConfiguration
+ {
+ public TestTenantConfiguration(string tenantIdentifier, bool isSqlServerDialect) : base(tenantIdentifier)
+ {
+ IsSqlServerDialect = isSqlServerDialect;
+ }
+
+ public bool IsSqlServerDialect { get; }
+ }
+}
diff --git a/src/NHibernate.Test/TestCase.cs b/src/NHibernate.Test/TestCase.cs
index f7505d3971f..f1d04a96640 100644
--- a/src/NHibernate.Test/TestCase.cs
+++ b/src/NHibernate.Test/TestCase.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Data;
+using System.Data.Common;
using System.Reflection;
using log4net;
using NHibernate.Cfg;
@@ -237,7 +238,7 @@ protected virtual bool CheckDatabaseWasCleaned()
}
bool empty;
- using (ISession s = Sfi.OpenSession())
+ using (ISession s = OpenSession())
{
IList objects = s.CreateQuery("from System.Object o").List();
empty = objects.Count == 0;
@@ -291,15 +292,16 @@ protected virtual void AddMappings(Configuration configuration)
protected virtual void CreateSchema()
{
- SchemaExport.Create(OutputDdl, true);
+ using (var optionalConnection = OpenConnectionForSchemaExport())
+ SchemaExport.Create(OutputDdl, true, optionalConnection);
}
protected virtual void DropSchema()
{
- DropSchema(OutputDdl, SchemaExport, Sfi);
+ DropSchema(OutputDdl, SchemaExport, Sfi, OpenConnectionForSchemaExport);
}
- public static void DropSchema(bool useStdOut, SchemaExport export, ISessionFactoryImplementor sfi)
+ public static void DropSchema(bool useStdOut, SchemaExport export, ISessionFactoryImplementor sfi, Func getConnection = null)
{
if (sfi?.ConnectionProvider.Driver is FirebirdClientDriver fbDriver)
{
@@ -312,7 +314,16 @@ public static void DropSchema(bool useStdOut, SchemaExport export, ISessionFacto
fbDriver.ClearPool(null);
}
- export.Drop(useStdOut, true);
+ using(var optionalConnection = getConnection?.Invoke())
+ export.Drop(useStdOut, true, optionalConnection);
+ }
+
+ ///
+ /// Specific connection is required only for Database multi-tenancy. In other cases can be null
+ ///
+ protected virtual DbConnection OpenConnectionForSchemaExport()
+ {
+ return null;
}
protected virtual DebugSessionFactory BuildSessionFactory()
diff --git a/src/NHibernate/Action/CollectionAction.cs b/src/NHibernate/Action/CollectionAction.cs
index ca5be713bf6..e676e7d4f18 100644
--- a/src/NHibernate/Action/CollectionAction.cs
+++ b/src/NHibernate/Action/CollectionAction.cs
@@ -124,7 +124,7 @@ public virtual void BeforeExecutions()
public virtual void ExecuteAfterTransactionCompletion(bool success)
{
- var ck = new CacheKey(key, persister.KeyType, persister.Role, Session.Factory);
+ var ck = session.GenerateCacheKey(key, persister.KeyType, persister.Role);
persister.Cache.Release(ck, softLock);
}
diff --git a/src/NHibernate/AdoNet/ConnectionManager.cs b/src/NHibernate/AdoNet/ConnectionManager.cs
index ee7f4898651..5ebf51a7fb1 100644
--- a/src/NHibernate/AdoNet/ConnectionManager.cs
+++ b/src/NHibernate/AdoNet/ConnectionManager.cs
@@ -4,8 +4,9 @@
using System.Data.Common;
using System.Runtime.Serialization;
using System.Security;
-
+using NHibernate.Connection;
using NHibernate.Engine;
+using NHibernate.Impl;
namespace NHibernate.AdoNet
{
@@ -19,6 +20,7 @@ namespace NHibernate.AdoNet
[Serializable]
public partial class ConnectionManager : ISerializable, IDeserializationCallback
{
+ private readonly IConnectionAccess _connectionAccess;
private static readonly INHibernateLogger _log = NHibernateLogger.For(typeof(ConnectionManager));
[NonSerialized]
@@ -78,7 +80,8 @@ public ConnectionManager(
DbConnection suppliedConnection,
ConnectionReleaseMode connectionReleaseMode,
IInterceptor interceptor,
- bool shouldAutoJoinTransaction)
+ bool shouldAutoJoinTransaction,
+ IConnectionAccess connectionAccess)
{
Session = session;
_connection = suppliedConnection;
@@ -89,6 +92,18 @@ public ConnectionManager(
_ownConnection = suppliedConnection == null;
ShouldAutoJoinTransaction = shouldAutoJoinTransaction;
+ _connectionAccess = connectionAccess ?? throw new ArgumentNullException(nameof(connectionAccess));
+ }
+
+ //Since 5.3
+ [Obsolete("Use overload with connectionAccess parameter")]
+ public ConnectionManager(
+ ISessionImplementor session,
+ DbConnection suppliedConnection,
+ ConnectionReleaseMode connectionReleaseMode,
+ IInterceptor interceptor,
+ bool shouldAutoJoinTransaction) : this(session, suppliedConnection, connectionReleaseMode, interceptor, shouldAutoJoinTransaction, new NonContextualConnectionAccess(session.Factory))
+ {
}
public void AddDependentSession(ISessionImplementor session)
@@ -148,7 +163,7 @@ public DbConnection Close()
if (_backupConnection != null)
{
_log.Warn("Backup connection was still defined at time of closing.");
- Factory.ConnectionProvider.CloseConnection(_backupConnection);
+ _connectionAccess.CloseConnection(_backupConnection);
_backupConnection = null;
}
@@ -205,7 +220,7 @@ public DbConnection Disconnect()
private void CloseConnection()
{
- Factory.ConnectionProvider.CloseConnection(_connection);
+ _connectionAccess.CloseConnection(_connection);
_connection = null;
}
@@ -239,7 +254,7 @@ public DbConnection GetConnection()
{
if (_ownConnection)
{
- _connection = Factory.ConnectionProvider.GetConnection();
+ _connection = _connectionAccess.GetConnection();
// Will fail if the connection is already enlisted in another transaction.
// Probable case: nested transaction scope with connection auto-enlistment enabled.
// That is an user error.
@@ -362,6 +377,7 @@ private ConnectionManager(SerializationInfo info, StreamingContext context)
_connectionReleaseMode =
(ConnectionReleaseMode)info.GetValue("connectionReleaseMode", typeof(ConnectionReleaseMode));
_interceptor = (IInterceptor)info.GetValue("interceptor", typeof(IInterceptor));
+ _connectionAccess = (IConnectionAccess) info.GetValue("connectionAccess", typeof(IConnectionAccess));
}
[SecurityCritical]
@@ -371,6 +387,7 @@ public void GetObjectData(SerializationInfo info, StreamingContext context)
info.AddValue("session", Session, typeof(ISessionImplementor));
info.AddValue("connectionReleaseMode", _connectionReleaseMode, typeof(ConnectionReleaseMode));
info.AddValue("interceptor", _interceptor, typeof(IInterceptor));
+ info.AddValue("connectionAccess", _connectionAccess, typeof(IConnectionAccess));
}
#endregion
diff --git a/src/NHibernate/Async/Action/CollectionAction.cs b/src/NHibernate/Async/Action/CollectionAction.cs
index 16a2a27460e..237d2c34afe 100644
--- a/src/NHibernate/Async/Action/CollectionAction.cs
+++ b/src/NHibernate/Async/Action/CollectionAction.cs
@@ -74,8 +74,15 @@ public virtual Task ExecuteAfterTransactionCompletionAsync(bool success, Cancell
{
return Task.FromCanceled