Skip to content

Commit 3c822b1

Browse files
authored
IEnumerable support (#20)
* Add support for mapping `IEnumerable` types. * Expand IEnumerable mapping to support additional collection types (`ICollection`, `IReadOnlyList`, `IReadOnlyCollection`) and add tests and documentation accordingly. * Add support for pre-map and post-map queries in enumerable mapping and updated documentation * Improve `Select` query generation to handle context parameter conditionally and add tests for pre-map and post-map query scenarios
1 parent ba70065 commit 3c822b1

9 files changed

Lines changed: 522 additions & 12 deletions

File tree

docs/AOMDocs.Source/Layout/MainLayout.razor

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@
4646
new (){ Id = "14", Text = "Nested Mapping", Href = "NestedMapping", ParentId = "7", },
4747
new (){ Id = "15", Text = "Reference Preservation", Href = "ReferencePreservation", ParentId = "7", },
4848
new (){ Id = "16", Text = "IEnumerable & Collections", Href = "IEnumerableAndCollections", ParentId = "7", },
49-
new (){ Id = "17", Text = "Pre/Post Map Actions", Href = "PrePostMapActions", ParentId = "7", },
50-
new (){ Id = "17", Text = "Mapping Polymorphism", Href = "MappingPolymorphism", ParentId = "7", },
51-
new (){ Id = "18", Text = "Benchmarks", Href = "Benchmarks", },
49+
new (){ Id = "16", Text = "Enumerable Queries", Href = "EnumerableQueries", ParentId = "7", },
50+
new (){ Id = "18", Text = "Pre/Post Map Actions", Href = "PrePostMapActions", ParentId = "7", },
51+
new (){ Id = "19", Text = "Mapping Polymorphism", Href = "MappingPolymorphism", ParentId = "7", },
52+
new (){ Id = "20", Text = "Benchmarks", Href = "Benchmarks", },
5253
};
5354
}
5455

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
@page "/EnumerableQueries"
2+
3+
<PageTitle>Enumerable Queries</PageTitle>
4+
5+
<p>
6+
In addition to automatic enumerable mapping, the mapper allows custom LINQ queries
7+
to be injected into the collection mapping pipeline.
8+
</p>
9+
10+
<p>
11+
Enumerable queries can be used to filter, reorder, or otherwise transform a collection
12+
either <em>before</em> or <em>after</em> element mapping occurs.
13+
</p>
14+
15+
<p>
16+
Enumerable queries can use the <code>MapperContext</code> to apply dynamic,
17+
runtime-controlled behavior during collection mapping.
18+
</p>
19+
20+
<p>
21+
The <code>MapperContext</code> exposes an <code>AdditionalContext</code> dictionary
22+
that can be populated by the caller and consumed by pre-map and post-map queries.
23+
</p>
24+
25+
<h3>Context-driven pre-map queries</h3>
26+
27+
<p>
28+
A <code>PreMapQuery</code> can inspect the context to decide which source elements
29+
should be mapped.
30+
</p>
31+
32+
<p>
33+
This example conditionally excludes inactive users based on a flag stored in the
34+
mapping context.
35+
</p>
36+
37+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@PreMapExample />
38+
39+
<p>
40+
The context value is provided by the caller at runtime:
41+
</p>
42+
43+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@ContextSetup />
44+
45+
<h3>Context-driven post-map queries</h3>
46+
47+
<p>
48+
A <code>PostMapQuery</code> can use the context to modify the mapped collection
49+
without affecting the source mapping.
50+
</p>
51+
52+
<p>
53+
This example conditionally limits the number of mapped results returned.
54+
</p>
55+
56+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@PostMapExample />
57+
58+
<h3>Combining pre-map and post-map queries</h3>
59+
60+
<p>
61+
Pre-map and post-map queries can be combined to implement complex, policy-driven
62+
mapping behavior using a single mapper.
63+
</p>
64+
65+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@BothExample />
66+
67+
@code
68+
{
69+
const string PreMapExample =
70+
"""
71+
[GenerateMapper]
72+
[UseMap<UserMapper, User, UserDto>]
73+
[Map<Company, CompanyDto>]
74+
public partial class CompanyMapper_FilterInactiveUsers
75+
{
76+
[PreMapQuery<User, UserDto>]
77+
public static IEnumerable<User> FilterInactiveUsers(IEnumerable<User> users, MapperContext ctx)
78+
{
79+
if (!ctx.AdditionalContext.TryGetValue("IncludeInactiveUsers", out var value) ||
80+
value is not bool includeInactive ||
81+
includeInactive)
82+
{
83+
return users;
84+
}
85+
86+
return users.Where(u => u.IsActive);
87+
}
88+
}
89+
""";
90+
91+
const string PostMapExample =
92+
"""
93+
[GenerateMapper]
94+
[UseMap<UserMapper, User, UserDto>]
95+
[Map<Company, CompanyDto>]
96+
public partial class CompanyMapper_LimitResults
97+
{
98+
[PostMapQuery<User, UserDto>]
99+
public static IEnumerable<UserDto> LimitResults(IEnumerable<UserDto> users, MapperContext ctx)
100+
{
101+
if (!ctx.AdditionalContext.TryGetValue("MaxUsers", out var value) ||
102+
value is not int maxUsers)
103+
{
104+
return users;
105+
}
106+
107+
return users.Take(maxUsers);
108+
}
109+
}
110+
""";
111+
112+
const string BothExample =
113+
"""
114+
[GenerateMapper]
115+
[UseMap<UserMapper, User, UserDto>]
116+
[Map<Company, CompanyDto>]
117+
public partial class CompanyMapper_PolicyDriven
118+
{
119+
[PreMapQuery<User, UserDto>]
120+
public static IEnumerable<User> FilterInactiveUsers(IEnumerable<User> users, MapperContext ctx)
121+
{
122+
if (ctx.AdditionalContext.TryGetValue("IncludeInactiveUsers", out var value) &&
123+
value is bool includeInactive &&
124+
!includeInactive)
125+
{
126+
return users.Where(u => u.IsActive);
127+
}
128+
129+
return users;
130+
}
131+
132+
[PostMapQuery<User, UserDto>]
133+
public static IEnumerable<UserDto> LimitResults(IEnumerable<UserDto> users, MapperContext ctx)
134+
{
135+
if (ctx.AdditionalContext.TryGetValue("MaxUsers", out var value) &&
136+
value is int maxUsers)
137+
{
138+
return users.Take(maxUsers);
139+
}
140+
141+
return users;
142+
}
143+
}
144+
""";
145+
146+
const string ContextSetup =
147+
"""
148+
var ctx = new MapperContext();
149+
ctx.AdditionalContext["IncludeInactiveUsers"] = false;
150+
ctx.AdditionalContext["MaxUsers"] = 10;
151+
152+
var dto = CompanyMapper_PolicyDriven.Map(company, ctx);
153+
""";
154+
}

