-
-
Notifications
You must be signed in to change notification settings - Fork 515
/
TenancyPerSchema.cs
116 lines (95 loc) · 3.75 KB
/
TenancyPerSchema.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System.Collections.Concurrent;
using System.Data;
using Marten.Integration.Tests.TestsInfrastructure;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Marten.Integration.Tests.Tenancy;
public interface ITenancyPerSchemaStoreFactory
{
IDocumentStore Get(string? tenantId);
}
public class TenancyPerSchemaStoreFactory(Action<string, StoreOptions> configure)
: IDisposable, ITenancyPerSchemaStoreFactory
{
private readonly ConcurrentDictionary<string, DocumentStore> stores = new ();
public IDocumentStore Get(string? tenant) =>
stores.GetOrAdd(tenant ?? "NO-TENANT", tenantId =>
{
var storeOptions = new StoreOptions();
configure.Invoke(tenantId, storeOptions);
return new DocumentStore(storeOptions);
});
public void Dispose()
{
foreach (var documentStore in stores.Values)
{
documentStore.Dispose();
}
}
}
public class DummyTenancyContext
{
// this should be normally taken from the http context or other
public string? TenantId { get; set; }
}
public class TenancyPerSchemaSessionFactory(
ITenancyPerSchemaStoreFactory storeFactory,
DummyTenancyContext tenancyContext)
: ISessionFactory
{
public IQuerySession QuerySession() =>
storeFactory.Get(tenancyContext.TenantId).QuerySession();
public IDocumentSession OpenSession() =>
storeFactory.Get(tenancyContext.TenantId).LightweightSession(IsolationLevel.Serializable);
}
public record TestDocumentForTenancy(
Guid Id,
string Name
);
public class TenancyPerSchema(MartenFixture fixture): MartenTest(fixture.PostgreSqlContainer, false)
{
private const string FirstTenant = "Tenant1";
private const string SecondTenant = "Tenant2";
[Fact]
public void GivenEvents_WhenInlineTransformationIsApplied_ThenReturnsSameNumberOfTransformedItems()
{
var services = new ServiceCollection();
AddMarten(services);
using var sp = services.BuildServiceProvider();
// simulate scope per HTTP request with different tenant
using (var firstScope = sp.CreateScope())
{
firstScope.ServiceProvider.GetRequiredService<DummyTenancyContext>().TenantId = FirstTenant;
using (var session = firstScope.ServiceProvider.GetRequiredService<IDocumentSession>())
{
session.Insert(new TestDocumentForTenancy(Guid.NewGuid(), FirstTenant));
session.SaveChanges();
}
}
// simulate scope per HTTP request with different tenant
using (var secondScope = sp.CreateScope())
{
secondScope.ServiceProvider.GetRequiredService<DummyTenancyContext>().TenantId = SecondTenant;
using (var session = secondScope.ServiceProvider.GetRequiredService<IDocumentSession>())
{
session.Insert(new TestDocumentForTenancy(Guid.NewGuid(), SecondTenant));
session.SaveChanges();
}
}
}
private void AddMarten(IServiceCollection services)
{
// simulate http context
services.AddScoped<DummyTenancyContext>();
services.AddSingleton<ITenancyPerSchemaStoreFactory, TenancyPerSchemaStoreFactory>();
// register options as function to resolve it per tenant
services.AddSingleton<Action<string, StoreOptions>>((tenantId, options) =>
{
options.DatabaseSchemaName = tenantId;
options.Connection(ConnectionString);
});
services.AddScoped<ISessionFactory, TenancyPerSchemaSessionFactory>();
services.AddScoped(s => s.GetRequiredService<ISessionFactory>().QuerySession());
services.AddScoped(s => s.GetRequiredService<ISessionFactory>().OpenSession());
}
}