Skip to content

Commit 8e56be8

Browse files
committed
Merge branch 'unify-mapping-attribute-support' into develop
2 parents ce26dbc + c61a3b0 commit 8e56be8

File tree

9 files changed

+178
-53
lines changed

9 files changed

+178
-53
lines changed

OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ public FakeViewModelLookupService() : base(null, typeof(object)) {
3939
Add(typeof(DefaultValueTopicViewModel));
4040
Add(typeof(DescendentSpecializedTopicViewModel));
4141
Add(typeof(DescendentTopicViewModel));
42+
Add(typeof(DisableMappingTopicViewModel));
4243
Add(typeof(FilteredTopicViewModel));
4344
Add(typeof(FlattenChildrenTopicViewModel));
4445
Add(typeof(InheritedPropertyTopicViewModel));
4546
Add(typeof(KeyOnlyTopicViewModel));
47+
Add(typeof(MapToParentTopicViewModel));
4648
Add(typeof(MethodBasedViewModel));
4749
Add(typeof(MinimumLengthPropertyTopicViewModel));
4850
Add(typeof(NestedTopicViewModel));

OnTopic.Tests/TopicMappingServiceTest.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,25 @@ public async Task Map_Dynamic_ReturnsNewModel() {
103103

104104
}
105105

106+
/*==========================================================================================================================
107+
| TEST: MAP: DISABLED PROPERTY: RETURNS NULL
108+
\-------------------------------------------------------------------------------------------------------------------------*/
109+
/// <summary>
110+
/// Establishes a <see cref="TopicMappingService"/> and tests whether it maps a property decorated with the <see
111+
/// cref="DisableMappingAttribute"/>.
112+
/// </summary>
113+
[TestMethod]
114+
public async Task Map_DisabledProperty_ReturnsNull() {
115+
116+
var topic = TopicFactory.Create("Test", "DisableMapping");
117+
118+
var viewModel = await _mappingService.MapAsync<DisableMappingTopicViewModel>(topic).ConfigureAwait(false);
119+
120+
Assert.IsNotNull(viewModel);
121+
Assert.IsNull(viewModel.Key);
122+
123+
}
124+
106125
/*==========================================================================================================================
107126
| TEST: MAP: PARENTS: RETURNS ASCENDENTS
108127
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -397,6 +416,32 @@ public async Task Map_Children_ReturnsMappedModel() {
397416
));
398417
}
399418

419+
/*==========================================================================================================================
420+
| TEST: MAP: MAP TO PARENT: RETURNS MAPPED MODEL
421+
\-------------------------------------------------------------------------------------------------------------------------*/
422+
/// <summary>
423+
/// Establishes a <see cref="TopicMappingService"/> and tests whether the resulting object's nested complex objects are
424+
/// property mapped with attribute values from the parent, based on their <see cref="MapToParentAttribute"/>
425+
/// configuration.
426+
/// </summary>
427+
[TestMethod]
428+
public async Task Map_MapToParent_ReturnsMappedModel() {
429+
430+
var topic = TopicFactory.Create("Test", "FlattenChildren");
431+
432+
topic.Attributes.SetValue("PrimaryKey", "Primary Key");
433+
topic.Attributes.SetValue("AlternateKey", "Alternate Key");
434+
topic.Attributes.SetValue("AncillaryKey", "Ancillary Key");
435+
topic.Attributes.SetValue("AliasedKey", "Aliased Key");
436+
437+
var target = await _mappingService.MapAsync<MapToParentTopicViewModel>(topic).ConfigureAwait(false);
438+
439+
Assert.AreEqual<string>("Test", target.Primary.Key);
440+
Assert.AreEqual<string>("Aliased Key", target.Alternate.Key);
441+
Assert.AreEqual<string>("Ancillary Key", target.Ancillary.Key);
442+
443+
}
444+
400445
/*==========================================================================================================================
401446
| TEST: MAP: TOPIC REFERENCES: RETURNS MAPPED MODEL
402447
\-------------------------------------------------------------------------------------------------------------------------*/
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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: DISABLE MAPPING
12+
\---------------------------------------------------------------------------------------------------------------------------*/
13+
/// <summary>
14+
/// Provides a simple view model with a single property (<see cref="Key") which is annotated with the <see
15+
/// cref="DisableMappingAttribute"/>.
16+
/// </summary>
17+
/// <remarks>
18+
/// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
19+
/// </remarks>
20+
public class DisableMappingTopicViewModel {
21+
22+
[DisableMapping]
23+
public string? Key { get; set; }
24+
25+
} //Class
26+
} //Namespace
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/Internal/Mapping/PropertyConfiguration.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public class PropertyConfiguration {
4747
/// instances.
4848
/// </summary>
4949
/// <param name="property">The <see cref="PropertyInfo"/> instance to check for <see cref="Attribute"/> values.</param>
50-
public PropertyConfiguration(PropertyInfo property) {
50+
/// <param name="attributePrefix">The prefix to apply to the attributes.</param>
51+
public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "") {
5152

5253
/*------------------------------------------------------------------------------------------------------------------------
5354
| Validate parameters
@@ -62,7 +63,8 @@ public PropertyConfiguration(PropertyInfo property) {
6263
/*------------------------------------------------------------------------------------------------------------------------
6364
| Set default values
6465
\-----------------------------------------------------------------------------------------------------------------------*/
65-
AttributeKey = property.Name;
66+
AttributeKey = attributePrefix + property.Name;
67+
AttributePrefix = attributePrefix;
6668
DefaultValue = null;
6769
InheritValue = false;
6870
RelationshipKey = AttributeKey;
@@ -78,9 +80,9 @@ public PropertyConfiguration(PropertyInfo property) {
7880
\-----------------------------------------------------------------------------------------------------------------------*/
7981
GetAttributeValue<DefaultValueAttribute>(property, a => DefaultValue = a.Value);
8082
GetAttributeValue<InheritAttribute>(property, a => InheritValue = true);
81-
GetAttributeValue<AttributeKeyAttribute>(property, a => AttributeKey = a.Value);
83+
GetAttributeValue<AttributeKeyAttribute>(property, a => AttributeKey = attributePrefix + a.Value);
8284
GetAttributeValue<MapToParentAttribute>(property, a => MapToParent = true);
83-
GetAttributeValue<MapToParentAttribute>(property, a => AttributePrefix = a.AttributePrefix?? property.Name);
85+
GetAttributeValue<MapToParentAttribute>(property, a => AttributePrefix += (a.AttributePrefix?? property.Name));
8486
GetAttributeValue<FollowAttribute>(property, a => CrawlRelationships = a.Relationships);
8587
GetAttributeValue<FlattenAttribute>(property, a => FlattenChildren = true);
8688
GetAttributeValue<MetadataAttribute>(property, a => MetadataKey = a.Key);
@@ -177,13 +179,25 @@ public PropertyConfiguration(PropertyInfo property) {
177179
/// (such as <see cref="IList{T}"/>). The <see cref="MapToParentAttribute"/> allows this behavior to be overwritten.
178180
/// When this occurs, the properties of the referenced object are treated as attributes on the topic. To distinguish
179181
/// them from properties on the parent topic, they are (by default) prefixed with the name of the property that the
180-
/// complex object is assigned to. Optionally, however, this can be overridden, or set to <see cref="String.Empty"/>.
182+
/// complex object is assigned to. Optionally, however, this can be overridden, or even set to <see
183+
/// cref="String.Empty"/>. In the latter case, the properties on the child object will be treated as synonymous with the
184+
/// properties on the parent object. And, in fact, the same property could even be applied to <i>both</i> the child
185+
/// object <i>and</i> the parent object—though this probably doesn't make much sense from a modeling perspective.
186+
/// </para>
187+
/// <para>
188+
/// Be aware that the <see cref="AttributePrefix"/> should <i>only</i> apply to actual attributes on the mapped <see
189+
/// cref="Topic"/> entity; it is <i>not</i> intended to be applied to e.g. collections or relationships.
181190
/// </para>
182191
/// <para>
183192
/// The <see cref="AttributePrefix"/> property corresponds to the <see cref="MapToParentAttribute.AttributePrefix"/>
184193
/// property. It can be assigned by decorating a DTO property with e.g. <c>[MapToParent(AttributePrefix
185194
/// = "AlternateAttributeKey")]</c>.
186195
/// </para>
196+
/// <para>
197+
/// The <see cref="AttributePrefix"/> can be compounded. That is, if a complex object is added to another complex object
198+
/// using <see cref="MapToParentAttribute"/>, then the <see cref="AttributePrefix"/> will reflect the combination of
199+
/// those two prefixes. This allows potentially very deep object models.
200+
/// </para>
187201
/// </remarks>
188202
public string? AttributePrefix { get; set; }
189203

OnTopic/Mapping/BindingModelValidator.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,10 @@ static internal class BindingModelValidator {
8787
/// <param name="contentTypeDescriptor">
8888
/// The <see cref="ContentTypeDescriptor"/> object against which to validate the model.
8989
/// </param>
90-
/// <param name="attributePrefix">The optional prefix to apply to the attributes.</param>
9190
static internal void ValidateModel(
9291
[AllowNull]Type sourceType,
9392
[AllowNull]MemberInfoCollection<PropertyInfo> properties,
94-
[AllowNull]ContentTypeDescriptor contentTypeDescriptor,
95-
string? attributePrefix = null
93+
[AllowNull]ContentTypeDescriptor contentTypeDescriptor
9694
) {
9795

9896
/*------------------------------------------------------------------------------------------------------------------------
@@ -113,7 +111,7 @@ static internal void ValidateModel(
113111
| Validate
114112
\-----------------------------------------------------------------------------------------------------------------------*/
115113
foreach (var property in properties) {
116-
ValidateProperty(sourceType, property, contentTypeDescriptor, attributePrefix);
114+
ValidateProperty(sourceType, property, contentTypeDescriptor);
117115
}
118116

119117
/*------------------------------------------------------------------------------------------------------------------------
@@ -141,12 +139,10 @@ static internal void ValidateModel(
141139
/// <param name="contentTypeDescriptor">
142140
/// The <see cref="ContentTypeDescriptor"/> object against which to validate the model.
143141
/// </param>
144-
/// <param name="attributePrefix">The optional prefix to apply to the attributes.</param>
145142
static internal void ValidateProperty(
146143
[AllowNull]Type sourceType,
147144
[AllowNull]PropertyInfo property,
148-
[AllowNull]ContentTypeDescriptor contentTypeDescriptor,
149-
string? attributePrefix
145+
[AllowNull]ContentTypeDescriptor contentTypeDescriptor
150146
) {
151147

152148
/*------------------------------------------------------------------------------------------------------------------------
@@ -161,7 +157,7 @@ static internal void ValidateProperty(
161157
\-----------------------------------------------------------------------------------------------------------------------*/
162158
var propertyType = property.PropertyType;
163159
var configuration = new PropertyConfiguration(property);
164-
var compositeAttributeKey = attributePrefix + configuration.AttributeKey;
160+
var compositeAttributeKey = configuration.AttributeKey;
165161
var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey);
166162
var childRelationships = new[] { RelationshipType.Children, RelationshipType.NestedTopics };
167163
var relationships = new[] { RelationshipType.Relationship, RelationshipType.IncomingRelationship };
@@ -182,8 +178,7 @@ static internal void ValidateProperty(
182178
ValidateModel(
183179
propertyType,
184180
childProperties,
185-
contentTypeDescriptor,
186-
attributePrefix + configuration.AttributePrefix
181+
contentTypeDescriptor
187182
);
188183
return;
189184
}

OnTopic/Mapping/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,8 @@ 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-
107-
### `ReverseTopicMappingService`
108-
- **`[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`.
106+
- **`[MapToParent]`**: Allows the attributes of a topic to be applied to a child complex object, optionally including a prefix.
107+
- **`[DisableMapping]`**: Prevents the mapping service from attempting to map the property to an attribute.
110108

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

0 commit comments

Comments
 (0)