Skip to content

Commit

Permalink
Feature/wpm 44 add configuration based indexes (#70)
Browse files Browse the repository at this point in the history
Add alternative approach to configure collections via class map configuration

---------

Co-authored-by: Jules Tremblay <[email protected]>
  • Loading branch information
jutrec and Jules Tremblay authored May 6, 2024
1 parent 6cf0368 commit 6ebdc57
Show file tree
Hide file tree
Showing 26 changed files with 670 additions and 111 deletions.
108 changes: 101 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ When using the `AddMongo()` method, multiple conventions are added automatically
- `EnumRepresentationConvention(BsonType.String)`, so changing an enum member name is a breaking change
- `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.

## Declaring and using MongoDB documents and collections
## Declaring Mongo Documents
### With Attributes Decoration

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

Expand All @@ -163,14 +164,89 @@ public class PersonDocument : IMongoDocument
}
```

### With Configuration

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`.

You can configure your Object to Database mapping throught `IMongoCollectionConfiguration<TDocument>` instead.

```csharp
public sealed class Person
{
// [...]
}
```

```csharp
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
{
public void Configure(IMongoCollectionBuilder<Person> builder)
{
builder.CollectionName("people");
}
}
```

#### Bootstrapping Configurations

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.

```csharp
services.AddMongo().AddCollectionConfigurations(InfrastructureAssemblyHandle.Assembly);
```

## Usage
Refer back to the [getting started section](#getting-started) to learn how to resolve `IMongoCollection<TDocument>` from the dependency injection services.

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

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

## Property Mapping

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.

### With Attributes

```csharp
[MongoCollection("people")]
public class PersonDocument : IMongoDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }

[BsonElement("n")]
public string Name { get; set; } = string.Empty;
}
```

[Mapping Models with Attributes](https://www.mongodb.com/docs/drivers/csharp/v2.19/fundamentals/serialization/poco/)

### With Configuration

```csharp
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
{
public void Configure(IMongoCollectionBuilder<Person> builder)
{
builder.CollectionName("people")
.BsonClassMap(map =>
{
map.MapIdProperty(x => x.Id)
.SetSerializer(new StringSerializer(BsonType.ObjectId));

map.MapProperty(x => x.Name).SetElementName("n");
});
}
}
```

[Mapping Models with ClassMaps](https://www.mongodb.com/docs/drivers/csharp/v2.19/fundamentals/serialization/class-mapping/)

## Logging and distributed tracing

**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`.
Expand All @@ -189,6 +265,21 @@ By default, some commands such as `isMaster`, `buildInfo`, `saslStart`, etc., ar

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>`:

```csharp
public class PersonDocumentIndexes : MongoIndexProvider<PersonDocument>
{
public override IEnumerable<CreateIndexModel<PersonDocument>> CreateIndexModels()
{
// Index name is mandatory
yield return new CreateIndexModel<PersonDocument>(
Builders<PersonDocument>.IndexKeys.Combine().Ascending(x => x.Name),
new CreateIndexOptions { Name = "name" });
}
}
```

### With Attributes Decoration

```csharp
[MongoCollection("people", IndexProviderType = typeof(PersonDocumentIndexes))]
public class PersonDocument : IMongoDocument
Expand All @@ -199,19 +290,22 @@ public class PersonDocument : IMongoDocument

public string Name { get; set; } = string.Empty;
}
```

public class PersonDocumentIndexes : MongoIndexProvider<PersonDocument>
### With Configuration

```csharp
internal sealed class PersonConfiguration: IMongoCollectionConfiguration<Person>
{
public override IEnumerable<CreateIndexModel<PersonDocument>> CreateIndexModels()
public void Configure(IMongoCollectionBuilder<Person> builder)
{
// Index name is mandatory
yield return new CreateIndexModel<PersonDocument>(
Builders<PersonDocument>.IndexKeys.Combine().Ascending(x => x.Name),
new CreateIndexOptions { Name = "name" });
builder.IndexProvider<PersonDocumentIndexes>();
}
}
```

### Updating Indexes

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:

```csharp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using MongoDB.Bson.Serialization;

namespace Workleap.Extensions.Mongo;

