Skip to content

Commit fbf41a6

Browse files
bahusoidmaca88
andauthored
Multi-Tenancy: Implement tenant per Database strategy (#2108)
Co-authored-by: maca88 <[email protected]>
1 parent 752fd62 commit fbf41a6

File tree

61 files changed

+1800
-153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1800
-153
lines changed

src/NHibernate.Test/Async/CacheTest/CacheFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task TestSimpleCacheAsync()
2929

3030
private CacheKey CreateCacheKey(string text)
3131
{
32-
return new CacheKey(text, NHibernateUtil.String, "Foo", null);
32+
return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
3333
}
3434

3535
public async Task DoTestCacheAsync(ICacheProvider cacheProvider, CancellationToken cancellationToken = default(CancellationToken))

src/NHibernate.Test/Async/ConnectionStringTest/NamedConnectionStringFixture.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ public partial class MockConnectionProvider : ConnectionProvider
2626
/// <summary>
2727
/// Get an open <see cref="DbConnection"/>.
2828
/// </summary>
29-
/// <param name="cancellationToken">A cancellation token that can be used to cancel the work</param>
3029
/// <returns>An open <see cref="DbConnection"/>.</returns>
31-
public override Task<DbConnection> GetConnectionAsync(CancellationToken cancellationToken)
30+
public override Task<DbConnection> GetConnectionAsync(string connectionString, CancellationToken cancellationToken)
3231
{
3332
throw new NotImplementedException();
3433
}

src/NHibernate.Test/Async/DebugSessionFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using NHibernate.Id;
2727
using NHibernate.Impl;
2828
using NHibernate.Metadata;
29+
using NHibernate.MultiTenancy;
2930
using NHibernate.Persister.Collection;
3031
using NHibernate.Persister.Entity;
3132
using NHibernate.Proxy;

src/NHibernate.Test/Async/FilterTest/DynamicFilterTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public async Task SecondLevelCachedCollectionsFilteringAsync()
4545
var persister = Sfi
4646
.GetCollectionPersister(typeof(Salesperson).FullName + ".Orders");
4747
var cacheKey =
48-
new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi);
48+
new CacheKey(testData.steveId, persister.KeyType, persister.Role, Sfi, null);
4949
CollectionCacheEntry cachedData;
5050

