Skip to content

Commit 6ebdc57

Browse files
jutrecJules Tremblay
andauthored
Feature/wpm 44 add configuration based indexes (#70)
Add alternative approach to configure collections via class map configuration --------- Co-authored-by: Jules Tremblay <[email protected]>
1 parent 6cf0368 commit 6ebdc57

26 files changed

+670
-111
lines changed

README.md

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ When using the `AddMongo()` method, multiple conventions are added automatically
148148
- `EnumRepresentationConvention(BsonType.String)`, so changing an enum member name is a breaking change
149149
- `DateTime` and `DateTimeOffset` are serialized as `DateTime` instead of the default Ticks or (Ticks, Offset). In MongoDB, DateTime only supports precision up to the milliseconds. If you need more precision, you need to set the serializer at property level.
150150

151-
## Declaring and using MongoDB documents and collections
151+
## Declaring Mongo Documents
152+
### With Attributes Decoration
152153

153154
The process doesn't deviate much from the standard way of declaring and using MongoDB collections in C#. However, there are two additional steps:
154155

@@ -163,14 +164,89 @@ public class PersonDocument : IMongoDocument
163164
}
164165
```
165166

167+
### With Configuration
168+
169+
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`.
170+
171+
You can configure your Object to Database mapping throught `IMongoCollectionConfiguration<TDocument>` instead.
172+
173+
```csharp
174+
public sealed class Person
175+
{
176+
// [...]
177+
}
178+
```
179+
180+
```csharp
181+
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
182+
{
183+
public void Configure(IMongoCollectionBuilder<Person> builder)
184+
{
185+
builder.CollectionName("people");
186+
}
187+
}
188+
```
189+
190+
#### Bootstrapping Configurations
191+
192+
Since the Configuration approach uses reflection to find the implementations of `IMongoCollectionConfiguration<T>` during the startup, we have to tell the library that we opt-in the Configuration mode by calling AddCollectionConfigurations and pass it the Assemblies where you can locate the Configurations.
193+
194+
```csharp
195+
services.AddMongo().AddCollectionConfigurations(InfrastructureAssemblyHandle.Assembly);
196+
```
197+
198+
## Usage
166199
Refer back to the [getting started section](#getting-started) to learn how to resolve `IMongoCollection<TDocument>` from the dependency injection services.
167200

201+
## Extensions
168202
We also provide `IAsyncEnumerable<TDocument>` extensions on `IAsyncCursor<TDocument>` and `IAsyncCursorSource<TDocument>`, eliminating the need to deal with cursors. For example:
169203

170204
```csharp
171205
var people = await collection.Find(FilterDefinition<PersonDocument>.Empty).ToAsyncEnumerable();
172206
```
173207

208+
## Property Mapping
209+
210+
You can use Mongo Attributes for Property Mapping, or BsonClassMaps. However, if you are using Configuration, you probably do not want to use Attributes on your Models.
211+
212+
### With Attributes
213+
214+
```csharp
215+
[MongoCollection("people")]
216+
public class PersonDocument : IMongoDocument
217+
{
218+
[BsonId]
219+
[BsonRepresentation(BsonType.ObjectId)]
220+
public string Id { get; set; }
221+
222+
[BsonElement("n")]
223+
public string Name { get; set; } = string.Empty;
224+
}
225+
```
226+
227+
[Mapping Models with Attributes](https://www.mongodb.com/docs/drivers/csharp/v2.19/fundamentals/serialization/poco/)
228+
229+
### With Configuration
230+
231+
```csharp
232+
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
233+
{
234+
public void Configure(IMongoCollectionBuilder<Person> builder)
235+
{
236+
builder.CollectionName("people")
237+
.BsonClassMap(map =>
238+
{
239+
map.MapIdProperty(x => x.Id)
240+
.SetSerializer(new StringSerializer(BsonType.ObjectId));
241+
242+
map.MapProperty(x => x.Name).SetElementName("n");
243+
});
244+
}
245+
}
246+
```
247+
248+
[Mapping Models with ClassMaps](https://www.mongodb.com/docs/drivers/csharp/v2.19/fundamentals/serialization/class-mapping/)
249+
174250
## Logging and distributed tracing
175251

176252
**Workleap.Extensions.Mongo** supports modern logging with `ILogger` and log level filtering. MongoDB commands can be logged at the `Debug` level and optionally with their BSON content only if you set `MongoClientOptions.Telemetry.CaptureCommandText` to `true`.
@@ -189,6 +265,21 @@ By default, some commands such as `isMaster`, `buildInfo`, `saslStart`, etc., ar
189265

190266
We provide a mechanism for you to declare your collection indexes and ensure they are applied to your database. To do this, declare your indexes by implementing a custom `MongoIndexProvider<TDocument>`:
191267

268+
```csharp
269+
public class PersonDocumentIndexes : MongoIndexProvider<PersonDocument>
270+
{
271+
public override IEnumerable<CreateIndexModel<PersonDocument>> CreateIndexModels()
272+
{
273+
// Index name is mandatory
274+
yield return new CreateIndexModel<PersonDocument>(
275+
Builders<PersonDocument>.IndexKeys.Combine().Ascending(x => x.Name),
276+
new CreateIndexOptions { Name = "name" });
277+
}
278+
}
279+
```
280+
281+
### With Attributes Decoration
282+
192283
```csharp
193284
[MongoCollection("people", IndexProviderType = typeof(PersonDocumentIndexes))]
194285
public class PersonDocument : IMongoDocument
@@ -199,19 +290,22 @@ public class PersonDocument : IMongoDocument
199290

200291
public string Name { get; set; } = string.Empty;
201292
}
293+
```
202294

203-
public class PersonDocumentIndexes : MongoIndexProvider<PersonDocument>
295+
### With Configuration
296+
297+
```csharp
298+
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
204299
{
205-
public override IEnumerable<CreateIndexModel<PersonDocument>> CreateIndexModels()
300+
public void Configure(IMongoCollectionBuilder<Person> builder)
206301
{
207-
// Index name is mandatory
208-
yield return new CreateIndexModel<PersonDocument>(
209-
Builders<PersonDocument>.IndexKeys.Combine().Ascending(x => x.Name),
210-
new CreateIndexOptions { Name = "name" });
302+
builder.IndexProvider<PersonDocumentIndexes>();
211303
}
212304
}
213305
```
214306

307+
### Updating Indexes
308+
215309
At this stage, nothing will happen. To actually create or update the index, you need to inject our `IMongoIndexer` service and then call one of its `UpdateIndexesAsync()` method overloads, for example:
216310

217311
```csharp
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using MongoDB.Bson.Serialization;
2+
3+
namespace Workleap.Extensions.Mongo;
4+
5+
public interface IMongoCollectionBuilder<TDocument>
6+
where TDocument : class
7+
{
8+
IMongoCollectionBuilder<TDocument> CollectionName(string collectionName);
9+
10+
IMongoCollectionBuilder<TDocument> IndexProvider<TIndexProvider>()
11+
where TIndexProvider : MongoIndexProvider<TDocument>;
12+
13+
IMongoCollectionBuilder<TDocument> BsonClassMap(Action<BsonClassMap<TDocument>> classMapInitializer);
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Workleap.Extensions.Mongo;
2+
3+
public interface IMongoCollectionConfiguration<TDocument>
4+
where TDocument : class
5+
{
6+
void Configure(IMongoCollectionBuilder<TDocument> builder);
7+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ namespace MongoDB.Driver;
66
public static class IMongoCollectionExtensions
77
{
88
public static string GetName<TDocument>(this IMongoCollection<TDocument> collection)
9-
where TDocument : IMongoDocument
9+
where TDocument : class
1010
{
11-
return MongoReflectionCache.GetCollectionName<TDocument>();
11+
return MongoCollectionNameCache.GetCollectionName<TDocument>();
1212
}
1313
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ namespace MongoDB.Driver;
66
public static class IMongoDatabaseExtensions
77
{
88
public static IMongoCollection<TDocument> GetCollection<TDocument>(this IMongoDatabase database, MongoCollectionSettings? settings = null)
9-
where TDocument : IMongoDocument
9+
where TDocument : class
1010
{
11-
return database.GetCollection<TDocument>(MongoReflectionCache.GetCollectionName<TDocument>(), settings);
11+
return database.GetCollection<TDocument>(MongoCollectionNameCache.GetCollectionName<TDocument>(), settings);
1212
}
1313

1414
public static string GetCollectionName<TDocument>(this IMongoDatabase database)
15-
where TDocument : IMongoDocument
15+
where TDocument : class
1616
{
17-
return MongoReflectionCache.GetCollectionName<TDocument>();
17+
return MongoCollectionNameCache.GetCollectionName<TDocument>();
1818
}
1919
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Concurrent;
2+
using System.Reflection;
3+
4+
namespace Workleap.Extensions.Mongo;
5+
6+
internal static class MongoCollectionNameCache
7+
{
8+
private static readonly ConcurrentDictionary<Type, string> CollectionNames = new();
9+
10+
public static string GetCollectionName(Type documentType)
11+
{
12+
if (CollectionNames.TryGetValue(documentType, out var collectionName))
13+
{
14+
return collectionName;
15+
}
16+
17+
// Configuration based CollectionNames are set manually by calling SetCollectionName.
18+
// When we reach here, we can validate the Attribute flow because it was not a document from the Configuration flow.
19+
if (!documentType.IsConcreteMongoDocumentType())
20+
{
21+
throw new ArgumentException(documentType + " must be a concrete type that implements " + nameof(IMongoDocument));
22+
}
23+
24+
return CollectionNames.GetOrAdd(documentType, static documentType =>
25+
{
26+
if (documentType.GetCustomAttribute<MongoCollectionAttribute>() is { } attribute)
27+
{
28+
return attribute.Name;
29+
}
30+
31+
throw new ArgumentException(documentType + " must be decorated with " + nameof(MongoCollectionAttribute) + " or be registered by a " + typeof(IMongoCollectionConfiguration<>).MakeGenericType(documentType).Name);
32+
});
33+
}
34+
35+
public static string GetCollectionName<TDocument>()
36+
where TDocument : class
37+
{
38+
return GetCollectionName(typeof(TDocument));
39+
}
40+
41+
internal static void SetCollectionName(Type documentType, string collectionName)
42+
{
43+
if (!CollectionNames.TryAdd(documentType, collectionName))
44+
{
45+
throw new ArgumentException($"Collection name for {documentType} already set.");
46+
}
47+
}
48+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace Workleap.Extensions.Mongo;
4+
5+
internal static class MongoConfigurationIndexStore
6+
{
7+
private static readonly ConcurrentDictionary<Type, Type?> IndexProviderTypes = new();
8+
9+
internal static void AddIndexProviderType(Type documentType, Type? indexProviderType)
10+
{
11+
if (!IndexProviderTypes.TryAdd(documentType, indexProviderType))
12+
{
13+
throw new ArgumentException($"IndexProviderType for {documentType} already set.");
14+
}
15+
}
16+
17+
internal static IReadOnlyDictionary<Type, Type?> GetIndexProviderTypes() => IndexProviderTypes;
18+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ namespace Workleap.Extensions.Mongo;
66
/// Inherit from this class to define indexes for a particular document type.
77
/// </summary>
88
public abstract class MongoIndexProvider<TDocument>
9-
where TDocument : IMongoDocument
9+
where TDocument : class
1010
{
11+
public IndexKeysDefinitionBuilder<TDocument> IndexKeys => Builders<TDocument>.IndexKeys;
12+
1113
public abstract IEnumerable<CreateIndexModel<TDocument>> CreateIndexModels();
1214
}

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

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Reflection;
2+
3+
namespace Workleap.Extensions.Mongo;
4+
5+
internal static class MongoTypeExtensions
6+
{
7+
internal static bool IsConcreteMongoDocumentType(this Type type) => !type.IsAbstract && typeof(IMongoDocument).IsAssignableFrom(type);
8+
9+
internal static bool IsMongoCollectionConfigurationInterface(this Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IMongoCollectionConfiguration<>);
10+
11+
internal static void EnsureHasPublicParameterlessConstructor(this Type type)
12+
{
13+
if (!type.HasPublicParameterlessConstructor())
14+
{
15+
throw new InvalidOperationException($"Type {type}' must have a public parameterless constructor");
16+
}
17+
}
18+
19+
private static bool HasPublicParameterlessConstructor(this Type type) => type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null) != null;
20+
}

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
#nullable enable
2-
abstract Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.CreateIndexModels() -> System.Collections.Generic.IEnumerable<MongoDB.Driver.CreateIndexModel<TDocument>!>!
3-
MongoDB.Driver.AsyncCursorExtensions
42
MongoDB.Driver.IMongoCollectionExtensions
53
MongoDB.Driver.IMongoDatabaseExtensions
64
Workleap.Extensions.Mongo.IMongoClientProvider
@@ -26,9 +24,18 @@ Workleap.Extensions.Mongo.StringGuidIdGenerator.IsEmpty(object! id) -> bool
2624
Workleap.Extensions.Mongo.StringGuidIdGenerator.StringGuidIdGenerator() -> void
2725
static MongoDB.Driver.AsyncCursorExtensions.ToAsyncEnumerable<TDocument>(this MongoDB.Driver.IAsyncCursor<TDocument>! cursor, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable<TDocument>!
2826
static MongoDB.Driver.AsyncCursorExtensions.ToAsyncEnumerable<TDocument>(this MongoDB.Driver.IAsyncCursorSource<TDocument>! source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable<TDocument>!
29-
static MongoDB.Driver.IMongoCollectionExtensions.GetName<TDocument>(this MongoDB.Driver.IMongoCollection<TDocument>! collection) -> string!
30-
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollection<TDocument>(this MongoDB.Driver.IMongoDatabase! database, MongoDB.Driver.MongoCollectionSettings? settings = null) -> MongoDB.Driver.IMongoCollection<TDocument>!
3127
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollectionName<TDocument>(this MongoDB.Driver.IMongoDatabase! database) -> string!
32-
static readonly Workleap.Extensions.Mongo.MongoDefaults.ClientName -> string!
3328
static Workleap.Extensions.Mongo.StringGuidIdGenerator.GenerateId() -> object!
34-
static Workleap.Extensions.Mongo.StringGuidIdGenerator.Instance.get -> MongoDB.Bson.Serialization.IIdGenerator!
29+
static Workleap.Extensions.Mongo.StringGuidIdGenerator.Instance.get -> MongoDB.Bson.Serialization.IIdGenerator!
30+
abstract Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.CreateIndexModels() -> System.Collections.Generic.IEnumerable<MongoDB.Driver.CreateIndexModel<TDocument!>!>!
31+
MongoDB.Driver.AsyncCursorExtensions
32+
static MongoDB.Driver.IMongoCollectionExtensions.GetName<TDocument>(this MongoDB.Driver.IMongoCollection<TDocument!>! collection) -> string!
33+
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollection<TDocument>(this MongoDB.Driver.IMongoDatabase! database, MongoDB.Driver.MongoCollectionSettings? settings = null) -> MongoDB.Driver.IMongoCollection<TDocument!>!
34+
static readonly Workleap.Extensions.Mongo.MongoDefaults.ClientName -> string!
35+
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>
36+
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.BsonClassMap(System.Action<MongoDB.Bson.Serialization.BsonClassMap<TDocument!>!>! classMapInitializer) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
37+
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.CollectionName(string! collectionName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
38+
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.IndexProvider<TIndexProvider>() -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
39+
Workleap.Extensions.Mongo.IMongoCollectionConfiguration<TDocument>
40+
Workleap.Extensions.Mongo.IMongoCollectionConfiguration<TDocument>.Configure(Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>! builder) -> void
41+
Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.IndexKeys.get -> MongoDB.Driver.IndexKeysDefinitionBuilder<TDocument!>!

0 commit comments

Comments
 (0)