Skip to content

Commit c1929ec

Browse files
authored
Add support for named clients in mongo collection (#187)
1 parent 9c3fb5d commit c1929ec

15 files changed

Lines changed: 152 additions & 18 deletions

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ public class PersonDocument : IMongoDocument
224224
}
225225
```
226226

227+
For multiple clusters setup, you can specify which named client owns the collection using `ClientName`. This will have the following effect:
228+
* The injection of `IMongoCollection<TDocument>` will resolve the collection from the named client instead of the default client
229+
* The index creation from `UpdateIndexesAsync` will route each document type to its declared cluster automatically when scanning an assembly (no need to call `UpdateIndexesAsync` once per client)
230+
231+
```csharp
232+
[MongoCollection("people", ClientName = "anotherCluster", DatabaseName = "foo")]
233+
public class PersonDocument : IMongoDocument
234+
{
235+
// [...]
236+
}
237+
```
238+
227239
### With Configuration
228240

229241
In certain scenarios, like in Domain Driven Design (DDD), one would like to persist their Domain Aggregates as is in the Document Database. These Domain objects are not aware of how they are persisted. They cannot be decorated with Persistence level attributes (ie `[MongoCollection()]`), nor can they implement `IMongoDocument`.
@@ -240,9 +252,10 @@ public sealed class Person
240252
```csharp
241253
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
242254
{
243-
public void Configure(IMongoCollectionBuilder<Person> builder)
255+
public void Configure(IMongoCollectionBuilder<Person> builder)
244256
{
245257
builder.CollectionName("people");
258+
builder.ClientName("anotherCluster"); // optional, not calling this will use the default client
246259
builder.DatabaseName("foo"); // optional, not calling this will use the default database
247260
}
248261
}

src/Workleap.Extensions.Mongo.Abstractions/IMongoCollectionBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public interface IMongoCollectionBuilder<TDocument>
77
{
88
IMongoCollectionBuilder<TDocument> CollectionName(string collectionName);
99

10+
IMongoCollectionBuilder<TDocument> ClientName(string clientName);
11+
1012
IMongoCollectionBuilder<TDocument> DatabaseName(string databaseName);
1113

1214
IMongoCollectionBuilder<TDocument> IndexProvider<TIndexProvider>()

src/Workleap.Extensions.Mongo.Abstractions/MongoCollectionAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public MongoCollectionAttribute(string name)
1414

1515
public string Name { get; }
1616

17+
public string? ClientName { get; set; }
18+
1719
public string? DatabaseName { get; set; }
1820

1921
public Type? IndexProviderType { get; set; }

src/Workleap.Extensions.Mongo.Abstractions/MongoCollectionInformationCache.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,40 @@ public static MongoCollectionInformation GetCollectionInformation(Type documentT
3636
{
3737
if (documentType.GetCustomAttribute<MongoCollectionAttribute>() is { } attribute)
3838
{
39-
return new MongoCollectionInformation(documentType, attribute.Name, attribute.DatabaseName, isRegisteredByConfiguration: false);
39+
return new MongoCollectionInformation(documentType, attribute.Name, attribute.ClientName, attribute.DatabaseName, isRegisteredByConfiguration: false);
4040
}
4141

4242
throw new ArgumentException(documentType + " must be decorated with " + nameof(MongoCollectionAttribute) + " or be registered by a " + typeof(IMongoCollectionConfiguration<>).MakeGenericType(documentType).Name);
4343
});
4444
}
4545

46-
internal static void SetCollectionInformation(Type documentType, string collectionName, string? databaseName)
46+
internal static void SetCollectionInformation(Type documentType, string collectionName, string? clientName, string? databaseName)
4747
{
48-
if (!CollectionInfos.TryAdd(documentType, new MongoCollectionInformation(documentType, collectionName, databaseName, isRegisteredByConfiguration: true)))
48+
if (!CollectionInfos.TryAdd(documentType, new MongoCollectionInformation(documentType, collectionName, clientName, databaseName, isRegisteredByConfiguration: true)))
4949
{
5050
throw new ArgumentException($"Collection name for {documentType} already set.");
5151
}
5252
}
5353

5454
public sealed class MongoCollectionInformation
5555
{
56-
public MongoCollectionInformation(Type documentType, string collectionName, string? databaseName, bool isRegisteredByConfiguration)
56+
public MongoCollectionInformation(Type documentType, string collectionName, string? clientName, string? databaseName, bool isRegisteredByConfiguration)
5757
{
5858
this.CollectionName = collectionName;
59+
this.ClientName = clientName;
5960
this.DatabaseName = databaseName;
6061
this.DocumentType = documentType;
6162
this.IsRegisteredByConfiguration = isRegisteredByConfiguration;
6263
}
6364

6465
public string CollectionName { get; }
6566

67+
public string? ClientName { get; }
68+
6669
public string? DatabaseName { get; }
6770

6871
public Type DocumentType { get; }
6972

7073
public bool IsRegisteredByConfiguration { get; }
7174
}
72-
}
75+
}

