Skip to content

Commit 2986140

Browse files
DocSvartzDocSvartz
authored andcommitted
add EFCoreProjectToType and base test
1 parent ea2da89 commit 2986140

File tree

8 files changed

+259
-5
lines changed

8 files changed

+259
-5
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace Mapster.Enums
4+
{
5+
public enum ProjectToTypeAutoMapping
6+
{
7+
AllTypes = 0,
8+
WithoutCollections = 1,
9+
OnlyPrimitiveTypes = 2,
10+
}
11+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Collections.Generic;
2+
using System.Linq.Expressions;
3+
4+
namespace Mapster.Utils
5+
{
6+
public sealed class TopLevelMemberNameVisitor : ExpressionVisitor
7+
{
8+
public string? MemeberName { get; private set; }
9+
10+
public override Expression Visit(Expression node)
11+
{
12+
if (node == null)
13+
return null;
14+
switch (node.NodeType)
15+
{
16+
case ExpressionType.MemberAccess:
17+
{
18+
if (string.IsNullOrEmpty(MemeberName))
19+
MemeberName = ((MemberExpression)node).Member.Name;
20+
21+
return base.Visit(node);
22+
}
23+
}
24+
25+
return base.Visit(node);
26+
}
27+
}
28+
29+
public sealed class QuoteVisitor : ExpressionVisitor
30+
{
31+
public List<UnaryExpression> Quotes { get; private set; } = new();
32+
33+
public override Expression Visit(Expression node)
34+
{
35+
if (node == null)
36+
return null;
37+
switch (node.NodeType)
38+
{
39+
case ExpressionType.Quote:
40+
{
41+
Quotes.Add((UnaryExpression)node);
42+
return base.Visit(node);
43+
}
44+
}
45+
46+
return base.Visit(node);
47+
}
48+
}
49+
}

src/Mapster.EFCore.Tests/EFCoreTest.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
41
using Mapster.EFCore.Tests.Models;
52
using MapsterMapper;
63
using Microsoft.EntityFrameworkCore;
74
using Microsoft.VisualStudio.TestTools.UnitTesting;
85
using Shouldly;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
99

1010
namespace Mapster.EFCore.Tests
1111
{
@@ -67,6 +67,27 @@ public void MapperInstance_From_OrderBy()
6767
var last = orderedQuery.Last();
6868
last.LastName.ShouldBe("Olivetto");
6969
}
70+
71+
[TestMethod]
72+
public void MergeIncludeWhenUsingEFCoreProjectToType()
73+
{
74+
var options = new DbContextOptionsBuilder<SchoolContext>()
75+
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"))
76+
.Options;
77+
var context = new SchoolContext(options);
78+
DbInitializer.Initialize(context);
79+
80+
var mapsterInstance = new Mapper();
81+
82+
var query = context.Students
83+
.Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1))
84+
.EFCoreProjectToType<StudentDto>();
85+
86+
var first = query.First();
87+
88+
first.Enrollments.Count.ShouldBe(1);
89+
first.LastName.ShouldBe("Alexander");
90+
}
7091
}
7192

7293
public class StudentDto
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using Mapster.Enums;
2+
using Mapster.Models;
3+
using Mapster.Utils;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
7+
using System.Linq.Expressions;
8+
9+
namespace Mapster.EFCore
10+
{
11+
public static class EFCoreExtensions
12+
{
13+
public static IQueryable<TDestination> EFCoreProjectToType<TDestination>(this IQueryable source,
14+
TypeAdapterConfig? config = null, ProjectToTypeAutoMapping autoMapConfig = ProjectToTypeAutoMapping.WithoutCollections)
15+
{
16+
var allInclude = new IncludeVisitor();
17+
allInclude.Visit(source.Expression);
18+
19+
if (config == null)
20+
{
21+
config = TypeAdapterConfig.GlobalSettings
22+
.Clone()
23+
.ForType(source.ElementType, typeof(TDestination))
24+
.Config;
25+
26+
var mapTuple = new TypeTuple(source.ElementType, typeof(TDestination));
27+
28+
TypeAdapterRule rule;
29+
config.RuleMap.TryGetValue(mapTuple, out rule);
30+
31+
if(rule != null)
32+
{
33+
rule.Settings.ProjectToTypeMapConfig = autoMapConfig;
34+
35+
foreach (var item in allInclude.IncludeExpression)
36+
{
37+
var find = rule.Settings.Resolvers.Find(x => x.SourceMemberName == item.Key);
38+
if (find != null)
39+
{
40+
find.Invoker = (LambdaExpression)item.Value.Operand;
41+
find.SourceMemberName = null;
42+
}
43+
else
44+
rule.Settings.ProjectToTypeResolvers.TryAdd(item.Key, item.Value);
45+
}
46+
}
47+
}
48+
else
49+
{
50+
config = config.Clone()
51+
.ForType(source.ElementType, typeof(TDestination))
52+
.Config;
53+
}
54+
55+
return source.ProjectToType<TDestination>(config);
56+
}
57+
}
58+
59+
60+
internal class IncludeVisitor : ExpressionVisitor
61+
{
62+
public Dictionary<string, UnaryExpression> IncludeExpression { get; protected set; } = new();
63+
private bool IsInclude(Expression node) => node.Type.Name.StartsWith("IIncludableQueryable");
64+
65+
[return: NotNullIfNotNull("node")]
66+
public override Expression Visit(Expression node)
67+
{
68+
if (node == null)
69+
return null;
70+
71+
switch (node.NodeType)
72+
{
73+
case ExpressionType.Call:
74+
{
75+
if (IsInclude(node))
76+
{
77+
var QuoteVisiter = new QuoteVisitor();
78+
QuoteVisiter.Visit(node);
79+
80+
foreach (var item in QuoteVisiter.Quotes)
81+
{
82+
var memberv = new TopLevelMemberNameVisitor();
83+
memberv.Visit(item);
84+
85+
IncludeExpression.TryAdd(memberv.MemeberName, item);
86+
}
87+
}
88+
return base.Visit(node);
89+
}
90+
}
91+
92+
return base.Visit(node);
93+
}
94+
}
95+
96+
}

