Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto generate maps for navigation properties / class collections #199

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading