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