Skip to content

Commit 9122325

Browse files
committed
Merge branch 'release/4.1.1'
2 parents 73829be + 6616794 commit 9122325

7 files changed

+79
-46
lines changed

OnTopic.TestDoubles/StubTopicRepository.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ private Topic CreateFakeData() {
222222

223223
var contactContentType = TopicFactory.Create("Contact", "ContentTypeDescriptor", contentTypes);
224224

225-
addAttribute(contactContentType, "Email");
225+
addAttribute(contactContentType, "Name");
226226
addAttribute(contactContentType, "AlternateEmail");
227227
addAttribute(contactContentType, "BillingContactEmail");
228228

OnTopic.Tests/BindingModels/ContactTopicBindingModel.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace OnTopic.Tests.BindingModels {
1818
/// </remarks>
1919
public class ContactTopicBindingModel {
2020

21-
public string? Email { get; set; }
21+
public string? Name { get; set; }
2222

2323
} //Class
2424
} //Namespace
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
7+
namespace OnTopic.Tests.BindingModels {
8+
9+
/*============================================================================================================================
10+
| BINDING MODEL: EMAIL
11+
\---------------------------------------------------------------------------------------------------------------------------*/
12+
/// <summary>
13+
/// Provides a minimal implementation of a custom topic binding model with a single property intended to be called from
14+
/// <see cref="MapToParentTopicBindingModel"/>.
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 EmailTopicBindingModel {
20+
21+
public string? Email { get; set; }
22+
23+
} //Class
24+
} //Namespace

OnTopic.Tests/BindingModels/MapToParentTopicBindingModel.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ public class MapToParentTopicBindingModel : BasicTopicBindingModel {
2222
public ContactTopicBindingModel PrimaryContact { get; set; } = new ContactTopicBindingModel();
2323

2424
[MapToParent(AttributePrefix="Alternate")]
25-
public ContactTopicBindingModel AlternateContact { get; set; } = new ContactTopicBindingModel();
25+
public EmailTopicBindingModel AlternateContact { get; set; } = new EmailTopicBindingModel();
2626

2727
[MapToParent]
28-
public ContactTopicBindingModel BillingContact { get; set; } = new ContactTopicBindingModel();
28+
public EmailTopicBindingModel BillingContact { get; set; } = new EmailTopicBindingModel();
2929

3030
} //Class
3131
} //Namespace

