Skip to content

Commit

Permalink
[IDP-643] Support multiple indexer for same collection (#34)
Browse files Browse the repository at this point in the history
Co-authored-by: Mathieu Gamache <mathieu.gamache@workleap.com>
  • Loading branch information
PrincessMadMath and Mathieu Gamache committed Nov 15, 2023
1 parent eae3299 commit 2b6aa11
Show file tree
Hide file tree
Showing 10 changed files with 442 additions and 82 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,59 @@ Our indexation engine handles:

> We include a Roslyn analyzer, detailed in a section below, that encourages developers to adorn classes that consume MongoDB collections with attributes (`IndexByAttribute` or `NoIndexNeededAttribute`). The aim is to increase awareness about which indexes should be used (or created) when querying MongoDB collections.
### Support for inheritance

The indexer mechanism support document inheritance and different indexer for a same collection. For example:

```csharp

[BsonKnownTypes(typeof(DogPersonDocument), typeof(DogPersonDocument))]
[MongoCollection("people", IndexProviderType = typeof(PersonDocumentIndexes))]
public class PersonDocument : IMongoDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }

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

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

// No special indexer for this class
[BsonDiscriminator("Dog")]
public class DogPersonDocument : PersonDocument
{
public int DogCount { get; set; } = string.Empty;
}

// Need to redefine MongoCollectionAttribute to use a different indexer
[BsonDiscriminator("Cat")]
[MongoCollection("people", IndexProviderType = typeof(CatDocumentIndexes))]
public class CatPersonDocument : PersonDocument
{
public int CatCount { get; set; }
}

public class CatDocumentIndexes : MongoIndexProvider<PersonDocument>
{
public override IEnumerable<CreateIndexModel<PersonDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<PersonDocument>(
Builders<PersonDocument>.IndexKeys.Combine().Ascending(x => x.CatCount),
new CreateIndexOptions { Name = "cat_count" });
}
}
```

## Field encryption

The Workleap.Extensions.Mongo library supports field-level encryption at rest, which means you can specify in your C# code which document fields should be encrypted in your MongoDB database. Any C# property can be encrypted, as long as you provide how data gets encrypted and decrypted. These properties then become binary data in your documents.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Workleap.Extensions.Mongo.Indexing;

namespace Workleap.Extensions.Mongo.Tests;

public class MongoIndexerAssemblyScanningTests
{
[Fact]
public async Task IsDocumentTypesWithExplicitMongoCollectionAttribute_Filter_Works()
{
Assert.False(MongoIndexer.IsDocumentTypesWithExplicitMongoCollectionAttribute(typeof(RandomObject)));
Assert.True(MongoIndexer.IsDocumentTypesWithExplicitMongoCollectionAttribute(typeof(BaseAssemblyScanningTestDocument)));
Assert.False(MongoIndexer.IsDocumentTypesWithExplicitMongoCollectionAttribute(typeof(ChildWithoutIndexerAssemblyScanningTestDocument)));
Assert.True(MongoIndexer.IsDocumentTypesWithExplicitMongoCollectionAttribute(typeof(ChildWithOwnIndexerAssemblyScanningTestDocument)));
}

private class RandomObject
{
public Guid WastedGuid { get; set; }
}

[BsonKnownTypes(typeof(ChildWithoutIndexerAssemblyScanningTestDocument))]
[MongoCollection("assemblyScanner", IndexProviderType = typeof(BaseAssemblyScanningTestProvider))]
private class BaseAssemblyScanningTestDocument : MongoDocument
{
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
}

[BsonDiscriminator("Children")]
private sealed class ChildWithoutIndexerAssemblyScanningTestDocument : BaseAssemblyScanningTestDocument
{
[BsonElement("email")]
public string Email { get; set; } = string.Empty;
}

[BsonDiscriminator("Children")]
[MongoCollection("assemblyScanner", IndexProviderType = typeof(ChildWithOwnIndexerAssemblyScanningTestProvider))]
private sealed class ChildWithOwnIndexerAssemblyScanningTestDocument : BaseAssemblyScanningTestDocument
{
[BsonElement("phone")]
public string PhoneNumber { get; set; } = string.Empty;
}

private sealed class BaseAssemblyScanningTestProvider : MongoIndexProvider<BaseAssemblyScanningTestDocument>
{
public override IEnumerable<CreateIndexModel<BaseAssemblyScanningTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<BaseAssemblyScanningTestDocument>(
Builders<BaseAssemblyScanningTestDocument>.IndexKeys.Ascending(x => x.Name),
new CreateIndexOptions { Name = "name" });
}
}

private sealed class ChildWithOwnIndexerAssemblyScanningTestProvider : MongoIndexProvider<ChildWithOwnIndexerAssemblyScanningTestDocument>
{
public override IEnumerable<CreateIndexModel<ChildWithOwnIndexerAssemblyScanningTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<ChildWithOwnIndexerAssemblyScanningTestDocument>(
Builders<ChildWithOwnIndexerAssemblyScanningTestDocument>
.IndexKeys.Ascending(x => x.PhoneNumber),
new CreateIndexOptions { Name = "contact" });
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Workleap.Extensions.Mongo;
using Workleap.Extensions.Mongo.Indexing;
using Workleap.Extensions.Xunit;

namespace Workleap.Extensions.Mongo.Tests;

public partial class MongoIndexerTests
public class MongoIndexerInheritanceTests : BaseIntegrationTest<MongoFixture>
{
public MongoIndexerInheritanceTests(MongoFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture, testOutputHelper)
{
}

[Fact]
public async Task UpdateIndexesAsync_Ignores_Automatically_Inherited_Index_Provider()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Workleap.Extensions.Mongo.Indexing;
using Workleap.Extensions.Xunit;

namespace Workleap.Extensions.Mongo.Tests;

public class MongoIndexerMultipleIndexersTests : BaseIntegrationTest<MongoFixture>
{
public MongoIndexerMultipleIndexersTests(MongoFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture, testOutputHelper)
{
}

[Fact]
public async Task UpdateIndexesAsync_Support_Creating_Indexes_When_Multiple_Indexers_For_Same_Collection()
{
async Task AssertIndexes<TDocument>()
where TDocument : IMongoDocument
{
using var indexCursor = await this.Services.GetRequiredService<IMongoCollection<TDocument>>().Indexes.ListAsync();
var indexNames = await indexCursor.ToAsyncEnumerable().Select(x => x["name"].AsString).ToArrayAsync();

Assert.Equal(4, indexNames.Length);
Assert.Contains("_id_", indexNames);
Assert.Contains("name_7a2f315f44b4b7813c1db6c66b1ae173", indexNames);
Assert.Contains("contact_2ec2ef6cef9b5c4182df182eb4e1e857", indexNames);
Assert.Contains("metadata_fbf67839566769a7660548cfcb674468", indexNames);
}

var documentTypes = new[] { typeof(BaseMultipleIndexersTestDocument), typeof(ChildMultipleIndexersTestDocument), typeof(SiblingMultipleIndexersTestDocument) };
await this.Services.GetRequiredService<IMongoIndexer>().UpdateIndexesAsync(documentTypes);

await AssertIndexes<BaseMultipleIndexersTestDocument>();
await AssertIndexes<ChildMultipleIndexersTestDocument>();
await AssertIndexes<SiblingMultipleIndexersTestDocument>();
}

[Fact]
public async Task UpdateIndexesAsync_Support_Deleting_Indexes_When_Multiple_Indexers_For_Same_Collection()
{
async Task AssertIndexes<TDocument>()
where TDocument : IMongoDocument
{
using var indexCursor = await this.Services.GetRequiredService<IMongoCollection<TDocument>>().Indexes.ListAsync();
var indexNames = await indexCursor.ToAsyncEnumerable().Select(x => x["name"].AsString).ToArrayAsync();

Assert.Equal(3, indexNames.Length);
Assert.Contains("_id_", indexNames);
Assert.Contains("name_7a2f315f44b4b7813c1db6c66b1ae173", indexNames);
Assert.Contains("contact_800a8c19f5008939a89b582cc7c22e40", indexNames);
}

// Simulate a N-1 release with some initial indexes
var documentTypes = new[] { typeof(BaseMultipleIndexersTestDocument), typeof(ChildMultipleIndexersTestDocument), typeof(SiblingMultipleIndexersTestDocument) };
await this.Services.GetRequiredService<IMongoIndexer>().UpdateIndexesAsync(documentTypes);

// Simulate the N release where we remove one indexer and update an other
var updatedDocumentTypes = new[] { typeof(BaseMultipleIndexersTestDocument), typeof(UpdatedChildMultipleIndexersTestDocument) };
await this.Services.GetRequiredService<IMongoIndexer>().UpdateIndexesAsync(updatedDocumentTypes);

await AssertIndexes<BaseMultipleIndexersTestDocument>();
}

[BsonKnownTypes(typeof(ChildMultipleIndexersTestDocument))]
[MongoCollection("multipleIndexProviders", IndexProviderType = typeof(BaseMultipleIndexersProvider))]
private class BaseMultipleIndexersTestDocument : MongoDocument
{
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
}

private sealed class BaseMultipleIndexersProvider : MongoIndexProvider<BaseMultipleIndexersTestDocument>
{
public override IEnumerable<CreateIndexModel<BaseMultipleIndexersTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<BaseMultipleIndexersTestDocument>(
Builders<BaseMultipleIndexersTestDocument>.IndexKeys.Ascending(x => x.Name),
new CreateIndexOptions { Name = "name" });
}
}

[BsonDiscriminator("Children")]
[MongoCollection("multipleIndexProviders", IndexProviderType = typeof(ChildMultipleIndexersProvider))]
private class ChildMultipleIndexersTestDocument : BaseMultipleIndexersTestDocument
{
[BsonElement("email")]
public string Email { get; set; } = string.Empty;

[BsonElement("phone")]
public string PhoneNumber { get; set; } = string.Empty;
}

private sealed class ChildMultipleIndexersProvider : MongoIndexProvider<ChildMultipleIndexersTestDocument>
{
public override IEnumerable<CreateIndexModel<ChildMultipleIndexersTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<ChildMultipleIndexersTestDocument>(
Builders<ChildMultipleIndexersTestDocument>
.IndexKeys.Ascending(x => x.Email),
new CreateIndexOptions { Name = "contact" });
}
}

// Represent an update in the indexes.
[BsonDiscriminator("Children")]
[MongoCollection("multipleIndexProviders", IndexProviderType = typeof(UpdatedChildMultipleIndexersProvider))]
private sealed class UpdatedChildMultipleIndexersTestDocument : ChildMultipleIndexersTestDocument
{
}

private sealed class UpdatedChildMultipleIndexersProvider : MongoIndexProvider<UpdatedChildMultipleIndexersTestDocument>
{
public override IEnumerable<CreateIndexModel<UpdatedChildMultipleIndexersTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<UpdatedChildMultipleIndexersTestDocument>(
Builders<UpdatedChildMultipleIndexersTestDocument>
.IndexKeys.Ascending(x => x.PhoneNumber),
new CreateIndexOptions { Name = "contact" });
}
}

[MongoCollection("multipleIndexProviders", IndexProviderType = typeof(SiblingMultipleIndexersProvider))]
private sealed class SiblingMultipleIndexersTestDocument : MongoDocument
{
[BsonElement("metadata")]
public string Metadata { get; set; } = string.Empty;
}

private sealed class SiblingMultipleIndexersProvider : MongoIndexProvider<SiblingMultipleIndexersTestDocument>
{
public override IEnumerable<CreateIndexModel<SiblingMultipleIndexersTestDocument>> CreateIndexModels()
{
yield return new CreateIndexModel<SiblingMultipleIndexersTestDocument>(
Builders<SiblingMultipleIndexersTestDocument>
.IndexKeys.Ascending(x => x.Metadata),
new CreateIndexOptions { Name = "metadata" });
}
}
}
2 changes: 1 addition & 1 deletion src/Workleap.Extensions.Mongo.Tests/MongoIndexerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace Workleap.Extensions.Mongo.Tests;

public partial class MongoIndexerTests : BaseIntegrationTest<MongoFixture>
public class MongoIndexerTests : BaseIntegrationTest<MongoFixture>
{
public MongoIndexerTests(MongoFixture fixture, ITestOutputHelper testOutputHelper)
: base(fixture, testOutputHelper)
Expand Down
6 changes: 6 additions & 0 deletions src/Workleap.Extensions.Mongo/Indexing/IndexCreationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Workleap.Extensions.Mongo.Indexing;

internal class IndexCreationResult
{
public IList<UniqueIndexName> ExpectedIndexes { get; } = new List<UniqueIndexName>();
}
Loading

0 comments on commit 2b6aa11

Please sign in to comment.