Skip to content

Commit 105cfdd

Browse files
committed
Introduced new [FilterByContentType()] attribute
Since `ContentType` has been moved from `Topic.Attributes` to `Topic.ContentType`, it can no longer be filtered using the `[FilterByAttribute()]` attribute during mapping (4a9f5b7). That's a big deal since that's the primary use case for the `[FilterByAttribute()]` attribute! To mitigate that, a new `[FilterByContentType()]` attribute is introduced, read by the `PropertyConfiguration`, and enforced by the `TopicMappingService`. In addition, a unit test was introduced to validate the functionality. (As part of this, an old unit test had to be renamed as this inadvertantly introduced an ambiguity with the test evaluating a collection filtered based on the collection type.)
1 parent 4a9f5b7 commit 105cfdd

File tree

5 files changed

+125
-2
lines changed

5 files changed

+125
-2
lines changed

OnTopic.Tests/TopicMappingServiceTest.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,14 +632,14 @@ public async Task Map_CircularReference_ReturnsCachedParent() {
632632
}
633633

634634
/*==========================================================================================================================
635-
| TEST: MAP: FILTER BY CONTENT TYPE: RETURNS FILTERED COLLECTION
635+
| TEST: MAP: FILTER BY COLLECTION TYPE: RETURNS FILTERED COLLECTION
636636
\-------------------------------------------------------------------------------------------------------------------------*/
637637
/// <summary>
638638
/// Establishes a <see cref="TopicMappingService"/> and tests whether the resulting object's <see
639639
/// cref="DescendentTopicViewModel.Children"/> property can be filtered by <see cref="TopicViewModel.ContentType"/>.
640640
/// </summary>
641641
[TestMethod]
642-
public async Task Map_FilterByContentType_ReturnsFilteredCollection() {
642+
public async Task Map_FilterByCollectionType_ReturnsFilteredCollection() {
643643

644644
var topic = TopicFactory.Create("Test", "Descendent");
645645
var childTopic1 = TopicFactory.Create("ChildTopic1", "Descendent", topic);
@@ -821,6 +821,29 @@ public async Task Map_FilterByAttribute_ReturnsFilteredCollection() {
821821

822822
}
823823

824+
/*==========================================================================================================================
825+
| TEST: MAP: FILTER BY CONTENT TYPE: RETURNS FILTERED COLLECTION
826+
\-------------------------------------------------------------------------------------------------------------------------*/
827+
/// <summary>
828+
/// Establishes a <see cref="TopicMappingService"/> and tests whether the resulting object's <see
829+
/// cref="FilteredTopicViewModel.Children"/> property can be filtered using a <see cref="FilterByContentTypeAttribute"/>
830+
/// instances.
831+
/// </summary>
832+
[TestMethod]
833+
public async Task Map_FilterByContentType_ReturnsFilteredCollection() {
834+
835+
var topic = TopicFactory.Create("Test", "Filtered");
836+
var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", topic);
837+
var childTopic2 = TopicFactory.Create("ChildTopic2", "Index", topic);
838+
var childTopic3 = TopicFactory.Create("ChildTopic3", "Page", topic);
839+
var childTopic4 = TopicFactory.Create("ChildTopic4", "Page", childTopic3);
840+
841+
var target = await _mappingService.MapAsync<FilteredContentTypeTopicViewModel>(topic).ConfigureAwait(false);
842+
843+
Assert.AreEqual<int>(3, target.Children.Count);
844+
845+
}
846+
824847
/*==========================================================================================================================
825848
| TEST: MAP: FLATTEN ATTRIBUTE: RETURNS FLAT COLLECTION
826849
\-------------------------------------------------------------------------------------------------------------------------*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using OnTopic.Mapping.Annotations;
7+
using OnTopic.ViewModels;
8+
9+
namespace OnTopic.Tests.ViewModels {
10+
11+
/*============================================================================================================================
12+
| VIEW MODEL: FILTERED CONTENT TYPE TOPIC
13+
\---------------------------------------------------------------------------------------------------------------------------*/
14+
/// <summary>
15+
/// Provides a strongly-typed data transfer object for testing views properties annotated with the <see
16+
/// cref="FilterByContentTypeAttribute"/>.
17+
/// </summary>
18+
/// <remarks>
19+
/// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
20+
/// </remarks>
21+
public class FilteredContentTypeTopicViewModel {
22+
23+
[FilterByContentType("Page")]
24+
public TopicViewModelCollection<TopicViewModel> Children { get; } = new();
25+
26+
} //Class
27+
} //Namespace

OnTopic/Internal/Mapping/PropertyConfiguration.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = ""
8787
GetAttributeValue<FlattenAttribute>(property, a => FlattenChildren = true);
8888
GetAttributeValue<MetadataAttribute>(property, a => MetadataKey = a.Key);
8989
GetAttributeValue<DisableMappingAttribute>(property, a => DisableMapping = true);
90+
GetAttributeValue<FilterByContentTypeAttribute>(property, a => ContentTypeFilter = a.ContentType);
9091

9192
/*------------------------------------------------------------------------------------------------------------------------
9293
| Attributes: Determine relationship key and type
@@ -364,6 +365,27 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = ""
364365
/// </remarks>
365366
public bool DisableMapping { get; set; }
366367

368+
/*==========================================================================================================================
369+
| PROPERTY: CONTENT TYPE FILTER
370+
\-------------------------------------------------------------------------------------------------------------------------*/
371+
/// <summary>
372+
/// Provides a <c>ContentType</c> which can optionally be used to filter a collection.
373+
/// </summary>
374+
/// <remarks>
375+
/// <para>
376+
/// By default, all <see cref="Topic"/>s in a source collection (e.g., <see cref="Topic.Children"/>) will be included in
377+
/// a corresponding collection on the DTO (assuming the mapped DTO is compatible with the collection type). If the
378+
/// <see cref="ContentTypeFilter"/> is set, however, then each <see cref="Topic"/> will be evaluated to confirm that
379+
/// it is of that content type.
380+
/// </para>
381+
/// <para>
382+
/// The <see cref="ContentTypeFilter"/> property corresponds to the <see cref="FilterByContentTypeAttribute.
383+
/// ContentType"/> property. It can be assigned by decorating a DTO property with e.g. <c>[FilterByContentType("Page")]
384+
/// </c>.
385+
/// </para>
386+
/// </remarks>
387+
public string? ContentTypeFilter { get; set; }
388+
367389
/*==========================================================================================================================
368390
| PROPERTY: ATTRIBUTE FILTERS
369391
\-------------------------------------------------------------------------------------------------------------------------*/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
7+
namespace OnTopic.Mapping.Annotations {
8+
9+
/*============================================================================================================================
10+
| ATTRIBUTE: FILTER BY CONTENT TYPE
11+
\---------------------------------------------------------------------------------------------------------------------------*/
12+
/// <summary>
13+
/// Flags that a collection property should be filtered by a specified <c>ContentType</c>.
14+
/// </summary>
15+
/// <remarks>
16+
/// By default, <see cref="ITopicMappingService"/> will add any corresponding relationships to a collection, assuming they
17+
/// are assignable to the collection's base type. With the <c>[FilterByContentType(contentType)]</c> attribute, the
18+
/// collection will instead be filtered to only those topics that have the specified content type.
19+
/// </remarks>
20+
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=true, Inherited=true)]
21+
public sealed class FilterByContentTypeAttribute : System.Attribute {
22+
23+
/*==========================================================================================================================
24+
| CONSTRUCTOR
25+
\-------------------------------------------------------------------------------------------------------------------------*/
26+
/// <summary>
27+
/// Annotates a property with the <see cref="FilterByContentTypeAttribute"/> class by providing a (required) content type.
28+
/// </summary>
29+
/// <param name="contentType">The content type to filter by.</param>
30+
public FilterByContentTypeAttribute(string contentType) {
31+
TopicFactory.ValidateKey(contentType, false);
32+
ContentType = contentType;
33+
}
34+
35+
/*==========================================================================================================================
36+
| PROPERTY: CONTENT TYPE
37+
\-------------------------------------------------------------------------------------------------------------------------*/
38+
/// <summary>
39+
/// Gets the content type.
40+
/// </summary>
41+
public string ContentType { get; }
42+
43+
} //Class
44+
} //Namespace

OnTopic/Mapping/TopicMappingService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,13 @@ ConcurrentDictionary<int, object> cache
625625
continue;
626626
}
627627

628+
if (
629+
configuration.ContentTypeFilter is not null &&
630+
childTopic.ContentType.Equals(configuration.ContentTypeFilter, StringComparison.OrdinalIgnoreCase)
631+
) {
632+
continue;
633+
}
634+
628635
//Skip nested topics; those should be explicitly mapped to their own collection or topic reference
629636
if (childTopic.ContentType.Equals("List", StringComparison.OrdinalIgnoreCase)) {
630637
continue;

0 commit comments

Comments
 (0)