src/Mapster/Adapters/BaseClassAdapter.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,63 @@ from src in sources
4040
select fn(src, destinationMember, arg))
4141
.FirstOrDefault(result => result != null);
4242

43+
if (arg.MapType == MapType.Projection && getter != null)
44+
{
45+
var s = new TopLevelMemberNameVisitor();
46+
47+
s.Visit(getter);
48+
49+
var match = arg.Settings.ProjectToTypeResolvers.GetValueOrDefault(s.MemeberName);
50+
51+
if (match != null)
52+
{
53+
arg.Settings.Resolvers.Add(new InvokerModel
54+
{
55+
Condition = null,
56+
DestinationMemberName = destinationMember.Name,
57+
Invoker = (LambdaExpression)match.Operand,
58+
SourceMemberName = null,
59+
IsChildPath = false
60+
61+
});
62+
}
63+
64+
getter = (from fn in resolvers
65+
from src in sources
66+
select fn(src, destinationMember, arg))
67+
.FirstOrDefault(result => result != null);
68+
}
69+
70+
71+
if (arg.MapType == MapType.Projection)
72+
{
73+
74+
var checkgetter = (from fn in resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains)
75+
from src in sources
76+
select fn(src, destinationMember, arg))
77+
.FirstOrDefault(result => result != null);
78+
79+
if (checkgetter == null)
80+
{
81+
Type destinationType;
82+
83+
if (destinationMember.Type.IsNullable())
84+
destinationType = destinationMember.Type.GetGenericArguments()[0];
85+
else
86+
destinationType = destinationMember.Type;
87+
88+
if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.OnlyPrimitiveTypes
89+
&& destinationType.IsMapsterPrimitive() == false)
90+
continue;
91+
92+
if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.WithoutCollections
93+
&& destinationType.IsCollectionCompatible() == true)
94+
continue;
95+
}
96+
97+
}
98+
99+
43100
var nextIgnore = arg.Settings.Ignore.Next((ParameterExpression)source, (ParameterExpression?)destination, destinationMember.Name);
44101
var nextResolvers = arg.Settings.Resolvers.Next(arg.Settings.Ignore, (ParameterExpression)source, destinationMember.Name)
45102
.ToList();

src/Mapster/Settings/SettingStore.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public void Set(string key, object? value)
2525
_objectStore[key] = value;
2626
}
2727

28+
29+
public T GetEnum<T>(string key, Func<T> initializer) where T : System.Enum
30+
{
31+
return (T)_objectStore.GetOrAdd(key, _ => initializer());
32+
}
33+
2834
public bool? Get(string key)
2935
{
3036
return _booleanStore.TryGetValue(key, out var value) ? value : null;

src/Mapster/TypeAdapterSettings.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Mapster.Models;
1+
using Mapster.Enums;
2+
using Mapster.Models;
23
using System;
34
using System.Collections.Generic;
45
using System.Linq.Expressions;
@@ -105,6 +106,19 @@ public bool? MapToTargetPrimitive
105106
set => Set(nameof(MapToTargetPrimitive), value);
106107
}
107108

109+
public ProjectToTypeAutoMapping ProjectToTypeMapConfig
110+
{
111+
get => GetEnum(nameof(ProjectToTypeMapConfig), ()=> default(ProjectToTypeAutoMapping));
112+
set => Set(nameof(ProjectToTypeMapConfig), value);
113+
}
114+
115+
public Dictionary<string,UnaryExpression> ProjectToTypeResolvers
116+
{
117+
get => Get(nameof(ProjectToTypeResolvers), () => new Dictionary<string, UnaryExpression>());
118+
set => Set(nameof(ProjectToTypeResolvers), value);
119+
}
120+
121+
108122
public List<Func<IMemberModel, MemberSide, bool?>> ShouldMapMember
109123
{
110124
get => Get(nameof(ShouldMapMember), () => new List<Func<IMemberModel, MemberSide, bool?>>());

src/Mapster/Utils/ReflectionUtils.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static Type GetTypeInfo(this Type type)
3838

3939
public static bool IsMapsterPrimitive(this Type type)
4040
{
41-
return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string);
41+
return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string) || type.IsEnum;
4242
}
4343

4444
public static bool IsNullable(this Type type)

0 commit comments

Comments
 (0)