Skip to content

Commit 0ca436b

Browse files
authored
Merge branch 'Alpha-9' into Polymorphic-map-to-target-Mapping-fix-Publish
2 parents 8d1c7cb + 4ace877 commit 0ca436b

23 files changed

+784
-47
lines changed

src/Mapster.Async.Tests/AsyncTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Threading.Tasks;
3+
using MapsterMapper;
34
using Microsoft.VisualStudio.TestTools.UnitTesting;
45
using Shouldly;
56

@@ -87,6 +88,22 @@ public async Task NestedAsync()
8788
dtoOwnership.Owner.Name.ShouldBe("John Doe");
8889
}
8990

91+
[TestMethod]
92+
public async Task SimplyAsync()
93+
{
94+
TypeAdapterConfig<Poco, Dto>.NewConfig()
95+
.AfterMappingAsync(async dest => { dest.Name = await GetName(); });
96+
97+
var poco = new Poco { Id = "foo" };
98+
var dto = await poco.AdaptAsync<Dto>();
99+
dto.Name.ShouldBe("bar");
100+
101+
IMapper instance = new Mapper();
102+
103+
var destination = await instance.MapAsync<Dto>(poco);
104+
destination.Name.ShouldBe("bar");
105+
}
106+
90107
private static async Task<string> GetName()
91108
{
92109
await Task.Delay(1);

src/Mapster.Async/TypeAdapterExtensions.cs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using MapsterMapper;
2+
using System;
23
using System.Collections.Generic;
34
using System.Threading.Tasks;
45

@@ -101,5 +102,44 @@ public static async Task<TDestination> AdaptToAsync<TDestination>(this IAdapterB
101102
}
102103
}
103104

105+
106+
/// <summary>
107+
/// Map asynchronously to destination type.
108+
/// </summary>
109+
/// <typeparam name="TDestination">Destination type to map.</typeparam>
110+
/// <param name="builder"></param>
111+
/// <returns>Type of destination object that mapped.</returns>
112+
public static async Task<TDestination> AdaptAsync<TDestination>(this object? source)
113+
{
114+
return await source.BuildAdapter().AdaptToTypeAsync<TDestination>();
115+
}
116+
117+
118+
/// <summary>
119+
/// Map asynchronously to destination type.
120+
/// </summary>
121+
/// <typeparam name="TDestination">Destination type to map.</typeparam>
122+
/// <param name="builder"></param>
123+
/// <param name="config">Configuration</param>
124+
/// <returns>Type of destination object that mapped.</returns>
125+
public static async Task<TDestination> AdaptAsync<TDestination>(this object? source, TypeAdapterConfig config)
126+
{
127+
return await source.BuildAdapter(config).AdaptToTypeAsync<TDestination>();
128+
}
129+
130+
}
131+
132+
public static class IMapperAsyncExtentions
133+
{
134+
/// <summary>
135+
/// Map asynchronously to destination type.
136+
/// </summary>
137+
/// <typeparam name="TDestination">Destination type to map.</typeparam>
138+
/// <param name="builder"></param>
139+
/// <returns>Type of destination object that mapped.</returns>
140+
public static async Task<TDestination> MapAsync<TDestination>(this IMapper mapper, object? source)
141+
{
142+
return await mapper.From(source).AdaptToTypeAsync<TDestination>();
143+
}
104144
}
105145
}
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

0 commit comments

Comments
 (0)