Skip to content

Commit bbc035d

Browse files
committed
Merge branch 'maintenance/mapping-exceptions' into develop
Introduced the more specific `InvalidTypeException` and `MappingModelValidationException`, both of which derive from the existing `TopicMappingException`, in order to provide (even more specific) exceptions for predictable errors with the various topic mapping services. Currently, the `InvalidTypeException` occurs when the target topic view model determined by the `Topic.ContentType` cannot be retrieved from the `ITypeLookupService`, while the `MappingModelValidationException` occurs when the `ReverseTopicMappingService` discovers a disconnect between the source model and the target `Topic` which might result in an unexpected data integrity or data loss issue when that `Topic` is `Save()`d to the persistence layer.
2 parents 164de05 + 4d23511 commit bbc035d

File tree

7 files changed

+148
-18
lines changed

7 files changed

+148
-18
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System;
7+
using System.Runtime.Serialization;
8+
using OnTopic.Internal.Diagnostics;
9+
using OnTopic.Lookup;
10+
11+
namespace OnTopic.Mapping {
12+
13+
/*============================================================================================================================
14+
| CLASS: INVALID TYPE EXCEPTION
15+
\---------------------------------------------------------------------------------------------------------------------------*/
16+
/// <summary>
17+
/// The <see cref="InvalidTypeException"/> is thrown when an <see cref="ITopicMappingService"/> implementation requests a
18+
/// target type that cannot be located using the supplied <see cref="ITypeLookupService"/>.
19+
/// </summary>
20+
/// <remarks>
21+
/// Having one (base) class used for all expected exceptions from the <see cref="ITopicMappingService"/> and other mapping
22+
/// service interfaces allows implementors to capture all exceptions—while, potentially, catching more specific exceptions
23+
/// based on derived classes, if we discover the need for more specific exceptions.
24+
/// </remarks>
25+
[Serializable]
26+
public class InvalidTypeException: TopicMappingException {
27+
28+
/*==========================================================================================================================
29+
| CONSTRUCTOR: INVALID TYPE EXCEPTION
30+
\-------------------------------------------------------------------------------------------------------------------------*/
31+
/// <summary>
32+
/// Initializes a new <see cref="InvalidTypeException" /> instance.
33+
/// </summary>
34+
public InvalidTypeException() : base() { }
35+
36+
/// <summary>
37+
/// Initializes a new <see cref="InvalidTypeException" /> instance with a specific error message.
38+
/// </summary>
39+
/// <param name="message">The message to display for this exception.</param>
40+
public InvalidTypeException(string message) : base(message) { }
41+
42+
/// <summary>
43+
/// Initializes a new <see cref="InvalidTypeException" /> instance with a specific error message and nested exception.
44+
/// </summary>
45+
/// <param name="message">The message to display for this exception.</param>
46+
/// <param name="innerException">The reference to the original, underlying exception.</param>
47+
public InvalidTypeException(string message, Exception innerException) : base(message, innerException) { }
48+
49+
/// <summary>
50+
/// Instantiates a new <see cref="InvalidTypeException"/> instance for serialization.
51+
/// </summary>
52+
/// <param name="info">A <see cref="SerializationInfo"/> instance with details about the serialization requirements.</param>
53+
/// <param name="context">A <see cref="StreamingContext"/> instance with details about the request context.</param>
54+
/// <returns>A new <see cref="InvalidKeyException"/> instance.</returns>
55+
protected InvalidTypeException(SerializationInfo info, StreamingContext context) : base(info, context) {
56+
Contract.Requires(info);
57+
}
58+
59+
} //Class
60+
} //Namespace
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System;
7+
using System.Runtime.Serialization;
8+
using OnTopic.Internal.Diagnostics;
9+
using OnTopic.Mapping.Reverse;
10+
using OnTopic.Metadata;
11+
12+
namespace OnTopic.Mapping {
13+
14+
/*============================================================================================================================
15+
| CLASS: MAPPING MODEL VALIDATION EXCEPTION
16+
\---------------------------------------------------------------------------------------------------------------------------*/
17+
/// <summary>
18+
/// The <see cref="MappingModelValidationException"/> is thrown when an <see cref="IReverseTopicMappingService"/>
19+
/// implementation is provided a model that cannot be reliably mapped back to the target <see cref="Topic"/>, thus
20+
/// introducing potential data integrity issues.
21+
/// </summary>
22+
/// <remarks>
23+
/// The read-only mapping services, such as <see cref="ITopicMappingService"/>, are generally designed to be forgiving of
24+
/// mismatches. By contrast, because the <see cref="IReverseTopicMappingService"/> is intended to update the persistence
25+
/// store, it is generally designed to fail if there are any discrepancies between the source model and the target <see cref
26+
/// ="ContentTypeDescriptor"/>. Given this, the <see cref="MappingModelValidationException"/> is expected to be thrown for
27+
/// validation errors caused by e.g. the <see cref="ReverseTopicMappingService"/>.
28+
/// </remarks>
29+
[Serializable]
30+
public class MappingModelValidationException: TopicMappingException {
31+
32+
/*==========================================================================================================================
33+
| CONSTRUCTOR: MAPPING MODEL VALIDATION EXCEPTION
34+
\-------------------------------------------------------------------------------------------------------------------------*/
35+
/// <summary>
36+
/// Initializes a new <see cref="MappingModelValidationException" /> instance.
37+
/// </summary>
38+
public MappingModelValidationException() : base() { }
39+
40+
/// <summary>
41+
/// Initializes a new <see cref="MappingModelValidationException" /> instance with a specific error message.
42+
/// </summary>
43+
/// <param name="message">The message to display for this exception.</param>
44+
public MappingModelValidationException(string message) : base(message) { }
45+
46+
/// <summary>
47+
/// Initializes a new <see cref="MappingModelValidationException" /> instance with a specific error message and nested exception.
48+
/// </summary>
49+
/// <param name="message">The message to display for this exception.</param>
50+
/// <param name="innerException">The reference to the original, underlying exception.</param>
51+
public MappingModelValidationException(string message, Exception innerException) : base(message, innerException) { }
52+
53+
/// <summary>
54+
/// Instantiates a new <see cref="MappingModelValidationException"/> instance for serialization.
55+
/// </summary>
56+
/// <param name="info">A <see cref="SerializationInfo"/> instance with details about the serialization requirements.</param>
57+
/// <param name="context">A <see cref="StreamingContext"/> instance with details about the request context.</param>
58+
/// <returns>A new <see cref="InvalidKeyException"/> instance.</returns>
59+
protected MappingModelValidationException(SerializationInfo info, StreamingContext context) : base(info, context) {
60+
Contract.Requires(info);
61+
}
62+
63+
} //Class
64+
} //Namespace

