Skip to content

Commit 908e5ff

Browse files
authored
Merge branch 'Alpha-9' into Polymorphic-map-to-target-Mapping-fix-Publish
2 parents 8d1c7cb + 3553d25 commit 908e5ff

19 files changed

+682
-42
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.Tests/WhenExplicitMappingRequired.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class WhenExplicitMappingRequired
1313
public void TestCleanup()
1414
{
1515
TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = false;
16+
TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = false;
1617
TypeAdapterConfig.GlobalSettings.Clear();
1718
}
1819

@@ -140,8 +141,60 @@ public void UnmappedChildPocoShouldFailed()
140141
setter.Compile(); // Should fail here
141142
}
142143

144+
[TestMethod]
145+
public void RequireExplicitMappingPrimitiveWork()
146+
{
147+
TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = true;
148+
149+
TypeAdapterConfig<Source783, Destination783>.NewConfig();
150+
151+
Should.Throw<CompileException>(() =>
152+
{
153+
TypeAdapterConfig.GlobalSettings.Compile(); // throw CompileException
154+
});
155+
156+
byte byteSource = 10;
157+
158+
byteSource.Adapt<byte>(); // Should work when the type is mapped to itself
159+
160+
Should.Throw<CompileException>(() =>
161+
{
162+
byteSource.Adapt<int>(); // throw CompileException, Do not map to another primitive type without registering the configuration
163+
});
164+
165+
Should.NotThrow(() =>
166+
{
167+
TypeAdapterConfig<byte, int>.NewConfig();
168+
169+
byteSource.Adapt<int>(); // Not throw CompileException when config is registering
170+
});
171+
172+
Should.NotThrow(() =>
173+
{
174+
TypeAdapterConfig<Source783, Destination783>.NewConfig()
175+
.Map(dest=> dest.MyProperty, src=> int.Parse(src.MyProperty));
176+
// it work works because int.Parse return Type Int. Type is mapped to itself (int -> int) without config.
177+
178+
var sourceMapconfig = new Source783() { MyProperty = "128" };
179+
var resultMapconfig = sourceMapconfig.Adapt<Destination783>();
180+
181+
resultMapconfig.MyProperty.ShouldBe(128);
182+
});
183+
184+
}
185+
186+
143187
#region TestClasses
188+
189+
public class Source783
190+
{
191+
public string MyProperty { get; set; } = "";
192+
}
144193

194+
public class Destination783
195+
{
196+
public int MyProperty { get; set; }
197+
}
145198

146199
public enum NameEnum
147200
{

src/Mapster.Tests/WhenIgnoringNonMapped.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public void Should_Ignore_Non_Mapped()
1212
{
1313
TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig()
1414
.Map(dest => dest.Id, src => src.Id)
15+
.RequireDestinationMemberSource(true)
1516
.IgnoreNonMapped(true)
1617
.Compile();
1718

src/Mapster.Tests/WhenMappingInitProperty.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void WhenMappingToHiddenandNewInitFieldDestination()
1919
var c = source.Adapt<BDestination>();
2020
var s = source.Adapt(new BDestination());
2121

22-
((ADestination)c).Id.ShouldBe(156);
22+
((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping
2323
s.Id.ShouldBe(156);
2424
}
2525

@@ -33,7 +33,7 @@ public void WhenMappingToHiddenandNewInitFieldWithConstructUsing()
3333
var c = source.Adapt<BDestination>();
3434
var s = source.Adapt(new BDestination());
3535

36-
((ADestination)c).Id.ShouldBe(256);
36+
((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping
3737
s.Id.ShouldBe(256);
3838
}
3939

src/Mapster.Tests/WhenMappingWithOpenGenerics.cs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using Shouldly;
3-
using System;
4-
using System.Collections.Generic;
5-
using System.Linq;
6-
using System.Text;
7-
using System.Threading.Tasks;
83

94
namespace Mapster.Tests
105
{
@@ -36,6 +31,60 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect()
3631
var cCopy = c.Adapt<C>(config);
3732
}
3833

34+
[TestMethod]
35+
public void MapOpenGenericsUseInherits()
36+
{
37+
TypeAdapterConfig.GlobalSettings
38+
.ForType(typeof(GenericPoco<>), typeof(GenericDto<>))
39+
.Map("value", "Value");
40+
41+
TypeAdapterConfig.GlobalSettings
42+
.ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>))
43+
.Map("derivedValue", "DerivedValue")
44+
.Inherits(typeof(GenericPoco<>), typeof(GenericDto<>));
45+
46+
var poco = new DerivedPoco<int> { Value = 123 , DerivedValue = 42 };
47+
var dto = poco.Adapt<DerivedDto<int>>();
48+
dto.value.ShouldBe(poco.Value);
49+
dto.derivedValue.ShouldBe(poco.DerivedValue);
50+
}
51+
52+
[TestMethod]
53+
public void MapOpenGenericsUseInclude()
54+
{
55+
TypeAdapterConfig.GlobalSettings.Clear();
56+
57+
TypeAdapterConfig.GlobalSettings
58+
.ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>))
59+
.Map("derivedValue", "DerivedValue");
60+
61+
TypeAdapterConfig.GlobalSettings
62+
.ForType(typeof(GenericPoco<>), typeof(GenericDto<>))
63+
.Map("value", "Value");
64+
65+
TypeAdapterConfig.GlobalSettings
66+
.ForType(typeof(GenericPoco<>), typeof(GenericDto<>))
67+
.Include(typeof(DerivedPoco<>), typeof(DerivedDto<>));
68+
69+
var poco = new DerivedPoco<int> { Value = 123, DerivedValue = 42 };
70+
var dto = poco.Adapt(typeof(GenericPoco<>), typeof(GenericDto<>));
71+
72+
dto.ShouldBeOfType<DerivedDto<int>>();
73+
74+
((DerivedDto<int>)dto).value.ShouldBe(poco.Value);
75+
((DerivedDto<int>)dto).derivedValue.ShouldBe(poco.DerivedValue);
76+
}
77+
78+
public class DerivedPoco<T> : GenericPoco<T>
79+
{
80+
public T DerivedValue { get; set; }
81+
}
82+
83+
public class DerivedDto<T> : GenericDto<T>
84+
{
85+
public T derivedValue { get; set; }
86+
}
87+
3988
public class GenericPoco<T>
4089
{
4190
public T Value { get; set; }

0 commit comments

Comments
 (0)