public interface IMongoCollectionBuilder<TDocument>
where TDocument : class
{
IMongoCollectionBuilder<TDocument> CollectionName(string collectionName);

IMongoCollectionBuilder<TDocument> IndexProvider<TIndexProvider>()
where TIndexProvider : MongoIndexProvider<TDocument>;

IMongoCollectionBuilder<TDocument> BsonClassMap(Action<BsonClassMap<TDocument>> classMapInitializer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Workleap.Extensions.Mongo;

public interface IMongoCollectionConfiguration<TDocument>
where TDocument : class
{
void Configure(IMongoCollectionBuilder<TDocument> builder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace MongoDB.Driver;
public static class IMongoCollectionExtensions
{
public static string GetName<TDocument>(this IMongoCollection<TDocument> collection)
where TDocument : IMongoDocument
where TDocument : class
{
return MongoReflectionCache.GetCollectionName<TDocument>();
return MongoCollectionNameCache.GetCollectionName<TDocument>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ namespace MongoDB.Driver;
public static class IMongoDatabaseExtensions
{
public static IMongoCollection<TDocument> GetCollection<TDocument>(this IMongoDatabase database, MongoCollectionSettings? settings = null)
where TDocument : IMongoDocument
where TDocument : class
{
return database.GetCollection<TDocument>(MongoReflectionCache.GetCollectionName<TDocument>(), settings);
return database.GetCollection<TDocument>(MongoCollectionNameCache.GetCollectionName<TDocument>(), settings);
}

public static string GetCollectionName<TDocument>(this IMongoDatabase database)
where TDocument : IMongoDocument
where TDocument : class
{
return MongoReflectionCache.GetCollectionName<TDocument>();
return MongoCollectionNameCache.GetCollectionName<TDocument>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Concurrent;
using System.Reflection;

namespace Workleap.Extensions.Mongo;

internal static class MongoCollectionNameCache
{
private static readonly ConcurrentDictionary<Type, string> CollectionNames = new();

public static string GetCollectionName(Type documentType)
{
if (CollectionNames.TryGetValue(documentType, out var collectionName))
{
return collectionName;
}

// Configuration based CollectionNames are set manually by calling SetCollectionName.
// When we reach here, we can validate the Attribute flow because it was not a document from the Configuration flow.
if (!documentType.IsConcreteMongoDocumentType())
{
throw new ArgumentException(documentType + " must be a concrete type that implements " + nameof(IMongoDocument));
}

return CollectionNames.GetOrAdd(documentType, static documentType =>
{
if (documentType.GetCustomAttribute<MongoCollectionAttribute>() is { } attribute)
{
return attribute.Name;
}

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

public static string GetCollectionName<TDocument>()
where TDocument : class
{
return GetCollectionName(typeof(TDocument));
}

internal static void SetCollectionName(Type documentType, string collectionName)
{
if (!CollectionNames.TryAdd(documentType, collectionName))
{
throw new ArgumentException($"Collection name for {documentType} already set.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Concurrent;

namespace Workleap.Extensions.Mongo;

internal static class MongoConfigurationIndexStore
{
private static readonly ConcurrentDictionary<Type, Type?> IndexProviderTypes = new();

internal static void AddIndexProviderType(Type documentType, Type? indexProviderType)
{
if (!IndexProviderTypes.TryAdd(documentType, indexProviderType))
{
throw new ArgumentException($"IndexProviderType for {documentType} already set.");
}
}

internal static IReadOnlyDictionary<Type, Type?> GetIndexProviderTypes() => IndexProviderTypes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ namespace Workleap.Extensions.Mongo;
/// Inherit from this class to define indexes for a particular document type.
/// </summary>
public abstract class MongoIndexProvider<TDocument>
where TDocument : IMongoDocument
where TDocument : class
{
public IndexKeysDefinitionBuilder<TDocument> IndexKeys => Builders<TDocument>.IndexKeys;

public abstract IEnumerable<CreateIndexModel<TDocument>> CreateIndexModels();
}
35 changes: 0 additions & 35 deletions src/Workleap.Extensions.Mongo.Abstractions/MongoReflectionCache.cs

This file was deleted.

20 changes: 20 additions & 0 deletions src/Workleap.Extensions.Mongo.Abstractions/MongoTypeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Reflection;

namespace Workleap.Extensions.Mongo;

internal static class MongoTypeExtensions
{
internal static bool IsConcreteMongoDocumentType(this Type type) => !type.IsAbstract && typeof(IMongoDocument).IsAssignableFrom(type);

internal static bool IsMongoCollectionConfigurationInterface(this Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IMongoCollectionConfiguration<>);

internal static void EnsureHasPublicParameterlessConstructor(this Type type)
{
if (!type.HasPublicParameterlessConstructor())
{
throw new InvalidOperationException($"Type {type}' must have a public parameterless constructor");
}
}

private static bool HasPublicParameterlessConstructor(this Type type) => type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null) != null;
}
19 changes: 13 additions & 6 deletions src/Workleap.Extensions.Mongo.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
#nullable enable
abstract Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.CreateIndexModels() -> System.Collections.Generic.IEnumerable<MongoDB.Driver.CreateIndexModel<TDocument>!>!
MongoDB.Driver.AsyncCursorExtensions
MongoDB.Driver.IMongoCollectionExtensions
MongoDB.Driver.IMongoDatabaseExtensions
Workleap.Extensions.Mongo.IMongoClientProvider
Expand All @@ -26,9 +24,18 @@ Workleap.Extensions.Mongo.StringGuidIdGenerator.IsEmpty(object! id) -> bool
Workleap.Extensions.Mongo.StringGuidIdGenerator.StringGuidIdGenerator() -> void
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>!
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>!
static MongoDB.Driver.IMongoCollectionExtensions.GetName<TDocument>(this MongoDB.Driver.IMongoCollection<TDocument>! collection) -> string!
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollection<TDocument>(this MongoDB.Driver.IMongoDatabase! database, MongoDB.Driver.MongoCollectionSettings? settings = null) -> MongoDB.Driver.IMongoCollection<TDocument>!
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollectionName<TDocument>(this MongoDB.Driver.IMongoDatabase! database) -> string!
static readonly Workleap.Extensions.Mongo.MongoDefaults.ClientName -> string!
static Workleap.Extensions.Mongo.StringGuidIdGenerator.GenerateId() -> object!
static Workleap.Extensions.Mongo.StringGuidIdGenerator.Instance.get -> MongoDB.Bson.Serialization.IIdGenerator!
static Workleap.Extensions.Mongo.StringGuidIdGenerator.Instance.get -> MongoDB.Bson.Serialization.IIdGenerator!
abstract Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.CreateIndexModels() -> System.Collections.Generic.IEnumerable<MongoDB.Driver.CreateIndexModel<TDocument!>!>!
MongoDB.Driver.AsyncCursorExtensions
static MongoDB.Driver.IMongoCollectionExtensions.GetName<TDocument>(this MongoDB.Driver.IMongoCollection<TDocument!>! collection) -> string!
static MongoDB.Driver.IMongoDatabaseExtensions.GetCollection<TDocument>(this MongoDB.Driver.IMongoDatabase! database, MongoDB.Driver.MongoCollectionSettings? settings = null) -> MongoDB.Driver.IMongoCollection<TDocument!>!
static readonly Workleap.Extensions.Mongo.MongoDefaults.ClientName -> string!
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.BsonClassMap(System.Action<MongoDB.Bson.Serialization.BsonClassMap<TDocument!>!>! classMapInitializer) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.CollectionName(string! collectionName) -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument>.IndexProvider<TIndexProvider>() -> Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>!
Workleap.Extensions.Mongo.IMongoCollectionConfiguration<TDocument>
Workleap.Extensions.Mongo.IMongoCollectionConfiguration<TDocument>.Configure(Workleap.Extensions.Mongo.IMongoCollectionBuilder<TDocument!>! builder) -> void
Workleap.Extensions.Mongo.MongoIndexProvider<TDocument>.IndexKeys.get -> MongoDB.Driver.IndexKeysDefinitionBuilder<TDocument!>!
Loading

0 comments on commit 6ebdc57

Please sign in to comment.