OnTopic/Mapping/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract inte
2020
- [Internal Caching](#internal-caching)
2121
- [`CachedTopicMappingService`](#cachedtopicmappingservice)
2222
- [Limitations](#limitations)
23+
- [Exceptions](#exceptions)
2324

2425
## `TopicMappingService`
2526
The [`TopicMappingService`](TopicMappingService.cs) provides a concrete implementation that is expected to satisfy the requirements of most consumers. This supports the following conventions.
@@ -186,4 +187,9 @@ While the `CachedTopicMappingService` can be useful for particular scenarios, it
186187

187188
1. It may take up considerable memory, depending on how many permutations of mapped objects the application has. This is especially true since it caches each unique object graph; no effort is made to centralize object instances referenced by e.g. relationships in multiple graphs.
188189
2. It makes no effort to validate or evict cache entries. Topics whose values change during the lifetime of the `CachedTopicMappingService` will not be reflected in the mapped responses.
189-
3. If a graph is manually constructed (by e.g. programmatically mapping `Children`) then each instance will be separated cached, thus potentially allowing an instance to be shared between multiple graphs. This can introduce concerns if edge maintenance is important.
190+
3. If a graph is manually constructed (by e.g. programmatically mapping `Children`) then each instance will be separated cached, thus potentially allowing an instance to be shared between multiple graphs. This can introduce concerns if edge maintenance is important.
191+
192+
## Exceptions
193+
The topic mapping services will throw a [`TopicMappingException`](TopicMappingException.cs) if a foreseeable exception occurs. Specifically, the exceptions expected will be:
194+
- **[`InvalidTypeException`](InvalidTypeException.cs):** The [`TopicMappingService`](TopicMappingService.cs) throws this exception if the source `Topic`'s `ContentType` maps to a `TopicViewModel` which cannot be located in the supplied `ITypeLookupService`.
195+
- **[`MappingModelValidationException`](MappingModelValidationException.cs):** The [`ReverseTopicMappingService`](Reverse/ReverseTopicMappingService.cs) throws this exception if the source model has any discrepancies with the target `Topic` which may introduce unexpected data integrity or data loss once that `Topic` is saved.

OnTopic/Mapping/Reverse/BindingModelValidator.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ static internal void ValidateProperty(
198198
| Handle children
199199
\-----------------------------------------------------------------------------------------------------------------------*/
200200
if (configuration.RelationshipType is RelationshipType.Children) {
201-
throw new TopicMappingException(
201+
throw new MappingModelValidationException(
202202
$"The {nameof(ReverseTopicMappingService)} does not support mapping child topics. This property should be " +
203203
$"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " +
204204
$"it from being evaluated by the {nameof(ReverseTopicMappingService)}. If children must be mapped, then the " +
@@ -211,7 +211,7 @@ static internal void ValidateProperty(
211211
| Handle parent
212212
\-----------------------------------------------------------------------------------------------------------------------*/
213213
if (configuration.AttributeKey is "Parent") {
214-
throw new TopicMappingException(
214+
throw new MappingModelValidationException(
215215
$"The {nameof(ReverseTopicMappingService)} does not support mapping Parent topics. This property should be " +
216216
$"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " +
217217
$"it from being evaluated by the {nameof(ReverseTopicMappingService)}."
@@ -222,7 +222,7 @@ static internal void ValidateProperty(
222222
| Validate attribute type
223223
\-----------------------------------------------------------------------------------------------------------------------*/
224224
if (attributeDescriptor is null) {
225-
throw new TopicMappingException(
225+
throw new MappingModelValidationException(
226226
$"A '{nameof(sourceType)}' object was provided with a content type set to '{contentTypeDescriptor.Key}'. This " +
227227
$"content type does not contain an attribute named '{compositeAttributeKey}', as requested by the " +
228228
$"'{configuration.Property.Name}' property. If this property is not intended to be mapped by the " +
@@ -245,7 +245,7 @@ attributeDescriptor.ModelType is ModelType.NestedTopic &&
245245
!typeof(ITopicBindingModel).IsAssignableFrom(listType) &&
246246
listType is not null
247247
) {
248-
throw new TopicMappingException(
248+
throw new MappingModelValidationException(
249249
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
250250
$"{configuration.RelationshipType}, but the generic type '{listType.Name}' does not implement the " +
251251
$"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " +
@@ -262,7 +262,7 @@ listType is not null
262262
attributeDescriptor.ModelType is ModelType.Reference &&
263263
!typeof(IRelatedTopicBindingModel).IsAssignableFrom(propertyType)
264264
) {
265-
throw new TopicMappingException(
265+
throw new MappingModelValidationException(
266266
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
267267
$"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " +
268268
$"{nameof(IRelatedTopicBindingModel)} interface. This is required for references. If this property is not intended " +
@@ -315,7 +315,7 @@ [AllowNull]Type listType
315315
| Validate list
316316
\-----------------------------------------------------------------------------------------------------------------------*/
317317
if (!typeof(IList).IsAssignableFrom(property.PropertyType)) {
318-
throw new TopicMappingException(
318+
throw new MappingModelValidationException(
319319
$"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " +
320320
$"'{attributeDescriptor.Key}', but does not implement {nameof(IList)}. Relationships must implement " +
321321
$"{nameof(IList)} or derive from a collection that does."
@@ -326,7 +326,7 @@ [AllowNull]Type listType
326326
| Validate relationship type
327327
\-----------------------------------------------------------------------------------------------------------------------*/
328328
if (!new[] { RelationshipType.Any, RelationshipType.Relationship }.Contains(configuration.RelationshipType)) {
329-
throw new TopicMappingException(
329+
throw new MappingModelValidationException(
330330
$"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " +
331331
$"'{attributeDescriptor.Key}', but is configured as a {configuration.RelationshipType}. The property should be " +
332332
$"flagged as either {nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}."
@@ -337,7 +337,7 @@ [AllowNull]Type listType
337337
| Validate the correct base class for relationships
338338
\-----------------------------------------------------------------------------------------------------------------------*/
339339
if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) {
340-
throw new TopicMappingException(
340+
throw new MappingModelValidationException(
341341
$"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " +
342342
$"{configuration.RelationshipType}, but the generic type '{listType?.Name}' does not implement the " +
343343
$"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " +

OnTopic/Mapping/Reverse/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The [`IReverseTopicMappingService`](IReverseTopicMappingService.cs) and its conc
1111
Unlike the `TopicMappingService`, the `ReverseTopicMappingService` will not map any plain-old C# object (POCO); binding models must implement the `ITopicBindingModel` interface, which requires a `Key` and `ContentType` property; without these, it can't create a new `Topic` instance. For relationships, it expects implementation of the `IReverseTopicMappingService`, which has a single `UniqueKey` property.
1212

1313
## Model Validation
14-
The `ReverseTopicMappingService` is deliberately more conservative than the `TopicMappingService` since assumptions in mapping won't just impact a view, but will be committed to the database. As a result, all properties are validated against the corresponding `ContentTypeDescriptor`'s `AttributeDescriptors` collection. If a property cannot be mapped back to an attribute, an `InvalidOperationException` is thrown.
14+
The `ReverseTopicMappingService` is deliberately more conservative than the `TopicMappingService` since assumptions in mapping won't just impact a view, but will be committed to the database. As a result, all properties are validated against the corresponding `ContentTypeDescriptor`'s `AttributeDescriptors` collection. If a property cannot be mapped back to an attribute, a `MappingModelValidationException` is thrown.
1515

1616
> _Important:_ If a binding model contains properties that are not intended to be mapped, they must explicitly be excluded from mapping using the `[DisableMapping]` attribute.
1717

0 commit comments

Comments
 (0)