OnTopic.Tests/ReverseTopicMappingServiceTest.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public async Task Map_Existing_ReturnsUpdatedTopic() {
139139
target.Title = "Original Attribute";
140140
target.DefaultValue = "Hello";
141141
target.IsRequired = true;
142-
target.IsExtendedAttribute = false;
142+
target.IsExtendedAttribute= false;
143143
target.Description = "Original Description";
144144

145145
target = (TextAttribute?)await mappingService.MapAsync(bindingModel, target).ConfigureAwait(false);
@@ -170,14 +170,14 @@ public async Task Map_ComplexObject_ReturnsFlattenedTopic() {
170170
ContentType = "Contact"
171171
};
172172

173-
bindingModel.PrimaryContact.Email = "[email protected]";
174-
bindingModel.AlternateContact.Email = "[email protected]";
175-
bindingModel.BillingContact.Email = "[email protected]";
176-
173+
bindingModel.PrimaryContact.Name = "Jeremy";
174+
bindingModel.AlternateContact.Email = "[email protected]";
175+
bindingModel.BillingContact.Email = "[email protected]";
176+
177177
var target = (Topic?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false);
178178

179179
Assert.IsNotNull(target);
180-
Assert.AreEqual<string>("[email protected]", target.Attributes.GetValue("Email"));
180+
Assert.AreEqual<string>("Jeremy", target.Attributes.GetValue("Name"));
181181
Assert.AreEqual<string>("[email protected]", target.Attributes.GetValue("AlternateEmail"));
182182
Assert.AreEqual<string>("[email protected]", target.Attributes.GetValue("BillingContactEmail"));
183183

OnTopic/Mapping/BindingModelValidator.cs

+43-34
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,12 @@ 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 prefix to apply to the attributes.</param>
9091
static internal void ValidateModel(
9192
[AllowNull]Type sourceType,
9293
[AllowNull]MemberInfoCollection<PropertyInfo> properties,
93-
[AllowNull]ContentTypeDescriptor contentTypeDescriptor
94+
[AllowNull]ContentTypeDescriptor contentTypeDescriptor,
95+
[AllowNull]string attributePrefix = ""
9496
) {
9597

9698
/*------------------------------------------------------------------------------------------------------------------------
@@ -111,7 +113,7 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
111113
| Validate
112114
\-----------------------------------------------------------------------------------------------------------------------*/
113115
foreach (var property in properties) {
114-
ValidateProperty(sourceType, property, contentTypeDescriptor);
116+
ValidateProperty(sourceType, property, contentTypeDescriptor, attributePrefix);
115117
}
116118

117119
/*------------------------------------------------------------------------------------------------------------------------
@@ -139,10 +141,12 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
139141
/// <param name="contentTypeDescriptor">
140142
/// The <see cref="ContentTypeDescriptor"/> object against which to validate the model.
141143
/// </param>
144+
/// <param name="attributePrefix">The prefix to apply to the attributes.</param>
142145
static internal void ValidateProperty(
143146
[AllowNull]Type sourceType,
144147
[AllowNull]PropertyInfo property,
145-
[AllowNull]ContentTypeDescriptor contentTypeDescriptor
148+
[AllowNull]ContentTypeDescriptor contentTypeDescriptor,
149+
[AllowNull]string attributePrefix = ""
146150
) {
147151

148152
/*------------------------------------------------------------------------------------------------------------------------
@@ -156,7 +160,7 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
156160
| Define variables
157161
\-----------------------------------------------------------------------------------------------------------------------*/
158162
var propertyType = property.PropertyType;
159-
var configuration = new PropertyConfiguration(property);
163+
var configuration = new PropertyConfiguration(property, attributePrefix);
160164
var compositeAttributeKey = configuration.AttributeKey;
161165
var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey);
162166
var childRelationships = new[] { RelationshipType.Children, RelationshipType.NestedTopics };
@@ -178,7 +182,8 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
178182
ValidateModel(
179183
propertyType,
180184
childProperties,
181-
contentTypeDescriptor
185+
contentTypeDescriptor,
186+
configuration.AttributePrefix
182187
);
183188
return;
184189
}
@@ -219,9 +224,9 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
219224
\-----------------------------------------------------------------------------------------------------------------------*/
220225
if (attributeDescriptor == null) {
221226
throw new InvalidOperationException(
222-
$"A {nameof(sourceType)} object was provided with a content type set to {contentTypeDescriptor.Key}'. This " +
227+
$"A '{nameof(sourceType)}' object was provided with a content type set to '{contentTypeDescriptor.Key}'. This " +
223228
$"content type does not contain an attribute named '{compositeAttributeKey}', as requested by the " +
224-
$"{configuration.Property.Name} property. If this property is not intended to be mapped by the " +
229+
$"'{configuration.Property.Name}' property. If this property is not intended to be mapped by the " +
225230
$"{nameof(ReverseTopicMappingService)}, then it should be decorated with {nameof(DisableMappingAttribute)}."
226231
);
227232
}
@@ -242,12 +247,12 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
242247
listType != null
243248
) {
244249
throw new InvalidOperationException(
245-
$"The {property.Name} on the {sourceType.Name} has been determined to be a {configuration.RelationshipType}, but " +
246-
$"the generic type {listType.Name} does not implement the {nameof(ITopicBindingModel)} interface. This is " +
247-
$"required for binding models. If this collection is not intended to be mapped to " +
248-
$"{ModelType.NestedTopic} then update the definition in the associated {nameof(ContentTypeDescriptor)}. If this " +
249-
$"collection is not intended to be mapped at all, include the {nameof(DisableMappingAttribute)} to exclude it from " +
250-
$"mapping."
250+
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
251+
$"{configuration.RelationshipType}, but the generic type '{listType.Name}' does not implement the " +
252+
$"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " +
253+
$"to be mapped as a {ModelType.NestedTopic} then update the definition in the associated " +
254+
$"{nameof(ContentTypeDescriptor)}. If this collection is not intended to be mapped at all, include the " +
255+
$"{nameof(DisableMappingAttribute)} to exclude it from mapping."
251256
);
252257
}
253258

@@ -259,11 +264,12 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
259264
!typeof(IRelatedTopicBindingModel).IsAssignableFrom(propertyType)
260265
) {
261266
throw new InvalidOperationException(
262-
$"The {property.Name} on the {sourceType.Name} has been determined to be a {ModelType.Reference}, but " +
263-
$"the generic type {propertyType.Name} does not implement the {nameof(IRelatedTopicBindingModel)} interface. This " +
264-
$"is required for references. If this property is not intended to be mapped to {ModelType.Reference} then update " +
265-
$"the definition in the associated {nameof(ContentTypeDescriptor)}. If this property is not intended to be mapped " +
266-
$"at all, include the {nameof(DisableMappingAttribute)} to exclude it from mapping."
267+
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
268+
$"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " +
269+
$"{nameof(IRelatedTopicBindingModel)} interface. This is required for references. If this property is not intended " +
270+
$"to be mapped as a {ModelType.Reference} then update the definition in the associated " +
271+
$"{nameof(ContentTypeDescriptor)}. If this property is not intended to be mapped at all, include the " +
272+
$"{nameof(DisableMappingAttribute)} to exclude it from mapping."
267273
);
268274
}
269275

