Skip to content

Commit

Permalink
feat: Auto generate nested maps (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
skolmer authored Aug 14, 2024
1 parent dd80e94 commit 93a1d87
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 55 deletions.
60 changes: 57 additions & 3 deletions src/Gridify/GridifyMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Gridify.Reflection;
using Gridify.Syntax;

Expand Down Expand Up @@ -81,9 +82,19 @@ private void GenerateMappingsRecursive(Type type, string prefix, ushort maxNesti
var propertyName = char.ToLowerInvariant(item.Name[0]) + item.Name.Substring(1); // camel-case name
var fullName = string.IsNullOrEmpty(prefix) ? propertyName : $"{prefix}.{propertyName}";

if (item.PropertyType.IsComplexTypeCollection(out var genericType))
{
if (currentDepth >= maxNestingDepth)
{
continue;
}

GenerateMappingsRecursive(genericType!, fullName, maxNestingDepth, (ushort)(currentDepth + 1));
continue;
}

// Skip classes if nestingLevel is exceeded
if (item.PropertyType.IsClass && item.PropertyType != typeof(string) && !item.PropertyType.IsSimpleTypeCollection(out _)
&& !item.PropertyType.IsCollection())
if (item.PropertyType.IsClass && item.PropertyType != typeof(string) && !item.PropertyType.IsSimpleTypeCollection(out _))
{
if (currentDepth >= maxNestingDepth)
{
Expand Down Expand Up @@ -166,6 +177,11 @@ public IGridifyMapper<T> RemoveMap(IGMap<T> gMap)
return this;
}

public void ClearMappings()
{
_mappings.Clear();
}

public bool HasMap(string from)
{
return Configuration.CaseSensitive
Expand Down Expand Up @@ -217,7 +233,14 @@ internal static Expression<Func<T, object>> CreateExpression(string from)
// Param_x =>
var parameter = Expression.Parameter(typeof(T), "__" + typeof(T).Name);
// Param_x.Name, Param_x.yyy.zz.xx
var mapProperty = from.Split('.').Aggregate<string, Expression>(parameter, Expression.Property);
var mapProperty = from.Split('.').Aggregate<string, Expression>(parameter, CreatePropertyAccessExrpression);

if (mapProperty is MethodCallExpression methodCallExpression
&& methodCallExpression.Method.Name.Equals("Select", StringComparison.InvariantCultureIgnoreCase)
&& methodCallExpression.Arguments.Last() is LambdaExpression)
{
return Expression.Lambda<Func<T, object>>(methodCallExpression, parameter);
}

if (!mapProperty.Type.IsSimpleTypeCollection(out var genericType))
{
Expand All @@ -235,5 +258,36 @@ internal static Expression<Func<T, object>> CreateExpression(string from)
return Expression.Lambda<Func<T, object>>(body, parameter);
}

internal static Expression CreatePropertyAccessExrpression(Expression expression, string propertyName)
{
Type? itemType;

if (
((expression is MemberExpression memberExpression && memberExpression.Member is PropertyInfo propertyInfo && propertyInfo.PropertyType.IsComplexTypeCollection(out itemType)) ||
(expression is MethodCallExpression methodCallExpression && methodCallExpression.Type.IsComplexTypeCollection(out itemType)))
&& itemType is not null
)
{
var selectFunction = "Select";
var predicateParameter = Expression.Parameter(itemType);
var propertyType = itemType.GetProperties().FirstOrDefault(p => p.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase))?.PropertyType ?? throw new GridifyMapperException($"Property '{propertyName}' not found.");

if (propertyType.IsComplexTypeCollection(out var propItemType) && propItemType is not null)
{
selectFunction = "SelectMany";
propertyType = propItemType;
}

var predicate = Expression.Lambda(Expression.Property(predicateParameter, propertyName), predicateParameter);
var selectMethod = typeof(Enumerable).GetMethods().First(m => m.Name.Equals(selectFunction, StringComparison.InvariantCulture)).MakeGenericMethod([itemType, propertyType]);

var selectExpression = Expression.Call(selectMethod, expression, predicate);

return selectExpression;
}

return Expression.Property(expression, propertyName);
}


}
1 change: 1 addition & 0 deletions src/Gridify/IGridifyMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ IGridifyMapper<T> GenerateMappings(ushort maxNestingDepth)
bool HasMap(string key);
public GridifyMapperConfiguration Configuration { get; }
IEnumerable<IGMap<T>> GetCurrentMaps();
void ClearMappings();
}
12 changes: 0 additions & 12 deletions src/Gridify/Reflection/CollectionTypeHelper.cs

This file was deleted.

47 changes: 37 additions & 10 deletions src/Gridify/Reflection/SimpleTypeHelper.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Gridify.Reflection;

public static class SimpleTypeHelper
{
public static bool IsSimpleTypeCollection(this Type type, out Type? itemType)
public static bool IsCollection(this Type type, out Type? itemType)
{
itemType = null;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
Type genericType = type;
if ((type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || (type != typeof(string) && type.GetInterfaces().Any(i => {
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
genericType = i;
return true;
}
return false;
})))
{
var arguments = type.GetGenericArguments();
if (arguments.Length != 1 || !IsSimpleType(arguments[0])) return false;
var arguments = genericType.GetGenericArguments();
if (arguments.Length == 1)
{
itemType = arguments[0];
return true;
}
}
return false;
}

itemType = arguments[0];
public static bool IsSimpleTypeCollection(this Type type, out Type? itemType)
{
itemType = null;
if (IsCollection(type, out var collectionType) && IsSimpleType(collectionType!))
{
itemType = collectionType;
return true;
}
if (!type.IsArray) return false;
return false;
}

var elementType = type.GetElementType();
if (elementType == null || !IsSimpleType(elementType)) return false;
itemType = elementType;
return true;
public static bool IsComplexTypeCollection(this Type type, out Type? itemType)
{
itemType = null;
if (IsCollection(type, out var collectionType) && !IsSimpleType(collectionType!))
{
itemType = collectionType;
return true;
}
return false;
}

public static bool IsSimpleType(this Type type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class GridifyEntityFrameworkTests
[Fact]
public void ApplyFiltering_GeneratedSqlShouldNotCreateParameterizedQuery_WhenCompatibilityLayerIsDisable_SqlServerProvider()
{
GridifyGlobalConfiguration.DisableEntityFrameworkCompatibilityLayer();
var actual = _dbContext.Users.ApplyFiltering("name = vahid").ToQueryString();
var expected = _dbContext.Users.Where(q => q.Name == "vahid").ToQueryString();

Expand Down Expand Up @@ -170,4 +171,17 @@ public void DictionaryMappingWithWhereStatement()
// assert
Assert.Equal(expected, actual.Replace(" @__Value_0", " @__user_Name_0"));
}

[Fact]
public void ApplyFiltering_NestedProperty()
{
GridifyGlobalConfiguration.EnableEntityFrameworkCompatibilityLayer();
var gm = new GridifyMapper<User>(true, 3);

var name = "test";
var expected = _dbContext.Users.Where(q => q.Groups.Any(g => g.Users.Any(u => u.Name == name))).ToQueryString();
var actual = _dbContext.Users.ApplyFiltering("groups.users.name = test", gm).ToQueryString();

Assert.Equal(expected, actual.Replace("@__Value_0", "@__name_0"));
}
}
95 changes: 86 additions & 9 deletions test/Gridify.Tests/GridifyMapperShould.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Gridify.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Xunit;

namespace Gridify.Tests;
Expand All @@ -13,15 +14,91 @@ public class GridifyMapperShould
[Fact]
public void GenerateMappings()
{
_sut.ClearMappings();
_sut.GenerateMappings();

var props = typeof(TestClass).GetProperties()
.Where(q => !q.PropertyType.IsClass || q.PropertyType == typeof(string));
.Where(q => !q.PropertyType.IsComplexTypeCollection(out _) && (!q.PropertyType.IsClass || q.PropertyType == typeof(string)));

Assert.Equal(props.Count(), _sut.GetCurrentMaps().Count());
Assert.True(_sut.HasMap("Id"));
}

[Fact]
public void GenerateNestedMappings()
{
_sut.ClearMappings();
_sut.GenerateMappings(5);

var maps = _sut.GetCurrentMaps();

var testClass = new TestClass
{
Id = 1,
ChildClass = new TestClass
{
Id = 11,
Children = [
new TestClass {
Id = 111,
ChildClass = new TestClass {
Id = 1111
}
}
]
},
Children = [
new TestClass {
Id = 12,
ChildClass = new TestClass {
Id = 121,
Children = [
new TestClass {
Id = 1211,
ChildClass = new TestClass {
Id = 12111
}
}
],
},
Children = [
new TestClass {
Id = 122
}
],
},
new TestClass {
Id = 13,
ChildClass = new TestClass {
Id = 131,
Children = [
new TestClass {
Id = 1311,
ChildClass = new TestClass {
Id = 13111
}
}
],
},
Children = [
new TestClass {
Id = 132
}
]
}
]
};

var func = maps.First(m => m.From == "children.childClass.children.childClass.id").To.Compile();
var result = func.DynamicInvoke(testClass)!;
Assert.Equivalent(result, new List<int> { 12111, 13111 });

var func2 = maps.First(m => m.From == "childClass.children.childClass.id").To.Compile();
var result2 = func2.DynamicInvoke(testClass)!;
Assert.Equivalent(result2, new List<int> { 1111 });

}

[Fact]
public void CaseSensitivity()
{
Expand All @@ -48,14 +125,14 @@ public void RemoveMap()
_sut.RemoveMap(nameof(TestClass.Name));
Assert.Single(_sut.GetCurrentMaps());
}

[Fact]
public void GridifyMapperToStringShouldReturnFieldsList()
{
_sut.AddMap("name", q => q.Name);
_sut.AddMap("childDate", q => q.ChildClass!.MyDateTime);
var actual = _sut.ToString();

Assert.Equal("name,childDate", actual);
}

Expand All @@ -72,8 +149,8 @@ public void AddMap_DuplicateKey_ShouldThrowErrorIfOverrideIfExistsIsFalse()
//The thrown exception can be used for even more detailed assertions.
Assert.Equal("Duplicate Key. the 'test' key already exists", exception.Message);
}




}



}
Loading

0 comments on commit 93a1d87

Please sign in to comment.