diff --git a/CodeCoverage.runsettings b/CodeCoverage.runsettings index 7c0cb6d2..a7663dcf 100644 --- a/CodeCoverage.runsettings +++ b/CodeCoverage.runsettings @@ -7,7 +7,7 @@ cobertura [MongoFramework.Tests]* [MongoFramework]*,[MongoFramework.*]* - Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute + Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,DebuggerNonUserCode,DebuggerStepThrough true true diff --git a/src/MongoFramework/Attributes/MappingAdapterAttribute.cs b/src/MongoFramework/Attributes/MappingAdapterAttribute.cs index 8d996e3f..9c805c69 100644 --- a/src/MongoFramework/Attributes/MappingAdapterAttribute.cs +++ b/src/MongoFramework/Attributes/MappingAdapterAttribute.cs @@ -1,28 +1,23 @@ using System; using MongoFramework.Infrastructure.Mapping; -namespace MongoFramework.Attributes +namespace MongoFramework.Attributes; + +/// +/// Applies the specific on the entity. +/// Runs after attribute processing, so the adapter can override attributes. +/// Adapter type must have a parameterless constructor. +/// +[AttributeUsage(AttributeTargets.Class)] +public class MappingAdapterAttribute : Attribute { /// - /// Allows an IMappingProcessor to override definitions in code. Runs after attribute processing, so the adapter can override attributes. - /// Adapter type must have a parameterless constructor. + /// Gets the adapter type for the attached class /// - [AttributeUsage(AttributeTargets.Class)] - public class MappingAdapterAttribute : Attribute - { - /// - /// Gets the adapter type for the attached class - /// - public Type MappingAdapter { get; } + public Type MappingAdapter { get; } - public MappingAdapterAttribute(Type adapterType) - { - if (!typeof(IMappingProcessor).IsAssignableFrom(adapterType)) - { - throw new ArgumentException("Mapping Adapter Type must implement IMappingProcessor", nameof(adapterType)); - } - - MappingAdapter = adapterType; - } + public MappingAdapterAttribute(Type adapterType) + { + MappingAdapter = adapterType; } } diff --git a/src/MongoFramework/EntityDefinitionBuilderExtensions.cs b/src/MongoFramework/EntityDefinitionBuilderExtensions.cs new file mode 100644 index 00000000..ac193c8d --- /dev/null +++ b/src/MongoFramework/EntityDefinitionBuilderExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MongoFramework.Infrastructure.Mapping; + +namespace MongoFramework; + +public static class EntityDefinitionBuilderExtensions +{ + private static PropertyInfo GetPropertyInfo(Type entityType, string propertyName) + { + return entityType.GetProperty(propertyName) ?? throw new ArgumentException($"Property \"{propertyName}\" can not be found on \"{entityType.Name}\".", nameof(propertyName)); + } + + public static EntityDefinitionBuilder HasKey(this EntityDefinitionBuilder definitionBuilder, string propertyName, Action builder = null) + => definitionBuilder.HasKey(GetPropertyInfo(definitionBuilder.EntityType, propertyName), builder); + + public static EntityDefinitionBuilder Ignore(this EntityDefinitionBuilder definitionBuilder, string propertyName) + => definitionBuilder.Ignore(GetPropertyInfo(definitionBuilder.EntityType, propertyName)); + + public static EntityDefinitionBuilder HasProperty(this EntityDefinitionBuilder definitionBuilder, string propertyName, Action builder = null) + => definitionBuilder.HasProperty(GetPropertyInfo(definitionBuilder.EntityType, propertyName), builder); + + public static EntityDefinitionBuilder HasIndex(this EntityDefinitionBuilder definitionBuilder, IEnumerable propertyPaths, Action builder = null) + { + var properties = new List(); + foreach (var propertyPath in propertyPaths) + { + properties.Add( + new IndexProperty( + PropertyPath.FromString(definitionBuilder.EntityType, propertyPath) + ) + ); + } + + return definitionBuilder.HasIndex(properties, builder); + } + public static EntityDefinitionBuilder HasIndex(this EntityDefinitionBuilder definitionBuilder, IEnumerable properties, Action builder = null) + { + return definitionBuilder.HasIndex(properties.Select(p => new IndexProperty(p)), builder); + } + + public static EntityDefinitionBuilder HasExtraElements(this EntityDefinitionBuilder definitionBuilder, string propertyName) + => definitionBuilder.HasExtraElements(GetPropertyInfo(definitionBuilder.EntityType, propertyName)); + +} diff --git a/src/MongoFramework/IHaveTenantId.cs b/src/MongoFramework/IHaveTenantId.cs index 9ec37ca2..8a47a5a8 100644 --- a/src/MongoFramework/IHaveTenantId.cs +++ b/src/MongoFramework/IHaveTenantId.cs @@ -2,6 +2,6 @@ { public interface IHaveTenantId { - string TenantId { get; set; } + public string TenantId { get; set; } } } diff --git a/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs b/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs index 192c34b3..67168c73 100644 --- a/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs +++ b/src/MongoFramework/Infrastructure/Commands/AddToBucketCommand.cs @@ -11,12 +11,12 @@ public class AddToBucketCommand : IWriteCommand typeof(EntityBucket); - public AddToBucketCommand(TGroup group, TSubEntity subEntity, IEntityPropertyDefinition entityTimeProperty, int bucketSize) + public AddToBucketCommand(TGroup group, TSubEntity subEntity, PropertyDefinition entityTimeProperty, int bucketSize) { Group = group; SubEntity = subEntity; diff --git a/src/MongoFramework/Infrastructure/Commands/EntityDefinitionExtensions.cs b/src/MongoFramework/Infrastructure/Commands/EntityDefinitionExtensions.cs index e8cd8951..7dded2a9 100644 --- a/src/MongoFramework/Infrastructure/Commands/EntityDefinitionExtensions.cs +++ b/src/MongoFramework/Infrastructure/Commands/EntityDefinitionExtensions.cs @@ -8,11 +8,11 @@ namespace MongoFramework.Infrastructure.Commands { public static class EntityDefinitionExtensions { - public static FilterDefinition CreateIdFilterFromEntity(this IEntityDefinition definition, TEntity entity) + public static FilterDefinition CreateIdFilterFromEntity(this EntityDefinition definition, TEntity entity) { return Builders.Filter.Eq(definition.GetIdName(), definition.GetIdValue(entity)); } - public static FilterDefinition CreateIdFilter(this IEntityDefinition definition, object entityId, string tenantId = null) + public static FilterDefinition CreateIdFilter(this EntityDefinition definition, object entityId, string tenantId = null) { if (typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity)) && tenantId == null) { diff --git a/src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs b/src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs index 421ef488..53d081d4 100644 --- a/src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs +++ b/src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs @@ -1,80 +1,52 @@ -using System.Collections.Generic; -using System.Linq; +using System; +using System.Collections.Generic; using MongoDB.Driver; using MongoFramework.Infrastructure.Mapping; -namespace MongoFramework.Infrastructure.Indexing +namespace MongoFramework.Infrastructure.Indexing; + +public static class IndexModelBuilder { - public static class IndexModelBuilder + public static IEnumerable> BuildModel() { - public static IEnumerable> BuildModel() - { - var indexBuilder = Builders.IndexKeys; - var indexes = EntityMapping.GetOrCreateDefinition(typeof(TEntity)).Indexes; - var groupedIndexes = indexes.OrderBy(i => i.IndexPriority).GroupBy(i => i.IndexName); - - foreach (var indexGroup in groupedIndexes) - { - if (indexGroup.Key != null) - { - var indexKeys = new List>(); - CreateIndexOptions indexOptions = default; - foreach (var index in indexGroup) - { - var indexModel = CreateIndexModel(index); - indexKeys.Add(indexModel.Keys); - - if (indexOptions == null) - { - indexOptions = indexModel.Options; - } - } + var indexBuilder = Builders.IndexKeys; + var indexes = EntityMapping.GetOrCreateDefinition(typeof(TEntity)).Indexes; - var combinedKeyDefinition = indexBuilder.Combine(indexKeys); - yield return new CreateIndexModel(combinedKeyDefinition, indexOptions); - } - else - { - foreach (var index in indexGroup) - { - yield return CreateIndexModel(index); - } - } - } - } - - private static CreateIndexModel CreateIndexModel(IEntityIndexDefinition indexDefinition) + foreach (var index in indexes) { - var builder = Builders.IndexKeys; - IndexKeysDefinition keyModel; - - if (indexDefinition.IndexType == IndexType.Text) + var indexKeyCount = index.IndexPaths.Count + (index.IsTenantExclusive ? 1 : 0); + var indexKeys = new IndexKeysDefinition[indexKeyCount]; + for (var i = 0; i < index.IndexPaths.Count; i++) { - keyModel = builder.Text(indexDefinition.Path); + indexKeys[i] = CreateIndexKey(index.IndexPaths[i]); } - else if (indexDefinition.IndexType == IndexType.Geo2dSphere) + + if (index.IsTenantExclusive) { - keyModel = builder.Geo2DSphere(indexDefinition.Path); - } - else - { - keyModel = indexDefinition.SortOrder == IndexSortOrder.Ascending ? - builder.Ascending(indexDefinition.Path) : builder.Descending(indexDefinition.Path); - } - - if (indexDefinition.IsTenantExclusive && typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity))) - { - var tenantKey = indexDefinition.SortOrder == IndexSortOrder.Ascending ? - builder.Ascending("TenantId") : builder.Descending("TenantId"); - keyModel = builder.Combine(tenantKey, keyModel); + indexKeys[indexKeys.Length - 1] = Builders.IndexKeys.Ascending(nameof(IHaveTenantId.TenantId)); } - return new CreateIndexModel(keyModel, new CreateIndexOptions + var combinedKeyDefinition = indexBuilder.Combine(indexKeys); + yield return new CreateIndexModel(combinedKeyDefinition, new CreateIndexOptions { - Name = indexDefinition.IndexName, - Unique = indexDefinition.IsUnique, + Name = index.IndexName, + Unique = index.IsUnique, Background = true }); } } + + private static IndexKeysDefinition CreateIndexKey(IndexPathDefinition indexPathDefinition) + { + var builder = Builders.IndexKeys; + Func, IndexKeysDefinition> builderMethod = indexPathDefinition.IndexType switch + { + IndexType.Standard when indexPathDefinition.SortOrder == IndexSortOrder.Ascending => builder.Ascending, + IndexType.Standard when indexPathDefinition.SortOrder == IndexSortOrder.Descending => builder.Descending, + IndexType.Text => builder.Text, + IndexType.Geo2dSphere => builder.Geo2DSphere, + _ => throw new ArgumentException($"Unsupported index type \"{indexPathDefinition.IndexType}\"", nameof(indexPathDefinition)) + }; + return builderMethod(indexPathDefinition.Path); + } } diff --git a/src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs b/src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs index 901ad720..24ffe480 100644 --- a/src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs +++ b/src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs @@ -14,7 +14,21 @@ internal static class TypeExtensions typeof(IReadOnlyCollection<>) }; - public static Type GetEnumerableItemTypeOrDefault(this Type type) + /// + /// Attempts to unwrap enumerable types (like ) from the current , returning the actual item type. + /// + /// + /// Unwrapped types include:
+ /// -
+ /// -
+ /// -
+ /// -
+ /// -
+ /// - + ///
+ /// + /// + public static Type UnwrapEnumerableTypes(this Type type) { if (type.IsArray) { diff --git a/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs b/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs index 82970cf2..65841d71 100644 --- a/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs +++ b/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs @@ -18,7 +18,7 @@ namespace MongoFramework.Infrastructure.Linq public class MongoFrameworkQueryProvider : IMongoFrameworkQueryProvider where TEntity : class { public IMongoDbConnection Connection { get; } - private IEntityDefinition EntityDefinition { get; } + private EntityDefinition EntityDefinition { get; } private BsonDocument PreStage { get; } diff --git a/src/MongoFramework/Infrastructure/Mapping/DefaultMappingPack.cs b/src/MongoFramework/Infrastructure/Mapping/DefaultMappingPack.cs deleted file mode 100644 index b7e7f0e7..00000000 --- a/src/MongoFramework/Infrastructure/Mapping/DefaultMappingPack.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using MongoFramework.Infrastructure.Mapping.Processors; - -namespace MongoFramework.Infrastructure.Mapping -{ - public static class DefaultProcessors - { - public static IEnumerable CreateProcessors() => new IMappingProcessor[] - { - new CollectionNameProcessor(), - new HierarchyProcessor(), - new PropertyMappingProcessor(), - new EntityIdProcessor(), - new NestedTypeProcessor(), - new ExtraElementsProcessor(), - new BsonKnownTypesProcessor(), - new IndexProcessor(), - new MappingAdapterProcessor() - }; - } -} diff --git a/src/MongoFramework/Infrastructure/Mapping/DefaultMappingProcessors.cs b/src/MongoFramework/Infrastructure/Mapping/DefaultMappingProcessors.cs new file mode 100644 index 00000000..e2fd7e77 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/DefaultMappingProcessors.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using MongoFramework.Infrastructure.Mapping.Processors; + +namespace MongoFramework.Infrastructure.Mapping; + +public static class DefaultMappingProcessors +{ + public static readonly IReadOnlyList Processors = new IMappingProcessor[] + { + new SkipMappingProcessor(), + new CollectionNameProcessor(), + new HierarchyProcessor(), + new PropertyMappingProcessor(), + new EntityIdProcessor(), + new NestedTypeProcessor(), + new ExtraElementsProcessor(), + new BsonKnownTypesProcessor(), + new IndexProcessor(), + new MappingAdapterProcessor() + }; +} diff --git a/src/MongoFramework/Infrastructure/Mapping/DriverMappingInterop.cs b/src/MongoFramework/Infrastructure/Mapping/DriverMappingInterop.cs index d24dc063..602a81d4 100644 --- a/src/MongoFramework/Infrastructure/Mapping/DriverMappingInterop.cs +++ b/src/MongoFramework/Infrastructure/Mapping/DriverMappingInterop.cs @@ -11,7 +11,7 @@ internal static class DriverMappingInterop /// Registers the as a with all appropriate properties configured. /// /// - public static void RegisterDefinition(IEntityDefinition definition) + public static void RegisterDefinition(EntityDefinition definition) { var classMap = new BsonClassMap(definition.EntityType); diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs index 8a040373..da034cff 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs @@ -1,73 +1,29 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Reflection; namespace MongoFramework.Infrastructure.Mapping; -public interface IEntityDefinition +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record EntityDefinition { - public Type EntityType { get; set; } - public string CollectionName { get; set; } - public IEntityKeyDefinition Key { get; set; } - public IEnumerable Properties { get; set; } - public IEnumerable Indexes { get; set; } - public IEntityExtraElementsDefinition ExtraElements { get; set; } -} - -public interface IEntityPropertyDefinition -{ - public IEntityDefinition EntityDefinition { get; } - public string ElementName { get; } - public PropertyInfo PropertyInfo { get; } + public Type EntityType { get; init; } + public string CollectionName { get; init; } + public KeyDefinition Key { get; init; } + public IReadOnlyList Properties { get; init; } = Array.Empty(); + public IReadOnlyList Indexes { get; init; } = Array.Empty(); + public ExtraElementsDefinition ExtraElements { get; init; } - public object GetValue(object entity); - public void SetValue(object entity, object value); + [DebuggerNonUserCode] + private string DebuggerDisplay => $"EntityType = {EntityType.Name}, Collection = {CollectionName}, Properties = {Properties.Count}, Indexes = {Indexes.Count}"; } -public interface IEntityIndexDefinition +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record PropertyDefinition { - public IReadOnlyCollection Properties { get; } - [Obsolete("Index definition can point to multiple properties directly")] - public IEntityPropertyDefinition Property { get; } - //TODO: This will be made redundant when the broader change to support fluent comes in - public string Path { get; } - public string IndexName { get; } - public bool IsUnique { get; } - public IndexSortOrder SortOrder { get; } - public int IndexPriority { get; } - public IndexType IndexType { get; } - public bool IsTenantExclusive { get; } -} - -public interface IEntityExtraElementsDefinition -{ - public IEntityPropertyDefinition Property { get; } - public bool IgnoreExtraElements { get; } - public bool IgnoreInherited { get; } -} - -public interface IEntityKeyDefinition -{ - public IEntityPropertyDefinition Property { get; } - public IEntityKeyGenerator KeyGenerator { get; } -} - -public class EntityDefinition : IEntityDefinition -{ - public Type EntityType { get; set; } - public string CollectionName { get; set; } - public IEntityKeyDefinition Key { get; set; } - public IEnumerable Properties { get; set; } = Enumerable.Empty(); - public IEnumerable Indexes { get; set; } = Enumerable.Empty(); - public IEntityExtraElementsDefinition ExtraElements { get; set; } -} - -public class EntityPropertyDefinition : IEntityPropertyDefinition -{ - public IEntityDefinition EntityDefinition { get; set; } - public string ElementName { get; set; } - public PropertyInfo PropertyInfo { get; set; } + public PropertyInfo PropertyInfo { get; init; } + public string ElementName { get; init; } public object GetValue(object entity) { @@ -78,30 +34,64 @@ public void SetValue(object entity, object value) { PropertyInfo.SetValue(entity, value); } + + [DebuggerNonUserCode] + private string DebuggerDisplay => $"PropertyInfo = {PropertyInfo.Name}, ElementName = {ElementName}"; +} + +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record IndexDefinition +{ + public IReadOnlyList IndexPaths { get; init; } + public string IndexName { get; init; } + public bool IsUnique { get; init; } + public bool IsTenantExclusive { get; init; } + + [DebuggerNonUserCode] + private string DebuggerDisplay => $"IndexName = {IndexName}, IndexPaths = {IndexPaths.Count}, IsUnique = {IsUnique}"; } -public class EntityIndexDefinition : IEntityIndexDefinition +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record IndexPathDefinition { - public IReadOnlyCollection Properties { get; set; } - public IEntityPropertyDefinition Property { get; set; } - public string Path { get; set; } - public string IndexName { get; set; } - public bool IsUnique { get; set; } - public IndexSortOrder SortOrder { get; set; } - public int IndexPriority { get; set; } - public IndexType IndexType { get; set; } - public bool IsTenantExclusive { get; set; } + public string Path { get; init; } + public IndexType IndexType { get; init; } + public IndexSortOrder SortOrder { get; init; } + + [DebuggerNonUserCode] + private string DebuggerDisplay => $"Path = {Path}, IndexType = {IndexType}, SortOrder = {SortOrder}"; } -public sealed record EntityKeyDefinition : IEntityKeyDefinition +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record KeyDefinition { - public IEntityPropertyDefinition Property { get; init; } + public PropertyDefinition Property { get; init; } public IEntityKeyGenerator KeyGenerator { get; init; } + + [DebuggerNonUserCode] + private string DebuggerDisplay => $"PropertyInfo = {Property.PropertyInfo.Name}, ElementName = {Property.ElementName}"; } -public sealed record EntityExtraElementsDefinition : IEntityExtraElementsDefinition +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record ExtraElementsDefinition { - public IEntityPropertyDefinition Property { get; init; } + public PropertyDefinition Property { get; init; } public bool IgnoreExtraElements { get; init; } public bool IgnoreInherited { get; init; } + + [DebuggerNonUserCode] + private string DebuggerDisplay + { + get + { + if (IgnoreExtraElements) + { + return "IgnoreExtraElements = true"; + } + else + { + return $"PropertyInfo = {Property.PropertyInfo.Name}, ElementName = {Property.ElementName}"; + } + } + } } \ No newline at end of file diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionBuilder.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionBuilder.cs new file mode 100644 index 00000000..d7b54640 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionBuilder.cs @@ -0,0 +1,390 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace MongoFramework.Infrastructure.Mapping; + +public class EntityDefinitionBuilder +{ + public Type EntityType { get; private set; } + public bool MappingSkipped { get; private set; } + public string CollectionName { get; private set; } + public PropertyInfo ExtraElementsProperty { get; private set; } + public EntityKeyBuilder KeyBuilder { get; private set; } + + + private readonly Dictionary propertyBuilders; + public IReadOnlyCollection Properties => propertyBuilders.Values; + + + private readonly List indexBuilders; + public IReadOnlyCollection Indexes => indexBuilders; + + public MappingBuilder MappingBuilder { get; } + + public EntityDefinitionBuilder(Type entityType, MappingBuilder mappingBuilder) + { + EntityType = entityType; + MappingBuilder = mappingBuilder; + propertyBuilders = new(); + indexBuilders = new(); + } + + public EntityDefinitionBuilder(EntityDefinitionBuilder definitionBuilder) + { + EntityType = definitionBuilder.EntityType; + MappingBuilder = definitionBuilder.MappingBuilder; + CollectionName = definitionBuilder.CollectionName; + ExtraElementsProperty = definitionBuilder.ExtraElementsProperty; + KeyBuilder = definitionBuilder.KeyBuilder; + propertyBuilders = definitionBuilder.propertyBuilders; + indexBuilders = definitionBuilder.indexBuilders; + } + + private static void CheckPropertyReadWrite(PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite || !propertyInfo.CanRead) + { + throw new ArgumentException($"Property \"{propertyInfo.Name}\" must be both readable and writeable.", nameof(propertyInfo)); + } + } + + public EntityDefinitionBuilder SkipMapping(bool skip = true) + { + MappingSkipped = skip; + return this; + } + + public EntityDefinitionBuilder ToCollection(string collectionName) + { + CollectionName = collectionName; + return this; + } + + private void ApplyKeyBuilder(EntityPropertyBuilder propertyBuilder) + { + KeyBuilder = new(propertyBuilder.PropertyInfo, propertyBuilder); + } + public EntityDefinitionBuilder HasKey(PropertyInfo propertyInfo, Action builder = null) + { + HasProperty(propertyInfo, ApplyKeyBuilder); + builder?.Invoke(KeyBuilder); + return this; + } + + public EntityDefinitionBuilder Ignore(PropertyInfo propertyInfo) + { + if (propertyInfo.DeclaringType != EntityType) + { + throw new ArgumentException($"Can not ignore properties that aren't declared on \"{EntityType.Name}\"", nameof(propertyInfo)); + } + + propertyBuilders.Remove(propertyInfo); + + if (KeyBuilder is not null && KeyBuilder.Property == propertyInfo) + { + KeyBuilder = null; + } + + if (ExtraElementsProperty == propertyInfo) + { + IgnoreExtraElements(); + } + + indexBuilders.RemoveAll(b => b.ContainsProperty(propertyInfo)); + + return this; + } + + public EntityDefinitionBuilder HasProperty(PropertyInfo propertyInfo, Action builder = null) + { + if (propertyInfo.DeclaringType != EntityType) + { + throw new ArgumentException($"You can only map properties that are declared on the type you're building for. Property \"{propertyInfo.Name}\" is not declared on \"{EntityType.Name}\".", nameof(propertyInfo)); + } + + CheckPropertyReadWrite(propertyInfo); + + if (!propertyBuilders.TryGetValue(propertyInfo, out var propertyBuilder)) + { + propertyBuilder = new EntityPropertyBuilder(propertyInfo); + propertyBuilders[propertyInfo] = propertyBuilder; + } + + builder?.Invoke(propertyBuilder); + return this; + } + + public EntityDefinitionBuilder HasIndex(IEnumerable indexProperties, Action builder = null) + { + //Ensure all properties of the indexes are mapped + foreach (var indexProperty in indexProperties) + { + foreach (var property in indexProperty.PropertyPath.Properties) + { + if (property.DeclaringType == EntityType) + { + HasProperty(property); + } + else + { + MappingBuilder.Entity(property.DeclaringType).HasProperty(property); + } + } + } + + var indexBuilder = new EntityIndexBuilder(indexProperties); + indexBuilders.Add(indexBuilder); + + builder?.Invoke(indexBuilder); + return this; + } + + public EntityDefinitionBuilder HasExtraElements(PropertyInfo propertyInfo) + { + if (!typeof(IDictionary).IsAssignableFrom(propertyInfo.PropertyType)) + { + throw new ArgumentException($"Property \"{propertyInfo.Name}\" must be assignable to \"IDictionary\".", nameof(propertyInfo)); + } + + HasProperty(propertyInfo); + + ExtraElementsProperty = propertyInfo; + return this; + } + + public EntityDefinitionBuilder IgnoreExtraElements() + { + ExtraElementsProperty = null; + return this; + } + + public EntityDefinitionBuilder WithDerivedEntity(Type derivedType, Action builder) + { + if (!derivedType.IsAssignableFrom(derivedType)) + { + throw new ArgumentException($"Type \"{derivedType}\" is not assignable from \"{EntityType}\""); + } + + var definitionBuilder = MappingBuilder.Entity(derivedType); + builder(definitionBuilder); + return this; + } +} + +public class EntityDefinitionBuilder : EntityDefinitionBuilder +{ + public EntityDefinitionBuilder(MappingBuilder mappingBuilder) : base(typeof(TEntity), mappingBuilder) { } + + private EntityDefinitionBuilder(EntityDefinitionBuilder definitionBuilder) : base(definitionBuilder) { } + public static EntityDefinitionBuilder CreateFrom(EntityDefinitionBuilder definitionBuilder) + { + if (typeof(TEntity) != definitionBuilder.EntityType) + { + throw new ArgumentException("Mismatched entity types when creating a generic entity definition", nameof(definitionBuilder)); + } + + return new(definitionBuilder); + } + + private static PropertyInfo GetPropertyInfo(Expression> propertyExpression) + { + var unwrappedExpression = UnwrapExpression(propertyExpression.Body); + if (unwrappedExpression is not MemberExpression memberExpression) + { + throw new ArgumentException("Must be a member expression", nameof(propertyExpression)); + } + + if (memberExpression.Member is not PropertyInfo propertyInfo) + { + throw new ArgumentException("Must be an expression to a property", nameof(propertyExpression)); + } + + return propertyInfo; + } + + private static Expression UnwrapExpression(Expression expression) + { + if (expression is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert) + { + return unaryExpression.Operand; + } + return expression; + } + + public new EntityDefinitionBuilder SkipMapping(bool skip = true) => base.SkipMapping(skip) as EntityDefinitionBuilder; + public new EntityDefinitionBuilder ToCollection(string collectionName) => base.ToCollection(collectionName) as EntityDefinitionBuilder; + + public EntityDefinitionBuilder HasKey(Expression> propertyExpression, Action builder = null) + => HasKey(GetPropertyInfo(propertyExpression), builder) as EntityDefinitionBuilder; + + public EntityDefinitionBuilder Ignore(Expression> propertyExpression) + => Ignore(GetPropertyInfo(propertyExpression)) as EntityDefinitionBuilder; + + public EntityDefinitionBuilder HasProperty(Expression> propertyExpression, Action builder = null) + => HasProperty(GetPropertyInfo(propertyExpression), builder) as EntityDefinitionBuilder; + + public EntityDefinitionBuilder HasIndex(Expression> indexExpression, Action builder = null) + { + var unwrappedExpression = UnwrapExpression(indexExpression.Body); + if (unwrappedExpression is MemberExpression memberExpression) + { + var properties = new[] { new IndexProperty(PropertyPath.FromExpression(memberExpression)) }; + return HasIndex(properties, builder) as EntityDefinitionBuilder; + } + else if (unwrappedExpression is NewExpression newObjExpression) + { + var properties = new List(); + foreach (var expression in newObjExpression.Arguments) + { + var propertyInfoChain = PropertyPath.FromExpression(expression); + properties.Add(new IndexProperty(propertyInfoChain)); + } + return HasIndex(properties, builder) as EntityDefinitionBuilder; + } + else + { + throw new ArgumentException("Must be a member expression to a property or a new expression to bind multiple properties as a single index", nameof(indexExpression)); + } + } + + public EntityDefinitionBuilder HasExtraElements(Expression>> propertyExpression) + => HasExtraElements(GetPropertyInfo(propertyExpression)) as EntityDefinitionBuilder; + + public new EntityDefinitionBuilder IgnoreExtraElements() => base.IgnoreExtraElements() as EntityDefinitionBuilder; + + public EntityDefinitionBuilder WithDerivedEntity(Action> builder) + { + if (!typeof(TEntity).IsAssignableFrom(typeof(TDerived))) + { + throw new ArgumentException($"Type \"{typeof(TDerived)}\" is not assignable from \"{typeof(TEntity)}\""); + } + + var definitionBuilder = MappingBuilder.Entity(); + builder(definitionBuilder); + return this; + } +} + +public sealed class EntityPropertyBuilder +{ + public PropertyInfo PropertyInfo { get; } + public string ElementName { get; private set; } + + public EntityPropertyBuilder(PropertyInfo propertyInfo) + { + PropertyInfo = propertyInfo; + ElementName = propertyInfo.Name; + } + + public EntityPropertyBuilder HasElementName(string elementName) + { + ElementName = elementName; + return this; + } +} + +public readonly record struct IndexProperty(PropertyPath PropertyPath, IndexType IndexType, IndexSortOrder SortOrder) +{ + public IndexProperty(PropertyPath propertyPath) : this(propertyPath, IndexType.Standard, IndexSortOrder.Ascending) { } +} + +public sealed class EntityIndexBuilder +{ + private readonly IndexProperty[] indexProperties; + public IReadOnlyList Properties => indexProperties; + + public string IndexName { get; private set; } + public bool Unique { get; private set; } + + public bool TenantExclusive { get; private set; } + + public EntityIndexBuilder(IEnumerable properties) + { + indexProperties = properties.ToArray(); + } + + public bool ContainsProperty(PropertyInfo propertyInfo) => Properties.Any(p => p.PropertyPath.Contains(propertyInfo)); + + public EntityIndexBuilder HasName(string indexName) + { + IndexName = indexName; + return this; + } + + public EntityIndexBuilder HasType(params IndexType[] indexTypes) + { + if (indexTypes.Length > Properties.Count) + { + throw new ArgumentException("Too many items in list of descending indexes", nameof(indexTypes)); + } + + for (var i = 0; i < indexTypes.Length; i++) + { + indexProperties[i] = indexProperties[i] with + { + IndexType = indexTypes[i] + }; + } + + return this; + } + + public EntityIndexBuilder IsDescending(params bool[] descending) + { + if (descending.Length > Properties.Count) + { + throw new ArgumentException("Too many items in list of descending indexes", nameof(descending)); + } + + for (var i = 0; i < descending.Length; i++) + { + indexProperties[i] = indexProperties[i] with + { + SortOrder = descending[i] ? IndexSortOrder.Descending : IndexSortOrder.Ascending + }; + } + + return this; + } + + public EntityIndexBuilder IsUnique(bool unique = true) + { + Unique = unique; + return this; + } + + public EntityIndexBuilder IsTenantExclusive(bool tenantExclusive = true) + { + TenantExclusive = tenantExclusive; + return this; + } +} + +public sealed class EntityKeyBuilder +{ + private readonly EntityPropertyBuilder propertyBuilder; + + public PropertyInfo Property { get; } + public IEntityKeyGenerator KeyGenerator { get; private set; } + + public EntityKeyBuilder(PropertyInfo property, EntityPropertyBuilder propertyBuilder) + { + Property = property; + this.propertyBuilder = propertyBuilder; + } + + public EntityKeyBuilder HasKeyGenerator(IEntityKeyGenerator keyGenerator) + { + KeyGenerator = keyGenerator; + return this; + } + + public EntityKeyBuilder WithProperty(Action builder) + { + builder(propertyBuilder); + return this; + } +} \ No newline at end of file diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs index dc285ce4..650c508b 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinitionExtensions.cs @@ -6,7 +6,7 @@ namespace MongoFramework.Infrastructure.Mapping { public static class EntityDefinitionExtensions { - public static IEntityPropertyDefinition GetIdProperty(this IEntityDefinition definition) + public static PropertyDefinition GetIdProperty(this EntityDefinition definition) { if (definition.Key is null) { @@ -16,17 +16,17 @@ public static IEntityPropertyDefinition GetIdProperty(this IEntityDefinition def return definition.Key?.Property; } - public static string GetIdName(this IEntityDefinition definition) + public static string GetIdName(this EntityDefinition definition) { return definition.GetIdProperty()?.ElementName; } - public static object GetIdValue(this IEntityDefinition definition, object entity) + public static object GetIdValue(this EntityDefinition definition, object entity) { return definition.GetIdProperty()?.GetValue(entity); } - public static object GetDefaultId(this IEntityDefinition definition) + public static object GetDefaultId(this EntityDefinition definition) { var idPropertyType = definition.GetIdProperty()?.PropertyInfo.PropertyType; if (idPropertyType is { IsValueType: true }) @@ -36,7 +36,7 @@ public static object GetDefaultId(this IEntityDefinition definition) return null; } - public static IEnumerable GetInheritedProperties(this IEntityDefinition definition) + public static IEnumerable GetInheritedProperties(this EntityDefinition definition) { var currentType = definition.EntityType.BaseType; while (currentType != typeof(object) && currentType != null) @@ -51,7 +51,7 @@ public static IEnumerable GetInheritedProperties(this } } - public static IEnumerable GetAllProperties(this IEntityDefinition definition) + public static IEnumerable GetAllProperties(this EntityDefinition definition) { foreach (var property in definition.Properties) { @@ -64,7 +64,7 @@ public static IEnumerable GetAllProperties(this IEnti } } - public static IEntityPropertyDefinition GetProperty(this IEntityDefinition definition, string name) + public static PropertyDefinition GetProperty(this EntityDefinition definition, string name) { foreach (var property in definition.GetAllProperties()) { diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingBuilder.cs b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingBuilder.cs new file mode 100644 index 00000000..3a231fa7 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingBuilder.cs @@ -0,0 +1,157 @@ +using System; +using System.Buffers; +using System.Linq; +using System.Reflection; + +namespace MongoFramework.Infrastructure.Mapping; + +public static partial class EntityMapping +{ + private static readonly string PathSeparator = "."; + + public static void RegisterMapping(Action builder) + { + var mappingBuilder = new MappingBuilder(MappingProcessors); + builder(mappingBuilder); + RegisterMapping(mappingBuilder); + } + + public static void RegisterMapping(MappingBuilder mappingBuilder) + { + MappingLock.EnterWriteLock(); + try + { + for (var i = 0; i < mappingBuilder.Definitions.Count; i++) + { + var definitionBuilder = mappingBuilder.Definitions[i]; + if (EntityDefinitions.ContainsKey(definitionBuilder.EntityType)) + { + continue; + } + + if (definitionBuilder.MappingSkipped) + { + continue; + } + + var definition = ResolveEntityDefinition(definitionBuilder); + + EntityDefinitions[definition.EntityType] = definition; + DriverMappingInterop.RegisterDefinition(definition); + } + } + finally + { + MappingLock.ExitWriteLock(); + } + } + + private static KeyDefinition ResolveKeyDefinition(EntityDefinitionBuilder definitionBuilder, PropertyDefinition[] properties) + { + if (definitionBuilder.KeyBuilder is null) + { + return null; + } + + return new KeyDefinition + { + Property = properties.First(p => p.PropertyInfo == definitionBuilder.KeyBuilder.Property), + KeyGenerator = definitionBuilder.KeyBuilder.KeyGenerator, + }; + } + + private static string ResolveElementName(EntityDefinitionBuilder definitionBuilder, PropertyInfo propertyInfo) + { + if (propertyInfo.DeclaringType == definitionBuilder.EntityType) + { + //When the definition builder is for the entity type that owns the property + return definitionBuilder.Properties.First(p => p.PropertyInfo == propertyInfo).ElementName; + } + else if (EntityDefinitions.TryGetValue(propertyInfo.DeclaringType, out var definition)) + { + //When the type that owns the property is already registered + var property = definition.GetProperty(propertyInfo.Name) ?? throw new ArgumentException($"Property \"{propertyInfo.Name}\" was not found on existing definition for \"{propertyInfo.DeclaringType}\""); + return property.ElementName; + } + else if (IsValidTypeToMap(propertyInfo.DeclaringType)) + { + //When all else fails, find or create the appropriate definition builder for the type that owns the property + var localDefinitionBuilder = definitionBuilder.MappingBuilder.Entity(propertyInfo.DeclaringType); + return localDefinitionBuilder.Properties.First(p => p.PropertyInfo == propertyInfo).ElementName; + } + else + { + throw new ArgumentException($"Property \"{propertyInfo.Name}\" has a declaring type that is not valid for mapping"); + } + } + + private static string ResolvePropertyPath(EntityDefinitionBuilder definitionBuilder, PropertyPath propertyPath) + { + var pool = ArrayPool.Shared.Rent(propertyPath.Properties.Count); + try + { + for (var i = 0; i < propertyPath.Properties.Count; i++) + { + var propertyInfo = propertyPath.Properties[i]; + pool[i] = ResolveElementName(definitionBuilder, propertyInfo); + } + return string.Join(PathSeparator, pool, 0, propertyPath.Properties.Count); + } + finally + { + ArrayPool.Shared.Return(pool); + } + } + + private static IndexDefinition[] ResolveIndexDefinitions(EntityDefinitionBuilder definitionBuilder) + { + return definitionBuilder.Indexes.Select(indexBuilder => new IndexDefinition + { + IndexPaths = indexBuilder.Properties.Select(p => new IndexPathDefinition + { + Path = ResolvePropertyPath(definitionBuilder, p.PropertyPath), + IndexType = p.IndexType, + SortOrder = p.SortOrder + }).ToArray(), + IndexName = indexBuilder.IndexName, + IsUnique = indexBuilder.Unique, + IsTenantExclusive = indexBuilder.TenantExclusive + }).ToArray(); + } + + private static ExtraElementsDefinition ResolveExtraElementsDefinition(EntityDefinitionBuilder definitionBuilder, PropertyDefinition[] properties) + { + if (definitionBuilder.ExtraElementsProperty is null) + { + return new ExtraElementsDefinition + { + IgnoreExtraElements = true, + IgnoreInherited = true + }; + } + + return new ExtraElementsDefinition + { + Property = properties.First(p => p.PropertyInfo == definitionBuilder.ExtraElementsProperty) + }; + } + + private static EntityDefinition ResolveEntityDefinition(EntityDefinitionBuilder definitionBuilder) + { + var properties = definitionBuilder.Properties.Select(p => new PropertyDefinition + { + PropertyInfo = p.PropertyInfo, + ElementName = p.ElementName + }).ToArray(); + + return new EntityDefinition + { + EntityType = definitionBuilder.EntityType, + CollectionName = definitionBuilder.CollectionName, + Key = ResolveKeyDefinition(definitionBuilder, properties), + Properties = properties, + ExtraElements = ResolveExtraElementsDefinition(definitionBuilder, properties), + Indexes = ResolveIndexDefinitions(definitionBuilder), + }; + } +} diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingProcessors.cs b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingProcessors.cs new file mode 100644 index 00000000..e466fe54 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.MappingProcessors.cs @@ -0,0 +1,51 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; + +namespace MongoFramework.Infrastructure.Mapping; + +public static partial class EntityMapping +{ + private static readonly List MappingProcessors = new(); + + private static void WithMappingWriteLock(Action action) + { + MappingLock.EnterWriteLock(); + try + { + action(); + } + finally + { + MappingLock.ExitWriteLock(); + } + } + + public static void AddMappingProcessor(IMappingProcessor mappingProcessor) + { + WithMappingWriteLock(() => MappingProcessors.Add(mappingProcessor)); + } + + public static void AddMappingProcessors(IEnumerable mappingProcessors) + { + WithMappingWriteLock(() => MappingProcessors.AddRange(mappingProcessors)); + } + + public static void RemoveMappingProcessor() where TProcessor : IMappingProcessor + { + WithMappingWriteLock(() => + { + var matchingItems = MappingProcessors.Where(p => p.GetType() == typeof(TProcessor)).ToArray(); + foreach (var matchingItem in matchingItems) + { + MappingProcessors.Remove(matchingItem); + } + }); + } + + public static void RemoveAllMappingProcessors() + { + WithMappingWriteLock(MappingProcessors.Clear); + } +} diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityMapping.cs b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.cs index 2f31a2ed..c91ac3cc 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityMapping.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityMapping.cs @@ -1,215 +1,160 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using MongoDB.Bson; using MongoDB.Bson.Serialization; -namespace MongoFramework.Infrastructure.Mapping +namespace MongoFramework.Infrastructure.Mapping; + +public static partial class EntityMapping { - public static class EntityMapping + private static ReaderWriterLockSlim MappingLock { get; } = new(LockRecursionPolicy.SupportsRecursion); + private static readonly ConcurrentDictionary EntityDefinitions = new(); + + static EntityMapping() { - private static ReaderWriterLockSlim MappingLock { get; } = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - private static ConcurrentDictionary EntityDefinitions { get; } - private static List MappingProcessors { get; } + DriverAbstractionRules.ApplyRules(); + AddMappingProcessors(DefaultMappingProcessors.Processors); + } - static EntityMapping() - { - DriverAbstractionRules.ApplyRules(); + internal static void RemoveAllDefinitions() + { + EntityDefinitions.Clear(); + } - EntityDefinitions = new ConcurrentDictionary(); - MappingProcessors = new List(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidTypeToMap(Type entityType) + { + return entityType.IsClass && + entityType != typeof(object) && + entityType != typeof(string) && + !typeof(BsonValue).IsAssignableFrom(entityType); + } - AddMappingProcessors(DefaultProcessors.CreateProcessors()); - } + public static bool IsRegistered(Type entityType) + { + return EntityDefinitions.ContainsKey(entityType); + } - public static IEntityDefinition SetEntityDefinition(IEntityDefinition definition) + public static EntityDefinition GetOrCreateDefinition(Type entityType) + { + MappingLock.EnterUpgradeableReadLock(); + try { - MappingLock.EnterWriteLock(); - try - { - return EntityDefinitions.AddOrUpdate(definition.EntityType, definition, (entityType, existingValue) => - { - return definition; - }); - } - finally + if (EntityDefinitions.TryGetValue(entityType, out var definition)) { - MappingLock.ExitWriteLock(); + return definition; } - } - public static void RemoveEntityDefinition(IEntityDefinition definition) - { - EntityDefinitions.TryRemove(definition.EntityType, out _); - } - - public static void RemoveAllDefinitions() - { - EntityDefinitions.Clear(); + return RegisterType(entityType); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidTypeToMap(Type entityType) + finally { - return entityType.IsClass && - entityType != typeof(object) && - entityType != typeof(string) && - !typeof(BsonValue).IsAssignableFrom(entityType); + MappingLock.ExitUpgradeableReadLock(); } + } - public static bool IsRegistered(Type entityType) + public static bool TryRegisterType(Type entityType, out EntityDefinition definition) + { + if (!IsValidTypeToMap(entityType)) { - return EntityDefinitions.ContainsKey(entityType); + definition = null; + return false; } - public static IEntityDefinition RegisterType(Type entityType) + MappingLock.EnterUpgradeableReadLock(); + try { - if (!IsValidTypeToMap(entityType)) + if (EntityDefinitions.ContainsKey(entityType) || BsonClassMap.IsClassMapRegistered(entityType)) { - throw new ArgumentException("Type is not a valid type to map", nameof(entityType)); + definition = null; + return false; } - MappingLock.EnterUpgradeableReadLock(); + MappingLock.EnterWriteLock(); try { - if (EntityDefinitions.ContainsKey(entityType)) + //Now we have the write lock, do one super last minute check + if (EntityDefinitions.TryGetValue(entityType, out definition)) { - throw new ArgumentException("Type is already registered", nameof(entityType)); + //We will treat success of this check as if we have registered it just now + return true; } - if (BsonClassMap.IsClassMapRegistered(entityType)) - { - throw new ArgumentException($"Type is already registered as a {nameof(BsonClassMap)}"); - } + var mappingBuilder = new MappingBuilder(MappingProcessors); + mappingBuilder.Entity(entityType); - MappingLock.EnterWriteLock(); - try - { - //Now we have the write lock, do one super last minute check - if (EntityDefinitions.TryGetValue(entityType, out var definition)) - { - //We will treat success of this check as if we have registered it just now - return definition; - } - definition = new EntityDefinition - { - EntityType = entityType - }; - - EntityDefinitions.TryAdd(entityType, definition); - - foreach (var processor in MappingProcessors) - { - processor.ApplyMapping(definition); - } - - DriverMappingInterop.RegisterDefinition(definition); - return definition; - } - finally - { - MappingLock.ExitWriteLock(); - } + RegisterMapping(mappingBuilder); + + return EntityDefinitions.TryGetValue(entityType, out definition); + } + catch + { + definition = null; + return false; } finally { - MappingLock.ExitUpgradeableReadLock(); + MappingLock.ExitWriteLock(); } } + finally + { + MappingLock.ExitUpgradeableReadLock(); + } + } - public static IEntityDefinition GetOrCreateDefinition(Type entityType) + public static EntityDefinition RegisterType(Type entityType) + { + if (!IsValidTypeToMap(entityType)) { - MappingLock.EnterUpgradeableReadLock(); - try - { - if (EntityDefinitions.TryGetValue(entityType, out var definition)) - { - return definition; - } + throw new ArgumentException("Type is not a valid type to map", nameof(entityType)); + } - return RegisterType(entityType); - } - finally + MappingLock.EnterUpgradeableReadLock(); + try + { + if (EntityDefinitions.ContainsKey(entityType)) { - MappingLock.ExitUpgradeableReadLock(); + throw new ArgumentException("Type is already registered", nameof(entityType)); } - } - public static bool TryRegisterType(Type entityType, out IEntityDefinition definition) - { - if (!IsValidTypeToMap(entityType)) + if (BsonClassMap.IsClassMapRegistered(entityType)) { - definition = null; - return false; + throw new ArgumentException($"Type is already registered as a {nameof(BsonClassMap)}"); } - MappingLock.EnterUpgradeableReadLock(); + MappingLock.EnterWriteLock(); try { - if (EntityDefinitions.ContainsKey(entityType) || BsonClassMap.IsClassMapRegistered(entityType)) + //Now we have the write lock, do one super last minute check + if (EntityDefinitions.TryGetValue(entityType, out var definition)) { - definition = null; - return false; + //We will treat success of this check as if we have registered it just now + return definition; } - MappingLock.EnterWriteLock(); - try - { - //Now we have the write lock, do one super last minute check - if (EntityDefinitions.TryGetValue(entityType, out definition)) - { - //We will treat success of this check as if we have registered it just now - return true; - } - - definition = new EntityDefinition - { - EntityType = entityType - }; - - EntityDefinitions.TryAdd(entityType, definition); - - foreach (var processor in MappingProcessors) - { - processor.ApplyMapping(definition); - } - - DriverMappingInterop.RegisterDefinition(definition); - return true; - } - finally + var mappingBuilder = new MappingBuilder(MappingProcessors); + mappingBuilder.Entity(entityType); + + RegisterMapping(mappingBuilder); + + if (EntityDefinitions.TryGetValue(entityType, out definition)) { - MappingLock.ExitWriteLock(); + return definition; } + + throw new ArgumentException($"Registration of type \"{entityType}\" was skipped", nameof(entityType)); } finally { - MappingLock.ExitUpgradeableReadLock(); - } - } - - public static void AddMappingProcessors(IEnumerable mappingProcessors) - { - MappingProcessors.AddRange(mappingProcessors); - } - public static void AddMappingProcessor(IMappingProcessor mappingProcessor) - { - MappingProcessors.Add(mappingProcessor); - } - public static void RemoveMappingProcessor() where TProcessor : IMappingProcessor - { - var matchingItems = MappingProcessors.Where(p => p.GetType() == typeof(TProcessor)).ToArray(); - foreach (var matchingItem in matchingItems) - { - MappingProcessors.Remove(matchingItem); + MappingLock.ExitWriteLock(); } } - public static void RemoveAllMappingProcessors() + finally { - MappingProcessors.Clear(); + MappingLock.ExitUpgradeableReadLock(); } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/IMappingProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/IMappingProcessor.cs index d7aab265..6544dc86 100644 --- a/src/MongoFramework/Infrastructure/Mapping/IMappingProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/IMappingProcessor.cs @@ -1,9 +1,6 @@ -using MongoDB.Bson.Serialization; +namespace MongoFramework.Infrastructure.Mapping; -namespace MongoFramework.Infrastructure.Mapping +public interface IMappingProcessor { - public interface IMappingProcessor - { - void ApplyMapping(IEntityDefinition definition); - } + void ApplyMapping(EntityDefinitionBuilder definitionBuilder); } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/BsonKnownTypesProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/BsonKnownTypesProcessor.cs index bd0940e8..1b35245b 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/BsonKnownTypesProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/BsonKnownTypesProcessor.cs @@ -1,21 +1,19 @@ using System.Reflection; -using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class BsonKnownTypesProcessor : IMappingProcessor { - public class BsonKnownTypesProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) + var entityType = definitionBuilder.EntityType; + var bsonKnownTypesAttribute = entityType.GetCustomAttribute(); + if (bsonKnownTypesAttribute != null) { - var entityType = definition.EntityType; - var bsonKnownTypesAttribute = entityType.GetCustomAttribute(); - if (bsonKnownTypesAttribute != null) + foreach (var type in bsonKnownTypesAttribute.KnownTypes) { - foreach (var type in bsonKnownTypesAttribute.KnownTypes) - { - EntityMapping.TryRegisterType(type, out _); - } + definitionBuilder.MappingBuilder.Entity(type); } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/CollectionNameProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/CollectionNameProcessor.cs index 5af71ea6..9e484564 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/CollectionNameProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/CollectionNameProcessor.cs @@ -1,41 +1,39 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; -using MongoDB.Bson.Serialization; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class CollectionNameProcessor : IMappingProcessor { - public class CollectionNameProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) - { - var entityType = definition.EntityType; - var collectionName = entityType.Name; + var entityType = definitionBuilder.EntityType; + var collectionName = entityType.Name; - var tableAttribute = entityType.GetCustomAttribute(); + var tableAttribute = entityType.GetCustomAttribute(); - if (tableAttribute == null && entityType.IsGenericType && entityType.GetGenericTypeDefinition() == typeof(EntityBucket<,>)) + if (tableAttribute == null && entityType.IsGenericType && entityType.GetGenericTypeDefinition() == typeof(EntityBucket<,>)) + { + var groupType = entityType.GetGenericArguments()[0]; + tableAttribute = groupType.GetCustomAttribute(); + if (tableAttribute == null) { - var groupType = entityType.GetGenericArguments()[0]; - tableAttribute = groupType.GetCustomAttribute(); - if (tableAttribute == null) - { - collectionName = groupType.Name; - } + collectionName = groupType.Name; } + } - if (tableAttribute != null) + if (tableAttribute != null) + { + if (string.IsNullOrEmpty(tableAttribute.Schema)) { - if (string.IsNullOrEmpty(tableAttribute.Schema)) - { - collectionName = tableAttribute.Name; - } - else - { - collectionName = tableAttribute.Schema + "." + tableAttribute.Name; - } + collectionName = tableAttribute.Name; + } + else + { + collectionName = tableAttribute.Schema + "." + tableAttribute.Name; } - - definition.CollectionName = collectionName; } + + definitionBuilder.ToCollection(collectionName); } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs index db0d334d..ee24fea4 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/EntityIdProcessor.cs @@ -2,57 +2,50 @@ using System.ComponentModel.DataAnnotations; using System.Reflection; using MongoDB.Bson; -using MongoDB.Bson.Serialization; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class EntityIdProcessor : IMappingProcessor { - public class EntityIdProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) + foreach (var propertyBuilder in definitionBuilder.Properties) { - var keyDefinition = definition.Key; - var idProperty = keyDefinition?.Property; - foreach (var property in definition.Properties) + if (Attribute.IsDefined(propertyBuilder.PropertyInfo, typeof(KeyAttribute))) { - if (property.PropertyInfo.GetCustomAttribute() != null) - { - idProperty = property; - break; - } - - if (property.ElementName.Equals("id", StringComparison.InvariantCultureIgnoreCase)) - { - //We don't break here just in case another property has the KeyAttribute - //We preference the attribute over the name match - idProperty = property; - } + definitionBuilder.HasKey( + propertyBuilder.PropertyInfo, + AutoPickKeyGenerator + ); + return; } - if (idProperty is EntityPropertyDefinition entityProperty) + if (propertyBuilder.ElementName.Equals("id", StringComparison.InvariantCultureIgnoreCase)) { - var keyGenerator = keyDefinition?.KeyGenerator; - - //Set an Id Generator based on the member type - var memberType = entityProperty.PropertyInfo.PropertyType; - if (memberType == typeof(string)) - { - keyGenerator = EntityKeyGenerators.StringKeyGenerator; - } - else if (memberType == typeof(Guid)) - { - keyGenerator = EntityKeyGenerators.GuidKeyGenerator; - } - else if (memberType == typeof(ObjectId)) - { - keyGenerator = EntityKeyGenerators.ObjectIdKeyGenerator; - } - - definition.Key = new EntityKeyDefinition - { - Property = entityProperty, - KeyGenerator = keyGenerator - }; + //We don't stop here just in case another property has the KeyAttribute + //We preference the attribute over the name match + definitionBuilder.HasKey( + propertyBuilder.PropertyInfo, + AutoPickKeyGenerator + ); } } } + + private static void AutoPickKeyGenerator(EntityKeyBuilder keyBuilder) + { + var propertyType = keyBuilder.Property.PropertyType; + if (propertyType == typeof(string)) + { + keyBuilder.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator); + } + else if (propertyType == typeof(Guid)) + { + keyBuilder.HasKeyGenerator(EntityKeyGenerators.GuidKeyGenerator); + } + else if (propertyType == typeof(ObjectId)) + { + keyBuilder.HasKeyGenerator(EntityKeyGenerators.ObjectIdKeyGenerator); + } + } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/ExtraElementsProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/ExtraElementsProcessor.cs index 71e969cd..56c4b8c3 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/ExtraElementsProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/ExtraElementsProcessor.cs @@ -1,43 +1,31 @@ -using System.Collections.Generic; -using System.Reflection; -using MongoDB.Bson.Serialization; +using System.Reflection; using MongoFramework.Attributes; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class ExtraElementsProcessor : IMappingProcessor { - public class ExtraElementsProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) - { - var entityType = definition.EntityType; + var entityType = definitionBuilder.EntityType; - //Ignore extra elements when the "IgnoreExtraElementsAttribute" is on the Entity - var ignoreExtraElements = entityType.GetCustomAttribute(); - if (ignoreExtraElements != null) - { - definition.ExtraElements = new EntityExtraElementsDefinition - { - IgnoreExtraElements = true, - IgnoreInherited = ignoreExtraElements.IgnoreInherited - }; - } - else + //Ignore extra elements when the "IgnoreExtraElementsAttribute" is on the Entity + var ignoreExtraElements = entityType.GetCustomAttribute(); + if (ignoreExtraElements != null) + { + definitionBuilder.IgnoreExtraElements(); + } + else + { + //If any of the Entity's properties have the "ExtraElementsAttribute", use that + foreach (var propertyBuilder in definitionBuilder.Properties) { - //If any of the Entity's properties have the "ExtraElementsAttribute", use that - foreach (var property in definition.Properties) + var extraElementsAttribute = propertyBuilder.PropertyInfo.GetCustomAttribute(); + if (extraElementsAttribute != null) { - var extraElementsAttribute = property.PropertyInfo.GetCustomAttribute(); - if (extraElementsAttribute != null && typeof(IDictionary).IsAssignableFrom(property.PropertyInfo.PropertyType)) - { - definition.ExtraElements = new EntityExtraElementsDefinition - { - Property = property, - IgnoreExtraElements = false - }; - break; - } + definitionBuilder.HasExtraElements(propertyBuilder.PropertyInfo); + break; } - } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/HierarchyProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/HierarchyProcessor.cs index 18b50632..7e5b2ec5 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/HierarchyProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/HierarchyProcessor.cs @@ -1,16 +1,13 @@ -using MongoDB.Bson.Serialization; +namespace MongoFramework.Infrastructure.Mapping.Processors; -namespace MongoFramework.Infrastructure.Mapping.Processors +public class HierarchyProcessor : IMappingProcessor { - public class HierarchyProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) + var baseType = definitionBuilder.EntityType.BaseType; + if (EntityMapping.IsValidTypeToMap(baseType)) { - var entityType = definition.EntityType; - if (EntityMapping.IsValidTypeToMap(entityType.BaseType)) - { - EntityMapping.TryRegisterType(entityType.BaseType, out _); - } + definitionBuilder.MappingBuilder.Entity(baseType); } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/IndexProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/IndexProcessor.cs index c1f25b61..61238989 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/IndexProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/IndexProcessor.cs @@ -1,33 +1,184 @@ -using System.Collections.Generic; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using MongoFramework.Infrastructure.Internal; using MongoFramework.Attributes; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class IndexProcessor : IMappingProcessor { - public class IndexProcessor : IMappingProcessor + internal record TraversedProperty + { + public TraversedProperty Parent { get; init; } + public EntityPropertyBuilder Property { get; init; } + public int Depth { get; init; } + + public PropertyPath GetPropertyPath() + { + if (Depth == 0) + { + return new PropertyPath(new[] { Property.PropertyInfo }); + } + + var path = new PropertyInfo[Depth + 1]; + var current = this; + for (var i = Depth; i >= 0; i--) + { + path[i] = current.Property.PropertyInfo; + current = current.Parent; + } + + return new PropertyPath(path); + } + } + private readonly record struct TraversalState + { + public HashSet SeenTypes { get; init; } + public IEnumerable Properties { get; init; } + } + + private IEnumerable GetAllProperties(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) + var mappingBuilder = definitionBuilder.MappingBuilder; + var baseType = definitionBuilder.EntityType.BaseType; + if (baseType != typeof(object) && baseType is not null) + { + var baseDefinitionBuilder = mappingBuilder.Entity(baseType); + foreach (var property in GetAllProperties(baseDefinitionBuilder)) + { + yield return property; + } + } + + foreach (var property in definitionBuilder.Properties) + { + yield return property; + } + } + + private IEnumerable TraverseProperties(EntityDefinitionBuilder definitionBuilder) + { + var mappingBuilder = definitionBuilder.MappingBuilder; + + var stack = new Stack(); + stack.Push(new TraversalState { - var definitionIndexes = new List(); - foreach (var property in definition.TraverseProperties()) + SeenTypes = new HashSet { definitionBuilder.EntityType }, + Properties = GetAllProperties(definitionBuilder).Select(p => new TraversedProperty { - foreach (var indexAttribute in property.Property.PropertyInfo.GetCustomAttributes()) + Property = p, + Depth = 0 + }) + }); + + while (stack.Count > 0) + { + var state = stack.Pop(); + foreach (var traversedProperty in state.Properties) + { + yield return traversedProperty; + + var propertyType = traversedProperty.Property.PropertyInfo.PropertyType; + propertyType = propertyType.UnwrapEnumerableTypes(); + + if (EntityMapping.IsValidTypeToMap(propertyType) && !state.SeenTypes.Contains(propertyType)) { - definitionIndexes.Add(new EntityIndexDefinition + var propertyDefinitionBuilder = mappingBuilder.Entity(propertyType); + var nestedProperties = GetAllProperties(propertyDefinitionBuilder) + .Select(p => new TraversedProperty + { + Parent = traversedProperty, + Property = p, + Depth = traversedProperty.Depth + 1 + }); + + stack.Push(new TraversalState { - Property = property.Property, - Path = property.GetPath(), - IndexName = indexAttribute.Name, - IsUnique = indexAttribute.IsUnique, - SortOrder = indexAttribute.SortOrder, - IndexPriority = indexAttribute.IndexPriority, - IndexType = indexAttribute.IndexType, - IsTenantExclusive = indexAttribute.IsTenantExclusive + SeenTypes = new HashSet(state.SeenTypes) + { + propertyType + }, + Properties = nestedProperties }); } } + } + } + + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) + { + var indexTracker = new Dictionary>(); + + //Find all index attributes (with their traversed property path) and group them by their name + foreach (var traversedProperty in TraverseProperties(definitionBuilder)) + { + foreach (var indexAttribute in traversedProperty.Property.PropertyInfo.GetCustomAttributes()) + { + var indexName = indexAttribute.Name ?? string.Empty; + if (!indexTracker.TryGetValue(indexName, out var traversedProperties)) + { + traversedProperties = new(); + indexTracker[indexName] = traversedProperties; + } + + traversedProperties.Add((traversedProperty, indexAttribute)); + } + } - definition.Indexes = definitionIndexes; + //Process the unnamed group as individual indexes + if (indexTracker.TryGetValue(string.Empty, out var ungroupedIndexes)) + { + foreach (var ungroupedIndex in ungroupedIndexes) + { + var indexAttr = ungroupedIndex.IndexAttribute; + var indexProperty = new IndexProperty( + ungroupedIndex.TraversedProperty.GetPropertyPath(), + indexAttr.IndexType, + indexAttr.SortOrder + ); + HasIndex( + definitionBuilder, + ungroupedIndex.IndexAttribute, + indexName: null, + indexProperty + ); + } + indexTracker.Remove(string.Empty); + } + + //Using the grouped indexes, apply them to the entity definition builder + foreach (var index in indexTracker) + { + var indexProperties = index.Value + .OrderBy(p => p.IndexAttribute.IndexPriority) + .Select(p => new IndexProperty( + p.TraversedProperty.GetPropertyPath(), + p.IndexAttribute.IndexType, + p.IndexAttribute.SortOrder + )).ToArray(); + HasIndex( + definitionBuilder, + index.Value[0].IndexAttribute, + indexName: index.Key, + indexProperties + ); } } + + private static void HasIndex( + EntityDefinitionBuilder definitionBuilder, + IndexAttribute indexAttribute, + string indexName = null, + params IndexProperty[] indexProperties + ) + { + definitionBuilder.HasIndex(indexProperties, b => b + .HasName(indexName) + .IsUnique(indexAttribute.IsUnique) + .IsTenantExclusive(indexAttribute.IsTenantExclusive) + ); + } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/MappingAdapterProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/MappingAdapterProcessor.cs index b28718e5..fcd220d4 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/MappingAdapterProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/MappingAdapterProcessor.cs @@ -1,28 +1,26 @@ using System; using System.Reflection; -using MongoDB.Bson.Serialization; using MongoFramework.Attributes; -namespace MongoFramework.Infrastructure.Mapping.Processors +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class MappingAdapterProcessor : IMappingProcessor { - public class MappingAdapterProcessor : IMappingProcessor + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - public void ApplyMapping(IEntityDefinition definition) + var adapterAttribute = definitionBuilder.EntityType.GetCustomAttribute(); + if (adapterAttribute == null) { - var adapterAttribute = definition.EntityType.GetCustomAttribute(); - - if (adapterAttribute == null) - { - return; - } - - var instance = (IMappingProcessor)Activator.CreateInstance(adapterAttribute.MappingAdapter); - - if (instance != null) - { - instance.ApplyMapping(definition); - } + return; + } + var adapterType = adapterAttribute.MappingAdapter; + if (!typeof(IMappingProcessor).IsAssignableFrom(adapterType)) + { + throw new InvalidOperationException($"Mapping adapter \"{adapterType}\" doesn't implement IMappingProcessor"); } + + var instance = (IMappingProcessor)Activator.CreateInstance(adapterType); + instance?.ApplyMapping(definitionBuilder); } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/NestedTypeProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/NestedTypeProcessor.cs index 798562e9..bbfbb21e 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/NestedTypeProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/NestedTypeProcessor.cs @@ -4,20 +4,20 @@ namespace MongoFramework.Infrastructure.Mapping.Processors { public class NestedTypeProcessor : IMappingProcessor { - public void ApplyMapping(IEntityDefinition definition) + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - var entityType = definition.EntityType; - var properties = definition.Properties; + var entityType = definitionBuilder.EntityType; + var properties = definitionBuilder.Properties; foreach (var property in properties) { var propertyType = property.PropertyInfo.PropertyType; - propertyType = propertyType.GetEnumerableItemTypeOrDefault(); + propertyType = propertyType.UnwrapEnumerableTypes(); //Maps the property type for handling property nesting if (propertyType != entityType && EntityMapping.IsValidTypeToMap(propertyType)) { - EntityMapping.TryRegisterType(propertyType, out _); + definitionBuilder.MappingBuilder.Entity(propertyType); } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs index 2b36fb63..bb7c1337 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs @@ -1,19 +1,15 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; -using MongoDB.Bson.Serialization; namespace MongoFramework.Infrastructure.Mapping.Processors { public class PropertyMappingProcessor : IMappingProcessor { - public void ApplyMapping(IEntityDefinition definition) + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - var entityType = definition.EntityType; + var entityType = definitionBuilder.EntityType; var properties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - var definitionProperties = new List(); - foreach (var property in properties) { if (!property.CanRead || !property.CanWrite) @@ -41,24 +37,20 @@ public void ApplyMapping(IEntityDefinition definition) continue; } - var elementName = property.Name; - - //Set custom element name with the "ColumnAttribute" - var columnAttribute = property.GetCustomAttribute(); - if (columnAttribute != null) + definitionBuilder.HasProperty(property, builder => { - elementName = columnAttribute.Name; - } + var elementName = property.Name; - definitionProperties.Add(new EntityPropertyDefinition - { - EntityDefinition = definition, - ElementName = elementName, - PropertyInfo = property + //Set custom element name with the "ColumnAttribute" + var columnAttribute = property.GetCustomAttribute(); + if (columnAttribute != null) + { + elementName = columnAttribute.Name; + } + + builder.HasElementName(elementName); }); } - - definition.Properties = definitionProperties; } } } diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/SkipMappingProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/SkipMappingProcessor.cs new file mode 100644 index 00000000..85d3c352 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/SkipMappingProcessor.cs @@ -0,0 +1,17 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MongoFramework.Infrastructure.Mapping.Processors; + +public class SkipMappingProcessor : IMappingProcessor +{ + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) + { + var entityType = definitionBuilder.EntityType; + + if (Attribute.IsDefined(entityType, typeof(NotMappedAttribute), true)) + { + definitionBuilder.SkipMapping(true); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Mapping/PropertyPath.cs b/src/MongoFramework/Infrastructure/Mapping/PropertyPath.cs new file mode 100644 index 00000000..5834bfee --- /dev/null +++ b/src/MongoFramework/Infrastructure/Mapping/PropertyPath.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using MongoFramework.Infrastructure.Internal; +using MongoFramework.Infrastructure.Linq; + +namespace MongoFramework.Infrastructure.Mapping; + +public readonly record struct PropertyPath(IReadOnlyList Properties) +{ + /// + /// Returns the entity types found through the property path. + /// + /// + public IEnumerable GetEntityTypes() + { + foreach (var property in Properties) + { + var possibleEntityType = property.PropertyType.UnwrapEnumerableTypes(); + if (EntityMapping.IsValidTypeToMap(possibleEntityType)) + { + yield return possibleEntityType; + } + } + } + + public bool Contains(PropertyInfo propertyInfo) => Properties.Contains(propertyInfo); + + /// + /// Returns a based on the resolved properties through the . + /// + /// + /// + /// For example, take the expression body: v.Thing.Items.First().Name
+ /// We want [Thing, Items, Name] but the expression is actually: Name.First().Items.Thing.v
+ /// This is also expressed as [MemberExpression, MethodCallExpression, MemberExpression, MemberExpression, ParameterExpression]. + ///
+ /// This is why we have a stack (for our result to be the "correct" order) and we exit on . + ///
+ /// + /// + /// + public static PropertyPath FromExpression(Expression pathExpression) + { + var propertyInfoChain = new Stack(); + var current = pathExpression; + + while (current is not ParameterExpression) + { + if (current is MemberExpression memberExpression && memberExpression.Member is PropertyInfo propertyInfo) + { + propertyInfoChain.Push(propertyInfo); + current = memberExpression.Expression; + } + else if (current is MethodCallExpression methodExpression) + { + var genericMethodDefinition = methodExpression.Method.GetGenericMethodDefinition(); + if (genericMethodDefinition == MethodInfoCache.Enumerable.First_1 || genericMethodDefinition == MethodInfoCache.Enumerable.Single_1) + { + var callerExpression = methodExpression.Arguments[0]; + current = callerExpression; + } + else + { + throw new ArgumentException($"Invalid method \"{methodExpression.Method.Name}\". Only \"Enumerable.First()\" and \"Enumerable.Single()\" methods are allowed in chained expressions", nameof(pathExpression)); + } + + } + else if (current is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert) + { + current = unaryExpression.Operand; + } + else + { + throw new ArgumentException($"Unexpected expression \"{current}\" when processing chained expression", nameof(pathExpression)); + } + } + + return new(propertyInfoChain.ToArray()); + } + + /// + /// Returns a based on the resolved properties (by name) through the provided string. + /// + /// + /// For example, take this string: Thing.Items.Name
+ /// This would be resolved as [Thing, Items, Name] including going through any array/enumerable that might exist. + ///
+ /// + /// + public static PropertyPath FromString(Type baseType, string propertyPath) + { + var inputChain = propertyPath.Split('.'); + var propertyInfoChain = new PropertyInfo[inputChain.Length]; + + var currentType = baseType; + for (var i = 0; i < inputChain.Length; i++) + { + var propertyName = inputChain[i]; + var property = currentType.GetProperty(propertyName) ?? throw new ArgumentException($"Property \"{propertyName}\" is not found on reflected entity types", nameof(propertyPath)); + propertyInfoChain[i] = property; + + var propertyType = property.PropertyType.UnwrapEnumerableTypes(); + currentType = propertyType; + } + + return new(propertyInfoChain); + } +} diff --git a/src/MongoFramework/Infrastructure/Mapping/PropertyTraversalExtensions.cs b/src/MongoFramework/Infrastructure/Mapping/PropertyTraversalExtensions.cs index 26a9cce6..f03ad914 100644 --- a/src/MongoFramework/Infrastructure/Mapping/PropertyTraversalExtensions.cs +++ b/src/MongoFramework/Infrastructure/Mapping/PropertyTraversalExtensions.cs @@ -7,21 +7,13 @@ namespace MongoFramework.Infrastructure.Mapping; -public interface ITraversedProperty -{ - public ITraversedProperty Parent { get; } - public IEntityPropertyDefinition Property { get; } - public int Depth { get; } - public string GetPath(); -} - -[DebuggerDisplay("Property = {Property.ElementName}, Parent = {Parent?.Property?.ElementName}, Depth = {Depth}")] -internal record TraversedProperty : ITraversedProperty +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed record TraversedProperty { private static readonly string ElementSeparator = "."; - public ITraversedProperty Parent { get; init; } - public IEntityPropertyDefinition Property { get; init; } + public TraversedProperty Parent { get; init; } + public PropertyDefinition Property { get; init; } public int Depth { get; init; } public string GetPath() @@ -34,7 +26,7 @@ public string GetPath() var pool = ArrayPool.Shared.Rent(Depth + 1); try { - ITraversedProperty current = this; + var current = this; for (var i = Depth; i >= 0; i--) { pool[i] = current.Property.ElementName; @@ -48,6 +40,9 @@ public string GetPath() ArrayPool.Shared.Return(pool); } } + + [DebuggerNonUserCode] + private string DebuggerDisplay => $"Property = {Property.ElementName}, Parent = {Parent?.Property?.ElementName}, Depth = {Depth}"; } public static class PropertyTraversalExtensions @@ -55,10 +50,10 @@ public static class PropertyTraversalExtensions private readonly record struct TraversalState { public HashSet SeenTypes { get; init; } - public IEnumerable Properties { get; init; } + public IEnumerable Properties { get; init; } } - public static IEnumerable TraverseProperties(this IEntityDefinition definition) + public static IEnumerable TraverseProperties(this EntityDefinition definition) { var stack = new Stack(); stack.Push(new TraversalState @@ -79,7 +74,7 @@ public static IEnumerable TraverseProperties(this IEntityDef yield return traversedProperty; var propertyType = traversedProperty.Property.PropertyInfo.PropertyType; - propertyType = propertyType.GetEnumerableItemTypeOrDefault(); + propertyType = propertyType.UnwrapEnumerableTypes(); if (EntityMapping.IsValidTypeToMap(propertyType) && !state.SeenTypes.Contains(propertyType)) { diff --git a/src/MongoFramework/Infrastructure/Serialization/TypeDiscoverySerializer.cs b/src/MongoFramework/Infrastructure/Serialization/TypeDiscoverySerializer.cs index bcce74e3..fc43b6ea 100644 --- a/src/MongoFramework/Infrastructure/Serialization/TypeDiscoverySerializer.cs +++ b/src/MongoFramework/Infrastructure/Serialization/TypeDiscoverySerializer.cs @@ -178,7 +178,6 @@ private IBsonSerializer GetRealSerializer(Type type) { //Force the type to be processed by the Entity Mapper EntityMapping.TryRegisterType(type, out _); - var classMap = BsonClassMap.LookupClassMap(type); var serializerType = typeof(BsonClassMapSerializer<>).MakeGenericType(type); var serializer = (IBsonSerializer)Activator.CreateInstance(serializerType, classMap); diff --git a/src/MongoFramework/Linq/LinqExtensions.cs b/src/MongoFramework/Linq/LinqExtensions.cs index 3f5b5d77..52f94b76 100644 --- a/src/MongoFramework/Linq/LinqExtensions.cs +++ b/src/MongoFramework/Linq/LinqExtensions.cs @@ -31,7 +31,7 @@ public static IQueryable WhereIdMatches(this IQueryable WherePropertyMatches(this IQueryable queryable, IEntityPropertyDefinition property, IEnumerable values) where TEntity : class + public static IQueryable WherePropertyMatches(this IQueryable queryable, PropertyDefinition property, IEnumerable values) where TEntity : class { //The cast allows for handling identifiers generically as "IEnumerable". Without the Cast call, we can't handle ObjectId etc. var castMethod = typeof(Enumerable).GetMethod("Cast", BindingFlags.Public | BindingFlags.Static); diff --git a/src/MongoFramework/MappingBuilder.cs b/src/MongoFramework/MappingBuilder.cs new file mode 100644 index 00000000..0d649a61 --- /dev/null +++ b/src/MongoFramework/MappingBuilder.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using MongoFramework.Infrastructure.Mapping; + +namespace MongoFramework; + +public class MappingBuilder +{ + private readonly IEnumerable mappingConventions; + private readonly List builders = new(); + + public IReadOnlyList Definitions => builders; + + public MappingBuilder(IEnumerable mappingProcessors) + { + mappingConventions = mappingProcessors; + } + + private bool TryGetBuilder(Type entityType, out EntityDefinitionBuilder builder) + { + builder = builders.Find(b => b.EntityType == entityType); + return builder != null; + } + + private void UpdateBuilder(Type entityType, EntityDefinitionBuilder builder) + { + var index = builders.FindIndex(b => b.EntityType == entityType); + builders[index] = builder; + } + + private void ApplyMappingConventions(EntityDefinitionBuilder definitionBuilder) + { + foreach (var processor in mappingConventions) + { + processor.ApplyMapping(definitionBuilder); + } + } + + public EntityDefinitionBuilder Entity(Type entityType) + { + if (!TryGetBuilder(entityType, out var definitionBuilder)) + { + definitionBuilder = new EntityDefinitionBuilder(entityType, this); + builders.Add(definitionBuilder); + ApplyMappingConventions(definitionBuilder); + } + + return definitionBuilder; + } + + public EntityDefinitionBuilder Entity() + { + if (!TryGetBuilder(typeof(TEntity), out var definitionBuilder)) + { + definitionBuilder = new EntityDefinitionBuilder(this); + builders.Add(definitionBuilder); + ApplyMappingConventions(definitionBuilder); + } + + //Allow upgrading from non-generic entity definition + if (definitionBuilder is not EntityDefinitionBuilder) + { + definitionBuilder = EntityDefinitionBuilder.CreateFrom(definitionBuilder); + UpdateBuilder(typeof(TEntity), definitionBuilder); + } + + return definitionBuilder as EntityDefinitionBuilder; + } +} diff --git a/src/MongoFramework/MongoDbBucketSet.cs b/src/MongoFramework/MongoDbBucketSet.cs index ce2d47eb..32a6ce93 100644 --- a/src/MongoFramework/MongoDbBucketSet.cs +++ b/src/MongoFramework/MongoDbBucketSet.cs @@ -19,7 +19,7 @@ public class MongoDbBucketSet : IMongoDbBucketSet ContextMappingLocks = new(); + private class ContextMappingLock + { + public bool HasCompleted { get; set; } + } + public IMongoDbConnection Connection { get; } public EntityEntryContainer ChangeTracker { get; } = new EntityEntryContainer(); @@ -24,8 +33,32 @@ public MongoDbContext(IMongoDbConnection connection) { Connection = connection; InitialiseDbSets(); + ConfigureMapping(); } + /// + /// Triggers the virtual method exactly once for + /// this context type for the lifetime of the application. + /// + private void ConfigureMapping() + { + var contextType = GetType(); + var contextMappingLock = ContextMappingLocks.GetOrAdd(contextType, static _ => new()); + if (!contextMappingLock.HasCompleted) + { + lock (contextMappingLock) + { + if (!contextMappingLock.HasCompleted) + { + EntityMapping.RegisterMapping(OnConfigureMapping); + contextMappingLock.HasCompleted = true; + } + } + } + } + + protected virtual void OnConfigureMapping(MappingBuilder mappingBuilder) { } + private void InitialiseDbSets() { var properties = DbSetInitializer.GetDbSetProperties(this); diff --git a/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs b/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs index b3288ba2..616115cf 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs @@ -208,9 +208,10 @@ public void AppliesTenantConstraint() { var indexModel = IndexModelBuilder.BuildModel(); - var indexBsonDocument = indexModel.First().Keys.Render(null, null).ToString(); + var indexBsonDocument = indexModel.First().Keys.Render(null, null); - Assert.AreEqual("{ \"TenantId\" : 1, \"UniqueIndex\" : 1 }", indexBsonDocument); + Assert.AreEqual(1, indexBsonDocument["TenantId"].AsInt32); + Assert.AreEqual(1, indexBsonDocument["UniqueIndex"].AsInt32); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs index 24cfc12e..721e5bbf 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs @@ -57,7 +57,7 @@ public void GetInheritedPropertiesTakesBaseProperties() var definition = EntityMapping.RegisterType(typeof(OverridePropertyGrandChildModel)); var inheritedProperties = definition.GetInheritedProperties().ToArray(); Assert.AreEqual(1, inheritedProperties.Length); - Assert.AreEqual(typeof(OverridePropertyBaseModel), inheritedProperties[0].EntityDefinition.EntityType); + Assert.AreEqual(typeof(OverridePropertyBaseModel), inheritedProperties[0].PropertyInfo.DeclaringType); } [TestMethod] public void GetAllPropertiesTakesBaseProperties() @@ -65,7 +65,7 @@ public void GetAllPropertiesTakesBaseProperties() var definition = EntityMapping.RegisterType(typeof(OverridePropertyChildModel)); var allProperties = definition.GetAllProperties().ToArray(); Assert.AreEqual(1, allProperties.Length); - Assert.AreEqual(typeof(OverridePropertyBaseModel), allProperties[0].EntityDefinition.EntityType); + Assert.AreEqual(typeof(OverridePropertyBaseModel), allProperties[0].PropertyInfo.DeclaringType); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs index ce3601e3..e1aa25c9 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs @@ -2,7 +2,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using MongoDB.Bson.Serialization; using MongoFramework.Attributes; using MongoFramework.Infrastructure.Mapping; using MongoFramework.Infrastructure.Mapping.Processors; @@ -15,20 +14,11 @@ public class MappingAdapterProcessorTests : MappingTestBase public class AdapterTestModelMappingAdapter : IMappingProcessor { - public void ApplyMapping(IEntityDefinition definition) + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { - definition.CollectionName = "Custom"; - - var definitionIndexes = definition.Indexes.ToList(); - - definitionIndexes.Add(new EntityIndexDefinition - { - Property = definition.GetProperty("UserName"), - IsUnique = true, - SortOrder = IndexSortOrder.Ascending - }); - - definition.Indexes = definitionIndexes; + definitionBuilder + .ToCollection("Custom") + .HasIndex(new[] { "UserName" }, b => b.IsUnique()); } } @@ -59,7 +49,7 @@ public AdapterTestModelMappingAdapterConstructor(string test) } - public void ApplyMapping(IEntityDefinition definition) + public void ApplyMapping(EntityDefinitionBuilder definitionBuilder) { throw new NotImplementedException(); } @@ -78,7 +68,7 @@ public void AdapterRequiresIMappingProcessor() EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); EntityMapping.AddMappingProcessor(new EntityIdProcessor()); EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); - Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelNoInterface))); + Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelNoInterface))); } [TestMethod] @@ -102,9 +92,6 @@ public void AdapterOverridesAttributes() Assert.AreEqual("Custom", definition.CollectionName); Assert.AreEqual(1, definition.Indexes.Count()); - - - } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs new file mode 100644 index 00000000..04945e20 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoFramework.Infrastructure.Mapping; +using MongoFramework.Infrastructure.Mapping.Processors; + +namespace MongoFramework.Tests.Infrastructure.Mapping.Processors +{ + [TestClass] + public class SkipMappingProcessorTests : MappingTestBase + { + [NotMapped] + public class SkippedMappingModel + { + } + + public class DefaultModel + { + } + + [TestMethod] + public void ModelSkippedWithAttribute() + { + EntityMapping.AddMappingProcessor(new SkipMappingProcessor()); + + var exception = Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(SkippedMappingModel))); + Assert.IsTrue(exception.Message.Contains("was skipped")); + } + + [TestMethod] + public void ModelNotSkipped() + { + EntityMapping.AddMappingProcessor(new SkipMappingProcessor()); + + var definition = EntityMapping.RegisterType(typeof(DefaultModel)); + Assert.IsNotNull(definition); + } + } +} diff --git a/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs b/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs new file mode 100644 index 00000000..21eca904 --- /dev/null +++ b/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoFramework.Infrastructure.Mapping; +using MongoFramework.Tests.Infrastructure.Mapping; + +namespace MongoFramework.Tests; + +[TestClass] +public class MappingBuilderExtensionTests : MappingTestBase +{ + public class TestModelBase + { + public string Id { get; set; } + public string Name { get; set; } + public IEnumerable ManyOfThem { get; set; } + } + + public class TestModel : TestModelBase + { + public Dictionary ExtraElements { get; set; } + public string OtherName { get; set; } + public int SomethingIndexable { get; set; } + public NestedModelBase OneOfThem { get; set; } + } + + public class NestedModelBase + { + public string Description { get; set; } + } + + public class NestedModel : NestedModelBase + { + public string Address { get; set; } + public int AnotherThingIndexable { get; set; } + } + + private static void SetupMapping(Action builder) + { + var mappingBuilder = new MappingBuilder(Array.Empty()); + builder(mappingBuilder); + EntityMapping.RegisterMapping(mappingBuilder); + } + private static EntityDefinition GetDefinition() + { + return EntityMapping.GetOrCreateDefinition(typeof(TEntity)); + } + + [TestMethod] + public void HasKey_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey("Id", b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator)); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModelBase).GetProperty("Id"), entityDefinition.Key.Property.PropertyInfo); + } + + [TestMethod] + public void HasProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasProperty("OtherName"); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("OtherName"), entityDefinition.Properties[0].PropertyInfo); + } + + [TestMethod] + public void HasIndex_SingleProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(new[] { "SomethingIndexable" }, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinition = entityDefinition.Indexes[0].IndexPaths[0]; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinition.Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinition.IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinition.SortOrder); + } + + [TestMethod] + public void HasIndex_MultiProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(new[] { "SomethingIndexable", "OtherName" }, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .HasType(IndexType.Standard, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + } + + [TestMethod] + public void HasIndex_NestedProperties_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(new[] { "SomethingIndexable", "OtherName", "OneOfThem.Description" }, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .HasType(IndexType.Standard, IndexType.Text, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + Assert.AreEqual("OneOfThem.Description", indexPathDefinitions[2].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[2].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[2].SortOrder); + } + + [TestMethod] + public void HasIndex_NestedPropertyThroughEnumerable_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(new[] { "SomethingIndexable", "OtherName", "OneOfThem.Description", "ManyOfThem.AnotherThingIndexable" }, b => + { + b.HasName("MyIndexable") + .IsDescending(true, false, false, true) + .HasType(IndexType.Standard, IndexType.Text, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + Assert.AreEqual("OneOfThem.Description", indexPathDefinitions[2].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[2].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[2].SortOrder); + Assert.AreEqual("ManyOfThem.AnotherThingIndexable", indexPathDefinitions[3].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[3].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[3].SortOrder); + } + + [TestMethod] + public void HasExtraElements_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasExtraElements("ExtraElements"); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("ExtraElements"), entityDefinition.ExtraElements.Property.PropertyInfo); + Assert.IsFalse(entityDefinition.ExtraElements.IgnoreExtraElements); + } + + [TestMethod] + public void Ignore_RemovesAllPropertyReferences_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey(m => m.Id, b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator)) + .WithDerivedEntity(b => + { + b.HasProperty(m => m.OtherName) + .HasExtraElements(m => m.ExtraElements) + .HasIndex(m => m.SomethingIndexable) + .HasIndex(m => new + { + m.SomethingIndexable, + m.OneOfThem.Description + }); + }); + + mappingBuilder.Entity() + .Ignore("Id") + .WithDerivedEntity(typeof(TestModel), b => + { + b.Ignore("OtherName") + .Ignore("ExtraElements") + .Ignore("SomethingIndexable"); + }); + }); + + var testModelDefinition = GetDefinition(); + Assert.AreEqual(1, testModelDefinition.Properties.Count); + Assert.AreEqual(typeof(TestModel).GetProperty("OneOfThem"), testModelDefinition.Properties[0].PropertyInfo); + Assert.IsTrue(testModelDefinition.ExtraElements.IgnoreExtraElements); + Assert.AreEqual(0, testModelDefinition.Indexes.Count); + + var testModelBaseDefinition = GetDefinition(); + Assert.IsNull(testModelBaseDefinition.Key); + Assert.AreEqual(0, testModelBaseDefinition.Properties.Count); + } +} diff --git a/tests/MongoFramework.Tests/MappingBuilderTests.cs b/tests/MongoFramework.Tests/MappingBuilderTests.cs new file mode 100644 index 00000000..0001ea8e --- /dev/null +++ b/tests/MongoFramework.Tests/MappingBuilderTests.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoFramework.Infrastructure.Mapping; +using MongoFramework.Tests.Infrastructure.Mapping; + +namespace MongoFramework.Tests; + +[TestClass] +public class MappingBuilderTests : MappingTestBase +{ + public class TestModelBase + { + public string Id { get; set; } + public string Name { get; set; } + public IEnumerable ManyOfThem { get; set; } + } + + public class TestModel : TestModelBase + { + public Dictionary ExtraElements { get; set; } + public string OtherName { get; set; } + public int SomethingIndexable { get; set; } + public NestedModelBase OneOfThem { get; set; } + } + + public class NestedModelBase + { + public string Description { get; set; } + } + + public class NestedModel : NestedModelBase + { + public string Address { get; set; } + public int AnotherThingIndexable { get; set; } + } + + private static void SetupMapping(Action builder) + { + var mappingBuilder = new MappingBuilder(Array.Empty()); + builder(mappingBuilder); + EntityMapping.RegisterMapping(mappingBuilder); + } + private static EntityDefinition GetDefinition() + { + return EntityMapping.GetOrCreateDefinition(typeof(TEntity)); + } + + [TestMethod] + public void ToCollection_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .ToCollection("TestCollection"); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual("TestCollection", entityDefinition.CollectionName); + } + + [TestMethod] + public void HasKey_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey(m => m.Id, b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator)); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModelBase).GetProperty("Id"), entityDefinition.Key.Property.PropertyInfo); + } + + [TestMethod] + public void HasKey_WithProperty_ElementName() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey( + m => m.Id, + b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator).WithProperty(p => p.HasElementName("_id")) + ); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModelBase).GetProperty("Id"), entityDefinition.Key.Property.PropertyInfo); + Assert.AreEqual("_id", entityDefinition.GetIdProperty().ElementName); + } + + [TestMethod] + public void HasKey_KeyMustBeDefinedOnDeclaredType() + { + Assert.ThrowsException(() => + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey( + m => m.Id, + b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator) + ); + }) + ); + } + + [TestMethod] + public void HasProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasProperty(m => m.OtherName); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("OtherName"), entityDefinition.Properties[0].PropertyInfo); + } + + [TestMethod] + public void HasProperty_HasElementName_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasProperty(m => m.OtherName, b => b.HasElementName("Name2")); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("OtherName"), entityDefinition.Properties[0].PropertyInfo); + Assert.AreEqual("Name2", entityDefinition.Properties[0].ElementName); + } + + [TestMethod] + public void HasProperty_PropertyMustBeDefinedOnDeclaredType() + { + Assert.ThrowsException(() => + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasProperty(m => m.Name); + }) + ); + } + + [TestMethod] + public void HasIndex_SingleProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => m.SomethingIndexable, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinition = entityDefinition.Indexes[0].IndexPaths[0]; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinition.Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinition.IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinition.SortOrder); + } + + [TestMethod] + public void HasIndex_MultiProperty_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => new + { + m.SomethingIndexable, + m.OtherName + }, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .HasType(IndexType.Standard, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + } + + [TestMethod] + public void HasIndex_NestedProperties_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => new + { + m.SomethingIndexable, + m.OtherName, + m.OneOfThem.Description + }, b => + { + b.HasName("MyIndexable") + .IsDescending(true) + .HasType(IndexType.Standard, IndexType.Text, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + Assert.AreEqual("OneOfThem.Description", indexPathDefinitions[2].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[2].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[2].SortOrder); + } + + [TestMethod] + public void HasIndex_NestedPropertyThroughEnumerable_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => new + { + m.SomethingIndexable, + m.OtherName, + m.OneOfThem.Description, + m.ManyOfThem.First().AnotherThingIndexable + }, b => + { + b.HasName("MyIndexable") + .IsDescending(true, false, false, true) + .HasType(IndexType.Standard, IndexType.Text, IndexType.Text) + .IsUnique(); + }); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("MyIndexable", entityDefinition.Indexes[0].IndexName); + Assert.IsTrue(entityDefinition.Indexes[0].IsUnique); + Assert.AreEqual("SomethingIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[0].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[0].SortOrder); + Assert.AreEqual("OtherName", indexPathDefinitions[1].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[1].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[1].SortOrder); + Assert.AreEqual("OneOfThem.Description", indexPathDefinitions[2].Path); + Assert.AreEqual(IndexType.Text, indexPathDefinitions[2].IndexType); + Assert.AreEqual(IndexSortOrder.Ascending, indexPathDefinitions[2].SortOrder); + Assert.AreEqual("ManyOfThem.AnotherThingIndexable", indexPathDefinitions[3].Path); + Assert.AreEqual(IndexType.Standard, indexPathDefinitions[3].IndexType); + Assert.AreEqual(IndexSortOrder.Descending, indexPathDefinitions[3].SortOrder); + } + + [TestMethod] + public void HasIndex_IndexPropertiesAreMappedAsProperties_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => new + { + m.SomethingIndexable, + m.OneOfThem.Description, + m.ManyOfThem.First().AnotherThingIndexable + }); + }); + + var testModelDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("SomethingIndexable"), testModelDefinition.GetProperty("SomethingIndexable").PropertyInfo); + Assert.AreEqual(typeof(TestModel).GetProperty("OneOfThem"), testModelDefinition.GetProperty("OneOfThem").PropertyInfo); + Assert.AreEqual(typeof(TestModelBase).GetProperty("ManyOfThem"), testModelDefinition.GetProperty("ManyOfThem").PropertyInfo); + + var nestedModelDefinition = GetDefinition(); + Assert.AreEqual(typeof(NestedModelBase).GetProperty("Description"), nestedModelDefinition.GetProperty("Description").PropertyInfo); + Assert.AreEqual(typeof(NestedModel).GetProperty("AnotherThingIndexable"), nestedModelDefinition.GetProperty("AnotherThingIndexable").PropertyInfo); + } + + [TestMethod] + public void HasIndex_ElementNamesApplyToIndexPath_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasIndex(m => new + { + m.SomethingIndexable, + m.OneOfThem.Description, + m.ManyOfThem.First().AnotherThingIndexable + }) + .HasProperty(m => m.SomethingIndexable, b => b.HasElementName("QuiteIndexable")); + + mappingBuilder.Entity() + .HasProperty(m => m.Description, b => b.HasElementName("VeryDescriptive")); + mappingBuilder.Entity() + .HasProperty(m => m.ManyOfThem, b => b.HasElementName("SoManyOfThem")); + }); + + var entityDefinition = GetDefinition(); + var indexPathDefinitions = entityDefinition.Indexes[0].IndexPaths; + Assert.AreEqual("QuiteIndexable", indexPathDefinitions[0].Path); + Assert.AreEqual("OneOfThem.VeryDescriptive", indexPathDefinitions[1].Path); + Assert.AreEqual("SoManyOfThem.AnotherThingIndexable", indexPathDefinitions[2].Path); + } + + [TestMethod] + public void HasExtraElements_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasExtraElements(m => m.ExtraElements); + }); + + var entityDefinition = GetDefinition(); + Assert.AreEqual(typeof(TestModel).GetProperty("ExtraElements"), entityDefinition.ExtraElements.Property.PropertyInfo); + Assert.IsFalse(entityDefinition.ExtraElements.IgnoreExtraElements); + } + + [TestMethod] + public void Ignore_RemovesAllPropertyReferences_Success() + { + SetupMapping(mappingBuilder => + { + mappingBuilder.Entity() + .HasKey(m => m.Id, b => b.HasKeyGenerator(EntityKeyGenerators.StringKeyGenerator)) + .WithDerivedEntity(b => + { + b.HasProperty(m => m.OtherName) + .HasExtraElements(m => m.ExtraElements) + .HasIndex(m => m.SomethingIndexable) + .HasIndex(m => new + { + m.SomethingIndexable, + m.OneOfThem.Description + }); + }); + + mappingBuilder.Entity() + .Ignore(m => m.Id) + .WithDerivedEntity(b => + { + b.Ignore(m => m.OtherName) + .Ignore(m => m.ExtraElements) + .Ignore(m => m.SomethingIndexable); + }); + }); + + var testModelDefinition = GetDefinition(); + Assert.AreEqual(1, testModelDefinition.Properties.Count); + Assert.AreEqual(typeof(TestModel).GetProperty("OneOfThem"), testModelDefinition.Properties[0].PropertyInfo); + Assert.IsTrue(testModelDefinition.ExtraElements.IgnoreExtraElements); + Assert.AreEqual(0, testModelDefinition.Indexes.Count); + + var testModelBaseDefinition = GetDefinition(); + Assert.IsNull(testModelBaseDefinition.Key); + Assert.AreEqual(0, testModelBaseDefinition.Properties.Count); + } +} diff --git a/tests/MongoFramework.Tests/MongoDbContextTests.cs b/tests/MongoFramework.Tests/MongoDbContextTests.cs index d6bf2438..3407731e 100644 --- a/tests/MongoFramework.Tests/MongoDbContextTests.cs +++ b/tests/MongoFramework.Tests/MongoDbContextTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using MongoFramework.Attributes; @@ -31,6 +32,18 @@ public MongoDbContextTestContext(IMongoDbConnection connection) : base(connectio public MongoDbBucketSet DbBucketSet { get; set; } } + class ConfigureMappingTestContext : MongoDbContext + { + public ConfigureMappingTestContext(IMongoDbConnection connection) : base(connection) { } + + public static int NumberOfTimesConfigured; + + protected override void OnConfigureMapping(MappingBuilder mappingBuilder) + { + Interlocked.Increment(ref NumberOfTimesConfigured); + } + } + [TestMethod] public void ContextCreatedWithOptions() { @@ -62,6 +75,14 @@ public void DbSetsHaveOptionsApplied() } } + [TestMethod] + public void MappingIsConfiguredOnlyOnce() + { + using var contextA = new ConfigureMappingTestContext(TestConfiguration.GetConnection()); + using var contextB = new ConfigureMappingTestContext(TestConfiguration.GetConnection()); + Assert.AreEqual(1, ConfigureMappingTestContext.NumberOfTimesConfigured); + } + [TestMethod] public void ContextSavesDbSets() { diff --git a/tests/MongoFramework.Tests/MongoDbDriverHelper.cs b/tests/MongoFramework.Tests/MongoDbDriverHelper.cs index c542a143..4b88677b 100644 --- a/tests/MongoFramework.Tests/MongoDbDriverHelper.cs +++ b/tests/MongoFramework.Tests/MongoDbDriverHelper.cs @@ -38,6 +38,14 @@ public static void ResetDriver() { discriminators.Clear(); } + + var serializerRegistryField = typeof(BsonSerializer).GetField("__serializerRegistry", BindingFlags.NonPublic | BindingFlags.Static); + if (serializerRegistryField.GetValue(null) is BsonSerializerRegistry registry) + { + var cacheField = typeof(BsonSerializerRegistry).GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance); + var registryCache = cacheField.GetValue(registry) as ConcurrentDictionary; + registryCache.Clear(); + } } } } diff --git a/tests/MongoFramework.Tests/TestBase.cs b/tests/MongoFramework.Tests/TestBase.cs index bf5cb512..7e32cea7 100644 --- a/tests/MongoFramework.Tests/TestBase.cs +++ b/tests/MongoFramework.Tests/TestBase.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using MongoDB.Driver; +using MongoFramework.Infrastructure; using MongoFramework.Infrastructure.Indexing; using MongoFramework.Infrastructure.Mapping; using MongoFramework.Infrastructure.Serialization; @@ -15,10 +16,12 @@ protected static void ResetMongoDb() EntityMapping.RemoveAllDefinitions(); EntityMapping.RemoveAllMappingProcessors(); - EntityMapping.AddMappingProcessors(DefaultProcessors.CreateProcessors()); + EntityMapping.AddMappingProcessors(DefaultMappingProcessors.Processors); TypeDiscovery.ClearCache(); EntityIndexWriter.ClearCache(); + + DriverAbstractionRules.ApplyRules(); } protected static void ClearDatabase()