Skip to content

Commit 58949b1

Browse files
committed
Introduced [MapToParent] functionality to TopicMappingService
The `[MapToParent]` capability was originally introduced as part of the `ReverseTopicMappingService`, to allow complex binding models to be mapped back to a single `Topic` entity. There's no reason the opposite shouldn't also be supported—nor is it all that difficult to add this feature. This commit introduces the initial versionof this.
1 parent 751c259 commit 58949b1

5 files changed

Lines changed: 84 additions & 7 deletions

File tree

OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public FakeViewModelLookupService() : base(null, typeof(object)) {
4343
Add(typeof(FlattenChildrenTopicViewModel));
4444
Add(typeof(InheritedPropertyTopicViewModel));
4545
Add(typeof(KeyOnlyTopicViewModel));
46+
Add(typeof(MapToParentTopicViewModel));
4647
Add(typeof(MethodBasedViewModel));
4748
Add(typeof(MinimumLengthPropertyTopicViewModel));
4849
Add(typeof(NestedTopicViewModel));

OnTopic.Tests/TopicMappingServiceTest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,32 @@ public async Task Map_Children_ReturnsMappedModel() {
397397
));
398398
}
399399

400+
/*==========================================================================================================================
401+
| TEST: MAP: MAP TO PARENT: RETURNS MAPPED MODEL
402+
\-------------------------------------------------------------------------------------------------------------------------*/
403+
/// <summary>
404+
/// Establishes a <see cref="TopicMappingService"/> and tests whether the resulting object's nested complex objects are
405+
/// property mapped with attribute values from the parent, based on their <see cref="MapToParentAttribute"/>
406+
/// configuration.
407+
/// </summary>
408+
[TestMethod]
409+
public async Task Map_MapToParent_ReturnsMappedModel() {
410+
411+
var topic = TopicFactory.Create("Test", "FlattenChildren");
412+
413+
topic.Attributes.SetValue("PrimaryKey", "Primary Key");
414+
topic.Attributes.SetValue("AlternateKey", "Alternate Key");
415+
topic.Attributes.SetValue("AncillaryKey", "Ancillary Key");
416+
topic.Attributes.SetValue("AliasedKey", "Aliased Key");
417+
418+
var target = await _mappingService.MapAsync<MapToParentTopicViewModel>(topic).ConfigureAwait(false);
419+
420+
Assert.AreEqual<string>("Test", target.Primary.Key);
421+
Assert.AreEqual<string>("Aliased Key", target.Alternate.Key);
422+
Assert.AreEqual<string>("Ancillary Key", target.Ancillary.Key);
423+
424+
}
425+
400426
/*==========================================================================================================================
401427
| TEST: MAP: TOPIC REFERENCES: RETURNS MAPPED MODEL
402428
\-------------------------------------------------------------------------------------------------------------------------*/
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using OnTopic.Mapping.Annotations;
7+
8+
namespace OnTopic.Tests.ViewModels {
9+
10+
/*============================================================================================================================
11+
| VIEW MODEL: MAP TO PARENT
12+
\---------------------------------------------------------------------------------------------------------------------------*/
13+
/// <summary>
14+
/// Provides a strongly-typed data transfer object for testing views with the <see cref="MapToParentAttribute"/>.
15+
/// </summary>
16+
/// <remarks>
17+
/// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
18+
/// </remarks>
19+
public class MapToParentTopicViewModel {
20+
21+
[MapToParent(AttributePrefix = "")]
22+
public KeyOnlyTopicViewModel? Primary { get; set; } = new KeyOnlyTopicViewModel();
23+
24+
[MapToParent(AttributePrefix = "Aliased")]
25+
public KeyOnlyTopicViewModel? Alternate { get; set; } = new KeyOnlyTopicViewModel();
26+
27+
[MapToParent]
28+
public KeyOnlyTopicViewModel? Ancillary { get; set; } = new KeyOnlyTopicViewModel();
29+
30+
} //Class
31+
} //Namespace

OnTopic/Mapping/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ To support the mapping, a variety of `Attribute` classes are provided for decora
103103
- **`[Relationship(key, type)]`**: For a collection, optionally specifies the name of the key to look for, instead of the property name, and the relationship type, in case the key name is ambiguous.
104104
- **`[Follow(relationships)]`**: Instructs the code to populate the specified relationships on any view models within a collection.
105105
- **`[Flatten]`**: Includes all descendants for every item in the collection. If the collection enforces uniqueness, duplicates will be removed.
106+
- **`[MapToParent]`**: Allows the attributes of a topic to be applied to a child complex object, optionally including a prefix.
106107

107108
### `ReverseTopicMappingService`
108109
- **`[DisableMapping]`**: Prevents the `ReverseTopicMappingService` from attempting to map the property back to the target `Topic`.
109-
- **`[MapToParent]`**: Allows the `ReverseTopicMappingService` to map a complex property type back to a `Topic`.
110110

