From 5caed5b3a9110149c5a4779c06907f547e66460c Mon Sep 17 00:00:00 2001 From: alexandre-spieser Date: Sun, 27 Aug 2017 17:35:21 +0000 Subject: [PATCH] added tests for partitioned collections --- IntegrationTests/CreatePartitionedTests.cs | 68 +++++ IntegrationTests/CreateTests.cs | 56 ++-- .../BaseMongoDbRepositoryTests.cs | 51 ++++ .../Infrastructure/ITestRepository.cs | 1 + .../Infrastructure/TestRepository.cs | 5 + IntegrationTests/IntegrationTests.csproj | 3 + IntegrationTests/ReadTests.cs | 19 ++ IntegrationTests/UpdateTests.cs | 8 +- MongoDbGenericRepository/IMongoDbContext.cs | 13 +- MongoDbGenericRepository/Models/Document.cs | 21 +- MongoDbGenericRepository/Models/IDocument.cs | 16 +- MongoDbGenericRepository/MongoDbContext.cs | 22 +- MongoDbGenericRepository/MongoDbRepository.cs | 257 ++++++++++-------- 13 files changed, 360 insertions(+), 180 deletions(-) create mode 100644 IntegrationTests/CreatePartitionedTests.cs create mode 100644 IntegrationTests/Infrastructure/BaseMongoDbRepositoryTests.cs create mode 100644 IntegrationTests/ReadTests.cs diff --git a/IntegrationTests/CreatePartitionedTests.cs b/IntegrationTests/CreatePartitionedTests.cs new file mode 100644 index 0000000..2061a18 --- /dev/null +++ b/IntegrationTests/CreatePartitionedTests.cs @@ -0,0 +1,68 @@ +using IntegrationTests.Infrastructure; +using MongoDbGenericRepository.Models; +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace IntegrationTests +{ + public class CreateTestsPartitionedDocument : PartitionedDocument + { + public CreateTestsPartitionedDocument() : base("TestPartitionKey") + { + Version = 1; + } + public string SomeContent { get; set; } + } + + public class CreatePartitionedTests : BaseMongoDbRepositoryTests + { + [Test] + public void AddOne() + { + // Arrange + var document = new CreateTestsPartitionedDocument(); + // Act + SUT.AddOne(document); + // Assert + long count = SUT.Count(e => e.Id == document.Id, PartitionKey); + Assert.AreEqual(1, count); + } + + [Test] + public async Task AddOneAsync() + { + // Arrange + var document = new CreateTestsPartitionedDocument(); + // Act + await SUT.AddOneAsync(document); + // Assert + long count = SUT.Count(e => e.Id == document.Id, PartitionKey); + Assert.AreEqual(1, count); + } + + [Test] + public void AddMany() + { + // Arrange + var documents = new List { new CreateTestsPartitionedDocument(), new CreateTestsPartitionedDocument() }; + // Act + SUT.AddMany(documents); + // Assert + long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id, PartitionKey); + Assert.AreEqual(2, count); + } + + [Test] + public async Task AddManyAsync() + { + // Arrange + var documents = new List { new CreateTestsPartitionedDocument(), new CreateTestsPartitionedDocument() }; + // Act + await SUT.AddManyAsync(documents); + // Assert + long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id, PartitionKey); + Assert.AreEqual(2, count); + } + } +} diff --git a/IntegrationTests/CreateTests.cs b/IntegrationTests/CreateTests.cs index 07baac3..007c8bf 100644 --- a/IntegrationTests/CreateTests.cs +++ b/IntegrationTests/CreateTests.cs @@ -2,41 +2,31 @@ using MongoDbGenericRepository.Models; using NUnit.Framework; using System.Collections.Generic; -using System.Configuration; using System.Threading.Tasks; namespace IntegrationTests { - public class CreateTests + public class CreateTestsDocument : Document { - /// - /// SUT: System Under Test - /// - private static ITestRepository SUT { get; set; } - - [OneTimeSetUp] - public void Init() + public CreateTestsDocument() { - var connectionString = ConfigurationManager.ConnectionStrings["MongoDbTests"].ConnectionString; - SUT = new TestRepository(connectionString, "MongoDbTests"); - } - - [OneTimeTearDown] - public void Cleanup() - { - // We drop the collection at the end of each test session. - SUT.DropTestCollection(); + Version = 2; } + public string SomeContent { get; set; } + } + [TestFixture] + public class CreateTests : BaseMongoDbRepositoryTests + { [Test] public void AddOne() { // Arrange - var document = new InsertTestsDocument(); + var document = new CreateTestsDocument(); // Act SUT.AddOne(document); // Assert - long count = SUT.Count(e => e.Id == document.Id); + long count = SUT.Count(e => e.Id == document.Id); Assert.AreEqual(1, count); } @@ -44,11 +34,11 @@ namespace IntegrationTests public async Task AddOneAsync() { // Arrange - var document = new InsertTestsDocument(); + var document = new CreateTestsDocument(); // Act await SUT.AddOneAsync(document); // Assert - long count = SUT.Count(e => e.Id == document.Id); + long count = SUT.Count(e => e.Id == document.Id); Assert.AreEqual(1, count); } @@ -56,11 +46,11 @@ namespace IntegrationTests public void AddMany() { // Arrange - var documents = new List { new InsertTestsDocument(), new InsertTestsDocument() }; + var documents = new List { new CreateTestsDocument(), new CreateTestsDocument() }; // Act SUT.AddMany(documents); // Assert - long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id); + long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id); Assert.AreEqual(2, count); } @@ -68,26 +58,12 @@ namespace IntegrationTests public async Task AddManyAsync() { // Arrange - var documents = new List { new InsertTestsDocument(), new InsertTestsDocument() }; + var documents = new List { new CreateTestsDocument(), new CreateTestsDocument() }; // Act await SUT.AddManyAsync(documents); // Assert - long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id); + long count = SUT.Count(e => e.Id == documents[0].Id || e.Id == documents[1].Id); Assert.AreEqual(2, count); } - - - #region Utils - - private class InsertTestsDocument : Document - { - public InsertTestsDocument() - { - Version = 2; - } - public string SomeContent { get; set; } - } - - #endregion } } diff --git a/IntegrationTests/Infrastructure/BaseMongoDbRepositoryTests.cs b/IntegrationTests/Infrastructure/BaseMongoDbRepositoryTests.cs new file mode 100644 index 0000000..fae98d9 --- /dev/null +++ b/IntegrationTests/Infrastructure/BaseMongoDbRepositoryTests.cs @@ -0,0 +1,51 @@ +using MongoDbGenericRepository.Models; +using NUnit.Framework; +using System.Configuration; + +namespace IntegrationTests.Infrastructure +{ + public class BaseMongoDbRepositoryTests where T : Document, new() + { + public T GetDocumentInstance() + { + return new T(); + } + + public BaseMongoDbRepositoryTests() + { + var type = GetDocumentInstance(); + if (type is IPartitionedDocument) + { + PartitionKey = ((IPartitionedDocument)type).PartitionKey; + } + } + + public string PartitionKey { get; set; } + + /// + /// SUT: System Under Test + /// + protected static ITestRepository SUT { get; set; } + + [OneTimeSetUp] + public void Init() + { + var connectionString = ConfigurationManager.ConnectionStrings["MongoDbTests"].ConnectionString; + SUT = new TestRepository(connectionString, "MongoDbTests"); + } + + [OneTimeTearDown] + public void Cleanup() + { + // We drop the collection at the end of each test session. + if (!string.IsNullOrEmpty(PartitionKey)) + { + SUT.DropTestCollection(PartitionKey); + } + else + { + SUT.DropTestCollection(); + } + } + } +} diff --git a/IntegrationTests/Infrastructure/ITestRepository.cs b/IntegrationTests/Infrastructure/ITestRepository.cs index 549bbdb..c2d672c 100644 --- a/IntegrationTests/Infrastructure/ITestRepository.cs +++ b/IntegrationTests/Infrastructure/ITestRepository.cs @@ -5,5 +5,6 @@ namespace IntegrationTests public interface ITestRepository : IBaseMongoRepository { void DropTestCollection(); + void DropTestCollection(string partitionKey); } } \ No newline at end of file diff --git a/IntegrationTests/Infrastructure/TestRepository.cs b/IntegrationTests/Infrastructure/TestRepository.cs index 0fb550a..1fdd63f 100644 --- a/IntegrationTests/Infrastructure/TestRepository.cs +++ b/IntegrationTests/Infrastructure/TestRepository.cs @@ -13,5 +13,10 @@ namespace IntegrationTests.Infrastructure { _mongoDbContext.DropCollection(); } + + public void DropTestCollection(string partitionKey) + { + _mongoDbContext.DropCollection(partitionKey); + } } } diff --git a/IntegrationTests/IntegrationTests.csproj b/IntegrationTests/IntegrationTests.csproj index b552942..39bf8af 100644 --- a/IntegrationTests/IntegrationTests.csproj +++ b/IntegrationTests/IntegrationTests.csproj @@ -56,10 +56,13 @@ + + + diff --git a/IntegrationTests/ReadTests.cs b/IntegrationTests/ReadTests.cs new file mode 100644 index 0000000..1e2ec8d --- /dev/null +++ b/IntegrationTests/ReadTests.cs @@ -0,0 +1,19 @@ +using MongoDbGenericRepository.Models; +using NUnit.Framework; + +namespace IntegrationTests +{ + public class ReadTestsDocument : Document + { + public ReadTestsDocument() + { + Version = 2; + } + public string SomeContent { get; set; } + } + + [TestFixture] + public class ReadTests + { + } +} diff --git a/IntegrationTests/UpdateTests.cs b/IntegrationTests/UpdateTests.cs index d015760..5aaf4cd 100644 --- a/IntegrationTests/UpdateTests.cs +++ b/IntegrationTests/UpdateTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IntegrationTests +namespace IntegrationTests { class UpdateTests { diff --git a/MongoDbGenericRepository/IMongoDbContext.cs b/MongoDbGenericRepository/IMongoDbContext.cs index 24cb708..aa3bd51 100644 --- a/MongoDbGenericRepository/IMongoDbContext.cs +++ b/MongoDbGenericRepository/IMongoDbContext.cs @@ -9,20 +9,25 @@ namespace MongoDbGenericRepository /// The private GetCollection method /// /// - /// IMongoCollection GetCollection(); /// - /// The private GetCollection method + /// Returns a collection for a document type that has a partition key. /// /// - /// - IMongoCollection GetCollection(TDocument document) where TDocument : IDocument; + /// The value of the partition key. + IMongoCollection GetCollection(string partitionKey) where TDocument : IDocument; /// /// Drops a collection, use very carefully. /// /// void DropCollection(); + + /// + /// Drops a collection having a partitionkey, use very carefully. + /// + /// + void DropCollection(string partitionKey); } } \ No newline at end of file diff --git a/MongoDbGenericRepository/Models/Document.cs b/MongoDbGenericRepository/Models/Document.cs index 11ee9a6..ec45871 100644 --- a/MongoDbGenericRepository/Models/Document.cs +++ b/MongoDbGenericRepository/Models/Document.cs @@ -23,13 +23,6 @@ namespace MongoDbGenericRepository.Models [BsonId] public Guid Id { get; set; } - /// - /// The name of the property used for partitioning the collection - /// This will not be inserted into the collection. - /// This partition key will be prepended to the collection name to create a new collection. - /// - public string PartitionKey { get; set; } - /// /// The datetime in UTC at which the document was added. /// @@ -40,4 +33,18 @@ namespace MongoDbGenericRepository.Models /// public int Version { get; set; } } + + public class PartitionedDocument : Document, IPartitionedDocument + { + public PartitionedDocument(string partitionKey) + { + PartitionKey = partitionKey; + } + /// + /// The name of the property used for partitioning the collection + /// This will not be inserted into the collection. + /// This partition key will be prepended to the collection name to create a new collection. + /// + public string PartitionKey { get; set; } + } } \ No newline at end of file diff --git a/MongoDbGenericRepository/Models/IDocument.cs b/MongoDbGenericRepository/Models/IDocument.cs index 4d2e4e5..9a3f4b2 100644 --- a/MongoDbGenericRepository/Models/IDocument.cs +++ b/MongoDbGenericRepository/Models/IDocument.cs @@ -2,11 +2,25 @@ namespace MongoDbGenericRepository.Models { + /// + /// This class represents a basic document that can be stored in MongoDb. + /// Your document must implement this class in order for the MongoDbRepository to handle them. + /// public interface IDocument { DateTime AddedAtUtc { get; set; } Guid Id { get; set; } - string PartitionKey { get; set; } int Version { get; set; } } + + /// + /// This class represents a document that can be inserted in a collection that can be partitioned. + /// The partition key allows for the creation of different collections having the same document schema. + /// This can be useful if you are planning to build a Software as a Service (SaaS) Platform, or if you want to reduce indexing. + /// You could for example insert Logs in different collections based on the week and year they where created, or their Log category/source. + /// + public interface IPartitionedDocument : IDocument + { + string PartitionKey { get; set; } + } } \ No newline at end of file diff --git a/MongoDbGenericRepository/MongoDbContext.cs b/MongoDbGenericRepository/MongoDbContext.cs index 53e174d..75b9bfd 100644 --- a/MongoDbGenericRepository/MongoDbContext.cs +++ b/MongoDbGenericRepository/MongoDbContext.cs @@ -35,13 +35,13 @@ namespace MongoDbGenericRepository } /// - /// The private GetCollection method + /// Returns a collection for a document type that has a partition key. /// /// - /// - public IMongoCollection GetCollection(TDocument document) where TDocument : IDocument + /// The value of the partition key. + public IMongoCollection GetCollection(string partitionKey) where TDocument : IDocument { - return _database.GetCollection(PluralizePartitioned(document)); + return _database.GetCollection(partitionKey +"-"+ Pluralize()); } /// @@ -53,14 +53,18 @@ namespace MongoDbGenericRepository _database.DropCollection(Pluralize()); } + /// + /// Drops a collection having a partitionkey, use very carefully. + /// + /// + public void DropCollection(string partitionKey) + { + _database.DropCollection(partitionKey + "-" + Pluralize()); + } + private string Pluralize() { return typeof(TDocument).Name.ToLower() + "s"; } - - private string PluralizePartitioned(TDocument document) where TDocument : IDocument - { - return document.PartitionKey + typeof(TDocument).Name.ToLower() + "s"; - } } } \ No newline at end of file diff --git a/MongoDbGenericRepository/MongoDbRepository.cs b/MongoDbGenericRepository/MongoDbRepository.cs index 5ba3108..d7dc163 100644 --- a/MongoDbGenericRepository/MongoDbRepository.cs +++ b/MongoDbGenericRepository/MongoDbRepository.cs @@ -10,6 +10,41 @@ namespace MongoDbGenericRepository { public interface IBaseMongoRepository { + #region Create + + /// + /// Asynchronously adds a document to the collection. + /// + /// + /// The document you want to add. + Task AddOneAsync(TDocument document) where TDocument : IDocument; + + /// + /// Adds a document to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + void AddOne(TDocument document) where TDocument : IDocument; + + /// + /// Asynchronously adds a list of documents to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + Task AddManyAsync(IEnumerable documents) where TDocument : IDocument; + + /// + /// Adds a list of documents to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + void AddMany(IEnumerable documents) where TDocument : IDocument; + + #endregion + #region Read /// @@ -80,46 +115,16 @@ namespace MongoDbGenericRepository /// /// /// A LINQ expression filter. - long Count(Expression> filter) where TDocument : IDocument; + long Count(Expression> filter, string partitionKey = null) where TDocument : IDocument; #endregion Get - #region Create - - /// - /// Asynchronously adds a document to the collection. - /// - /// - /// The document you want to add. - Task AddOneAsync(TDocument document) where TDocument : IDocument; - - /// - /// Adds a document to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - void AddOne(TDocument document) where TDocument : IDocument; - - /// - /// Asynchronously adds a list of documents to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - Task AddManyAsync(IEnumerable documents) where TDocument : IDocument; - - /// - /// Adds a list of documents to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - void AddMany(IEnumerable documents) where TDocument : IDocument; - - #endregion } + /// + /// The base Repository, it is meant to be inherited from by your custom custom MongoRepository implementation. + /// Its constructor must be given a connection string and a database name. + /// public abstract class BaseMongoRepository : IBaseMongoRepository { public string ConnectionString { get; set; } @@ -137,6 +142,68 @@ namespace MongoDbGenericRepository protected IMongoDbContext _mongoDbContext = null; + #region Create + + /// + /// Asynchronously adds a document to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + public async Task AddOneAsync(TDocument document) where TDocument : IDocument + { + FormatDocument(document); + await HandlePartitioned(document).InsertOneAsync(document); + } + + /// + /// Adds a document to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + public void AddOne(TDocument document) where TDocument : IDocument + { + FormatDocument(document); + HandlePartitioned(document).InsertOne(document); + } + + /// + /// Asynchronously adds a list of documents to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + public async Task AddManyAsync(IEnumerable documents) where TDocument : IDocument + { + if (!documents.Any()) + { + return; + } + foreach (var doc in documents) + { + FormatDocument(doc); + } + await HandlePartitioned(documents.FirstOrDefault()).InsertManyAsync(documents); + } + + /// + /// Adds a list of documents to the collection. + /// Populates the Id and AddedAtUtc fields if necessary. + /// + /// + /// The document you want to add. + public void AddMany(IEnumerable documents) where TDocument : IDocument + { + foreach (var document in documents) + { + FormatDocument(document); + } + HandlePartitioned(documents.FirstOrDefault()).InsertMany(documents); + } + + #endregion Create + #region Read /// @@ -227,6 +294,7 @@ namespace MongoDbGenericRepository /// /// /// A LINQ expression filter. + /// An optional partitionKey public async Task CountAsync(Expression> filter) where TDocument : IDocument { return await GetCollection().CountAsync(filter); @@ -237,9 +305,10 @@ namespace MongoDbGenericRepository /// /// /// A LINQ expression filter. - public long Count(Expression> filter) where TDocument : IDocument + /// An optional partitionKey + public long Count(Expression> filter, string partitionKey = null) where TDocument : IDocument { - return GetCollection().Find(filter).Count(); + return HandlePartitioned(partitionKey).Find(filter).Count(); } /// @@ -272,78 +341,6 @@ namespace MongoDbGenericRepository #endregion Get - #region Create - - - private void FormatDocument(TDocument document) where TDocument : IDocument - { - if (document.Id == default(Guid)) - { - document.Id = Guid.NewGuid(); - } - - if (document.AddedAtUtc == default(DateTime)) - { - document.AddedAtUtc = DateTime.UtcNow; - } - } - - /// - /// Asynchronously adds a document to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - public async Task AddOneAsync(TDocument document) where TDocument : IDocument - { - FormatDocument(document); - await GetCollection().InsertOneAsync(document); - } - - /// - /// Adds a document to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - public void AddOne(TDocument document) where TDocument : IDocument - { - FormatDocument(document); - GetCollection().InsertOne(document); - } - - /// - /// Asynchronously adds a list of documents to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - public async Task AddManyAsync(IEnumerable documents) where TDocument : IDocument - { - foreach (var document in documents) - { - FormatDocument(document); - } - await GetCollection().InsertManyAsync(documents); - } - - /// - /// Adds a list of documents to the collection. - /// Populates the Id and AddedAtUtc fields if necessary. - /// - /// - /// The document you want to add. - public void AddMany(IEnumerable documents) where TDocument : IDocument - { - foreach (var document in documents) - { - FormatDocument(document); - } - GetCollection().InsertMany(documents); - } - - #endregion Create - #region Delete /// @@ -509,13 +506,14 @@ namespace MongoDbGenericRepository #endregion Find And Update /// - /// The private GetCollection method + /// /// /// + /// /// - private IMongoCollection GetCollection(TDocument document) where TDocument : IDocument + private IMongoCollection GetCollection(string partitionKey) where TDocument : IDocument { - return _mongoDbContext.GetCollection(document); + return _mongoDbContext.GetCollection(partitionKey); } /// @@ -527,5 +525,40 @@ namespace MongoDbGenericRepository { return _mongoDbContext.GetCollection(); } + + private IMongoCollection HandlePartitioned(TDocument document) where TDocument : IDocument + { + if (document is IPartitionedDocument) + { + return GetCollection(((IPartitionedDocument)document).PartitionKey); + } + return GetCollection(); + } + + private IMongoCollection HandlePartitioned(string partitionKey) where TDocument : IDocument + { + if (!string.IsNullOrEmpty(partitionKey)) + { + return GetCollection(partitionKey); + } + return GetCollection(); + } + + private void FormatDocument(TDocument document) where TDocument : IDocument + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + if (document.Id == default(Guid)) + { + document.Id = Guid.NewGuid(); + } + + if (document.AddedAtUtc == default(DateTime)) + { + document.AddedAtUtc = DateTime.UtcNow; + } + } } } \ No newline at end of file