Skip to content

Multi-Tenancy: Implement tenant per Database strategy #2108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jul 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
58ff5c5
MultiTenancy: Implement tenant per Database strategy
bahusoid Apr 8, 2019
7422868
Fix serialization
bahusoid Apr 8, 2019
36d6d98
Fix ODBC test
bahusoid Apr 8, 2019
67bac34
Small clean up and refactoring
bahusoid Apr 9, 2019
674ab71
Tenant aware SchemaExport
bahusoid Apr 9, 2019
07700c5
Fix async ambiguous methods
bahusoid Apr 9, 2019
6203c57
Commented
bahusoid Apr 9, 2019
37553dd
Fix firebird tests
bahusoid Apr 9, 2019
b68ed76
Shared and stateless session support;
bahusoid Apr 10, 2019
5017569
Fix CI compiler error
bahusoid Apr 10, 2019
ba95ae6
Undo obsolete change
bahusoid Jun 18, 2019
9461f93
Fix CodeFactor issues
bahusoid May 19, 2020
16a596d
whitespaces
bahusoid May 24, 2020
8c80941
fix build after merge
bahusoid Jun 8, 2020
d628e84
Move IMultiTenancyConnectionProvider to session factory configuration
bahusoid Jun 22, 2020
6d11bad
Fix CodeFactor issues
bahusoid Jun 22, 2020
839cfb5
Commented
bahusoid Jun 22, 2020
4fcd32f
Code refactoring
maca88 Jun 25, 2020
d9227cd
Commented
bahusoid Jun 26, 2020
1371a73
Commented
bahusoid Jun 26, 2020
fa3eddd
Avoid breaking change
bahusoid Jun 26, 2020
ebcb668
fixup! Avoid breaking change
bahusoid Jun 26, 2020
f368c66
Code review changes
bahusoid Jul 5, 2020
d74aded
Add multi-tenancy EvictEntity and EvictCollection
bahusoid Jul 5, 2020
6e39aa1
Code review changes
bahusoid Jul 5, 2020
a4013ac
Code review changes. Made GetConnection() virtual
bahusoid Jul 5, 2020
f4dc185
Expose session TenantConfiguration
bahusoid Jul 5, 2020
50a64ad
Merge branch 'master' into multiTenancy
fredericDelaporte Jul 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/NHibernate.Test/Async/CacheTest/CacheFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ public partial class MockConnectionProvider : ConnectionProvider
/// <summary>
/// Get an open <see cref="DbConnection"/>.
/// </summary>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
/// <returns>An open <see cref="DbConnection"/>.</returns>
public override Task<DbConnection> GetConnectionAsync(CancellationToken cancellationToken)
public override Task<DbConnection> GetConnectionAsync(string connectionString, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
Expand Down
1 change: 1 addition & 0 deletions src/NHibernate.Test/Async/DebugSessionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


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<TestMultiTenancyConnectionProvider>();
});
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<Entity>(_id));
}

Sfi.Statistics.Clear();
using (var sesTen2 = OpenTenantSession("tenant1"))
{
var entity = await (sesTen2.GetAsync<Entity>(_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<Entity>(_id));
}

Sfi.Statistics.Clear();
using (var sesTen2 = OpenTenantSession("tenant2"))
{
var entity = await (sesTen2.GetAsync<Entity>(_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<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
}

Sfi.Statistics.Clear();
using (var sesTen2 = OpenTenantSession("tenant1"))
{
var entity = await (sesTen2.Query<Entity>().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<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
}

Sfi.Statistics.Clear();
using (var sesTen2 = OpenTenantSession("tenant2"))
{
var entity = await (sesTen2.Query<Entity>().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<Entity>().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<Entity>(_id));
if (IsSqlServerDialect)
ValidateSqlServerConnectionAppName(deserializedSession, "tenant1");
deserializedSession.Clear();

//Expect second level cache hit
await (deserializedSession.GetAsync<Entity>(_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>(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<Entity>(
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
}
}
2 changes: 1 addition & 1 deletion src/NHibernate.Test/CacheTest/CacheFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions src/NHibernate.Test/CacheTest/QueryKeyFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ private void QueryKeyFilterDescLikeToCompare(out QueryKey qk, out QueryKey qk1,
f.SetParameter("pLike", "so%");
var fk = new FilterKey(f);
ISet<FilterKey> fks = new HashSet<FilterKey> { 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<FilterKey> { 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)
Expand All @@ -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<FilterKey> fks = new HashSet<FilterKey> { 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<FilterKey> { fk1 };
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
}

[Test]
Expand Down Expand Up @@ -122,15 +122,15 @@ public void ToStringWithFilters()
f.SetParameter("pLike", "so%");
var fk = new FilterKey(f);
ISet<FilterKey> fks = new HashSet<FilterKey> { 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";
f = new FilterImpl(Sfi.GetFilterDefinition(filterName));
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
fk = new FilterKey(f);
fks = new HashSet<FilterKey> { 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");
}

Expand All @@ -148,7 +148,7 @@ public void ToStringWithMoreFilters()
var fvk = new FilterKey(fv);

ISet<FilterKey> fks = new HashSet<FilterKey> { 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}']"));
}
}
Expand Down
Loading