5151
using (var session = OpenSession())
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Data.Common;
13+
using System.Data.SqlClient;
14+
using System.IO;
15+
using System.Linq;
16+
using System.Runtime.Serialization.Formatters.Binary;
17+
using NHibernate.Cfg;
18+
using NHibernate.Cfg.MappingSchema;
19+
using NHibernate.Dialect;
20+
using NHibernate.Driver;
21+
using NHibernate.Engine;
22+
using NHibernate.Linq;
23+
using NHibernate.Mapping.ByCode;
24+
using NHibernate.MultiTenancy;
25+
using NHibernate.Util;
26+
using NUnit.Framework;
27+
28+
namespace NHibernate.Test.MultiTenancy
29+
{
30+
using System.Threading.Tasks;
31+
[TestFixture]
32+
public class DatabaseStrategyNoDbSpecificFixtureAsync : TestCaseMappingByCode
33+
{
34+
private Guid _id;
35+
36+
protected override void Configure(Configuration configuration)
37+
{
38+
configuration.DataBaseIntegration(
39+
x =>
40+
{
41+
x.MultiTenancy = MultiTenancyStrategy.Database;
42+
x.MultiTenancyConnectionProvider<TestMultiTenancyConnectionProvider>();
43+
});
44+
configuration.Properties[Cfg.Environment.GenerateStatistics] = "true";
45+
base.Configure(configuration);
46+
}
47+
48+
private static void ValidateSqlServerConnectionAppName(ISession s, string tenantId)
49+
{
50+
var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
51+
Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
52+
}
53+
54+
private static void ValidateSqlServerConnectionAppName(IStatelessSession s, string tenantId)
55+
{
56+
var builder = new SqlConnectionStringBuilder(s.Connection.ConnectionString);
57+
Assert.That(builder.ApplicationName, Is.EqualTo(tenantId));
58+
}
59+
60+
[Test]
61+
public async Task SecondLevelCacheReusedForSameTenantAsync()
62+
{
63+
using (var sesTen1 = OpenTenantSession("tenant1"))
64+
{
65+
var entity = await (sesTen1.GetAsync<Entity>(_id));
66+
}
67+
68+
Sfi.Statistics.Clear();
69+
using (var sesTen2 = OpenTenantSession("tenant1"))
70+
{
71+
var entity = await (sesTen2.GetAsync<Entity>(_id));
72+
}
73+
74+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
75+
Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
76+
}
77+
78+
[Test]
79+
public async Task SecondLevelCacheSeparationPerTenantAsync()
80+
{
81+
using (var sesTen1 = OpenTenantSession("tenant1"))
82+
{
83+
var entity = await (sesTen1.GetAsync<Entity>(_id));
84+
}
85+
86+
Sfi.Statistics.Clear();
87+
using (var sesTen2 = OpenTenantSession("tenant2"))
88+
{
89+
var entity = await (sesTen2.GetAsync<Entity>(_id));
90+
}
91+
92+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
93+
Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(0));
94+
}
95+
96+
[Test]
97+
public async Task QueryCacheReusedForSameTenantAsync()
98+
{
99+
using (var sesTen1 = OpenTenantSession("tenant1"))
100+
{
101+
var entity = await (sesTen1.Query<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
102+
}
103+
104+
Sfi.Statistics.Clear();
105+
using (var sesTen2 = OpenTenantSession("tenant1"))
106+
{
107+
var entity = await (sesTen2.Query<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
108+
}
109+
110+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
111+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1));
112+
}
113+
114+
[Test]
115+
public async Task QueryCacheSeparationPerTenantAsync()
116+
{
117+
using (var sesTen1 = OpenTenantSession("tenant1"))
118+
{
119+
var entity = await (sesTen1.Query<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
120+
}
121+
122+
Sfi.Statistics.Clear();
123+
using (var sesTen2 = OpenTenantSession("tenant2"))
124+
{
125+
var entity = await (sesTen2.Query<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
126+
}
127+
128+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(1));
129+
Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(0));
130+
}
131+
132+
[Test]
133+
public async Task TenantSessionIsSerializableAndCanBeReconnectedAsync()
134+
{
135+
ISession deserializedSession = null;
136+
using (var sesTen1 = OpenTenantSession("tenant1"))
137+
{
138+
var entity = await (sesTen1.Query<Entity>().WithOptions(x => x.SetCacheable(true)).Where(e => e.Id == _id).SingleOrDefaultAsync());
139+
sesTen1.Disconnect();
140+
deserializedSession = SpoofSerialization(sesTen1);
141+
}
142+
143+
Sfi.Statistics.Clear();
144+
using (deserializedSession)
145+
{
146+
deserializedSession.Reconnect();
147+
148+
//Expect session cache hit
149+
var entity = await (deserializedSession.GetAsync<Entity>(_id));
150+
if (IsSqlServerDialect)
151+
ValidateSqlServerConnectionAppName(deserializedSession, "tenant1");
152+
deserializedSession.Clear();
153+
154+
//Expect second level cache hit
155+
await (deserializedSession.GetAsync<Entity>(_id));
156+
Assert.That(GetTenantId(deserializedSession), Is.EqualTo("tenant1"));
157+
}
158+
159+
Assert.That(Sfi.Statistics.PrepareStatementCount, Is.EqualTo(0));
160+
Assert.That(Sfi.Statistics.SecondLevelCacheHitCount, Is.EqualTo(1));
161+
}
162+
163+
private static string GetTenantId(ISession session)
164+
{
165+
return session.GetSessionImplementation().GetTenantIdentifier();
166+
}
167+
168+
private static string GetTenantId(IStatelessSession session)
169+
{
170+
return session.GetSessionImplementation().GetTenantIdentifier();
171+
}
172+
173+
private T SpoofSerialization<T>(T session)
174+
{
175+
var formatter = new BinaryFormatter
176+
{
177+
#if !NETFX
178+
SurrogateSelector = new SerializationHelper.SurrogateSelector()
179+
#endif
180+
};
181+
var stream = new MemoryStream();
182+
formatter.Serialize(stream, session);
183+
184+
stream.Position = 0;
185+
186+
return (T) formatter.Deserialize(stream);
187+
}
188+
189+
private ISession OpenTenantSession(string tenantId)
190+
{
191+
return Sfi.WithOptions().Tenant(GetTenantConfig(tenantId)).OpenSession();
192+
}
193+
194+
private IStatelessSession OpenTenantStatelessSession(string tenantId)
195+
{
196+
return Sfi.WithStatelessOptions().Tenant(GetTenantConfig(tenantId)).OpenStatelessSession();
197+
}
198+
199+
private TenantConfiguration GetTenantConfig(string tenantId)
200+
{
201+
return new TestTenantConfiguration(tenantId, IsSqlServerDialect);
202+
}
203+
204+
private bool IsSqlServerDialect => Sfi.Dialect is MsSql2000Dialect && !(Sfi.ConnectionProvider.Driver is OdbcDriver);
205+
206+
#region Test Setup
207+
208+
protected override HbmMapping GetMappings()
209+
{
210+
var mapper = new ModelMapper();
211+
212+
mapper.Class<Entity>(
213+
rc =>
214+
{
215+
rc.Cache(m => m.Usage(CacheUsage.NonstrictReadWrite));
216+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
217+
rc.Property(x => x.Name);
218+
});
219+
220+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
221+
}
222+
223+
protected override DbConnection OpenConnectionForSchemaExport()
224+
{
225+
return Sfi.Settings.MultiTenancyConnectionProvider
226+
.GetConnectionAccess(GetTenantConfig("defaultTenant"), Sfi).GetConnection();
227+
}
228+
229+
protected override ISession OpenSession()
230+
{
231+
return OpenTenantSession("defaultTenant");
232+
}
233+
234+
protected override void OnTearDown()
235+
{
236+
using (var session = OpenSession())
237+
using (var transaction = session.BeginTransaction())
238+
{
239+
session.Delete("from System.Object");
240+
241+
session.Flush();
242+
transaction.Commit();
243+
}
244+
}
245+
246+
protected override void OnSetUp()
247+
{
248+
using (var session = OpenSession())
249+
using (var transaction = session.BeginTransaction())
250+
{
251+
var e1 = new Entity {Name = "Bob"};
252+
session.Save(e1);
253+
254+
var e2 = new Entity {Name = "Sally"};
255+
session.Save(e2);
256+
257+
session.Flush();
258+
transaction.Commit();
259+
_id = e1.Id;
260+
}
261+
}
262+
263+
#endregion Test Setup
264+
}
265+
}