@@ -275,12 +281,13 @@ [AllowNull]ContentTypeDescriptor contentTypeDescriptor
275281
!configuration.AttributeKey.EndsWith("Id", StringComparison.InvariantCulture)
276282
) {
277283
throw new InvalidOperationException(
278-
$"The {property.Name} on the {sourceType.Name} has been determined to be a topic reference, but the generic type " +
279-
$"{compositeAttributeKey} does not end in <c>Id</c>. By convention, all topic reference are expected to end " +
280-
$"in <c>Id</c>. To keep the property name set to {propertyType.Name}, use the {nameof(AttributeKeyAttribute)} to " +
281-
$"specify the name of the topic reference this should map to. If this property is not intended to be mapped at " +
282-
$"all, include the {nameof(DisableMappingAttribute)}. If the {contentTypeDescriptor.Key} defines a topic reference " +
283-
$"attribute that doesn't follow this convention, then it should be updated."
284+
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a topic reference, but " +
285+
$"the generic type '{compositeAttributeKey}' does not end in <c>Id</c>. By convention, all topic reference are " +
286+
$"expected to end in <c>Id</c>. To keep the property name set to '{propertyType.Name}', use the " +
287+
$"{nameof(AttributeKeyAttribute)} to specify the name of the topic reference this should map to. If this property " +
288+
$"is not intended to be mapped at all, include the {nameof(DisableMappingAttribute)}. If the " +
289+
$"'{contentTypeDescriptor.Key}' defines a topic reference attribute that doesn't follow this convention, then it " +
290+
$"should be updated."
284291
);
285292
}
286293

@@ -328,8 +335,9 @@ [AllowNull]Type listType
328335
\-----------------------------------------------------------------------------------------------------------------------*/
329336
if (!typeof(IList).IsAssignableFrom(property.PropertyType)) {
330337
throw new InvalidOperationException(
331-
$"The {property.Name} on the {sourceType.Name} maps to a relationship attribute {attributeDescriptor.Key}, but does" +
332-
$"not implement {nameof(IList)}. Relationships must implement {nameof(IList)} or derive from a collection that does."
338+
$"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " +
339+
$"'{attributeDescriptor.Key}', but does not implement {nameof(IList)}. Relationships must implement " +
340+
$"{nameof(IList)} or derive from a collection that does."
333341
);
334342
}
335343

@@ -338,9 +346,9 @@ [AllowNull]Type listType
338346
\-----------------------------------------------------------------------------------------------------------------------*/
339347
if (!new[] { RelationshipType.Any, RelationshipType.Relationship }.Contains(configuration.RelationshipType)) {
340348
throw new InvalidOperationException(
341-
$"The {property.Name} on the {sourceType.Name} maps to a relationship attribute {attributeDescriptor.Key}, but is " +
342-
$"configured as a {configuration.RelationshipType}. The property should be flagged as either " +
343-
$"{nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}."
349+
$"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " +
350+
$"'{attributeDescriptor.Key}', but is configured as a {configuration.RelationshipType}. The property should be " +
351+
$"flagged as either {nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}."
344352
);
345353
}
346354

@@ -349,11 +357,12 @@ [AllowNull]Type listType
349357
\-----------------------------------------------------------------------------------------------------------------------*/
350358
if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) {
351359
throw new InvalidOperationException(
352-
$"The {property.Name} on the {sourceType.Name} has been determined to be a {configuration.RelationshipType}, but " +
353-
$"the generic type {listType.Name} does not implement the {nameof(IRelatedTopicBindingModel)} interface. This is " +
354-
$"required for binding models. If this collection is not intended to be mapped to {configuration.RelationshipType} " +
355-
$"then update the definition in the associated {nameof(ContentTypeDescriptor)}. If this collection is not intended " +
356-
$"to be mapped at all, include the {nameof(DisableMappingAttribute)} to exclude it from mapping."
360+
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
361+
$"{configuration.RelationshipType}, but the generic type '{listType?.Name}' does not implement the " +
362+
$"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " +
363+
$"intended to be mapped as a {configuration.RelationshipType} then update the definition in the associated " +
364+
$"{nameof(ContentTypeDescriptor)}. If this collection is not intended to be mapped at all, include the " +
365+
$"{nameof(DisableMappingAttribute)} to exclude it from mapping."
357366
);
358367
}
359368

OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) {
213213
var properties = _typeCache.GetMembers<PropertyInfo>(source.GetType());
214214
var contentTypeDescriptor = _contentTypeDescriptors.GetTopic(target.ContentType);
215215

216-
BindingModelValidator.ValidateModel(source.GetType(), properties, contentTypeDescriptor);
216+
BindingModelValidator.ValidateModel(source.GetType(), properties, contentTypeDescriptor, attributePrefix);
217217

218218
/*------------------------------------------------------------------------------------------------------------------------
219219
| Loop through properties, mapping each one
@@ -608,7 +608,7 @@ TopicCollection targetList
608608
var topicTask = await Task.WhenAny(taskQueue).ConfigureAwait(false);
609609
taskQueue.Remove(topicTask);
610610
var topic = await topicTask.ConfigureAwait(false);
611-
if (!targetList.Contains(topic.Key)) {
611+
if (topic != null && !targetList.Contains(topic.Key)) {
612612
targetList.Add(topic);
613613
}
614614
}

0 commit comments

Comments
 (0)