Skip to content

Commit 71a4c43

Browse files
authored
Polymorphism support (#17)
* Implement polymorphism support * Add tests for polymorphism handling and fixed a few bugs * Add documentation for polymorphism mapping with examples
1 parent 3e0b0d0 commit 71a4c43

8 files changed

Lines changed: 453 additions & 22 deletions

File tree

docs/AOMDocs.Source/Layout/MainLayout.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
new (){ Id = "15", Text = "Reference Preservation", Href = "ReferencePreservation", ParentId = "7", },
4848
new (){ Id = "16", Text = "IEnumerable & Collections", Href = "IEnumerableAndCollections", ParentId = "7", },
4949
new (){ Id = "17", Text = "Pre/Post Map Actions", Href = "PrePostMapActions", ParentId = "7", },
50+
new (){ Id = "17", Text = "Mapping Polymorphism", Href = "MappingPolymorphism", ParentId = "7", },
5051
new (){ Id = "18", Text = "Benchmarks", Href = "Benchmarks", },
5152
};
5253
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
2+
@page "/MappingPolymorphism"
3+
4+
<PageTitle>Mapping Polymorphism</PageTitle>
5+
6+
<h1>Polymorphic Mapping</h1>
7+
8+
<p>
9+
Polymorphic mapping enables you to map interface or base class types to their runtime implementations automatically.
10+
</p>
11+
12+
<h2>Overview</h2>
13+
14+
<p>
15+
When you map an interface or base class reference, the mapper examines the actual runtime type and routes it to the correct mapping.
16+
This is particularly useful when working with inheritance hierarchies or polymorphic collections.
17+
</p>
18+
19+
<h2>Example Configuration:</h2>
20+
21+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@MapperCode />
22+
23+
<h2>Core Concepts</h2>
24+
25+
<h3>Type Dispatch</h3>
26+
27+
<p>
28+
The mapper automatically detects the actual runtime type of your source object and dispatches it to the appropriate handler:
29+
</p>
30+
31+
<ul>
32+
<li>A <code>Dog</code> instance referenced as <code>IAnimal</code> is automatically mapped to <code>DogDto</code></li>
33+
<li>A <code>Cat</code> instance referenced as <code>IAnimal</code> is automatically mapped to <code>CatDto</code></li>
34+
<li>The dispatch happens transparently at runtime through pattern matching</li>
35+
</ul>
36+
37+
<h2>Runtime Behavior</h2>
38+
39+
<h3>Successful Dispatch</h3>
40+
41+
<p>
42+
When mapping an interface or base class reference:
43+
</p>
44+
45+
<ol>
46+
<li>The mapper checks the actual runtime type of the source object</li>
47+
<li>Finds the corresponding mapping in its registry</li>
48+
<li>Calls the appropriate <code>Map()</code> method</li>
49+
<li>Returns the correctly typed result</li>
50+
</ol>
51+
52+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@DispatchExample />
53+
54+
<h3>Inheritance Chain Handling</h3>
55+
56+
<p>
57+
When mapping a derived type through a base class reference:
58+
</p>
59+
60+
<ol>
61+
<li>The mapper first checks for an exact type match</li>
62+
<li>If found, it uses a specialized mapper (via <code>[UseMap&lt;&gt;]</code>)</li>
63+
<li>The specialized mapper handles all derived-type-specific properties</li>
64+
</ol>
65+
66+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@InheritanceExample />
67+
68+
<h3>Unhandled Types</h3>
69+
70+
<p>
71+
If the mapper encounters a type without a registered mapping, it throws an <code>UnhandledPolymorphicTypeException</code>:
72+
</p>
73+
74+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@UnhandledTypeExample />
75+
76+
<p>
77+
<strong>Mitigation:</strong> Register all types that might be encountered:
78+
</p>
79+
80+
<CodeBlock Language="CodeLanguage.CSharp" EnableLineNumbers="false" Code=@MitigationExample />
81+
82+
<h2>Best Practices</h2>
83+
84+
<ul>
85+
<li><strong>Register all possible types</strong> that might be encountered during mapping</li>
86+
<li><strong>Use <code>[UseMap&lt;&gt;]</code></strong> for inheritance hierarchies with specialized behavior</li>
87+
<li><strong>Keep interfaces aligned</strong> between source and destination types</li>
88+
<li><strong>Test polymorphic paths</strong> to ensure all derived types are covered</li>
89+
<li><strong>Handle exceptions gracefully</strong> if unhandled types are possible</li>
90+
</ul>
91+
92+
@code {
93+
94+
public const string MapperCode =
95+
"""
96+
public interface IAnimal { public string Name { get; set; } }
97+
public interface IAnimalDto { public string Name { get; set; } }
98+
99+
public class Dog : IAnimal { public string Name { get; set; } }
100+
public class DogDto : IAnimalDto { public string Name { get; set; } }
101+
102+
public class Cat : IAnimal { public string Name { get; set; } }
103+
public class CatDto : IAnimalDto { public string Name { get; set; } }
104+
105+
public class Bird : IAnimal { public string Name { get; set; } }
106+
public class BirdDto : IAnimalDto { public string Name { get; set; } }
107+
108+
public class Wolf : Dog { public Color FurColor { get; set; } }
109+
public class WolfDto : DogDto { public Color FurColor { get; set; } }
110+
111+
[GenerateMapper]
112+
[Map<IAnimal, IAnimalDto>]
113+
[Map<Dog, DogDto>]
114+
[Map<Cat, CatDto>]
115+
[UseMap<WolfMapper, Wolf, WolfDto>]
116+
public partial class AnimalMapper;
117+
118+
[GenerateMapper(options: /* Adv. mapping configuration */)]
119+
[Map<Wolf, WolfDto>]
120+
public partial class WolfMapper
121+
{
122+
// Adv. mapping configuration
123+
}
124+
""";
125+
126+
public const string DispatchExample =
127+
"""
128+
IAnimal source = new Dog { Name = "Piper" };
129+
IAnimalDto result = AnimalMapper.Map(source); // Returns DogDto with Name = "Piper"
130+
""";
131+
132+
public const string InheritanceExample =
133+
"""
134+
IAnimal source = new Wolf { Name = "Ghost", FurColor = Color.GhostWhite };
135+
IAnimalDto result = AnimalMapper.Map(source); // Uses WolfMapper.Map(source)
136+
137+
// Result is WolfDto with both Name and FurColor populated
138+
var wolf = result as WolfDto;
139+
Console.WriteLine(wolf.Name); // "Ghost"
140+
Console.WriteLine(wolf.FurColor); // Color.GhostWhite
141+
""";
142+
143+
public const string UnhandledTypeExample =
144+
"""
145+
IAnimal source = new Bird { Name = "Iago" };
146+
AnimalMapper.Map(source); // Throws UnhandledPolymorphicTypeException!
147+
148+
// Message: "Could not map type `Bird` to `IAnimalDto` - no matching destination type found."
149+
""";
150+
151+
public const string MitigationExample =
152+
"""
153+
[GenerateMapper]
154+
[Map<IAnimal, IAnimalDto>]
155+
[Map<Dog, DogDto>]
156+
[Map<Cat, CatDto>]
157+
[Map<Bird, BirdDto>] // Register the Bird type
158+
[UseMap<WolfMapper, Wolf, WolfDto>]
159+
public partial class AnimalMapper;
160+
""";
161+
162+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace AotObjectMapper.Abstractions.Exceptions;
2+
3+
/// Represents an exception thrown when a polymorphic type cannot be properly handled or resolved.
4+
public class UnhandledPolymorphicTypeException : Exception
5+
{
6+
///
7+
public UnhandledPolymorphicTypeException(string? message, Exception? innerException) : base(message, innerException)
8+
{ }
9+
10+
///
11+
public UnhandledPolymorphicTypeException(string? message) : base(message)
12+
{ }
13+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Collections.Generic;
2+
using Microsoft.CodeAnalysis;
3+
4+
namespace AotObjectMapper.Mapper;
5+
6+
public static class InheritanceUtils
7+
{
8+
public static Dictionary<ITypeSymbol, List<ITypeSymbol>> CreatePolymorphismMap(IEnumerable<ITypeSymbol> types)
9+
{
10+
Dictionary<ITypeSymbol, List<ITypeSymbol>> result = new (SymbolEqualityComparer.Default);
11+
12+
foreach (var type in types)
13+
{
14+
// Base class
15+
if (type.BaseType is not null && type.BaseType.SpecialType != SpecialType.System_Object)
16+
{
17+
if(result.TryGetValue(type.BaseType, out var list))
18+
list.Add(type);
19+
else
20+
result.Add(type.BaseType, [type]);
21+
}
22+
23+
// Interfaces
24+
foreach (var iface in type.Interfaces)
25+
{
26+
if(result.TryGetValue(iface, out var list))
27+
list.Add(type);
28+
else
29+
result.Add(iface, [type]);
30+
}
31+
}
32+
33+
return result;
34+
}
35+
}

src/AotObjectMapper.Mapper/Mapper/MapperGenerator.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@ private static void Execute(Compilation compilation, ImmutableArray<INamedTypeSy
6262

6363
var info = new MethodGenerationInfo((INamedTypeSymbol)mapper, sourceType, destinationType);
6464

65-
var populateCode = GeneratePopulationMethod(compilation, info);
66-
context.AddSource($"Populate_{destinationType.Name}_From_{sourceType.Name}_{i++}.g.cs", SourceText.From(populateCode, Encoding.UTF8));
65+
if (info.DestinationType.TypeKind is not TypeKind.Interface || !info.DestinationType.IsAbstract)
66+
{
67+
var populateCode = GeneratePopulationMethod(compilation, info);
68+
context.AddSource($"Populate_{mapper.Name}_{sourceType.Name}_To_{destinationType.Name}.g.cs", SourceText.From(populateCode, Encoding.UTF8));
69+
}
6770

6871
var code = GenerateMapperMethod(compilation, info);
6972
context.AddSource($"{mapper.Name}_{sourceType.Name}_To_{destinationType.Name}.g.cs", SourceText.From(code, Encoding.UTF8));
@@ -116,10 +119,15 @@ private static string GenerateMapperMethod(Compilation compilation, MethodGenera
116119

117120
string mapMethod;
118121

119-
if (info.PreserveReferences)
122+
if (info.DestinationType.TypeKind is TypeKind.Interface || info.DestinationType.IsAbstract)
123+
{
124+
mapMethod = $" return {Utils.NoInstanceTypeMapSwitchStatement("source", info)};";
125+
}
126+
else if (info.PreserveReferences)
120127
{
121128
mapMethod =
122129
$$"""
130+
{{Utils.InstanceTypeMapSwitchStatement("source", info)}}
123131
context ??= new MapperContext();
124132
125133
return context.GetOrMapObject<{{info.SourceType.Name}}, {{info.DestinationType.Name}}>(source, context, static () => {{Utils.BlankTypeConstructor(info.DestinationType)}}, {{info.DestinationType.Name}}_Utils.Populate);
@@ -130,6 +138,7 @@ private static string GenerateMapperMethod(Compilation compilation, MethodGenera
130138
{
131139
mapMethod =
132140
$$"""
141+
{{Utils.InstanceTypeMapSwitchStatement("source", info)}}
133142
context ??= new MapperContext();
134143
135144
// Pre Map Actions

0 commit comments

Comments
 (0)