diff --git a/src/MongoFramework/AssemblyInternals.cs b/src/MongoFramework/AssemblyInternals.cs new file mode 100644 index 00000000..90119388 --- /dev/null +++ b/src/MongoFramework/AssemblyInternals.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MongoFramework.Tests")] \ No newline at end of file diff --git a/src/MongoFramework/Attributes/BucketSetOptionsAttribute.cs b/src/MongoFramework/Attributes/BucketSetOptionsAttribute.cs index 926d2f1d..9357fd53 100644 --- a/src/MongoFramework/Attributes/BucketSetOptionsAttribute.cs +++ b/src/MongoFramework/Attributes/BucketSetOptionsAttribute.cs @@ -5,18 +5,21 @@ namespace MongoFramework.Attributes [AttributeUsage(AttributeTargets.Property)] public class BucketSetOptionsAttribute : DbSetOptionsAttribute { - public int BucketSize { get; private set; } + public int BucketSize { get; } + public string EntityTimeProperty { get; } - public BucketSetOptionsAttribute(int bucketSize) + public BucketSetOptionsAttribute(int bucketSize, string entityTimeProperty) { BucketSize = bucketSize; + EntityTimeProperty = entityTimeProperty; } public override IDbSetOptions GetOptions() { return new BucketSetOptions { - BucketSize = BucketSize + BucketSize = BucketSize, + EntityTimeProperty = EntityTimeProperty }; } } diff --git a/src/MongoFramework/BucketSetOptions.cs b/src/MongoFramework/BucketSetOptions.cs index c4b44aff..197913b6 100644 --- a/src/MongoFramework/BucketSetOptions.cs +++ b/src/MongoFramework/BucketSetOptions.cs @@ -4,5 +4,6 @@ namespace MongoFramework public class BucketSetOptions : IDbSetOptions { public int BucketSize { get; set; } + public string EntityTimeProperty { get; set; } } } diff --git a/src/MongoFramework/EntityBucket.cs b/src/MongoFramework/EntityBucket.cs index ee765cb1..f9d5bd45 100644 --- a/src/MongoFramework/EntityBucket.cs +++ b/src/MongoFramework/EntityBucket.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace MongoFramework { @@ -6,7 +7,8 @@ public class EntityBucket where TGroup : class { public string Id { get; set; } public TGroup Group { get; set; } - public int Index { get; set; } + public DateTime Min { get; set; } + public DateTime Max { get; set; } public int ItemCount { get; set; } public int BucketSize { get; set; } public List Items { get; set; } diff --git a/src/MongoFramework/IMongoDbBucketSet.cs b/src/MongoFramework/IMongoDbBucketSet.cs index 0b170bb5..4d96e66e 100644 --- a/src/MongoFramework/IMongoDbBucketSet.cs +++ b/src/MongoFramework/IMongoDbBucketSet.cs @@ -3,10 +3,13 @@ namespace MongoFramework { - public interface IMongoDbBucketSet : IMongoDbSet, IQueryable> where TGroup : class + public interface IMongoDbBucketSet : IMongoDbSet, IQueryable> + where TGroup : class + where TSubEntity : class { void Add(TGroup group, TSubEntity entity); void AddRange(TGroup group, IEnumerable entities); + void Remove(TGroup group); IQueryable WithGroup(TGroup group); IQueryable Groups(); } diff --git a/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs b/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs new file mode 100644 index 00000000..fc3d7012 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using MongoDB.Driver; +using MongoFramework.Infrastructure.Mapping; + +namespace MongoFramework.Infrastructure.Commands +{ + public class AddToBucketCommand : IWriteCommand> + where TGroup : class + where TSubEntity : class + { + private TGroup Group { get; } + private TSubEntity SubEntity { get; } + private IEntityProperty EntityTimeProperty { get; } + private int BucketSize { get; } + + public AddToBucketCommand(TGroup group, TSubEntity subEntity, IEntityProperty entityTimeProperty, int bucketSize) + { + Group = group; + SubEntity = subEntity; + BucketSize = bucketSize; + EntityTimeProperty = entityTimeProperty; + } + + public IEnumerable>> GetModel() + { + var filterBuilder = Builders>.Filter; + var filter = filterBuilder.And( + filterBuilder.Eq(b => b.Group, Group), + filterBuilder.Where(b => b.ItemCount < BucketSize) + ); + + var entityDefinition = EntityMapping.GetOrCreateDefinition(typeof(EntityBucket)); + + var itemTimeValue = (DateTime)EntityTimeProperty.GetValue(SubEntity); + + var updateDefinition = Builders>.Update + .Inc(b => b.ItemCount, 1) + .Push(b => b.Items, SubEntity) + .Min(b => b.Min, itemTimeValue) + .Max(b => b.Max, itemTimeValue) + .SetOnInsert(b => b.BucketSize, BucketSize) + .SetOnInsert(b => b.Id, entityDefinition.KeyGenerator.Generate()); + + yield return new UpdateOneModel>(filter, updateDefinition) + { + IsUpsert = true + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Commands/RemoveBucketCommand.cs b/src/MongoFramework/Infrastructure/Commands/RemoveBucketCommand.cs new file mode 100644 index 00000000..62a535b3 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Commands/RemoveBucketCommand.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoDB.Driver; + +namespace MongoFramework.Infrastructure.Commands +{ + public class RemoveBucketCommand : IWriteCommand> + where TGroup : class + where TSubEntity : class + { + private TGroup Group { get; } + + public RemoveBucketCommand(TGroup group) + { + Group = group; + } + + public IEnumerable>> GetModel() + { + var filterBuilder = Builders>.Filter; + var filter = filterBuilder.Eq(b => b.Group, Group); + yield return new DeleteManyModel>(filter); + } + } +} diff --git a/src/MongoFramework/Infrastructure/EntityBucketStagingCollection.cs b/src/MongoFramework/Infrastructure/EntityBucketStagingCollection.cs deleted file mode 100644 index 2ba2d14d..00000000 --- a/src/MongoFramework/Infrastructure/EntityBucketStagingCollection.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using MongoFramework.Infrastructure.Linq; -using MongoFramework.Infrastructure.Linq.Processors; - -namespace MongoFramework.Infrastructure -{ - public class EntityBucketStagingCollection : IEntityCollectionBase> where TGroup : class - { - private Dictionary> SubEntityStaging { get; } - private IEntityCollection> ChangeTracker { get; } - private IEntityReader> EntityReader { get; } - - public int BucketSize { get; } - - public EntityBucketStagingCollection(IEntityReader> entityReader, int bucketSize) - { - SubEntityStaging = new Dictionary>(new ShallowPropertyEqualityComparer()); - ChangeTracker = new EntityCollection>(); - EntityReader = entityReader; - BucketSize = bucketSize; - } - - public void AddEntity(TGroup group, TSubEntity entity) - { - if (!SubEntityStaging.ContainsKey(group)) - { - SubEntityStaging.Add(group, new List { entity }); - } - else - { - SubEntityStaging[group].Add(entity); - } - } - - public void Clear() - { - ChangeTracker.Clear(); - SubEntityStaging.Clear(); - } - - private IQueryable> QueryDatabase() - { - var queryable = EntityReader.AsQueryable(); - var provider = queryable.Provider as IMongoFrameworkQueryProvider>; - provider.EntityProcessors.Add(new EntityTrackingProcessor>(ChangeTracker)); - return queryable; - } - - public IEnumerable>> GetEntries() - { - foreach (var grouping in SubEntityStaging) - { - var entityList = grouping.Value; - var sliceAt = 0; - var currentBucketIndex = 1; - - var remainingEntitiesCount = entityList.Count; - - //Identify last bucket of the group for the last index and to potentially backfill into it (if there is space) - var bucket = QueryDatabase().Where(e => e.Group == grouping.Key).OrderByDescending(e => e.Index).FirstOrDefault(); - if (bucket != null) - { - //Check if there is room to backfill into the existing bucket - if (bucket.ItemCount < bucket.BucketSize) - { - var sliceSize = Math.Min(bucket.BucketSize - bucket.ItemCount, remainingEntitiesCount); - var sliceEntities = entityList.Take(sliceSize).ToArray(); - - bucket.Items.AddRange(sliceEntities); - bucket.ItemCount += sliceSize; - - yield return new EntityEntry>(bucket, EntityEntryState.Updated); - - sliceAt += sliceSize; - remainingEntitiesCount -= sliceSize; - } - - currentBucketIndex = bucket.Index + 1; - } - - while (remainingEntitiesCount > 0) - { - var sliceSize = Math.Min(BucketSize, remainingEntitiesCount); - var sliceEntities = entityList.Skip(sliceAt).Take(sliceSize).ToList(); - - yield return new EntityEntry>(new EntityBucket - { - Group = grouping.Key, - Index = currentBucketIndex, - ItemCount = sliceSize, - BucketSize = BucketSize, - Items = sliceEntities - }, EntityEntryState.Added); - - currentBucketIndex++; - - sliceAt += sliceSize; - remainingEntitiesCount -= sliceSize; - } - } - } - - public EntityEntry> GetEntry(EntityBucket entity) - { - throw new NotImplementedException(); - } - - public void Update(EntityBucket entity, EntityEntryState state) - { - throw new NotImplementedException(); - } - public bool Remove(EntityBucket entity) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs index b9777c87..102f5b6e 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs @@ -9,6 +9,7 @@ public class EntityDefinition : IEntityDefinition { public Type EntityType { get; set; } public string CollectionName { get; set; } + public IEntityKeyGenerator KeyGenerator { get; set; } public IEnumerable Properties { get; set; } = Enumerable.Empty(); public IEnumerable Relationships { get; set; } = Enumerable.Empty(); public IEnumerable Indexes { get; set; } = Enumerable.Empty(); diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs index 5c3784fd..c293c793 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using System.Reflection; -using System.Text; -using MongoDB.Bson; using MongoFramework.Infrastructure.Internal; namespace MongoFramework.Infrastructure.Mapping diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityKeyGenerator.cs b/src/MongoFramework/Infrastructure/Mapping/EntityKeyGenerator.cs new file mode 100644 index 00000000..f9e48c6b --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/EntityKeyGenerator.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoDB.Bson.Serialization; + +namespace MongoFramework.Infrastructure.Mapping +{ + internal class EntityKeyGenerator : IEntityKeyGenerator + { + private IIdGenerator IdGenerator { get; } + + public EntityKeyGenerator(IIdGenerator idGenerator) + { + IdGenerator = idGenerator; + } + + public object Generate() + { + return IdGenerator.GenerateId(null, null); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Mapping/IEntityDefinition.cs b/src/MongoFramework/Infrastructure/Mapping/IEntityDefinition.cs index 00bcd912..c20627e8 100644 --- a/src/MongoFramework/Infrastructure/Mapping/IEntityDefinition.cs +++ b/src/MongoFramework/Infrastructure/Mapping/IEntityDefinition.cs @@ -7,6 +7,7 @@ public interface IEntityDefinition { Type EntityType { get; set; } string CollectionName { get; set; } + IEntityKeyGenerator KeyGenerator { get; set; } IEnumerable Properties { get; set; } IEnumerable Relationships { get; set; } IEnumerable Indexes { get; set; } diff --git a/src/MongoFramework/Infrastructure/Mapping/IEntityKeyGenerator.cs b/src/MongoFramework/Infrastructure/Mapping/IEntityKeyGenerator.cs new file mode 100644 index 00000000..ee712aac --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/IEntityKeyGenerator.cs @@ -0,0 +1,7 @@ +namespace MongoFramework.Infrastructure.Mapping +{ + public interface IEntityKeyGenerator + { + object Generate(); + } +} \ No newline at end of file diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs index b73e6b10..67b33dd1 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs @@ -34,23 +34,23 @@ public void ApplyMapping(IEntityDefinition definition, BsonClassMap classMap) classMap.MapIdMember(idProperty.PropertyInfo); entityProperty.IsKey = true; - //If there is no Id generator, set a default based on the member type - if (classMap.IdMemberMap.IdGenerator == null) + //Set an Id Generator based on the member type + var idMemberMap = classMap.IdMemberMap; + var memberType = BsonClassMap.GetMemberInfoType(idMemberMap.MemberInfo); + if (memberType == typeof(string)) { - var idMemberMap = classMap.IdMemberMap; - var memberType = BsonClassMap.GetMemberInfoType(idMemberMap.MemberInfo); - if (memberType == typeof(string)) - { - idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance); - } - else if (memberType == typeof(Guid)) - { - idMemberMap.SetIdGenerator(CombGuidGenerator.Instance); - } - else if (memberType == typeof(ObjectId)) - { - idMemberMap.SetIdGenerator(ObjectIdGenerator.Instance); - } + idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance); + definition.KeyGenerator = new EntityKeyGenerator(StringObjectIdGenerator.Instance); + } + else if (memberType == typeof(Guid)) + { + idMemberMap.SetIdGenerator(CombGuidGenerator.Instance); + definition.KeyGenerator = new EntityKeyGenerator(CombGuidGenerator.Instance); + } + else if (memberType == typeof(ObjectId)) + { + idMemberMap.SetIdGenerator(ObjectIdGenerator.Instance); + definition.KeyGenerator = new EntityKeyGenerator(ObjectIdGenerator.Instance); } } } diff --git a/src/MongoFramework/Infrastructure/ShallowPropertyEqualityComparer.cs b/src/MongoFramework/Infrastructure/ShallowPropertyEqualityComparer.cs deleted file mode 100644 index 7b544ebb..00000000 --- a/src/MongoFramework/Infrastructure/ShallowPropertyEqualityComparer.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace MongoFramework.Infrastructure -{ - public class ShallowPropertyEqualityComparer : IEqualityComparer - { - private PropertyInfo[] Properties { get; } - private static bool ImplementsIEquatable { get; } = typeof(TObject).GetInterfaces().Any(i => i == typeof(IEquatable)); - - public ShallowPropertyEqualityComparer() - { - if (!ImplementsIEquatable) - { - Properties = typeof(TObject).GetProperties(BindingFlags.Public | BindingFlags.Instance); - } - } - - public bool Equals(TObject x, TObject y) - { - if (object.Equals(x, default(TObject)) && object.Equals(y, default(TObject))) - { - return true; - } - else if (object.Equals(x, default(TObject)) || object.Equals(y, default(TObject))) - { - return false; - } - - if (ImplementsIEquatable) - { - return (x as IEquatable).Equals(y); - } - - foreach (var property in Properties) - { - var xValue = property.GetValue(x); - var yValue = property.GetValue(y); - - if (!Equals(xValue, yValue)) - { - return false; - } - } - - return true; - } - - public int GetHashCode(TObject obj) - { - if (obj is null) - { - throw new ArgumentNullException(nameof(obj)); - } - - if (ImplementsIEquatable) - { - return obj.GetHashCode(); - } - - var combinedHashCode = 1; - foreach (var property in Properties) - { - var xValue = property.GetValue(obj); - if (xValue != null) - { - var localHashCode = xValue.GetHashCode(); - combinedHashCode = (((localHashCode << 5) + localHashCode) ^ combinedHashCode); - } - } - - return combinedHashCode; - } - } -} diff --git a/src/MongoFramework/MongoDbBucketSet.cs b/src/MongoFramework/MongoDbBucketSet.cs index b2fa851d..d016f809 100644 --- a/src/MongoFramework/MongoDbBucketSet.cs +++ b/src/MongoFramework/MongoDbBucketSet.cs @@ -6,22 +6,24 @@ using System.Threading; using System.Threading.Tasks; using MongoFramework.Infrastructure; +using MongoFramework.Infrastructure.Commands; using MongoFramework.Infrastructure.Indexing; -using MongoFramework.Infrastructure.Linq; -using MongoFramework.Infrastructure.Linq.Processors; +using MongoFramework.Infrastructure.Mapping; namespace MongoFramework { - public class MongoDbBucketSet : IMongoDbBucketSet where TGroup : class + public class MongoDbBucketSet : IMongoDbBucketSet + where TGroup : class + where TSubEntity : class { private IEntityWriterPipeline> EntityWriterPipeline { get; set; } private IEntityReader> EntityReader { get; set; } private IEntityIndexWriter> EntityIndexWriter { get; set; } - private EntityBucketStagingCollection BucketStagingCollection { get; set; } - private IEntityCollection> ChangeTracker { get; set; } + internal int BucketSize { get; } + + internal IEntityProperty EntityTimeProperty { get; } - public int BucketSize { get; } public MongoDbBucketSet(IDbSetOptions options) { @@ -32,6 +34,19 @@ public MongoDbBucketSet(IDbSetOptions options) throw new ArgumentException($"Invalid bucket size of {bucketOptions.BucketSize}"); } BucketSize = bucketOptions.BucketSize; + + var property = EntityMapping.GetOrCreateDefinition(typeof(TSubEntity)).GetProperty(bucketOptions.EntityTimeProperty); + if (property == null) + { + throw new ArgumentException($"Property {bucketOptions.EntityTimeProperty} doesn't exist on bucket item."); + } + + if (property.PropertyType != typeof(DateTime)) + { + throw new ArgumentException($"Property {bucketOptions.EntityTimeProperty} on bucket item isn't of type DateTime"); + } + + EntityTimeProperty = property; } else { @@ -44,11 +59,6 @@ public void SetConnection(IMongoDbConnection connection) EntityWriterPipeline = new EntityWriterPipeline>(connection); EntityReader = new EntityReader>(connection); EntityIndexWriter = new EntityIndexWriter>(connection); - BucketStagingCollection = new EntityBucketStagingCollection(EntityReader, BucketSize); - ChangeTracker = new EntityCollection>(); - - EntityWriterPipeline.AddCollection(ChangeTracker); - EntityWriterPipeline.AddCollection(BucketStagingCollection); } public virtual void Add(TGroup group, TSubEntity entity) @@ -58,7 +68,12 @@ public virtual void Add(TGroup group, TSubEntity entity) throw new ArgumentNullException(nameof(group)); } - BucketStagingCollection.AddEntity(group, entity); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + EntityWriterPipeline.StageCommand(new AddToBucketCommand(group, entity, EntityTimeProperty, BucketSize)); } public virtual void AddRange(TGroup group, IEnumerable entities) @@ -75,13 +90,23 @@ public virtual void AddRange(TGroup group, IEnumerable entities) foreach (var entity in entities) { - BucketStagingCollection.AddEntity(group, entity); + EntityWriterPipeline.StageCommand(new AddToBucketCommand(group, entity, EntityTimeProperty, BucketSize)); + } + } + + public virtual void Remove(TGroup group) + { + if (group == null) + { + throw new ArgumentNullException(nameof(group)); } + + EntityWriterPipeline.StageCommand(new RemoveBucketCommand(group)); } public virtual IQueryable WithGroup(TGroup group) { - return EntityReader.AsQueryable().Where(e => e.Group == group).OrderBy(e => e.Index).SelectMany(e => e.Items); + return EntityReader.AsQueryable().Where(e => e.Group == group).OrderBy(e => e.Min).SelectMany(e => e.Items); } public virtual IQueryable Groups() @@ -93,25 +118,20 @@ public virtual void SaveChanges() { EntityIndexWriter.ApplyIndexing(); EntityWriterPipeline.Write(); - BucketStagingCollection.Clear(); } public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = default) { - await EntityIndexWriter.ApplyIndexingAsync(); + await EntityIndexWriter.ApplyIndexingAsync(cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); await EntityWriterPipeline.WriteAsync(cancellationToken).ConfigureAwait(false); - BucketStagingCollection.Clear(); } #region IQueryable Implementation private IQueryable> GetQueryable() { - var queryable = EntityReader.AsQueryable(); - var provider = queryable.Provider as IMongoFrameworkQueryProvider>; - provider.EntityProcessors.Add(new EntityTrackingProcessor>(ChangeTracker)); - return queryable; + return EntityReader.AsQueryable(); } public Type ElementType => GetQueryable().ElementType; diff --git a/tests/MongoFramework.Tests/Infrastructure/ShallowPropertyEqualityComparerTests.cs b/tests/MongoFramework.Tests/Infrastructure/ShallowPropertyEqualityComparerTests.cs deleted file mode 100644 index 21d11b5c..00000000 --- a/tests/MongoFramework.Tests/Infrastructure/ShallowPropertyEqualityComparerTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MongoFramework.Infrastructure; - -namespace MongoFramework.Tests.Infrastructure -{ - [TestClass] - public class ShallowPropertyEqualityComparerTests - { - class SlowPathTestClass - { - public string Test { get; set; } - public int Number { get; set; } - } - - class FastPathTestClass : IEquatable - { - public string Test { get; set; } - public int Number { get; set; } - - public override bool Equals(object obj) - { - return Equals(obj as FastPathTestClass); - } - - public override int GetHashCode() - { - var hashCode = 1; - var testHashCode = Test?.GetHashCode() ?? 1; - var numberHashCode = Number.GetHashCode(); - hashCode = (((testHashCode << 5) + testHashCode) ^ hashCode); - hashCode = (((numberHashCode << 5) + numberHashCode) ^ hashCode); - return hashCode; - } - - public bool Equals(FastPathTestClass other) - { - if (other == null) - { - return false; - } - - return object.Equals(Test, other.Test) && object.Equals(Number, other.Number); - } - } - - [TestMethod] - public void SlowPathEqual() - { - var comparer = new ShallowPropertyEqualityComparer(); - - Assert.IsTrue(comparer.Equals(new SlowPathTestClass(), new SlowPathTestClass())); - Assert.IsTrue(comparer.Equals(null, null)); - Assert.IsTrue(comparer.Equals(new SlowPathTestClass - { - Number = 5 - }, new SlowPathTestClass - { - Number = 5 - })); - Assert.IsTrue(comparer.Equals(new SlowPathTestClass - { - Test = "ABC" - }, new SlowPathTestClass - { - Test = "ABC" - })); - Assert.IsTrue(comparer.Equals(new SlowPathTestClass - { - Number = 1, - Test = "Foo" - }, new SlowPathTestClass - { - Number = 1, - Test = "Foo" - })); - } - [TestMethod] - public void SlowPathNotEqual() - { - var comparer = new ShallowPropertyEqualityComparer(); - - Assert.IsFalse(comparer.Equals(new SlowPathTestClass(), null)); - Assert.IsFalse(comparer.Equals(null, new SlowPathTestClass())); - Assert.IsFalse(comparer.Equals(new SlowPathTestClass - { - Number = 5 - }, new SlowPathTestClass - { - Number = 10 - })); - Assert.IsFalse(comparer.Equals(new SlowPathTestClass - { - Test = "FOO" - }, new SlowPathTestClass - { - Test = "BAR" - })); - } - - [TestMethod] - public void FastPathEqual() - { - var comparer = new ShallowPropertyEqualityComparer(); - - Assert.IsTrue(comparer.Equals(new FastPathTestClass(), new FastPathTestClass())); - Assert.IsTrue(comparer.Equals(null, null)); - Assert.IsTrue(comparer.Equals(new FastPathTestClass - { - Number = 5 - }, new FastPathTestClass - { - Number = 5 - })); - Assert.IsTrue(comparer.Equals(new FastPathTestClass - { - Test = "ABC" - }, new FastPathTestClass - { - Test = "ABC" - })); - Assert.IsTrue(comparer.Equals(new FastPathTestClass - { - Number = 1, - Test = "Foo" - }, new FastPathTestClass - { - Number = 1, - Test = "Foo" - })); - } - [TestMethod] - public void FastPathNotEqual() - { - var comparer = new ShallowPropertyEqualityComparer(); - - Assert.IsFalse(comparer.Equals(new FastPathTestClass(), null)); - Assert.IsFalse(comparer.Equals(null, new FastPathTestClass())); - Assert.IsFalse(comparer.Equals(new FastPathTestClass - { - Number = 5 - }, new FastPathTestClass - { - Number = 10 - })); - Assert.IsFalse(comparer.Equals(new FastPathTestClass - { - Test = "FOO" - }, new FastPathTestClass - { - Test = "BAR" - })); - } - } -} diff --git a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs index 824e5032..5e540551 100644 --- a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs +++ b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs @@ -17,6 +17,7 @@ public class EntityGroup public class SubEntityClass { public string Label { get; set; } + public DateTime Date { get; set; } } [TestMethod] @@ -24,7 +25,8 @@ public void InitialiseDbSet() { AssertExtensions.DoesNotThrow(() => new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 100 + BucketSize = 100, + EntityTimeProperty = "Date" })); } @@ -39,7 +41,8 @@ public void SuccessfullyInsertAndQueryBackEntityBuckets() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 100 + BucketSize = 100, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -48,12 +51,13 @@ public void SuccessfullyInsertAndQueryBackEntityBuckets() Name = "Group1" }, new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }); Assert.IsFalse(dbSet.Any(b => b.Group.Name == "Group1")); dbSet.SaveChanges(); - Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.Items.Any(i => i.Label == "Entry1"))); + Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.Items.Any(i => i.Label == "Entry1" && i.Date == new DateTime(2020, 1, 1)))); } [TestMethod] @@ -61,7 +65,8 @@ public async Task SuccessfullyInsertAndQueryBackEntityBucketsAsync() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 100 + BucketSize = 100, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -70,12 +75,13 @@ public async Task SuccessfullyInsertAndQueryBackEntityBucketsAsync() Name = "Group1" }, new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }); Assert.IsFalse(dbSet.Any(b => b.Group.Name == "Group1")); await dbSet.SaveChangesAsync(); - Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.Items.Any(i => i.Label == "Entry1"))); + Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.Items.Any(i => i.Label == "Entry1" && i.Date == new DateTime(2020, 1, 1)))); } [TestMethod, ExpectedException(typeof(ArgumentException))] @@ -83,8 +89,74 @@ public void InvalidBucketSize() { new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 0 + BucketSize = 0, + EntityTimeProperty = "Date" + }); + } + + [TestMethod, ExpectedException(typeof(ArgumentException))] + public void InvalidSubEntityTimeProperty_Missing() + { + new MongoDbBucketSet(new BucketSetOptions + { + BucketSize = 100, + EntityTimeProperty = "MissingField" + }); + } + + [TestMethod, ExpectedException(typeof(ArgumentException))] + public void InvalidSubEntityTimeProperty_WrongType() + { + new MongoDbBucketSet(new BucketSetOptions + { + BucketSize = 100, + EntityTimeProperty = "Label" + }); + } + + [TestMethod] + public void RemoveBucket() + { + var dbSet = new MongoDbBucketSet(new BucketSetOptions + { + BucketSize = 2, + EntityTimeProperty = "Date" + }); + dbSet.SetConnection(TestConfiguration.GetConnection()); + + dbSet.AddRange(new EntityGroup + { + Name = "Group1" + }, new[] { + new SubEntityClass + { + Label = "Entry1", + Date = new DateTime(2020, 1, 1) + }, + new SubEntityClass + { + Label = "Entry2", + Date = new DateTime(2020, 1, 2) + }, + new SubEntityClass + { + Label = "Entry2", + Date = new DateTime(2020, 1, 3) + } + }); + + Assert.IsFalse(dbSet.Any(b => b.Group.Name == "Group1")); + dbSet.SaveChanges(); + Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.ItemCount == 2)); + Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.ItemCount == 1)); + + dbSet.Remove(new EntityGroup + { + Name = "Group1" }); + dbSet.SaveChanges(); + + Assert.IsFalse(dbSet.Any(b => b.Group.Name == "Group1")); } [TestMethod] @@ -92,7 +164,8 @@ public void FillIntoAdditionalBuckets() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -102,18 +175,23 @@ public void FillIntoAdditionalBuckets() }, new[] { new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }, new SubEntityClass { - Label = "Entry2" + Label = "Entry2", + Date = new DateTime(2020, 1, 2) }, new SubEntityClass { - Label = "Entry2" + Label = "Entry3", + Date = new DateTime(2020, 1, 3) } }); + var a = MongoFramework.Infrastructure.Mapping.EntityMapping.GetOrCreateDefinition(typeof(EntityBucket)); + var cb = MongoDB.Bson.Serialization.BsonClassMap.LookupClassMap(typeof(EntityBucket)); Assert.IsFalse(dbSet.Any(b => b.Group.Name == "Group1")); dbSet.SaveChanges(); Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.ItemCount == 2)); @@ -125,7 +203,8 @@ public void BackfillIntoExistingBucket() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -134,7 +213,8 @@ public void BackfillIntoExistingBucket() Name = "Group1" }, new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc) }); dbSet.SaveChanges(); Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.ItemCount == 1)); @@ -145,23 +225,30 @@ public void BackfillIntoExistingBucket() }, new[] { new SubEntityClass { - Label = "Entry2" + Label = "Entry2", + Date = new DateTime(2020, 1, 2, 0, 0, 0, DateTimeKind.Utc) }, new SubEntityClass { - Label = "Entry3" + Label = "Entry3", + Date = new DateTime(2020, 1, 3, 0, 0, 0, DateTimeKind.Utc) } }); dbSet.SaveChanges(); var buckets = dbSet.Where(b => b.Group.Name == "Group1").ToArray(); Assert.AreEqual(2, buckets.Count()); - var backfilledBucket = buckets.FirstOrDefault(); + + var backfilledBucket = buckets[0]; + Assert.AreEqual(new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc), backfilledBucket.Min); + Assert.AreEqual(new DateTime(2020, 1, 2, 0, 0, 0, DateTimeKind.Utc), backfilledBucket.Max); Assert.AreEqual(2, backfilledBucket.ItemCount); Assert.AreEqual("Entry1", backfilledBucket.Items[0].Label); Assert.AreEqual("Entry2", backfilledBucket.Items[1].Label); - var additionalBucket = buckets.LastOrDefault(); + var additionalBucket = buckets[1]; + Assert.AreEqual(new DateTime(2020, 1, 3, 0, 0, 0, DateTimeKind.Utc), additionalBucket.Min); + Assert.AreEqual(new DateTime(2020, 1, 3, 0, 0, 0, DateTimeKind.Utc), additionalBucket.Max); Assert.AreEqual(1, additionalBucket.ItemCount); Assert.AreEqual("Entry3", additionalBucket.Items[0].Label); } @@ -171,7 +258,8 @@ public void ContinuousSubEntityAccessAcrossBuckets() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -181,23 +269,28 @@ public void ContinuousSubEntityAccessAcrossBuckets() }, new[] { new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }, new SubEntityClass { - Label = "Entry2" + Label = "Entry2", + Date = new DateTime(2020, 1, 2) }, new SubEntityClass { - Label = "Entry3" + Label = "Entry3", + Date = new DateTime(2020, 1, 3) }, new SubEntityClass { - Label = "Entry4" + Label = "Entry4", + Date = new DateTime(2020, 1, 4) }, new SubEntityClass { - Label = "Entry5" + Label = "Entry5", + Date = new DateTime(2020, 1, 5) } }); dbSet.SaveChanges(); @@ -222,7 +315,8 @@ public void DistinctGroups() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -232,15 +326,18 @@ public void DistinctGroups() }, new[] { new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }, new SubEntityClass { - Label = "Entry2" + Label = "Entry2", + Date = new DateTime(2020, 1, 2) }, new SubEntityClass { - Label = "Entry3" + Label = "Entry3", + Date = new DateTime(2020, 1, 3) } }); dbSet.AddRange(new EntityGroup @@ -249,15 +346,18 @@ public void DistinctGroups() }, new[] { new SubEntityClass { - Label = "Entry1" + Label = "Entry1", + Date = new DateTime(2020, 1, 1) }, new SubEntityClass { - Label = "Entry2" + Label = "Entry2", + Date = new DateTime(2020, 1, 2) }, new SubEntityClass { - Label = "Entry3" + Label = "Entry3", + Date = new DateTime(2020, 1, 3) } }); dbSet.SaveChanges(); @@ -276,7 +376,8 @@ public void IterateQueryable() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); @@ -313,7 +414,8 @@ public void InvalidAddArguments() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); Assert.ThrowsException(() => dbSet.Add(null, new SubEntityClass())); @@ -322,33 +424,15 @@ public void InvalidAddArguments() } [TestMethod] - public void ValueTypeSubEntity() + public void InvalidRemoveArguments() { - var dbSet = new MongoDbBucketSet(new BucketSetOptions + var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); - dbSet.SetConnection(TestConfiguration.GetConnection()); - - dbSet.AddRange(new EntityGroup - { - Name = "Group1" - }, new[] { 2, 4, 6, 8, 10 }); - dbSet.SaveChanges(); - Assert.AreEqual(3, dbSet.Count()); - - var results = dbSet.WithGroup(new EntityGroup - { - Name = "Group1" - }).ToArray(); - - Assert.AreEqual(5, results.Length); - Assert.AreEqual(2, results[0]); - Assert.AreEqual(4, results[1]); - Assert.AreEqual(6, results[2]); - Assert.AreEqual(8, results[3]); - Assert.AreEqual(10, results[4]); + Assert.ThrowsException(() => dbSet.Remove(null)); } [TestMethod] @@ -356,7 +440,8 @@ public void WithGroupOnEmptyBucket() { var dbSet = new MongoDbBucketSet(new BucketSetOptions { - BucketSize = 2 + BucketSize = 2, + EntityTimeProperty = "Date" }); dbSet.SetConnection(TestConfiguration.GetConnection()); diff --git a/tests/MongoFramework.Tests/MongoDbContextTests.cs b/tests/MongoFramework.Tests/MongoDbContextTests.cs index fb6a931c..6c652a66 100644 --- a/tests/MongoFramework.Tests/MongoDbContextTests.cs +++ b/tests/MongoFramework.Tests/MongoDbContextTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MongoFramework.Attributes; +using System; using System.Linq; using System.Threading.Tasks; @@ -19,13 +20,14 @@ class BucketGroupModel class BucketSubEntity { public string Label { get; set; } + public DateTime Date { get; set; } } class MongoDbContextTestContext : MongoDbContext { public MongoDbContextTestContext(IMongoDbConnection connection) : base(connection) { } public MongoDbSet DbSet { get; set; } - [BucketSetOptions(5)] + [BucketSetOptions(5, nameof(BucketSubEntity.Date))] public MongoDbBucketSet DbBucketSet { get; set; } }