src/NHibernate.Test/CacheTest/CacheFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void TestSimpleCache()
1818

1919
private CacheKey CreateCacheKey(string text)
2020
{
21-
return new CacheKey(text, NHibernateUtil.String, "Foo", null);
21+
return new CacheKey(text, NHibernateUtil.String, "Foo", null, null);
2222
}
2323

2424
public void DoTestCache(ICacheProvider cacheProvider)

src/NHibernate.Test/CacheTest/QueryKeyFixture.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ private void QueryKeyFilterDescLikeToCompare(out QueryKey qk, out QueryKey qk1,
3535
f.SetParameter("pLike", "so%");
3636
var fk = new FilterKey(f);
3737
ISet<FilterKey> fks = new HashSet<FilterKey> { fk };
38-
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
38+
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
3939

4040
var f1 = new FilterImpl(Sfi.GetFilterDefinition(filterName));
4141
f1.SetParameter("pLike", sameValue ? "so%" : "%ing");
4242
var fk1 = new FilterKey(f1);
4343
fks = new HashSet<FilterKey> { fk1 };
44-
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
44+
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
4545
}
4646

4747
private void QueryKeyFilterDescValueToCompare(out QueryKey qk, out QueryKey qk1, bool sameValue)
@@ -52,13 +52,13 @@ private void QueryKeyFilterDescValueToCompare(out QueryKey qk, out QueryKey qk1,
5252
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
5353
var fk = new FilterKey(f);
5454
ISet<FilterKey> fks = new HashSet<FilterKey> { fk };
55-
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
55+
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
5656

5757
var f1 = new FilterImpl(Sfi.GetFilterDefinition(filterName));
5858
f1.SetParameter("pDesc", "something").SetParameter("pValue", sameValue ? 10 : 11);
5959
var fk1 = new FilterKey(f1);
6060
fks = new HashSet<FilterKey> { fk1 };
61-
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
61+
qk1 = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
6262
}
6363

6464
[Test]
@@ -122,15 +122,15 @@ public void ToStringWithFilters()
122122
f.SetParameter("pLike", "so%");
123123
var fk = new FilterKey(f);
124124
ISet<FilterKey> fks = new HashSet<FilterKey> { fk };
125-
var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
125+
var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
126126
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}']"), "Like");
127127

128128
filterName = "DescriptionEqualAndValueGT";
129129
f = new FilterImpl(Sfi.GetFilterDefinition(filterName));
130130
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
131131
fk = new FilterKey(f);
132132
fks = new HashSet<FilterKey> { fk };
133-
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
133+
qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
134134
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}']"), "Value");
135135
}
136136

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

150150
ISet<FilterKey> fks = new HashSet<FilterKey> { fk, fvk };
151-
var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null);
151+
var qk = new QueryKey(Sfi, SqlAll, new QueryParameters(), fks, null, null);
152152
Assert.That(qk.ToString(), Does.Contain($"filters: ['{fk}', '{fvk}']"));
153153
}
154154
}

0 commit comments

Comments
 (0)