111111
### Example
112112
The following is an example of a data transfer object that implements the above attributes:

OnTopic/Mapping/TopicMappingService.cs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService
8585
/// <param name="topic">The <see cref="Topic"/> entity to derive the data from.</param>
8686
/// <param name="relationships">Determines what relationships the mapping should follow, if any.</param>
8787
/// <param name="cache">A cache to keep track of already-mapped object instances.</param>
88+
/// <param name="attributePrefix">The prefix to apply to the attributes.</param>
8889
/// <returns>An instance of the dynamically determined View Model with properties appropriately mapped.</returns>
89-
private async Task<object?> MapAsync(Topic? topic, Relationships relationships, ConcurrentDictionary<int, object> cache) {
90+
private async Task<object?> MapAsync(
91+
Topic? topic,
92+
Relationships relationships,
93+
ConcurrentDictionary<int, object> cache,
94+
string? attributePrefix = null
95+
) {
9096

9197
/*------------------------------------------------------------------------------------------------------------------------
9298
| Validate input
@@ -119,7 +125,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService
119125
/*------------------------------------------------------------------------------------------------------------------------
120126
| Provide mapping
121127
\-----------------------------------------------------------------------------------------------------------------------*/
122-
return await MapAsync(topic, target, relationships, cache).ConfigureAwait(false);
128+
return await MapAsync(topic, target, relationships, cache, attributePrefix).ConfigureAwait(false);
123129

124130
}
125131

@@ -150,6 +156,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService
150156
/// <param name="target">The target object to map the data to.</param>
151157
/// <param name="relationships">Determines what relationships the mapping should follow, if any.</param>
152158
/// <param name="cache">A cache to keep track of already-mapped object instances.</param>
159+
/// <param name="attributePrefix">The prefix to apply to the attributes.</param>
153160
/// <remarks>
154161
/// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with
155162
/// recursion in case <see cref="Topic"/> is referred to multiple times (e.g., a <c>Children</c> collection with
@@ -162,7 +169,8 @@ private async Task<object> MapAsync(
162169
Topic? topic,
163170
object target,
164171
Relationships relationships,
165-
ConcurrentDictionary<int, object> cache
172+
ConcurrentDictionary<int, object> cache,
173+
string? attributePrefix = null
166174
) {
167175

168176
/*------------------------------------------------------------------------------------------------------------------------
@@ -194,7 +202,7 @@ ConcurrentDictionary<int, object> cache
194202
\-----------------------------------------------------------------------------------------------------------------------*/
195203
var taskQueue = new List<Task>();
196204
foreach (var property in _typeCache.GetMembers<PropertyInfo>(target.GetType())) {
197-
taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache));
205+
taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache, attributePrefix));
198206
}
199207
await Task.WhenAll(taskQueue.ToArray()).ConfigureAwait(false);
200208

@@ -217,12 +225,14 @@ ConcurrentDictionary<int, object> cache
217225
/// <param name="relationships">Determines what relationships the mapping should follow, if any.</param>
218226
/// <param name="property">Information related to the current property.</param>
219227
/// <param name="cache">A cache to keep track of already-mapped object instances.</param>
228+
/// <param name="attributePrefix">The prefix to apply to the attributes.</param>
220229
protected async Task SetPropertyAsync(
221230
Topic source,
222231
object target,
223232
Relationships relationships,
224233
PropertyInfo property,
225-
ConcurrentDictionary<int, object> cache
234+
ConcurrentDictionary<int, object> cache,
235+
string? attributePrefix = null
226236
) {
227237

228238
/*------------------------------------------------------------------------------------------------------------------------
@@ -237,7 +247,7 @@ ConcurrentDictionary<int, object> cache
237247
/*------------------------------------------------------------------------------------------------------------------------
238248
| Establish per-property variables
239249
\-----------------------------------------------------------------------------------------------------------------------*/
240-
var configuration = new PropertyConfiguration(property);
250+
var configuration = new PropertyConfiguration(property, attributePrefix);
241251
var topicReferenceId = source.Attributes.GetInteger($"{configuration.AttributeKey}Id", 0);
242252

243253
if (topicReferenceId == 0 && configuration.AttributeKey.EndsWith("Id", StringComparison.InvariantCultureIgnoreCase)) {
@@ -274,6 +284,15 @@ ConcurrentDictionary<int, object> cache
274284
await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false);
275285
}
276286
}
287+
else if (configuration.MapToParent) {
288+
await MapAsync(
289+
source,
290+
property.GetValue(target),
291+
relationships,
292+
cache,
293+
configuration.AttributePrefix
294+
).ConfigureAwait(false);
295+
}
277296

278297
/*------------------------------------------------------------------------------------------------------------------------
279298
| Validate fields

0 commit comments

Comments
 (0)