docs/AOMDocs.Source/Pages/IEnumerableAndCollections.razor

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,87 @@
22

33
<PageTitle>IEnumerable & Collections</PageTitle>
44

5-
@code {
6-
5+
<p>
6+
The mapper supports mapping between enumerable types automatically when a mapping exists for the element type.
7+
</p>
8+
9+
<p>
10+
Any source property that implements <code>IEnumerable&lt;TSource&gt;</code> can be mapped to the following destination types:
11+
</p>
12+
13+
<ul>
14+
<li><code>IEnumerable&lt;TDestination&gt;</code></li>
15+
<li><code>ICollection&lt;TDestination&gt;</code></li>
16+
<li><code>List&lt;TDestination&gt;</code></li>
17+
<li><code>TDestination[]</code></li>
18+
<li><code>IReadOnlyList&lt;TDestination&gt;</code></li>
19+
<li><code>IReadOnlyCollection&lt;TDestination&gt;</code></li>
20+
</ul>
21+
22+
<p>
23+
The only requirement is that a mapper exists for <code>TSource</code> to <code>TDestination</code>.
24+
</p>
25+
26+
<p>Below is an example mapping a collection of <code>User</code> to <code>UserDto</code>.</p>
27+
28+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@EnumerableMap />
29+
30+
<p>
31+
In this example, the <code>Users</code> property on <code>Company</code> implements
32+
<code>IEnumerable&lt;User&gt;</code>, while the destination property on <code>CompanyDto</code>
33+
is an array of <code>UserDto</code>.
34+
</p>
35+
36+
<p>
37+
When the project is built, the generated mapper will automatically map each element in the
38+
source collection using the <code>User → UserDto</code> mapping and materialize the result
39+
into the target collection type.
40+
</p>
41+
42+
<p>
43+
The generated mapping code will be similar to the following:
44+
</p>
45+
46+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@GeneratedCode />
47+
48+
<p>
49+
Ordering is preserved, and a new destination instance is created for each element in the source collection.
50+
</p>
51+
52+
<p>
53+
This behavior applies equally when mapping to <code>List&lt;T&gt;</code>,
54+
<code>ICollection&lt;T&gt;</code>, <code>IReadOnlyList&lt;T&gt;</code> and
55+
<code>IReadOnlyCollection&lt;T&gt;</code>.
56+
</p>
57+
58+
@code
59+
{
60+
const string EnumerableMap =
61+
"""
62+
public class Company
63+
{
64+
public ICollection<User> Users { get; set; } =
65+
[
66+
User.Jim(),
67+
User.Jim(),
68+
];
69+
}
70+
71+
public class CompanyDto
72+
{
73+
public UserDto[] Users { get; set; } = [];
74+
}
75+
76+
[GenerateMapper]
77+
[Map<User, UserDto>]
78+
[Map<Company, CompanyDto>]
79+
public partial class CompanyMapper;
80+
""";
81+
82+
const string GeneratedCode =
83+
"""
84+
dest.Users = src.Users
85+
.Select(x => CompanyMapper.Map(x, ctx))
86+
.ToArray();
87+
""";
788
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace AotObjectMapper.Abstractions.Attributes;
2+
3+
/// <summary>
4+
/// An attribute that allows the configuration or customization of post-map queries
5+
/// between the specified source and destination types in an object mapping process.
6+
/// </summary>
7+
/// <typeparam name="TSource">The type of the source object being mapped.</typeparam>
8+
/// <typeparam name="TDestination">The type of the destination object being mapped.</typeparam>
9+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
10+
public sealed class PostMapQueryAttribute<TSource, TDestination> : Attribute;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace AotObjectMapper.Abstractions.Attributes;
2+
3+
/// <summary>
4+
/// An attribute that allows the configuration or customization of pre-map queries
5+
/// between the specified source and destination types in an object mapping process.
6+
/// </summary>
7+
/// <typeparam name="TSource">The type of the source object being mapped.</typeparam>
8+
/// <typeparam name="TDestination">The type of the destination object being mapped.</typeparam>
9+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
10+
public class PreMapQueryAttribute<TSource, TDestination> : Attribute { }

src/AotObjectMapper.Mapper/Mapper/MapperGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ private static string GeneratePopulationMethod(Compilation compilation, MethodGe
8181
sb.AppendLine("// <auto-generated />");
8282
sb.AppendLine("");
8383
sb.AppendLine("using System;");
84+
sb.AppendLine("using System.Collections.Generic;");
85+
sb.AppendLine("using System.Linq;");
8486
sb.AppendLine("using System.ComponentModel;");
8587
sb.AppendLine("using AotObjectMapper.Abstractions.Models;");
8688
sb.AppendLine("");

0 commit comments

Comments
 (0)