src/Workleap.Extensions.Mongo.Abstractions/PublicAPI.Shipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Workleap.Extensions.Mongo.IndexedByAttribute
88
Workleap.Extensions.Mongo.IndexedByAttribute.IndexedByAttribute(params string![]! indexes) -> void
99
Workleap.Extensions.Mongo.IndexedByAttribute.Indexes.get -> string![]!
1010
Workleap.Extensions.Mongo.MongoCollectionAttribute
11+
Workleap.Extensions.Mongo.MongoCollectionAttribute.ClientName.get -> string?
12+
Workleap.Extensions.Mongo.MongoCollectionAttribute.ClientName.set -> void
1113
Workleap.Extensions.Mongo.MongoCollectionAttribute.DatabaseName.get -> string?
1214
Workleap.Extensions.Mongo.MongoCollectionAttribute.DatabaseName.set -> void
1315
Workleap.Extensions.Mongo.MongoCollectionAttribute.IndexProviderType.get -> System.Type?
@@ -37,6 +39,7 @@ static readonly Workleap.Extensions.Mongo.MongoDefaults.ClientName -> string!
3739
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>
3840
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.BsonClassMap(System.Action<MongoDB.Bson.Serialization.BsonClassMap<TDocument!>!>! classMapInitializer) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
3941
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.CollectionName(string! collectionName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
42+
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.ClientName(string! clientName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
4043
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.DatabaseName(string! databaseName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
4144
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.IndexProvider<TIndexProvider>() -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
4245
Workleap.Extensions.Mongo.IMongoCollectionConfiguration<TDocument>

src/Workleap.Extensions.Mongo.Tests/MongoCollectionAttributeTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,18 @@ public void Attribute_Throws_Given_Invalid_CollectionName(string collectionName)
3030
{
3131
Assert.Throws<ArgumentException>(() => new MongoCollectionAttribute(collectionName));
3232
}
33+
34+
[Fact]
35+
public void Attribute_ClientName_Is_Null_By_Default()
36+
{
37+
var attr = new MongoCollectionAttribute("people");
38+
Assert.Null(attr.ClientName);
39+
}
40+
41+
[Fact]
42+
public void Attribute_Sets_ClientName_Correctly()
43+
{
44+
var attr = new MongoCollectionAttribute("people") { ClientName = "myCluster" };
45+
Assert.Equal("myCluster", attr.ClientName);
46+
}
3347
}

src/Workleap.Extensions.Mongo.Tests/MongoCollectionBuilderTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,30 @@ namespace Workleap.Extensions.Mongo.Tests;
66

77
public sealed class MongoCollectionBuilderTests
88
{
9+
[Fact]
10+
public void Builder_Sets_ClientName_Correctly()
11+
{
12+
var builder = new MongoCollectionBuilder<TestDocument>();
13+
builder.CollectionName("collection1").ClientName("myCluster");
14+
15+
var metadata = builder.Build() as MongoCollectionMetadata<TestDocument>;
16+
17+
Assert.NotNull(metadata);
18+
Assert.Equal("myCluster", metadata.ClientName);
19+
}
20+
21+
[Fact]
22+
public void Builder_ClientName_Is_Null_By_Default()
23+
{
24+
var builder = new MongoCollectionBuilder<TestDocument>();
25+
builder.CollectionName("collection1");
26+
27+
var metadata = builder.Build() as MongoCollectionMetadata<TestDocument>;
28+
29+
Assert.NotNull(metadata);
30+
Assert.Null(metadata.ClientName);
31+
}
32+
933
[Fact]
1034
public void Builder_Builds_Correctly()
1135
{

src/Workleap.Extensions.Mongo.Tests/MultipleMongoClientTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ public override IServiceCollection ConfigureServices(IServiceCollection services
6868
}
6969
}
7070

71+
[Fact]
72+
public void Collection_WithClientName_Resolves_To_Named_Client()
73+
{
74+
var collection = this.Services.GetRequiredService<IMongoCollection<NamedClientDocument>>();
75+
76+
// NamedClientDocument declares ClientName = FooClient ("foo"), whose app name is "app2"
77+
Assert.Equal("app2", collection.Database.Client.Settings.ApplicationName);
78+
}
79+
80+
[Fact]
81+
public async Task UpdateIndexesAsync_WithoutClientNameOverride_Routes_Each_Type_To_Its_Declared_ClientName()
82+
{
83+
const string databaseName = "clientnameindextest";
84+
var indexer = this.Services.GetRequiredService<IMongoIndexer>();
85+
86+
// No clientName override — each type should use its own declared ClientName
87+
await indexer.UpdateIndexesAsync(new[] { typeof(NamedClientIndexDocument) }, databaseName: databaseName);
88+
89+
var fooClient = this.Services.GetRequiredService<IMongoClientProvider>().GetClient(FooClient);
90+
var fooCollection = fooClient.GetDatabase(databaseName).GetCollection<NamedClientIndexDocument>();
91+
var fooIndexNames = (await fooCollection.Indexes.ListAsync()).ToList()
92+
.Select(i => i.GetElement("name").Value.AsString)
93+
.ToList();
94+
95+
Assert.Contains(fooIndexNames, name => name.StartsWith("namedclientidx"));
96+
97+
// Verify the default client did NOT get the index
98+
var defaultCollection = this.Services.GetRequiredService<IMongoClient>()
99+
.GetDatabase(databaseName).GetCollection<NamedClientIndexDocument>();
100+
var defaultIndexNames = (await defaultCollection.Indexes.ListAsync()).ToList()
101+
.Select(i => i.GetElement("name").Value.AsString)
102+
.ToList();
103+
104+
Assert.DoesNotContain(defaultIndexNames, name => name.StartsWith("namedclientidx"));
105+
}
106+
71107
[MongoCollection("cats", IndexProviderType = typeof(CatDocumentIndexes))]
72108
private sealed class CatDocument : MongoDocument
73109
{
@@ -83,4 +119,25 @@ public override IEnumerable<CreateIndexModel<CatDocument>> CreateIndexModels()
83119
new CreateIndexOptions { Name = "name", Unique = true });
84120
}
85121
}
122+
123+
[MongoCollection("namedclientdoc", ClientName = FooClient)]
124+
private sealed class NamedClientDocument : MongoDocument
125+
{
126+
}
127+
128+
[MongoCollection("namedclientidxdoc", ClientName = FooClient, IndexProviderType = typeof(NamedClientIndexDocumentIndexes))]
129+
private sealed class NamedClientIndexDocument : MongoDocument
130+
{
131+
public string Tag { get; set; } = string.Empty;
132+
}
133+
134+
private sealed class NamedClientIndexDocumentIndexes : MongoIndexProvider<NamedClientIndexDocument>
135+
{
136+
public override IEnumerable<CreateIndexModel<NamedClientIndexDocument>> CreateIndexModels()
137+
{
138+
yield return new CreateIndexModel<NamedClientIndexDocument>(
139+
Builders<NamedClientIndexDocument>.IndexKeys.Ascending(x => x.Tag),
140+
new CreateIndexOptions { Name = "namedclientidx" });
141+
}
142+
}
86143
}

