Skip to content

Add .EFCoreProjectToType() - start of combine EF and mapping config in Projection mapping #787

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

Open
wants to merge 1 commit into
base: development
Choose a base branch
from
Open
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
11 changes: 11 additions & 0 deletions src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace Mapster.Enums
{
public enum ProjectToTypeAutoMapping
{
AllTypes = 0,
WithoutCollections = 1,
OnlyPrimitiveTypes = 2,
}
}
49 changes: 49 additions & 0 deletions src/Mapster.Core/Utils/ProjectToTypeVisitors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Mapster.Utils
{
public sealed class TopLevelMemberNameVisitor : ExpressionVisitor
{
public string? MemeberName { get; private set; }

public override Expression Visit(Expression node)

Check warning on line 10 in src/Mapster.Core/Utils/ProjectToTypeVisitors.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of type of parameter 'node' doesn't match overridden member (possibly because of nullability attributes).
{
if (node == null)
return null;

Check warning on line 13 in src/Mapster.Core/Utils/ProjectToTypeVisitors.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
switch (node.NodeType)
{
case ExpressionType.MemberAccess:
{
if (string.IsNullOrEmpty(MemeberName))
MemeberName = ((MemberExpression)node).Member.Name;

return base.Visit(node);
}
}

return base.Visit(node);
}
}

public sealed class QuoteVisitor : ExpressionVisitor
{
public List<UnaryExpression> Quotes { get; private set; } = new();

public override Expression Visit(Expression node)

Check warning on line 33 in src/Mapster.Core/Utils/ProjectToTypeVisitors.cs

View workflow job for this annotation

GitHub Actions / build

Nullability of type of parameter 'node' doesn't match overridden member (possibly because of nullability attributes).
{
if (node == null)
return null;

Check warning on line 36 in src/Mapster.Core/Utils/ProjectToTypeVisitors.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
switch (node.NodeType)
{
case ExpressionType.Quote:
{
Quotes.Add((UnaryExpression)node);
return base.Visit(node);
}
}

return base.Visit(node);
}
}
}
27 changes: 24 additions & 3 deletions src/Mapster.EFCore.Tests/EFCoreTest.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Mapster.EFCore.Tests.Models;
using MapsterMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Mapster.EFCore.Tests
{
Expand Down Expand Up @@ -67,6 +67,27 @@ public void MapperInstance_From_OrderBy()
var last = orderedQuery.Last();
last.LastName.ShouldBe("Olivetto");
}

[TestMethod]
public void MergeIncludeWhenUsingEFCoreProjectToType()
{
var options = new DbContextOptionsBuilder<SchoolContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"))
.Options;
var context = new SchoolContext(options);
DbInitializer.Initialize(context);

var mapsterInstance = new Mapper();

var query = context.Students
.Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1))
.EFCoreProjectToType<StudentDto>();

var first = query.First();

first.Enrollments.Count.ShouldBe(1);
first.LastName.ShouldBe("Alexander");
}
}

public class StudentDto
Expand Down
96 changes: 96 additions & 0 deletions src/Mapster.EFCore/EFCoreExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Mapster.Enums;
using Mapster.Models;
using Mapster.Utils;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;

namespace Mapster.EFCore
{
public static class EFCoreExtensions
{
public static IQueryable<TDestination> EFCoreProjectToType<TDestination>(this IQueryable source,
TypeAdapterConfig? config = null, ProjectToTypeAutoMapping autoMapConfig = ProjectToTypeAutoMapping.WithoutCollections)
{
var allInclude = new IncludeVisitor();
allInclude.Visit(source.Expression);

if (config == null)
{
config = TypeAdapterConfig.GlobalSettings
.Clone()
.ForType(source.ElementType, typeof(TDestination))
.Config;

var mapTuple = new TypeTuple(source.ElementType, typeof(TDestination));

TypeAdapterRule rule;
config.RuleMap.TryGetValue(mapTuple, out rule);

if(rule != null)
{
rule.Settings.ProjectToTypeMapConfig = autoMapConfig;

foreach (var item in allInclude.IncludeExpression)
{
var find = rule.Settings.Resolvers.Find(x => x.SourceMemberName == item.Key);
if (find != null)
{
find.Invoker = (LambdaExpression)item.Value.Operand;
find.SourceMemberName = null;
}
else
rule.Settings.ProjectToTypeResolvers.TryAdd(item.Key, item.Value);
}
}
}
else
{
config = config.Clone()
.ForType(source.ElementType, typeof(TDestination))
.Config;
}

return source.ProjectToType<TDestination>(config);
}
}


internal class IncludeVisitor : ExpressionVisitor
{
public Dictionary<string, UnaryExpression> IncludeExpression { get; protected set; } = new();
private bool IsInclude(Expression node) => node.Type.Name.StartsWith("IIncludableQueryable");

[return: NotNullIfNotNull("node")]
public override Expression Visit(Expression node)
{
if (node == null)
return null;

switch (node.NodeType)
{
case ExpressionType.Call:
{
if (IsInclude(node))
{
var QuoteVisiter = new QuoteVisitor();
QuoteVisiter.Visit(node);

foreach (var item in QuoteVisiter.Quotes)
{
var memberv = new TopLevelMemberNameVisitor();
memberv.Visit(item);

IncludeExpression.TryAdd(memberv.MemeberName, item);
}
}
return base.Visit(node);
}
}

return base.Visit(node);
}
}

}
57 changes: 57 additions & 0 deletions src/Mapster/Adapters/BaseClassAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,63 @@ from src in sources
select fn(src, destinationMember, arg))
.FirstOrDefault(result => result != null);

