Skip to content

Commit

Permalink
Mapping Builder Support (#336)
Browse files Browse the repository at this point in the history
* First pass at the entity definition builder

* Support for property paths on indexes

* Add missing derived method for IgnoreExtraElements

* Fix null reference

* Remove array type

* The major changes to have functional fluent in

This is a rough first cut, it will need some work still. Compiles, probably won't work though.

* Fix stack overflow

* Fix empty string index name

* Fixed invalid extra elements

* Fix ID property detection

* Fix stack overflow

* Fix missing mapping

* Mapping attribute fixes

* Fix unnamed indexing grouping

* Updated mapping adapter test for new exception

* Re-add removed driver reset feature

* Re-apply driver abstraction rules after reset

* Updating definition types to records

* Simplifying the entity definitions

* Updated debugger display of entity definitions

* Updated debugger display for traversed property

* Hide debugger display properties from code coverage

* Removed unused variable

* Splitting EntityMapping into separate files

* Moving MappingBuilder to root

This mimics what is done for Entity Framework

* Clean up context mapping lock

* Minor cleaning of index processor

After taking another look, isn't really anything to clean. It is more complex than I'd like but it is perfectly serviceable.

* Move some EntityDefinitionBuilder methods to extensions

Simplifies the core entity builder and, if needed, an easier path to more overloads.

* Cleaning up the entity mapping builder

* Minor general refactor

* Whoops, missed a file

* Added overloads and support unary expression unwrapping

* Code fixes and other updates

* Main tests for mapping builder

* Updated runsettings

* Added extension tests

* Ensure mapping is completed only once

* Support skipped mapping

* Undo change to type discovery serializer
  • Loading branch information
Turnerj authored Feb 26, 2023
1 parent b2d0459 commit 8a5d02c
Show file tree
Hide file tree
Showing 45 changed files with 2,148 additions and 571 deletions.
2 changes: 1 addition & 1 deletion CodeCoverage.runsettings
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Format>cobertura</Format>
<Exclude>[MongoFramework.Tests]*</Exclude>
<Include>[MongoFramework]*,[MongoFramework.*]*</Include>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,DebuggerNonUserCode,DebuggerStepThrough</ExcludeByAttribute>
<UseSourceLink>true</UseSourceLink>
<SkipAutoProps>true</SkipAutoProps>
</Configuration>
Expand Down
33 changes: 14 additions & 19 deletions src/MongoFramework/Attributes/MappingAdapterAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
using System;
using MongoFramework.Infrastructure.Mapping;

namespace MongoFramework.Attributes
namespace MongoFramework.Attributes;

/// <summary>
/// Applies the specific <see cref="IMappingProcessor"/> on the entity.
/// Runs after attribute processing, so the adapter can override attributes.
/// Adapter type must have a parameterless constructor.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class MappingAdapterAttribute : Attribute
{
/// <summary>
/// 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
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class MappingAdapterAttribute : Attribute
{
/// <summary>
/// Gets the adapter type for the attached class
/// </summary>
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;
}
}
47 changes: 47 additions & 0 deletions src/MongoFramework/EntityDefinitionBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<EntityKeyBuilder> 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<EntityPropertyBuilder> builder = null)
=> definitionBuilder.HasProperty(GetPropertyInfo(definitionBuilder.EntityType, propertyName), builder);

public static EntityDefinitionBuilder HasIndex(this EntityDefinitionBuilder definitionBuilder, IEnumerable<string> propertyPaths, Action<EntityIndexBuilder> builder = null)
{
var properties = new List<IndexProperty>();
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<PropertyPath> properties, Action<EntityIndexBuilder> 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));

}
2 changes: 1 addition & 1 deletion src/MongoFramework/IHaveTenantId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
{
public interface IHaveTenantId
{
string TenantId { get; set; }
public string TenantId { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public class AddToBucketCommand<TGroup, TSubEntity> : IWriteCommand<EntityBucket
{
private TGroup Group { get; }
private TSubEntity SubEntity { get; }
private IEntityPropertyDefinition EntityTimeProperty { get; }
private PropertyDefinition EntityTimeProperty { get; }
private int BucketSize { get; }

public Type EntityType => typeof(EntityBucket<TGroup, TSubEntity>);

public AddToBucketCommand(TGroup group, TSubEntity subEntity, IEntityPropertyDefinition entityTimeProperty, int bucketSize)
public AddToBucketCommand(TGroup group, TSubEntity subEntity, PropertyDefinition entityTimeProperty, int bucketSize)
{
Group = group;
SubEntity = subEntity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ namespace MongoFramework.Infrastructure.Commands
{
public static class EntityDefinitionExtensions
{
public static FilterDefinition<TEntity> CreateIdFilterFromEntity<TEntity>(this IEntityDefinition definition, TEntity entity)
public static FilterDefinition<TEntity> CreateIdFilterFromEntity<TEntity>(this EntityDefinition definition, TEntity entity)
{
return Builders<TEntity>.Filter.Eq(definition.GetIdName(), definition.GetIdValue(entity));
}
public static FilterDefinition<TEntity> CreateIdFilter<TEntity>(this IEntityDefinition definition, object entityId, string tenantId = null)
public static FilterDefinition<TEntity> CreateIdFilter<TEntity>(this EntityDefinition definition, object entityId, string tenantId = null)
{
if (typeof(IHaveTenantId).IsAssignableFrom(typeof(TEntity)) && tenantId == null)
{
Expand Down
96 changes: 34 additions & 62 deletions src/MongoFramework/Infrastructure/Indexing/IndexModelBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<TEntity>
{
public static class IndexModelBuilder<TEntity>
public static IEnumerable<CreateIndexModel<TEntity>> BuildModel()
{
public static IEnumerable<CreateIndexModel<TEntity>> BuildModel()
{
var indexBuilder = Builders<TEntity>.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<IndexKeysDefinition<TEntity>>();
CreateIndexOptions<TEntity> indexOptions = default;
foreach (var index in indexGroup)
{
var indexModel = CreateIndexModel(index);
indexKeys.Add(indexModel.Keys);

if (indexOptions == null)
{
indexOptions = indexModel.Options;
}
}
var indexBuilder = Builders<TEntity>.IndexKeys;
var indexes = EntityMapping.GetOrCreateDefinition(typeof(TEntity)).Indexes;

var combinedKeyDefinition = indexBuilder.Combine(indexKeys);
yield return new CreateIndexModel<TEntity>(combinedKeyDefinition, indexOptions);
}
else
{
foreach (var index in indexGroup)
{
yield return CreateIndexModel(index);
}
}
}
}

private static CreateIndexModel<TEntity> CreateIndexModel(IEntityIndexDefinition indexDefinition)
foreach (var index in indexes)
{
var builder = Builders<TEntity>.IndexKeys;
IndexKeysDefinition<TEntity> keyModel;

if (indexDefinition.IndexType == IndexType.Text)
var indexKeyCount = index.IndexPaths.Count + (index.IsTenantExclusive ? 1 : 0);
var indexKeys = new IndexKeysDefinition<TEntity>[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<TEntity>.IndexKeys.Ascending(nameof(IHaveTenantId.TenantId));
}

return new CreateIndexModel<TEntity>(keyModel, new CreateIndexOptions
var combinedKeyDefinition = indexBuilder.Combine(indexKeys);
yield return new CreateIndexModel<TEntity>(combinedKeyDefinition, new CreateIndexOptions
{
Name = indexDefinition.IndexName,
Unique = indexDefinition.IsUnique,
Name = index.IndexName,
Unique = index.IsUnique,
Background = true
});
}
}

private static IndexKeysDefinition<TEntity> CreateIndexKey(IndexPathDefinition indexPathDefinition)
{
var builder = Builders<TEntity>.IndexKeys;
Func<FieldDefinition<TEntity>, IndexKeysDefinition<TEntity>> 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);
}
}
16 changes: 15 additions & 1 deletion src/MongoFramework/Infrastructure/Internal/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,21 @@ internal static class TypeExtensions
typeof(IReadOnlyCollection<>)
};

public static Type GetEnumerableItemTypeOrDefault(this Type type)
/// <summary>
/// Attempts to unwrap enumerable types (like <see cref="IEnumerable{T}"/>) from the current <paramref name="type"/>, returning the actual item type.
/// </summary>
/// <remarks>
/// Unwrapped types include:<br/>
/// - <see cref="Array"/><br/>
/// - <see cref="IEnumerable{T}"/><br/>
/// - <see cref="IList{T}"/><br/>
/// - <see cref="ICollection{T}"/><br/>
/// - <see cref="IReadOnlyList{T}"/><br/>
/// - <see cref="IReadOnlyCollection{T}"/>
/// </remarks>
/// <param name="type"></param>
/// <returns></returns>
public static Type UnwrapEnumerableTypes(this Type type)
{
if (type.IsArray)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace MongoFramework.Infrastructure.Linq
public class MongoFrameworkQueryProvider<TEntity> : IMongoFrameworkQueryProvider<TEntity> where TEntity : class
{
public IMongoDbConnection Connection { get; }
private IEntityDefinition EntityDefinition { get; }
private EntityDefinition EntityDefinition { get; }

private BsonDocument PreStage { get; }

Expand Down
21 changes: 0 additions & 21 deletions src/MongoFramework/Infrastructure/Mapping/DefaultMappingPack.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<IMappingProcessor> Processors = new IMappingProcessor[]
{
new SkipMappingProcessor(),
new CollectionNameProcessor(),
new HierarchyProcessor(),
new PropertyMappingProcessor(),
new EntityIdProcessor(),
new NestedTypeProcessor(),
new ExtraElementsProcessor(),
new BsonKnownTypesProcessor(),
new IndexProcessor(),
new MappingAdapterProcessor()
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal static class DriverMappingInterop
/// Registers the <paramref name="definition"/> as a <see cref="BsonClassMap"/> with all appropriate properties configured.
/// </summary>
/// <param name="definition"></param>
public static void RegisterDefinition(IEntityDefinition definition)
public static void RegisterDefinition(EntityDefinition definition)
{
var classMap = new BsonClassMap(definition.EntityType);

Expand Down
Loading

0 comments on commit 8a5d02c

Please sign in to comment.