src/Workleap.Extensions.Mongo.v2/PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Workleap.Extensions.Mongo.MongoCollectionBuilder.MongoCollectionBuilder() -> voi
6969
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>
7070
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.BsonClassMap(System.Action<MongoDB.Bson.Serialization.BsonClassMap<TDocument!>!>! classMapInitializer) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
7171
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.CollectionName(string! collectionName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
72+
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.ClientName(string! clientName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
7273
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.DatabaseName(string! databaseName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
7374
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.IndexProvider<TIndexProvider>() -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
7475
Workleap.Extensions.Mongo.MongoCollectionBuilder<TDocument>.MongoCollectionBuilder() -> void

src/Workleap.Extensions.Mongo/Indexing/MongoIndexer.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,22 @@ public async Task UpdateIndexesAsync(IEnumerable<Type> types, string? clientName
7474
enumeratedTypes.Add(type);
7575
}
7676

77-
// Use default MongoDB client by default
78-
clientName ??= MongoDefaults.ClientName;
79-
var mongoClient = this._mongoClientProvider.GetClient(clientName);
80-
var options = this._optionsMonitor.Get(clientName);
77+
// When clientName is explicitly provided it overrides all document types; otherwise each type uses its own declared ClientName.
78+
var explicitClientOverride = clientName;
8179

82-
var collectionInfoByDatabase = enumeratedTypes
80+
var collectionInfoByClientAndDatabase = enumeratedTypes
8381
.Select(t => MongoCollectionInformationCache.GetCollectionInformation(t))
84-
.GroupBy(info => info.DatabaseName);
82+
// Use the effective database name (override if provided, otherwise the collection's DatabaseName) in the grouping key
83+
.GroupBy(info => (ClientName: explicitClientOverride ?? info.ClientName ?? MongoDefaults.ClientName, DatabaseName: databaseName ?? info.DatabaseName));
8584

86-
foreach (var collectionInfos in collectionInfoByDatabase)
85+
foreach (var collectionInfos in collectionInfoByClientAndDatabase)
8786
{
88-
// User provided parameter has precedance over the MongoCollectionAttribute, otherwise use the default MongoDB database
89-
var database = mongoClient.GetDatabase(databaseName ?? collectionInfos.Key ?? options.DefaultDatabaseName);
87+
var effectiveClientName = collectionInfos.Key.ClientName;
88+
var mongoClient = this._mongoClientProvider.GetClient(effectiveClientName);
89+
var options = this._optionsMonitor.Get(effectiveClientName);
90+
91+
// User provided databaseName parameter has precedence over the collection's DatabaseName, otherwise use the client's default database
92+
var database = mongoClient.GetDatabase(databaseName ?? collectionInfos.Key.DatabaseName ?? options.DefaultDatabaseName);
9093

9194
// Attempt to update the indexes if we acquire the distributed lock
9295
var lockId = options.Indexing.DistributedLockName;

0 commit comments

Comments
 (0)