if (arg.MapType == MapType.Projection && getter != null)
{
var s = new TopLevelMemberNameVisitor();

s.Visit(getter);

var match = arg.Settings.ProjectToTypeResolvers.GetValueOrDefault(s.MemeberName);

if (match != null)
{
arg.Settings.Resolvers.Add(new InvokerModel
{
Condition = null,
DestinationMemberName = destinationMember.Name,
Invoker = (LambdaExpression)match.Operand,
SourceMemberName = null,
IsChildPath = false

});
}

getter = (from fn in resolvers
from src in sources
select fn(src, destinationMember, arg))
.FirstOrDefault(result => result != null);
}


if (arg.MapType == MapType.Projection)
{

var checkgetter = (from fn in resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains)
from src in sources
select fn(src, destinationMember, arg))
.FirstOrDefault(result => result != null);

if (checkgetter == null)
{
Type destinationType;

if (destinationMember.Type.IsNullable())
destinationType = destinationMember.Type.GetGenericArguments()[0];
else
destinationType = destinationMember.Type;

if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.OnlyPrimitiveTypes
&& destinationType.IsMapsterPrimitive() == false)
continue;

if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.WithoutCollections
&& destinationType.IsCollectionCompatible() == true)
continue;
}

}


var nextIgnore = arg.Settings.Ignore.Next((ParameterExpression)source, (ParameterExpression?)destination, destinationMember.Name);
var nextResolvers = arg.Settings.Resolvers.Next(arg.Settings.Ignore, (ParameterExpression)source, destinationMember.Name)
.ToList();
Expand Down
6 changes: 6 additions & 0 deletions src/Mapster/Settings/SettingStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public void Set(string key, object? value)
_objectStore[key] = value;
}


public T GetEnum<T>(string key, Func<T> initializer) where T : System.Enum
{
return (T)_objectStore.GetOrAdd(key, _ => initializer());
}

public bool? Get(string key)
{
return _booleanStore.TryGetValue(key, out var value) ? value : null;
Expand Down
16 changes: 15 additions & 1 deletion src/Mapster/TypeAdapterSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Mapster.Models;
using Mapster.Enums;
using Mapster.Models;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
Expand Down Expand Up @@ -105,6 +106,19 @@ public bool? MapToTargetPrimitive
set => Set(nameof(MapToTargetPrimitive), value);
}

public ProjectToTypeAutoMapping ProjectToTypeMapConfig
{
get => GetEnum(nameof(ProjectToTypeMapConfig), ()=> default(ProjectToTypeAutoMapping));
set => Set(nameof(ProjectToTypeMapConfig), value);
}

public Dictionary<string,UnaryExpression> ProjectToTypeResolvers
{
get => Get(nameof(ProjectToTypeResolvers), () => new Dictionary<string, UnaryExpression>());
set => Set(nameof(ProjectToTypeResolvers), value);
}


public List<Func<IMemberModel, MemberSide, bool?>> ShouldMapMember
{
get => Get(nameof(ShouldMapMember), () => new List<Func<IMemberModel, MemberSide, bool?>>());
Expand Down
2 changes: 1 addition & 1 deletion src/Mapster/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static Type GetTypeInfo(this Type type)

public static bool IsMapsterPrimitive(this Type type)
{
return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string);
return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string) || type.IsEnum;
}

public static bool IsNullable(this Type type)
Expand Down
Loading