From d027d35cc2ef86bc11a190de08046293249d8860 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:25:13 -0700 Subject: [PATCH 001/778] Introduced `MappedTopicCache` and `MappedTopicCacheEntry` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, a `ConcurrentDictionary` is declared in the `TopicMappingService`. As we want to expand this to also track what `Relationships` flags were used to map the currently cached object, we're simplifying this by establishing a new `MappedTopicCache` object, which tracks a collection of `MappedTopicCacheEntry` objects. A `MappedTopicCacheEntry` provides access to both the `MappedTopic`—similar to the previous cache collection—as well as a `Relationships` property. This way, when an object from the cache is retrieved, it can be determined what relationships have already been mapped. --- .../Internal/Collections/MappedTopicCache.cs | 22 ++++++++++ .../Internal/Mapping/MappedTopicCacheEntry.cs | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 OnTopic/Internal/Collections/MappedTopicCache.cs create mode 100644 OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs diff --git a/OnTopic/Internal/Collections/MappedTopicCache.cs b/OnTopic/Internal/Collections/MappedTopicCache.cs new file mode 100644 index 00000000..7628ec62 --- /dev/null +++ b/OnTopic/Internal/Collections/MappedTopicCache.cs @@ -0,0 +1,22 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Concurrent; +using OnTopic.Mapping; +using OnTopic.Internal.Mapping; + +namespace OnTopic.Internal.Collections { + + /*============================================================================================================================ + | CLASS: MAPPED TOPIC CACHE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a collection intended to track local caching of objects mapped using the . + /// + public class MappedTopicCache: ConcurrentDictionary { + + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs new file mode 100644 index 00000000..825e61c5 --- /dev/null +++ b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs @@ -0,0 +1,40 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping; +using OnTopic.Mapping.Annotations; + +namespace OnTopic.Internal.Mapping { + + /*============================================================================================================================ + | CLASS: MAPPED TOPIC CACHE ENTRY + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides an entry to tracking an object mapped using the . + /// + /// + /// In addition to the actual , this also includes a property for + /// tracking what relationships were mapped to the . + /// + public class MappedTopicCacheEntry { + + /*========================================================================================================================== + | PROPERTY: MAPPED TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the mapped object. + /// + public object MappedTopic { get; set; } = null!; + + /*========================================================================================================================== + | PROPERTY: RELATIONSHIPS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the relationships that the was mapped with. + /// + public Relationships Relationships { get; set; } = Relationships.None; + + } //Class +} //Namespace \ No newline at end of file From d8e3c68f8fb27fc53ae6cec36bfd70d818f740be Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:31:51 -0700 Subject: [PATCH 002/778] Updated `TopicMappingService` to use new `MappedTopicCache` For the most part, this is a direct swap of the legacy `ConcurrentDictionary` with the new `MappedTopicCache`. In addition, the `TryGetValue()` and `GetOrAdd()` references need to be updated to work with the new `MappedTopicCacheEntry` object. --- OnTopic/Mapping/TopicMappingService.cs | 69 ++++++++++++++------------ 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 43f13a6f..85ada92e 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -5,13 +5,13 @@ \=============================================================================================================================*/ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; using OnTopic.Attributes; +using OnTopic.Internal.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; @@ -60,7 +60,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// [return: NotNullIfNotNull("topic")] public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) => - await MapAsync(topic, relationships, new ConcurrentDictionary()).ConfigureAwait(false); + await MapAsync(topic, relationships, new MappedTopicCache()).ConfigureAwait(false); /// /// Given a topic, will identify any View Models named, by convention, "{ContentType}TopicViewModel" and populate them @@ -90,7 +90,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService private async Task MapAsync( Topic? topic, Relationships relationships, - ConcurrentDictionary cache, + MappedTopicCache cache, string? attributePrefix = null ) { @@ -104,8 +104,8 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects \-----------------------------------------------------------------------------------------------------------------------*/ - if (cache.TryGetValue(topic.Id, out var dto)) { - return dto; + if (cache.TryGetValue(topic.Id, out var cacheEntry)) { + return cacheEntry.MappedTopic; } /*------------------------------------------------------------------------------------------------------------------------ @@ -146,7 +146,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// public async Task MapAsync(Topic? topic, object target, Relationships relationships = Relationships.All) { Contract.Requires(target, nameof(target)); - return await MapAsync(topic, target, relationships, new ConcurrentDictionary()).ConfigureAwait(false); + return await MapAsync(topic, target, relationships, new MappedTopicCache()).ConfigureAwait(false); } /// @@ -166,10 +166,10 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// The target view model with the properties appropriately mapped. /// private async Task MapAsync( - Topic? topic, - object target, - Relationships relationships, - ConcurrentDictionary cache, + Topic? topic, + object target, + Relationships relationships, + MappedTopicCache cache, string? attributePrefix = null ) { @@ -190,11 +190,17 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects \-----------------------------------------------------------------------------------------------------------------------*/ - if (cache.TryGetValue(topic.Id, out var dto)) { - return dto; + if (cache.TryGetValue(topic.Id, out var cacheEntry)) { + return cacheEntry.MappedTopic; } else if (!topic.IsNew) { - cache.GetOrAdd(topic.Id, target); + cache.GetOrAdd( + topic.Id, + new MappedTopicCacheEntry() { + MappedTopic = target, + Relationships = relationships + } + ); } /*------------------------------------------------------------------------------------------------------------------------ @@ -227,12 +233,11 @@ private async Task MapAsync( /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. protected async Task SetPropertyAsync( - Topic source, - object target, - Relationships relationships, - PropertyInfo property, - ConcurrentDictionary cache, - string? attributePrefix = null + Topic source, + object target, + Relationships relationships, + PropertyInfo property, + MappedTopicCache cache, ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -394,11 +399,11 @@ protected static void SetScalarValue(Topic source, object target, PropertyConfig /// /// A cache to keep track of already-mapped object instances. protected async Task SetCollectionValueAsync( - Topic source, - object target, - Relationships relationships, - PropertyConfiguration configuration, - ConcurrentDictionary cache + Topic source, + object target, + Relationships relationships, + PropertyConfiguration configuration, + MappedTopicCache cache ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -586,10 +591,10 @@ IList GetRelationship(RelationshipType relationship, Func c /// /// A cache to keep track of already-mapped object instances. protected async Task PopulateTargetCollectionAsync( - IList sourceList, - IList targetList, - PropertyConfiguration configuration, - ConcurrentDictionary cache + IList sourceList, + IList targetList, + PropertyConfiguration configuration, + MappedTopicCache cache ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -689,10 +694,10 @@ void AddToList(object dto) { /// /// A cache to keep track of already-mapped object instances. protected async Task SetTopicReferenceAsync( - Topic source, - object target, - PropertyConfiguration configuration, - ConcurrentDictionary cache + Topic source, + object target, + PropertyConfiguration configuration, + MappedTopicCache cache ) { /*------------------------------------------------------------------------------------------------------------------------ From 80452d2bc2f54a7923af25cc209367923a2c2b25 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:34:22 -0700 Subject: [PATCH 003/778] Introduced `GetMissingRelationships()` and `AddMissingRelationships()` Given a `Relationships` enum, the `GetMissingRelationships()` will determine what relationships, if any, are not covered by the existing `MappedTopicCacheEntry.Relationships` enum, such that the `TopicMappingService` can map those. The `AddMissingRelationships()` updates the `Relationships` to include the new relationships, thus ensuring that the new relationships are accounted for in the cache. --- .../Internal/Mapping/MappedTopicCacheEntry.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs index 825e61c5..98f3d6ad 100644 --- a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs +++ b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs @@ -16,7 +16,11 @@ namespace OnTopic.Internal.Mapping { /// /// /// In addition to the actual , this also includes a property for - /// tracking what relationships were mapped to the . + /// tracking what relationships were mapped to the . This allows the to be update the cached object with any missing relationships, which can be identified using the + /// method. In turn, the cache can then be updated to reflect those + /// new relationships by using . This ensures that even if a topic has + /// already been mapped, its scope can be expanded without duplicating effort. /// public class MappedTopicCacheEntry { @@ -36,5 +40,23 @@ public class MappedTopicCacheEntry { /// public Relationships Relationships { get; set; } = Relationships.None; + /*========================================================================================================================== + | METHOD: GET MISSING RELATIONSHIPS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Given a target , identifies any relationships not covered by and returns them as a new instance. + /// + public Relationships GetMissingRelationships(Relationships relationships) => relationships ^ (relationships | Relationships); + + /*========================================================================================================================== + | METHOD: ADD MISSING RELATIONSHIPS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Given a target , adds any missing to the property. + /// + public void AddMissingRelationships(Relationships relationships) => Relationships = relationships | Relationships; + } //Class } //Namespace \ No newline at end of file From 49f0597710c7441deed5422cd563572a65a9925a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:36:42 -0700 Subject: [PATCH 004/778] Incorporate `GetMissingRelationships()` into `TopicMappingService` By encorporating `GetMissingRelationships()` into `MapAsync()`, we ensure that cached objects are only returned if they satisfy all requested relationships. If they aren't, the cached object is set as the `target`, but it will continue to go through subsequent mapping to ensure that the missing relationships are picked up. --- OnTopic/Mapping/TopicMappingService.cs | 35 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 85ada92e..f1221953 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -104,23 +104,32 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects \-----------------------------------------------------------------------------------------------------------------------*/ + object? target; + if (cache.TryGetValue(topic.Id, out var cacheEntry)) { - return cacheEntry.MappedTopic; + target = cacheEntry.MappedTopic; + if (cacheEntry.GetMissingRelationships(relationships) == Relationships.None) { + return target; + } } /*------------------------------------------------------------------------------------------------------------------------ | Instantiate object \-----------------------------------------------------------------------------------------------------------------------*/ - var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); + else { - if (viewModelType == null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { - throw new InvalidOperationException( - $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + - $"to map the topic '{topic.GetUniqueKey()}'." - ); - } + var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); - var target = Activator.CreateInstance(viewModelType); + if (viewModelType == null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { + throw new InvalidOperationException( + $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + + $"to map the topic '{topic.GetUniqueKey()}'." + ); + } + + target = Activator.CreateInstance(viewModelType); + + } /*------------------------------------------------------------------------------------------------------------------------ | Provide mapping @@ -189,9 +198,17 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects + >------------------------------------------------------------------------------------------------------------------------- + | If the cache contains an entry, check to make sure it includes all of the requested relationships. If it does, return + | it. If it doesn't, determine the missing relationships and request to have those mapped. \-----------------------------------------------------------------------------------------------------------------------*/ if (cache.TryGetValue(topic.Id, out var cacheEntry)) { + relationships = cacheEntry.GetMissingRelationships(relationships); + target = cacheEntry.MappedTopic; + if (relationships == Relationships.None) { return cacheEntry.MappedTopic; + } + cacheEntry.AddMissingRelationships(relationships); } else if (!topic.IsNew) { cache.GetOrAdd( From 7399e42cfac87beaa6f7b97c5bed675abfe0e3ad Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:45:24 -0700 Subject: [PATCH 005/778] Prevent properties from being remapped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the new `GetMissingRelationships()` logic incorporated into the `TopicMappingService`, an already mapped object may be passed to `SetPropertyAsync()` with the goal of picked up any missing relationships. Because `GetMissingRelationships()` only includes any missing relationships, this won't remap previously map relationships. But in this scenario, we have confidence that properties which don't correspond to relationships—such as scalar values—are already mapped, and thus don't need to be mapped again. To address this, a new `mapRelationshipsOnly` parameter is defined on `SetPropertyAsync()` which excludes properties that aren't associated with the missing relationships from being mapped. --- OnTopic/Mapping/TopicMappingService.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index f1221953..a8862ed0 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -225,7 +225,7 @@ private async Task MapAsync( \-----------------------------------------------------------------------------------------------------------------------*/ var taskQueue = new List(); foreach (var property in _typeCache.GetMembers(target.GetType())) { - taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache, attributePrefix)); + taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache, attributePrefix, cacheEntry != null)); } await Task.WhenAll(taskQueue.ToArray()).ConfigureAwait(false); @@ -249,12 +249,15 @@ private async Task MapAsync( /// Information related to the current property. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. + /// Determines if properties not associated with properties should be mapped. protected async Task SetPropertyAsync( Topic source, object target, Relationships relationships, PropertyInfo property, MappedTopicCache cache, + string? attributePrefix = null, + bool mapRelationshipsOnly = false ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -279,7 +282,7 @@ protected async Task SetPropertyAsync( /*------------------------------------------------------------------------------------------------------------------------ | Assign default value \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.DefaultValue != null) { + if (!mapRelationshipsOnly && configuration.DefaultValue != null) { property.SetValue(target, configuration.DefaultValue); } @@ -292,7 +295,7 @@ protected async Task SetPropertyAsync( else if (SetCompatibleProperty(source, target, configuration)) { //Performed 1:1 mapping between source and target } - else if (_typeCache.HasSettableProperty(target.GetType(), property.Name)) { + else if (!mapRelationshipsOnly && _typeCache.HasSettableProperty(target.GetType(), property.Name)) { SetScalarValue(source, target, configuration); } else if (typeof(IList).IsAssignableFrom(property.PropertyType)) { From 9e0187b4d32132007136090299f8b47a73751ce3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:56:02 -0700 Subject: [PATCH 006/778] Fixed bitwise logic in `GetMissingRelationships()` The logic for `GetMissingRelationships()` was backwards, returning the relationships that were exclusive to the _existing_ relationships (i.e., the `Relationships` property), instead of those exclusive to the _new_ relationships (i.e., the `relationships` parameter). Confused? Yes, that's why we have this convenience method for what is otherwise pretty simple logic. --- OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs index 98f3d6ad..7fb339d7 100644 --- a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs +++ b/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs @@ -47,7 +47,7 @@ public class MappedTopicCacheEntry { /// Given a target , identifies any relationships not covered by and returns them as a new instance. /// - public Relationships GetMissingRelationships(Relationships relationships) => relationships ^ (relationships | Relationships); + public Relationships GetMissingRelationships(Relationships relationships) => Relationships ^ (relationships | Relationships); /*========================================================================================================================== | METHOD: ADD MISSING RELATIONSHIPS From 0e1edb53ab4a9dae776bca85758f45a592a516dd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 13 Sep 2020 16:57:01 -0700 Subject: [PATCH 007/778] Added unit test to validate `GetMissingRelationships()` The bug corrected in the previous commit (9e0187b) exposes that we really need a unit test to validate this functionality, even as simple as it is. --- OnTopic.Tests/TopicMappingServiceTest.cs | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index d03a8e96..afa45078 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; using OnTopic.Data.Caching; +using OnTopic.Internal.Mapping; using OnTopic.Mapping; using OnTopic.Mapping.Annotations; using OnTopic.Metadata; @@ -266,6 +267,30 @@ public async Task Map_AlternateAttributeKey_ReturnsMappedModel() { } + /*========================================================================================================================== + | TEST: MAPPED TOPIC CACHE ENTRY: GET MISSING RELATIONSHIPS: RETURNS DIFFERENCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a with a set of , and then confirms that + /// its correctly returns the missing + /// relationships. + /// + [TestMethod] + public void MappedTopicCacheEntry_GetMissingRelationships_ReturnsDifference() { + + var cacheEntry = new MappedTopicCacheEntry() { + Relationships = Relationships.Children | Relationships.Parents + }; + var relationships = Relationships.Children | Relationships.References; + + var difference = cacheEntry.GetMissingRelationships(relationships); + + Assert.IsTrue(difference.HasFlag(Relationships.References)); + Assert.IsFalse(difference.HasFlag(Relationships.Children)); + Assert.IsFalse(difference.HasFlag(Relationships.Parents)); + + } + /*========================================================================================================================== | TEST: MAP: RELATIONSHIPS: RETURNS MAPPED MODEL \-------------------------------------------------------------------------------------------------------------------------*/ From 7733c0c7753cfd160188b571a6ecb703ffc3b59b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 17:11:37 -0800 Subject: [PATCH 008/778] Enable `Microsoft.CodeAnalysis.NetAnalyzers` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the release of .NET 5, new Roslyn code analyzers are available which replace the legacy `Microsoft.CodeAnalysis.FxCopAnalyzers`—but while they're built into the .NET 5 SDK, they need to be explicitly enabled via a `csproj` flag in order to enable for .NET Core and .NET Standard projects. --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 6 ++---- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 6 ++---- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 6 ++---- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 9 ++------- OnTopic.Tests/OnTopic.Tests.csproj | 6 ++---- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 6 ++---- OnTopic/OnTopic.csproj | 6 ++---- 7 files changed, 14 insertions(+), 31 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 6fd4c861..e57122db 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -8,6 +8,8 @@ False 9.0 enable + latest + AllEnabledByDefault @@ -43,10 +45,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 99bf2851..3dd80357 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -8,6 +8,8 @@ False 9.0 enable + latest + AllEnabledByDefault @@ -43,10 +45,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 4159a9ab..f50d316b 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -6,6 +6,8 @@ netstandard2.0;netstandard2.1 9.0 enable + latest + AllEnabledByDefault @@ -40,10 +42,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 973f048c..d9a66695 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -4,6 +4,8 @@ netstandard2.0 9.0 enable + latest + AllEnabledByDefault @@ -22,13 +24,6 @@ true - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 1b966a42..ee44dfd4 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -6,6 +6,8 @@ CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 9.0 enable + latest + AllEnabledByDefault @@ -27,10 +29,6 @@ - - all - runtime; build; native; contentfiles; analyzers - diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 01b79cbb..1e51cc95 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -8,6 +8,8 @@ False 9.0 enable + latest + AllEnabledByDefault @@ -42,10 +44,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index c051515f..2a6f0c3f 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -8,6 +8,8 @@ False 9.0 enable + latest + AllEnabledByDefault @@ -43,10 +45,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - all From 7b5da01a6d188ee4f618dd4ca897c30fb0d4b6bc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 17:15:37 -0800 Subject: [PATCH 009/778] Prefer `StringComparison.Ordinal` to `StringComparison.InvariantCulture` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `StringComparison.Ordinal` (e.g., `StringComparison.OrdinalIgnoreCase`) is faster and more reliable than the legacy `StringComparison.InvariantCulture` (e.g., `StringComparison.InvariantCultureIgnoreCase`)—and also shorter to type. This resolves CA1309. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 6 +++--- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic.ViewModels/TopicViewModelCollection.cs | 2 +- OnTopic/Internal/Mapping/PropertyConfiguration.cs | 4 ++-- OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs | 4 ++-- OnTopic/Mapping/TopicMappingService.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 4 ++-- OnTopic/Topic.cs | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index fc311cc4..17840af2 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -175,7 +175,7 @@ public IEnumerable AddTopic(Topic topic, bool includeMetadata = false) if (topic is null) return topics; if (topic.Attributes.GetValue("NoIndex") is "1") return topics; if (topic.Attributes.GetValue("IsDisabled") is "1") return topics; - if (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.InvariantCultureIgnoreCase))) return topics; + if (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics; /*------------------------------------------------------------------------------------------------------------------------ | Establish variables @@ -199,7 +199,7 @@ public IEnumerable AddTopic(Topic topic, bool includeMetadata = false) getRelationships() ) : null ); - if (!topic.ContentType!.Equals("Container", StringComparison.InvariantCultureIgnoreCase)) { + if (!topic.ContentType!.Equals("Container", StringComparison.OrdinalIgnoreCase)) { topics.Add(topicElement); } @@ -217,7 +217,7 @@ public IEnumerable AddTopic(Topic topic, bool includeMetadata = false) \-----------------------------------------------------------------------------------------------------------------------*/ IEnumerable getAttributes() => from attribute in topic.Attributes - where !ExcludeAttributes.Contains(attribute.Key, StringComparer.InvariantCultureIgnoreCase) + where !ExcludeAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) where topic.Attributes.GetValue(attribute.Key)?.Length < 256 select new XElement(_pagemapNamespace + "Attribute", new XAttribute("name", attribute.Key), diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 072b4cd4..62c2f5d8 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -60,7 +60,7 @@ public StubTopicRepository() : base() { \-----------------------------------------------------------------------------------------------------------------------*/ if (topicKey is not null && topicKey.Length > 0) { topicKey = topicKey.Contains(":") ? topicKey : "Root:" + topicKey; - return _cache.FindFirst(t => t.GetUniqueKey().Equals(topicKey, StringComparison.InvariantCultureIgnoreCase)); + return _cache.FindFirst(t => t.GetUniqueKey().Equals(topicKey, StringComparison.OrdinalIgnoreCase)); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.ViewModels/TopicViewModelCollection.cs b/OnTopic.ViewModels/TopicViewModelCollection.cs index a3ac3e24..0e91e4eb 100644 --- a/OnTopic.ViewModels/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/TopicViewModelCollection.cs @@ -61,7 +61,7 @@ public TopicViewModelCollection GetByContentType(string contentType) { $"A {nameof(contentType)} argument is required." ); return new( - Items.Where(t => t.ContentType?.Equals(contentType, StringComparison.InvariantCultureIgnoreCase)?? false) + Items.Where(t => t.ContentType?.Equals(contentType, StringComparison.OrdinalIgnoreCase)?? false) ); } diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Internal/Mapping/PropertyConfiguration.cs index 702af574..96d13520 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Internal/Mapping/PropertyConfiguration.cs @@ -101,7 +101,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" if ( RelationshipType is RelationshipType.Any && - RelationshipKey.Equals("Children", StringComparison.InvariantCultureIgnoreCase) + RelationshipKey.Equals("Children", StringComparison.OrdinalIgnoreCase) ) { RelationshipType = RelationshipType.Children; } @@ -401,7 +401,7 @@ RelationshipType is RelationshipType.Any && /// public bool SatisfiesAttributeFilters(Topic source) => AttributeFilters.All(f => - source?.Attributes?.GetValue(f.Key, "")?.Equals(f.Value, StringComparison.InvariantCultureIgnoreCase)?? false + source?.Attributes?.GetValue(f.Key, "")?.Equals(f.Value, StringComparison.OrdinalIgnoreCase)?? false ); /*========================================================================================================================== diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index 730818bd..3a719675 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -416,10 +416,10 @@ private static bool IsSettableType(Type sourceType, Type? targetType = null) { valueObject = value; } else if (type.Equals(typeof(bool)) || type.Equals(typeof(bool?))) { - if (value is "1" || value.Equals("true", StringComparison.InvariantCultureIgnoreCase)) { + if (value is "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase)) { valueObject = true; } - else if (value is "0" || value.Equals("false", StringComparison.InvariantCultureIgnoreCase)) { + else if (value is "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase)) { valueObject = false; } } diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 2dacf800..8cfb2711 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -622,7 +622,7 @@ ConcurrentDictionary cache } //Skip nested topics; those should be explicitly mapped to their own collection or topic reference - if (childTopic.ContentType.Equals("List", StringComparison.InvariantCultureIgnoreCase)) { + if (childTopic.ContentType.Equals("List", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 71cd91dd..4d6e23ae 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -277,7 +277,7 @@ public bool IsTypeOf(string contentTypeName) { var contentType = (ContentTypeDescriptor?)this; while (contentType is not null) { - if (contentType.Key.Equals(contentTypeName, StringComparison.CurrentCultureIgnoreCase)) { + if (contentType.Key.Equals(contentTypeName, StringComparison.OrdinalIgnoreCase)) { return true; } contentType = contentType.Parent as ContentTypeDescriptor; diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index f035fc11..bce33ba3 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -226,14 +226,14 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic /*------------------------------------------------------------------------------------------------------------------------ | Handle request for root \-----------------------------------------------------------------------------------------------------------------------*/ - if (currentTopic!.Key.Equals(uniqueKey, StringComparison.InvariantCultureIgnoreCase)) { + if (currentTopic!.Key.Equals(uniqueKey, StringComparison.OrdinalIgnoreCase)) { return currentTopic; } /*------------------------------------------------------------------------------------------------------------------------ | Process keys \-----------------------------------------------------------------------------------------------------------------------*/ - if (uniqueKey.StartsWith(currentTopic!.Key + ":", StringComparison.InvariantCultureIgnoreCase)) { + if (uniqueKey.StartsWith(currentTopic!.Key + ":", StringComparison.OrdinalIgnoreCase)) { uniqueKey = uniqueKey.Substring(currentTopic!.Key.Length + 1); } var keys = uniqueKey.Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries); diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 793ccd17..4d79d277 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -211,7 +211,7 @@ public string Key { _originalKey = Attributes.GetValue("Key", _key, false, false); } //If an established key value is changed, the parent's index must be manually updated; this won't happen automatically. - if (_originalKey is not null && !value.Equals(_key, StringComparison.InvariantCultureIgnoreCase) && Parent is not null) { + if (_originalKey is not null && !value.Equals(_key, StringComparison.OrdinalIgnoreCase) && Parent is not null) { Parent.Children.ChangeKey(this, value); } SetAttributeValue("Key", value); From 7324ca5ee83d638cdedaebfb1711cbb4625b5304 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 17:23:32 -0800 Subject: [PATCH 010/778] Allow class names to end in `Attribute` Normally, class names should not end in `Attribute` unless they derive from `Attribute`. That's a good rule. Unfortunately, OnTopic has preexisting naming conventions with `Attribute` representing an attribute descriptor, and class names named after content type descriptors. As a result, there are classes that need to be named e.g. `BooleanAttribute` or `TextAttribute`. Perhaps these should be named `*AttributeDescriptor`, but that's a breaking change and will need to be reevaluated in a future major release. For now, supressing CA1711 resolves this warning. --- OnTopic/GlobalSuppressions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index 573e6296..575d56f4 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -6,4 +6,5 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Invalid overload; known bug in code analysis", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] -[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] \ No newline at end of file +[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] From bde497f1cd53b20e61e47d2509f9b6fdd19251e0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 17:44:22 -0800 Subject: [PATCH 011/778] Remove unnecessary null checks There are a number of areas in the code where we perform a null check against references that should never be null due e.g. to preexisting guard clauses. This removes those unnecessary checks, and resolves most CA1508 warnings. Note: There are a few instances which remain, due to conflicts with CS8602, and will need to be addressed later. --- .../TopicViewResultExecutor.cs | 2 +- OnTopic.Tests/ContractTest.cs | 3 +-- OnTopic.ViewModels/NavigationTopicViewModel.cs | 2 +- .../Internal/Mapping/PropertyConfiguration.cs | 5 +---- OnTopic/TopicFactory.cs | 17 +++-------------- 5 files changed, 7 insertions(+), 22 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index 2aa253aa..2f42e257 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -101,7 +101,7 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi >------------------------------------------------------------------------------------------------------------------------- | Determines if the view is defined in the querystring. \-----------------------------------------------------------------------------------------------------------------------*/ - if (!(view?.Success ?? false) && requestContext.Query.ContainsKey("View")) { + if (requestContext.Query.ContainsKey("View")) { var queryStringValue = requestContext.Query["View"].First(); if (queryStringValue is not null) { view = viewEngine.FindView(actionContext, queryStringValue, isMainPage: true); diff --git a/OnTopic.Tests/ContractTest.cs b/OnTopic.Tests/ContractTest.cs index 726b322d..a8a253da 100644 --- a/OnTopic.Tests/ContractTest.cs +++ b/OnTopic.Tests/ContractTest.cs @@ -72,11 +72,10 @@ public void Requires_ObjectIsNull_ThrowArgumentNullException() => [TestMethod] public void Requires_MessageExists_ThrowExceptionWithMessage() { - var argument = (object?)null; var errorMessage = "The argument cannot be null"; try { - Contract.Requires(argument is not null, errorMessage); + Contract.Requires(false, errorMessage); } catch (ArgumentException ex) { Assert.AreEqual(errorMessage, ex.Message); diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 918fc0e1..29f72ada 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -54,7 +54,7 @@ public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicV /// typically meaning the user is on the page this object is pointing to. /// public bool IsSelected(string uniqueKey) => - $"{uniqueKey}:"?.StartsWith($"{UniqueKey}:", StringComparison.InvariantCultureIgnoreCase) ?? false; + $"{uniqueKey}:".StartsWith($"{UniqueKey}:", StringComparison.InvariantCultureIgnoreCase); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Internal/Mapping/PropertyConfiguration.cs index 96d13520..d63524d0 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Internal/Mapping/PropertyConfiguration.cs @@ -99,10 +99,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" } ); - if ( - RelationshipType is RelationshipType.Any && - RelationshipKey.Equals("Children", StringComparison.OrdinalIgnoreCase) - ) { + if (RelationshipKey.Equals("Children", StringComparison.OrdinalIgnoreCase)) { RelationshipType = RelationshipType.Children; } diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 664b4b07..74b6548b 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -80,13 +80,7 @@ public static Topic Create(string key, string contentType, Topic? parent = null) /*------------------------------------------------------------------------------------------------------------------------ | Identify the appropriate topic \-----------------------------------------------------------------------------------------------------------------------*/ - var topic = (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1); - - /*------------------------------------------------------------------------------------------------------------------------ - | Return the topic - \-----------------------------------------------------------------------------------------------------------------------*/ - return topic?? throw new NullReferenceException("The Create() method failed to successfully create a Topic instance"); - + return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1); } @@ -127,12 +121,7 @@ public static Topic Create(string key, string contentType, int id, Topic? parent /*------------------------------------------------------------------------------------------------------------------------ | Identify the appropriate topic \---------------------------------------------------------------------------------------------------------------------*/ - var topic = (Topic)Activator.CreateInstance(targetType, key, contentType, parent, id); - - /*------------------------------------------------------------------------------------------------------------------------ - | Return object - \-----------------------------------------------------------------------------------------------------------------------*/ - return topic?? throw new NullReferenceException("The Create() method failed to successfully create a Topic instance"); + return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, id); } @@ -152,7 +141,7 @@ public static Topic Create(string key, string contentType, int id, Topic? parent public static void ValidateKey(string? topicKey, bool isOptional = false) { Contract.Requires(isOptional || !String.IsNullOrEmpty(topicKey)); Contract.Requires( - String.IsNullOrEmpty(topicKey) || Regex.IsMatch(topicKey ?? "", @"^[a-zA-Z0-9\.\-_]+$"), + String.IsNullOrEmpty(topicKey) || Regex.IsMatch(topicKey, @"^[a-zA-Z0-9\.\-_]+$"), "Key names should only contain letters, numbers, hyphens, and/or underscores." ); } From db701515395639b370b7ab8216b53cfa426b6109 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 17:53:20 -0800 Subject: [PATCH 012/778] Renamed attributes to align with corresponding properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typically, constructor parameters which set properties should have identifiers that match those properties. This is a best practice that Ignia normally follows. We don't, however, for some cases with attributes, in order to avoid seemingly redundant property names. For example, `FilterByAttribute.Key` makes more sense than `FilterByAttribute.AttributeKey` since attribute is implied by the name of the class—but to keep IntelliSense explicit for parameters, the full `attributeKey` was used. There's an argument that perhaps the attributes and the properties should use the full name, but that's a potentially breaking change. To limit the impact, the parameter name is being updated. (Technically, renaming the parameter is also a potentially breaking change, but as we have insight into current implementers, we know that no one is currently explicitly referencing the parameter by name.) This resolves CA1019. --- OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs | 8 ++++---- .../Annotations/FilterByAttributeAttribute.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs index 5202c4f2..f2873838 100644 --- a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs +++ b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs @@ -30,10 +30,10 @@ public sealed class AttributeKeyAttribute : System.Attribute { /// /// Annotates a property with the class by providing a (required) attribute key. /// - /// The key value of the attribute associated with the current property. - public AttributeKeyAttribute(string attributeKey) { - TopicFactory.ValidateKey(attributeKey, false); - Value = attributeKey; + /// The key value of the attribute associated with the current property. + public AttributeKeyAttribute(string value) { + TopicFactory.ValidateKey(value, false); + Value = value; } /*========================================================================================================================== diff --git a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs index 38166ef7..89984278 100644 --- a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs +++ b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs @@ -28,12 +28,12 @@ public sealed class FilterByAttributeAttribute : System.Attribute { /// Annotates a property with the class by providing a (required) attribute key /// and value. /// - /// The key of the attribute to filter by. - /// The value of the attribute to filter by. - public FilterByAttributeAttribute(string attributeKey, string attributeValue) { - TopicFactory.ValidateKey(attributeKey, false); - Key = attributeKey; - Value = attributeValue; + /// The key of the attribute to filter by. + /// The value of the attribute to filter by. + public FilterByAttributeAttribute(string key, string value) { + TopicFactory.ValidateKey(key, false); + Key = key; + Value = value; } /*========================================================================================================================== From 07f2ba9cc9274b007467e1357d9ea151dbe87b0b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 18:08:50 -0800 Subject: [PATCH 013/778] Throw `InvalidOperationException` instead of `Exception` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are a few scenarios where we were throwing `Exception` instead of more specific exceptions. These were not documented or expected calls, and we have a fair amount of confidence that implementers are not catching `Exception` in order to handle these specific errors. In fact, generally, these exceptions are thrown to warn developers of unexpected scenarios usually related to the data model, such as infinite loops—i.e., we don't expect these to occur in production environments. Given this, it doesn't really matter _which_ exception they throw, and while swapping out an exception is potentially a breaking change, we have confidence in these scenarios that they won't actually break anything, as these scenarios represent a design flaw and would be caught during testing. (Indeed, technically, a couple of these could probably be replaced with code analyzers in the future for design time and compile time support.) This resolves most instances of CA2201. Note that there remain a couple of false positives of that warning that can't be removed due to generic constraints requiring the _possibility_ of an `Exception` (in order to restrict the generic to any exception type) even though, in practice, we would never expect a caller to pass in `Exception` as the type parameter. We may revisit that in the future by supressing those particular warnings. --- OnTopic/Collections/AttributeValueCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 11bd096c..4914ebc1 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -520,7 +520,7 @@ private bool EnforceBusinessLogic(AttributeValue originalAttribute, out Attribut else if (_typeCache.HasSettableProperty(_associatedTopic.GetType(), originalAttribute.Key)) { _setCounter++; if (_setCounter > 3) { - throw new Exception( + throw new InvalidOperationException( $"An infinite loop has occurred when setting '{originalAttribute.Key}'; be sure that you are referencing " + $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties." ); From 3f12e58f3d2e90ecf86a517132928118cf24efef Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 2 Dec 2020 18:12:59 -0800 Subject: [PATCH 014/778] Suppress warning for unexpected throwing of `Exception` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CA2201 occurs when an exception of type `Exception` is thrown, as more specific exceptions should be preferred. In practice, we don't expect this to occur on the `Contract` class, either, but due to limitations of generic type constraints, there isn't a way to restrict the type parameter to a derivative of `Exception` while not permitting `Exception` to potentially be passed. Ideally, this warning would occur at the source, when the generic type is defined, but that's not supported by the code analyzer—and would likely be quite complicated (if not impossible) to create. As such, instead, we're suppressing this warning. Should we decide this is being abused, we might consider writing a custom code analyzer for this scenario, or adding a runtime exception if the `typeof(T)` is `typeof(Exception)`. As this is an internal library, however, we don't anticipate that being an issue. --- OnTopic/Internal/Diagnostics/Contract.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index b91b1d1b..7c291d68 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -40,6 +40,11 @@ namespace OnTopic.Internal.Diagnostics { /// /// /// + [SuppressMessage( + "Usage", + "CA2201:Do not raise reserved exception types", + Justification = "This is an unexpected usage scenario, but permitted due to limitations on generic constraints." + )] public static class Contract { /*========================================================================================================================== @@ -66,7 +71,7 @@ public static class Contract { #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, string? errorMessage = null) => Requires(requiredObject is not null, errorMessage); - #pragma warning restore CS8777 // Parameter must have a non-null value when exiting. +#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Will throw the provided generic exception if the supplied expression evaluates to false. From d4a584dce9173aff5863c6da5b4084c8784e3ea6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 13:14:07 -0800 Subject: [PATCH 015/778] Convert `List<>` to `Collection<>` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CA1002 advises using the more comprehensive and familiar `Collection<>` type for public members over the faster, but more bare bones, `List<>` type. This is a potentially breaking change so we'll need to hold off until the next version to make that change to any libraries published as NuGet packages. For internal tests, however, we can make that change now in order to validate future compatibility. As expected, this doesn't impact any of our unit tests since the expected members supported by `List<>` are also supported by `Collection<>`—and, in fact, as many of these relate to the `ITopicMappingService`, it's exclusively interacting with them as `IList`, which both support. In practice, we could probably convert the remaining items over to `Collection<>` due to this, without worrying about introducing a potentially breaking change. But as this could cause issues with e.g. casting or variable compatibility, we'll wait until OnTopic 5.0. For those outliers, I introduced a global suppression (for `OnTopic`) and a single local suppression (for `OnTopic.Tests`). --- .../BindingModels/ContentTypeDescriptorTopicBindingModel.cs | 6 +++--- .../BindingModels/InvalidChildrenTopicBindingModel.cs | 4 ++-- .../InvalidRelationshipBaseTypeTopicBindingModel.cs | 4 ++-- .../InvalidRelationshipTypeTopicBindingModel.cs | 4 ++-- OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs | 4 ++-- OnTopic.Tests/ViewModels/CircularTopicViewModel.cs | 4 ++-- .../ViewModels/CompatiblePropertyTopicViewModel.cs | 3 ++- OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs | 4 ++-- .../Metadata/ContentTypeDescriptorTopicViewModel.cs | 6 +++--- OnTopic.Tests/ViewModels/NestedTopicViewModel.cs | 4 ++-- OnTopic.Tests/ViewModels/RelationTopicViewModel.cs | 4 ++-- .../ViewModels/RelationWithChildrenTopicViewModel.cs | 4 ++-- OnTopic/GlobalSuppressions.cs | 1 + 13 files changed, 27 insertions(+), 25 deletions(-) diff --git a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs index 56857732..8c26cfbe 100644 --- a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Models; namespace OnTopic.Tests.BindingModels { @@ -21,9 +21,9 @@ public class ContentTypeDescriptorTopicBindingModel : BasicTopicBindingModel { public ContentTypeDescriptorTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - public List ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); - public List Attributes { get; } = new(); + public Collection Attributes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs index 0660f090..b6b10542 100644 --- a/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidChildrenTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; namespace OnTopic.Tests.BindingModels { @@ -22,7 +22,7 @@ public class InvalidChildrenTopicBindingModel : BasicTopicBindingModel { public InvalidChildrenTopicBindingModel(string? key = null) : base(key, "Page") { } - public List Children { get; } = new(); + public Collection Children { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs index 8f8d7eac..73f7d6e9 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Models; using OnTopic.ViewModels; @@ -24,7 +24,7 @@ public class InvalidRelationshipBaseTypeTopicBindingModel : BasicTopicBindingMod public InvalidRelationshipBaseTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - public List ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index d25b69bc..a091d1a9 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; using OnTopic.Models; @@ -26,7 +26,7 @@ public class InvalidRelationshipTypeTopicBindingModel : BasicTopicBindingModel { public InvalidRelationshipTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } [Relationship(RelationshipType.NestedTopics)] - public List ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs index 0caa89db..bd4e1610 100644 --- a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { @@ -28,7 +28,7 @@ namespace OnTopic.Tests.ViewModels { public class AmbiguousRelationTopicViewModel: KeyOnlyTopicViewModel { [Relationship("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)] - public List RelationshipAlias { get; } = new(); + public Collection RelationshipAlias { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs index 305f0fe1..3009c666 100644 --- a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { @@ -23,7 +23,7 @@ public class CircularTopicViewModel { public CircularTopicViewModel? Parent { get; set; } [Follow(Relationships.Children | Relationships.Parents)] - public List Children { get; } = new(); + public Collection Children { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs index 15c054cb..b4296702 100644 --- a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using OnTopic.Metadata; -using OnTopic.ViewModels; namespace OnTopic.Tests.ViewModels { @@ -26,7 +25,9 @@ public class CompatiblePropertyTopicViewModel { public ModelType ModelType { get; set; } + #pragma warning disable CA1002 // Do not expose generic lists public List? VersionHistory { get; set; } + #pragma warning restore CA1002 // Do not expose generic lists } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs index 83552139..211606d4 100644 --- a/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FlattenChildrenTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { @@ -21,7 +21,7 @@ namespace OnTopic.Tests.ViewModels { public class FlattenChildrenTopicViewModel { [Flatten] - public List Children { get; } = new(); + public Collection Children { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index 83a64f66..40202b9c 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels.Metadata { @@ -20,11 +20,11 @@ namespace OnTopic.Tests.ViewModels.Metadata { /// public class ContentTypeDescriptorTopicViewModel { - public List AttributeDescriptors { get; } = new(); + public Collection AttributeDescriptors { get; } = new(); [Relationship(RelationshipType.MappedCollection)] [Follow(Relationships.None)] - public List PermittedContentTypes { get; } = new(); + public Collection PermittedContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs b/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs index 7248a4cd..849015da 100644 --- a/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/NestedTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; namespace OnTopic.Tests.ViewModels { @@ -18,7 +18,7 @@ namespace OnTopic.Tests.ViewModels { /// public class NestedTopicViewModel { - public List Categories { get; } = new(); + public Collection Categories { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs index 3fe4224d..c6eccfc8 100644 --- a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { @@ -26,7 +26,7 @@ namespace OnTopic.Tests.ViewModels { public class RelationTopicViewModel: KeyOnlyTopicViewModel { [Follow(Relationships.Children)] - public List Cousins { get; } = new(); + public Collection Cousins { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs index f00dfa10..7a1f759c 100644 --- a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; namespace OnTopic.Tests.ViewModels { @@ -26,7 +26,7 @@ namespace OnTopic.Tests.ViewModels { public class RelationWithChildrenTopicViewModel: RelationTopicViewModel { [Follow(Relationships.Relationships)] - public List Children { get; } = new(); + public Collection Children { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index 575d56f4..ebad71a7 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -8,3 +8,4 @@ [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Invalid overload; known bug in code analysis", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] [assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] +[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Breaking change; deferred to major version.", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file From 6affe6234df39ec2a49d160ea28254358625e488 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 13:19:13 -0800 Subject: [PATCH 016/778] Remove C# 9.0 "or" pattern combinator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `NetAnalyzers` code analysis library doesn't yet understand C# 9.0's new "or" pattern combinator and, thus, throws a CA1062 false positive when a parameter is validated via a guard clause with the form, `parameter is null or …` (see dotnet/roslyn-analyzers#4496). Until that issue is resolved, I'm falling back to the legacy `paramer is null || parameter is …` format, which `NetAnalyzers` understands. --- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- OnTopic/Metadata/ContentTypeDescriptorCollection.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 8cfb2711..4166087d 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -97,7 +97,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - if (topic is null or { IsDisabled: true }) { + if (topic is null || topic is { IsDisabled: true }) { return null; } @@ -176,7 +176,7 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - if (topic is null or { IsDisabled: true }) { + if (topic is null || topic is { IsDisabled: true }) { return target; } diff --git a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs index 455a3bd6..75bea3a0 100644 --- a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs +++ b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs @@ -58,7 +58,7 @@ public void Refresh(ContentTypeDescriptor? rootContentType) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - if (rootContentType is null or { Children: { Count: 0 } }) { + if (rootContentType is null || rootContentType is { Children: { Count: 0 } }) { return; } From 0980e84ed9436b7479803de62e6d6fa0266999ae Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 13:45:02 -0800 Subject: [PATCH 017/778] Suppress check for injection vulnerabilities Due to a bug in the `Microsoft.CodeAnalysis.NetAnalyzers`, a number of CA30XX analyzers throw exceptions, cluttering up the warnings in Visual Studio. A fix has been proposed for these (dotnet/roslyn-analyzers#4495), and will hopefully be available in the next release of the .NET SDK. Until then, these errors are being suppressed. Unfortunately, it doesn't appear to be possible to suppress these warnings at the source level or even via a global suppressions file. Instead, they need to be suppressed at the project level. That's unfortunate, as it means they're not targeted toward the one scenario which trips these exceptions (i.e., the public setter for `TopicController.CurrentTopic`). We'll want to remove these as soon as this issue is resolved. --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index e57122db..85999afb 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -32,7 +32,7 @@ full false latest - 1701;1702;CA1303 + 1701;1702;CA1303;CA3001;CA3003;CA3006;CA3008;CA3009;CA3011;CA3012 pdbonly From e88f0eacf14a64056e93581498d03f7ca24d8e1b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 13:52:00 -0800 Subject: [PATCH 018/778] Supress suggestion to use C# 9.0's "or" pattern combinator In a previous commit, we removed use of C# 9.0's "or" pattern combinator due to a bug in `NetAnalyzers` which yielded a false positive for CA1062 (6affe62). This results in an IDE0078, suggesting the use of the "or" pattern combinator. To temporarily mitigate that, I'm suppressing that warning. This should be revisited later. --- OnTopic/Mapping/TopicMappingService.cs | 4 ++++ OnTopic/Metadata/ContentTypeDescriptorCollection.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 4166087d..3b5fc6ad 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -97,9 +97,11 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ + #pragma warning disable IDE0078 // Use pattern matching if (topic is null || topic is { IsDisabled: true }) { return null; } + #pragma warning restore IDE0078 // Use pattern matching /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects @@ -176,9 +178,11 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ + #pragma warning disable IDE0078 // Use pattern matching if (topic is null || topic is { IsDisabled: true }) { return target; } + #pragma warning restore IDE0078 // Use pattern matching /*------------------------------------------------------------------------------------------------------------------------ | Handle topics diff --git a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs index 75bea3a0..176a3210 100644 --- a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs +++ b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs @@ -58,9 +58,11 @@ public void Refresh(ContentTypeDescriptor? rootContentType) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ + #pragma warning disable IDE0078 // Use pattern matching if (rootContentType is null || rootContentType is { Children: { Count: 0 } }) { return; } + #pragma warning restore IDE0078 // Use pattern matching Contract.Requires( rootContentType.Key.Equals("ContentTypes", StringComparison.OrdinalIgnoreCase), From 9126aae00be1cf96ed54deef79b353a96a1a02cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 14:19:18 -0800 Subject: [PATCH 019/778] Mitigate CS8602 false positive in .NET Standard 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .NET Standard 2.0 doesn't support full nullability analysis. That means checks for `String.IsNullOrEmpty()` will result in a false positive when attempting to dereference the value checked. That said, if a null propagation operator (`?.`) is used, then a CA1508 is thrown due to the unnecessary null check. One solution is to disable nullable analysis just for .NET Standard 2.0, but then all nullable types will throw a CS8632 since they shouldn't be used without nullable analysis enabled. Those could be suppressed for .NET Standard 2.0, but we're pretty far down this rabbit hole now. The simple workaround is to use `item is null || item.Length == 0` in place of `String.IsNullOrEmpty(item)`, which satisfies everyone—even if it's a clumsier articulation. --- OnTopic/Internal/Diagnostics/Contract.cs | 2 +- OnTopic/Repositories/TopicRepositoryBase.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index 7c291d68..89f5d5a5 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -92,7 +92,7 @@ public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, s /// public static void Requires(bool isValid, string? errorMessage = null) where T : Exception, new() { if (isValid) return; - if (errorMessage is null || String.IsNullOrEmpty(errorMessage)) { + if (errorMessage is null || errorMessage.Length == 0) { throw new(); } try { diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index cb8d95be..ddf5148c 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -599,7 +599,7 @@ protected IEnumerable GetAttributes( //Skip if the value is null or empty; these values are not persisted to storage and should be treated as equivalent to //non-existent values. - if (String.IsNullOrEmpty(attributeValue.Value)) { + if (attributeValue.Value is null || attributeValue.Value.Length == 0) { continue; } @@ -619,7 +619,7 @@ isDirty is null || //Add the attribute based on the isExtendedAttribute paramter. Add all parameters if isExtendedAttribute is null. Assume //an attribute is extended if the corresponding attribute descriptor cannot be located and the value is over 255 //characters. - if (isExtendedAttribute?.Equals(attribute?.IsExtendedAttribute?? attributeValue.Value?.Length > 255)?? true) { + if (isExtendedAttribute?.Equals(attribute?.IsExtendedAttribute?? attributeValue.Value.Length > 255)?? true) { attributes.Add(attributeValue); } From a1db85700ea5f56108a370b1663e9eb7eae7068a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 14:25:37 -0800 Subject: [PATCH 020/778] Renamed attributes to correspond to base method names Methods that override underlying methods should use the same parameter names as the underlying methods to avoid confusion. There were two cases where this was not applied, and they are fixed here. Technically, this _could_ be a breaking change if explicit parameter names are set as part of the call. That's highly unlikely here, however, since a) these have very few parameters (and thus there isn't an incentive to help distinguish or shortcut them), and b) while potentially accessible externally, these methods are generally used internally only. As such, I'm not worried about this breaking implementations. Addresses CA1725 warning regarding mismatch in parameter name from base method. --- .../ValidateTopicAttribute.cs | 24 +++++++++---------- OnTopic/Collections/NamedTopicCollection.cs | 16 ++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs index ddd1eaae..f9951dbe 100644 --- a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs +++ b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs @@ -55,17 +55,17 @@ public sealed class ValidateTopicAttribute : ActionFilterAttribute { /// /// A view associated with the requested topic's Content Type and view. [NonAction] - public override void OnActionExecuting(ActionExecutingContext filterContext) { + public override void OnActionExecuting(ActionExecutingContext context) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(filterContext, nameof(filterContext)); + Contract.Requires(context, nameof(context)); /*------------------------------------------------------------------------------------------------------------------------ | Establish variables \-----------------------------------------------------------------------------------------------------------------------*/ - var controller = filterContext.Controller as TopicController; + var controller = context.Controller as TopicController; var currentTopic = controller?.CurrentTopic; /*------------------------------------------------------------------------------------------------------------------------ @@ -82,7 +82,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { \-----------------------------------------------------------------------------------------------------------------------*/ if (currentTopic is null) { if (!AllowNull) { - filterContext.Result = controller.NotFound("There is no topic associated with this path."); + context.Result = controller.NotFound("There is no topic associated with this path."); } return; } @@ -93,7 +93,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { //### TODO JJC082817: Should allow this to be bypassed for administrators; requires introduction of Role dependency //### e.g., if (!Roles.IsUserInRole(Page?.User?.Identity?.Name ?? "", "Administrators")) {...} if (currentTopic.IsDisabled) { - filterContext.Result = new UnauthorizedResult(); + context.Result = new UnauthorizedResult(); return; } @@ -101,7 +101,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { | Handle redirect \-----------------------------------------------------------------------------------------------------------------------*/ if (!String.IsNullOrEmpty(currentTopic.Attributes.GetValue("URL"))) { - filterContext.Result = controller.RedirectPermanent(currentTopic.Attributes.GetValue("URL")); + context.Result = controller.RedirectPermanent(currentTopic.Attributes.GetValue("URL")); return; } @@ -112,7 +112,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { | the request is valid, but forbidden. \-----------------------------------------------------------------------------------------------------------------------*/ if (currentTopic is { ContentType: "List"} or { Parent: {ContentType: "List" } }) { - filterContext.Result = new StatusCodeResult(403); + context.Result = new StatusCodeResult(403); return; } @@ -123,7 +123,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { | indicate that the request is valid, but forbidden. Unlike nested topics, children of containers are potentially valid. \-----------------------------------------------------------------------------------------------------------------------*/ if (currentTopic.ContentType is "Container") { - filterContext.Result = new StatusCodeResult(403); + context.Result = new StatusCodeResult(403); return; } @@ -134,7 +134,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { | redirected to the first (non-hidden, non-disabled) page in the page group. \-----------------------------------------------------------------------------------------------------------------------*/ if (currentTopic.ContentType is "PageGroup") { - filterContext.Result = controller.Redirect( + context.Result = controller.Redirect( currentTopic.Children.Where(t => t.IsVisible()).FirstOrDefault().GetWebPath() ); return; @@ -147,15 +147,15 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) { | mismatches between the requested URL and the canonical URL, and to help ensure that references to topics maintain the | same case as assigned in the topic graph, URLs that vary only by case will be redirected to the expected case. \-----------------------------------------------------------------------------------------------------------------------*/ - if (!currentTopic.GetWebPath().Equals(filterContext.HttpContext.Request.Path, StringComparison.Ordinal)) { - filterContext.Result = controller.RedirectPermanent(currentTopic.GetWebPath()); + if (!currentTopic.GetWebPath().Equals(context.HttpContext.Request.Path, StringComparison.Ordinal)) { + context.Result = controller.RedirectPermanent(currentTopic.GetWebPath()); return; } /*------------------------------------------------------------------------------------------------------------------------ | Base processing \-----------------------------------------------------------------------------------------------------------------------*/ - base.OnActionExecuting(filterContext); + base.OnActionExecuting(context); } diff --git a/OnTopic/Collections/NamedTopicCollection.cs b/OnTopic/Collections/NamedTopicCollection.cs index 233d7a58..b32c65fc 100644 --- a/OnTopic/Collections/NamedTopicCollection.cs +++ b/OnTopic/Collections/NamedTopicCollection.cs @@ -50,11 +50,11 @@ public NamedTopicCollection(string name = "", IEnumerable? topics = null) /// When inserting an item, determine if it will change the collection; if it will, mark the collection as . /// - protected override void InsertItem(int index, Topic topic) { + protected override void InsertItem(int index, Topic item) { Contract.Requires(index, nameof(index)); - Contract.Requires(topic, nameof(topic)); - IsDirty = IsDirty || !Contains(topic.Key); - base.InsertItem(index, topic); + Contract.Requires(item, nameof(item)); + IsDirty = IsDirty || !Contains(item.Key); + base.InsertItem(index, item); } /*========================================================================================================================== @@ -64,11 +64,11 @@ protected override void InsertItem(int index, Topic topic) { /// When updating an existing item, determine if it will change the collection; if it will, mark the collection as . /// - protected override void SetItem(int index, Topic topic) { + protected override void SetItem(int index, Topic item) { Contract.Requires(index, nameof(index)); - Contract.Requires(topic, nameof(topic)); - IsDirty = IsDirty || !Contains(topic.Key); - base.SetItem(index, topic); + Contract.Requires(item, nameof(item)); + IsDirty = IsDirty || !Contains(item.Key); + base.SetItem(index, item); } /*========================================================================================================================== From 53d731fdbfc8ff000b9c82c524beb3534aa69a19 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 14:42:12 -0800 Subject: [PATCH 021/778] Suppress warning due to double-checked lock pattern The CA1508 occurs when dead code is created as the result of a redundant condition. When dealing with multi-threading, however, a double-checked lock pattern isn't necessarily redundant. --- .../Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index a0c97975..263ea66c 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -212,9 +212,11 @@ private static int DistanceFromRoot(Topic? sourceTopic) { \-----------------------------------------------------------------------------------------------------------------------*/ if (viewModel.Children.Count == 0) { lock (viewModel) { + #pragma warning disable CA1508 // Avoid dead conditional code if (viewModel.Children.Count == 0) { children.ForEach(c => viewModel.Children.Add(c)); } + #pragma warning restore CA1508 // Avoid dead conditional code } } From 0b8a54632fe3c76814cc9f4178a29e539aed68ab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 14:47:51 -0800 Subject: [PATCH 022/778] Ensure `ModelType` has default value In practice, we expect the `ModelType` enum to always have a value, based on how it is designed. However, enums default to 0 when initialized, and so not havine a `None` assigned to 0 can open up for unexpected bugs, and especially in downlevel code. To avoid any potential confusion, a `None` option is added. This mitigates CA1008. --- OnTopic/Metadata/ModelType.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OnTopic/Metadata/ModelType.cs b/OnTopic/Metadata/ModelType.cs index c3485e8c..9ad179f5 100644 --- a/OnTopic/Metadata/ModelType.cs +++ b/OnTopic/Metadata/ModelType.cs @@ -14,6 +14,14 @@ namespace OnTopic.Metadata { /// public enum ModelType { + /*-------------------------------------------------------------------------------------------------------------------------- + | NONE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// No value is configured. + /// + None = 0, + /*-------------------------------------------------------------------------------------------------------------------------- | SCALAR VALUE \-------------------------------------------------------------------------------------------------------------------------*/ From 8124334378b0d990e41c6e555fccb28dd4754318 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 14:54:09 -0800 Subject: [PATCH 023/778] Establish `AssemblyInfo` class for `TestDoubles` project Normally, all of our projects have an `AssemblyInfo` class for storing properties setup via the default template. As the `TestDoubles` project was created later, with a newer template, it didn't include one. Most properties are now handled via the `csproj` file so that's not a huge issue. One property that remains needed, however, is the `CLSCompliant` attribute, which confirms that the assembly conforms to CLS conventions. This resolves CA1014. While I was at it, I also assigned the correct GUID for the assembly, as previously assigned in the `sln` file. --- OnTopic.TestDoubles/Properties/AssemblyInfo.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 OnTopic.TestDoubles/Properties/AssemblyInfo.cs diff --git a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..770c70b0 --- /dev/null +++ b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.InteropServices; + +/*============================================================================================================================== +| DEFINE ASSEMBLY ATTRIBUTES +>=============================================================================================================================== +| Declare and define attributes used in the compiling of the finished assembly. +\-----------------------------------------------------------------------------------------------------------------------------*/ +[assembly: ComVisible(false)] +[assembly: CLSCompliant(true)] +[assembly: Guid("FE175884-59C1-4C4D-A663-4CC570432ECC")] From 1bbfc32256d42d917f1af86eb160d7a142651ca1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 15:03:58 -0800 Subject: [PATCH 024/778] Suppress CA1024 as appropriate CA1024 recommends avoiding `Get()` methods without parameters in preference for properties. That's good advice. But there are a few cases where it doesn't make sense. In these cases, a value is being dynamically calculated based on context. This isn't terribly time consuming, but it is certainly more than just returning a field. And the value could change based on contextual updates outside the scope of the current object. Further, there are conventions built into e.g. the `TopicMappingService` which recognize `Get{Attribute}()` methods as alternatives to `{Attribute} { get; }` accessors when mapping DTOs. In these cases, these methods are deliberate and should be persisted. --- .../Components/PageLevelNavigationViewComponentBase{T}.cs | 2 ++ OnTopic.Tests/ViewModels/MethodBasedViewModel.cs | 2 ++ OnTopic/Topic.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index a6e45802..ca3f2fba 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -63,6 +63,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService /// /// The navigation root in the case of the page-level navigation any parent of content type PageGroup. /// + #pragma warning disable CA1024 // Use properties where appropriate protected Topic? GetNavigationRoot() { /*------------------------------------------------------------------------------------------------------------------------ @@ -88,6 +89,7 @@ navigationRootTopic is not null and not ({ Parent: null } or { ContentType: "Pag return navigationRootTopic?.Parent is null? null : navigationRootTopic; } + #pragma warning restore CA1024 // Use properties where appropriate /*========================================================================================================================== | METHOD: MAP NAVIGATION TOPIC VIEW MODELS diff --git a/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs b/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs index caf081ab..428b9c46 100644 --- a/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs +++ b/OnTopic.Tests/ViewModels/MethodBasedViewModel.cs @@ -20,7 +20,9 @@ public class MethodBasedViewModel { private int _methodValue; public void SetMethod(int methodValue) => _methodValue = methodValue; + #pragma warning disable CA1024 // Use properties where appropriate public int GetMethod() => _methodValue; + #pragma warning restore CA1024 // Use properties where appropriate } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 4d79d277..6f339ebd 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -493,6 +493,7 @@ public void SetParent(Topic parent, Topic? sibling = null) { /// Example: "Root:Configuration:ContentTypes:Page". /// /// The unique key of the current . + #pragma warning disable CA1024 // Use properties where appropriate public string GetUniqueKey() { /*------------------------------------------------------------------------------------------------------------------------ @@ -513,6 +514,7 @@ public string GetUniqueKey() { return uniqueKey; } + #pragma warning restore CA1024 // Use properties where appropriate /*========================================================================================================================== | METHOD: GET WEB PATH From 0f7cec1930292104540a35c6425f0a8bfe9822c3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 7 Dec 2020 15:06:35 -0800 Subject: [PATCH 025/778] Suppress CA1019 as appropriate Normally, properties that can be set via the constructor should not have a setter. That makes sense. In the case that there are overloaded constructors, however, not all of which set that property, this doesn't make much sense. That is the case here, for the `[Relationship]` attribute. --- OnTopic/Mapping/Annotations/RelationshipAttribute.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic/Mapping/Annotations/RelationshipAttribute.cs b/OnTopic/Mapping/Annotations/RelationshipAttribute.cs index fa13d842..66af9c22 100644 --- a/OnTopic/Mapping/Annotations/RelationshipAttribute.cs +++ b/OnTopic/Mapping/Annotations/RelationshipAttribute.cs @@ -64,7 +64,9 @@ public RelationshipAttribute(RelationshipType type = RelationshipType.Any) { /// /// Gets the value of the relationship type. /// + #pragma warning disable CA1019 // Define accessors for attribute arguments public RelationshipType Type { get; set; } + #pragma warning restore CA1019 // Define accessors for attribute arguments } //Class } //Namespace \ No newline at end of file From 2c9ef88a658cf9fceded8ab412bf74e035415355 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 10 Dec 2020 12:58:37 -0800 Subject: [PATCH 026/778] Installed NuGet version of `NetAnalyzers` With the migration from `FxCopAnalyzers` to `NetAnalyzers`, we no longer _need_ to use the NuGet package, as the analyzers are now built into the SDK (7733c0c). Unfortunately, that also means that they're going to lag behind the latest fixes. When we originally tried installing the NuGet package (7733c0c), we got duplicate warnings, and so had relied on the SDK implementation. That appears to have been subsequently resolved. As such, I'm moving back to the NuGet implementation to ensure we're always using the latest version. --- .../OnTopic.AspNetCore.Mvc.Host.csproj | 7 +++++++ .../OnTopic.AspNetCore.Mvc.Tests.csproj | 4 ++++ OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 4 ++++ OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 4 ++++ OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 4 ++++ OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 7 +++++++ OnTopic.Tests/OnTopic.Tests.csproj | 4 ++++ OnTopic.ViewModels/OnTopic.ViewModels.csproj | 4 ++++ OnTopic/OnTopic.csproj | 4 ++++ 9 files changed, 42 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index da24af8d..9ebfca4f 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -6,6 +6,13 @@ false + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index 890a03c6..f2c36a75 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -7,6 +7,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 85999afb..c237431c 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -45,6 +45,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 3dd80357..fae32cad 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -45,6 +45,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index f50d316b..41ac0014 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -42,6 +42,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index d9a66695..77ff107c 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -24,6 +24,13 @@ true + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index ee44dfd4..c667655d 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -29,6 +29,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 1e51cc95..93b688a8 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -44,6 +44,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 2a6f0c3f..88f413dc 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -45,6 +45,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all From 4628231e104fc23b08c6c9753117e54468f0618c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 10 Dec 2020 13:00:04 -0800 Subject: [PATCH 027/778] Prefer `Count` over `Count()` The `Count` property is faster than relying on LINQ's `Count()` method, which iterates over the collection. Resolves CA1829. --- OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index 93fb868a..db298799 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -116,7 +116,7 @@ public async Task Menu_Invoke_ReturnsNavigationViewModel() { Assert.IsNotNull(model); Assert.AreEqual(_topic.GetUniqueKey(), model.CurrentKey); Assert.AreEqual("Root:Web", model.NavigationRoot.UniqueKey); - Assert.AreEqual(3, model.NavigationRoot.Children.Count()); + Assert.AreEqual(3, model.NavigationRoot.Children.Count); Assert.IsTrue(model.NavigationRoot.IsSelected(_topic.GetUniqueKey())); } @@ -141,7 +141,7 @@ public async Task PageLevelNavigation_Invoke_ReturnsNavigationViewModel() { Assert.IsNotNull(model); Assert.AreEqual(_topic.GetUniqueKey(), model.CurrentKey); Assert.AreEqual("Root:Web:Web_3", model.NavigationRoot.UniqueKey); - Assert.AreEqual(2, model.NavigationRoot.Children.Count()); + Assert.AreEqual(2, model.NavigationRoot.Children.Count); Assert.IsTrue(model.NavigationRoot.IsSelected(_topic.GetUniqueKey())); } From 2d36cd01bf0885e9fad0da24ea3c6a78428afb0d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 10 Dec 2020 13:02:12 -0800 Subject: [PATCH 028/778] Fixed justification for suppression of CA1307 This isn't caused by a bug so much as competing guidance due to overload availability in .NET Standard 2.0 v. 2.1. --- OnTopic/GlobalSuppressions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index ebad71a7..52eb59f6 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "Invalid overload; known bug in code analysis", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] +[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "StringComparison overload not supported by .NET Standard 2.0", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] [assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] [assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Breaking change; deferred to major version.", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file From fb0e056227e052124d9a7fe9b25fb50476ef18cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 10 Dec 2020 13:09:50 -0800 Subject: [PATCH 029/778] Fixed indentation of `#pragma` suppression --- OnTopic/Internal/Diagnostics/Contract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index 89f5d5a5..e2237292 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -71,7 +71,7 @@ public static class Contract { #pragma warning disable CS8777 // Parameter must have a non-null value when exiting. public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, string? errorMessage = null) => Requires(requiredObject is not null, errorMessage); -#pragma warning restore CS8777 // Parameter must have a non-null value when exiting. + #pragma warning restore CS8777 // Parameter must have a non-null value when exiting. /// /// Will throw the provided generic exception if the supplied expression evaluates to false. From aaafa3a4dcfd70aebd778bd847b9923f3052e220 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 16:18:25 -0800 Subject: [PATCH 030/778] Introduced a new `BusinessLogicCache` If `Topic` or a derivative contains a property with the same name as an attribute, it should be called whenever an attribute is updated in order to ensure any business logic is enforced. To do this, we need to keep track of the initial `AttributeValue` (before executing the business logic) as well as whether the business logic has been triggered already (to avoid an infinite loop). We'd like to move back toward a read-only `AttributeValue` class, which was originally intended as a value object. To do that, we need to start by removing state tracking from it. That includes the `EnabledBusinessLogic` property. To replace this, we need to establish a different way of tracking whether or not the business logic is currently in progress, and to maintain access to the original `AttributeValue` in order to maintain optional attributes that `InsertItem()` and `SetItem()` aren't otherwise aware of (such as `IsDirty` and `LastModified`). To do this, we implement a new `Dictionary` as a local cache. If a record is in this cache, that means it has already gone through its initial evaluation, and is now having its business logic enforced. This cache can be checked to get the `AttributeValue` from that initial evaluation, and to confirm the current state of enforcement. As this is a bit unintuitive, I've included plenty of documentation, even though it's a `private` member. In subsequent commits, we'll be wiring up this logic elsewhere in the `AttributeValueCollection` class. --- .../Collections/AttributeValueCollection.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 4914ebc1..7498e12e 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -54,6 +55,46 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar _associatedTopic = parentTopic; } + /*========================================================================================================================== + | PROPERTY: BUSINESS LOGIC CACHE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a local cache of objects, keyed by their , prior + /// to them having their business logic enforced. + /// + /// + /// + /// By default, there is no business logic enforced for objects. This can be mitigate by + /// implementing properties that correspond to the attribute names on or a derivative class. + /// + /// + /// The enforces this business logic by forcing updates to go through that + /// property if it exists. To ensure this is enforced at all entry points, this is handled via the and methods. This ensures that the + /// business logic is enforced even if implementors bypass the method, and instead use e.g. 's indexer or underlying methods + /// such as . + /// + /// + /// Since neither the or + /// methods, nor the properties that calls, accept the optional + /// parameters from , however, that means that + /// parameter values corresponding to e.g. and will get lost in the process. In addition, there needs to be a way to track whether + /// the call to e.g., is being triggered by a direct call, or as a round- + /// trip through one of these property setters. + /// + /// + /// The addresses this issue by providing a cache of the original instances, indexed by their , for attributes currently being + /// routed through their corresponding property setter. If a record exists for the current attribute, the method knows it should not enforce business logic again�as that would result + /// in an infinite loop�and should instead persist the record to the collection. Further, because the includes the original , the original parameters such as the are not lost, and can be applied to the final object. + /// + /// + private Dictionary BusinessLogicCache { get; } = new(); /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ From b9de46d5c48c416b896c9c78788b28833c15a95b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 16:47:46 -0800 Subject: [PATCH 031/778] Add logic for maintaining `BusinessLogicCache` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `enableBusinessLogic` parameter of `SetValue()` was evaluated at the top so that it could be added to the `AttributeValue` objects when they were created. With the introduction of the `BusinessLogicCache`, that no longer works as the `BusinessLogicCache` needs to maintain a copy of the `AttributeValue` and, thus, that logic can't happen until _after_ the `AttributeValue` has been created. In addition to this, the cache must be populated _prior_ to calling `Add()` or `InsertAt()`; otherwise, an infinite loop will occur since the underlying `SetItem()` and `InsertItem()` methods will enforce the business logic; if the business logic cache isn't established yet, they won't be able to detect that the business logic has already been triggered. Remember, the whole purpose of the `BusinessLogicCache` is to keep track of this state so that `AttributeValue` doesn't, while still ensuring that `SetItem()` and `InsertItem()` can determine the current state. Given this, we not only need to move the `enforceBusinessLogic` evaluate to the bottom, but we need to then move call to add (`Add(…)`) or replace (`this[…]`) the `AttributeValue` below that. This requires some minor rearranging of the variables so that we can establish the `updatedAttributeValue` at the top, set it within each condition, and finally add it after the `BusinessLogicCache` has been established. --- .../Collections/AttributeValueCollection.cs | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 7498e12e..073bc734 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -88,13 +88,14 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar /// The addresses this issue by providing a cache of the original instances, indexed by their , for attributes currently being /// routed through their corresponding property setter. If a record exists for the current attribute, the method knows it should not enforce business logic again�as that would result - /// in an infinite loop�and should instead persist the record to the collection. Further, because the method knows it should not enforce business logic again—as that would result + /// in an infinite loop—and should instead persist the record to the collection. Further, because the includes the original , the original parameters such as the are not lost, and can be applied to the final object. /// /// private Dictionary BusinessLogicCache { get; } = new(); + /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ @@ -406,35 +407,27 @@ internal void SetValue( Contract.Requires(!String.IsNullOrWhiteSpace(key), "key"); TopicFactory.ValidateKey(key); - /*------------------------------------------------------------------------------------------------------------------------ - | Establish secret handshake for later enforcement of properties - >------------------------------------------------------------------------------------------------------------------------- - | ###HACK JJC100617: We want to ensure that any attempt to set attributes that have corresponding (writable) properties - | use those properties, thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by - | KeyedCollection, and not just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) will look for the - | EnforceBusinessLogic property. If it is set to false, they assume the property set the value (e.g., by calling the - | protected SetValue method with enforceBusinessLogic set to false). Otherwise, the corresponding property will be called. - | The EnforceBusinessLogic thus avoids a redirect loop in this scenario. This, of course, assumes that properties are - | correctly written to call the enforceBusinessLogic parameter. - \-----------------------------------------------------------------------------------------------------------------------*/ - enforceBusinessLogic = (enforceBusinessLogic && _typeCache.HasSettableProperty(_associatedTopic.GetType(), key)); + AttributeValue? originalAttributeValue = null; + AttributeValue? updatedAttributeValue = null; + + if (Contains(key)) { + originalAttributeValue = this[key]; + } /*------------------------------------------------------------------------------------------------------------------------ | Update existing attribute value >-----------------------------------------------------------------------------------------------------------------------— | Because AttributeValue is immutable, a new instance must be constructed to replace the previous version. \-----------------------------------------------------------------------------------------------------------------------*/ - if (Contains(key)) { - var originalAttribute = this[key]; - var markAsDirty = originalAttribute.IsDirty; + if (originalAttributeValue is not null) { + var markAsDirty = originalAttributeValue.IsDirty; if (isDirty.HasValue) { markAsDirty = isDirty.Value; } - else if (originalAttribute.Value != value) { + else if (originalAttributeValue.Value != value) { markAsDirty = true; } - var newAttribute = new AttributeValue(key, value, markAsDirty, enforceBusinessLogic, version, isExtendedAttribute); - this[IndexOf(originalAttribute)] = newAttribute; + updatedAttributeValue = new AttributeValue(key, value, markAsDirty, enforceBusinessLogic, version, isExtendedAttribute); } /*------------------------------------------------------------------------------------------------------------------------ @@ -451,7 +444,36 @@ internal void SetValue( | Create new attribute value \-----------------------------------------------------------------------------------------------------------------------*/ else { - Add(new AttributeValue(key, value, isDirty ?? true, enforceBusinessLogic, version, isExtendedAttribute)); + updatedAttributeValue = new AttributeValue(key, value, isDirty ?? true, version, isExtendedAttribute); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish secret handshake for later enforcement of properties + >------------------------------------------------------------------------------------------------------------------------- + | ###HACK JJC100617: We want to ensure that any attempt to set attributes that have corresponding (writable) properties + | use those properties, thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by + | KeyedCollection, and not just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) will look for the + | EnforceBusinessLogic property. If it is set to false, they assume the property set the value (e.g., by calling the + | protected SetValue method with enforceBusinessLogic set to false). Otherwise, the corresponding property will be called. + | The EnforceBusinessLogic thus avoids a redirect loop in this scenario. This, of course, assumes that properties are + | correctly written to call the enforceBusinessLogic parameter. + \-----------------------------------------------------------------------------------------------------------------------*/ + enforceBusinessLogic = !enforceBusinessLogic && _typeCache.HasSettableProperty(_associatedTopic.GetType(), key); + if (enforceBusinessLogic && !BusinessLogicCache.ContainsKey(key)) { + BusinessLogicCache.Add(key, updatedAttributeValue); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Persist attribute value + \-----------------------------------------------------------------------------------------------------------------------*/ + if (updatedAttributeValue is null) { + return; + } + else if (originalAttributeValue is not null) { + this[IndexOf(originalAttributeValue)] = updatedAttributeValue; + } + else { + Add(updatedAttributeValue); } } @@ -554,8 +576,8 @@ protected override void RemoveItem(int index) { /// The with the business logic applied. private bool EnforceBusinessLogic(AttributeValue originalAttribute, out AttributeValue settableAttribute) { settableAttribute = originalAttribute; - if (!originalAttribute.EnforceBusinessLogic) { - originalAttribute.EnforceBusinessLogic = true; + if (BusinessLogicCache.ContainsKey(originalAttribute.Key)) { + BusinessLogicCache.Remove(originalAttribute.Key); return true; } else if (_typeCache.HasSettableProperty(_associatedTopic.GetType(), originalAttribute.Key)) { From bea6d6bdf5e60f327bb5d3b079e188de856ae8f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 17:10:56 -0800 Subject: [PATCH 032/778] Handle business logic within `SetValue()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `EnforceBusinessLogic()` method not only triggered the business logic, but also handled returning the original `AttributeValue` back to `SetItem()` or `InsertItem()`. This was necessary to ensure that any optional parameters sent to `SetValue()`—such as `isDirty` or `version`—were maintained, since `SetItem()` and `InsertItem()` wouldn't otherwise be aware of them. This is no longer necessary since the `BusinessLogicCache` now offers access to the original `AttributeValue` from before `EnforceBusinessLogic()` called out to the underlying property. As such, for call to `SetValue()` triggered by the `Topic` properties, we can now detect that it's on the return trip from calling the business logic, and resurrect that original `AttributeValue`, with any optional parameters already set to its properties, and persist that directly. This is handy as it not only simplifies the logic, but also prevents us from needing to create a new `AttributeValue` for the business logic, only to discard it at the last minute. Now, we can just reinsert the original `AttributeValue`. (We are assuming now, just as we assumed previously, that the `AttributeValue.Value` will be the same on the new version and the original version, since we don't expect calling a property setter to _alter_ the value. If the value is invalid, and _exception_ will be thrown, at which point this code won't execute. As such, the only differences between the initial `AttributeValue` and the `AttributeValue` that would have been created by a second call to `SetValue()` should be those state properties that we weren't able to relay through the `EnforceBusinessLogic()` behavior, such as `IsDirty` and `LastModified`.) This allows us to get rid of the `out` parameter on `EnforceBusinessLogic()` since `SetItem()` and `InsertItem()` are now operating off of the original version, and don't need the new version to be swapped out with the original. --- .../Collections/AttributeValueCollection.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 073bc734..5bb57c5f 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -407,6 +407,9 @@ internal void SetValue( Contract.Requires(!String.IsNullOrWhiteSpace(key), "key"); TopicFactory.ValidateKey(key); + /*------------------------------------------------------------------------------------------------------------------------ + | Retrieve original attribute + \-----------------------------------------------------------------------------------------------------------------------*/ AttributeValue? originalAttributeValue = null; AttributeValue? updatedAttributeValue = null; @@ -414,12 +417,22 @@ internal void SetValue( originalAttributeValue = this[key]; } + /*------------------------------------------------------------------------------------------------------------------------ + | Update from business logic + >-----------------------------------------------------------------------------------------------------------------------— + | If the original values have already been applied, and SetValue() is being triggered a second time after enforcing + | business logic, then use the original values, while applying any change in the value triggered by the business logic. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (BusinessLogicCache.ContainsKey(key)) { + BusinessLogicCache.TryGetValue(key, out updatedAttributeValue); + } + /*------------------------------------------------------------------------------------------------------------------------ | Update existing attribute value >-----------------------------------------------------------------------------------------------------------------------— | Because AttributeValue is immutable, a new instance must be constructed to replace the previous version. \-----------------------------------------------------------------------------------------------------------------------*/ - if (originalAttributeValue is not null) { + else if (originalAttributeValue is not null) { var markAsDirty = originalAttributeValue.IsDirty; if (isDirty.HasValue) { markAsDirty = isDirty.Value; @@ -505,7 +518,7 @@ internal void SetValue( /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." /// protected override void InsertItem(int index, AttributeValue item) { - if (EnforceBusinessLogic(item, out item)) { + if (EnforceBusinessLogic(item)) { if (!Contains(item.Key)) { base.InsertItem(index, item); } @@ -535,7 +548,7 @@ protected override void InsertItem(int index, AttributeValue item) { /// The location that the should be set. /// The object which is being inserted. protected override void SetItem(int index, AttributeValue item) { - if (EnforceBusinessLogic(item, out item)) { + if (EnforceBusinessLogic(item)) { base.SetItem(index, item); } } @@ -570,12 +583,8 @@ protected override void RemoveItem(int index) { /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. /// /// The object which is being inserted. - /// - /// Outputs the that should be set; will return null if it should not be set. - /// /// The with the business logic applied. - private bool EnforceBusinessLogic(AttributeValue originalAttribute, out AttributeValue settableAttribute) { - settableAttribute = originalAttribute; + private bool EnforceBusinessLogic(AttributeValue originalAttribute) { if (BusinessLogicCache.ContainsKey(originalAttribute.Key)) { BusinessLogicCache.Remove(originalAttribute.Key); return true; From 7b1377fab1f6e38f3222b23bb9ab59d52d42e4ba Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 17:24:53 -0800 Subject: [PATCH 033/778] Remove legacy `EnforceBusinessLogic` property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the business logic state now successfully migrated over to the new `BusinessLogicCache` within `AttributeValueCollection`, there's no longer a need to maintain the `EnforceBusinessLogic` property on `AttributeValue`. This brings us one step closer to being able to return `AttributeValue` to a read-only value object—and, in the meanwhile, has effectively cleaned up the logic within `AttributeValueCollection`. --- OnTopic/Attributes/AttributeValue.cs | 37 +------------------ .../Collections/AttributeValueCollection.cs | 2 +- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index 77e1f2f0..779c3b5b 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -73,7 +73,6 @@ public AttributeValue(string key, string? value, bool isDirty = true) { Key = key; Value = value; IsDirty = isDirty; - EnforceBusinessLogic = true; } @@ -90,10 +89,6 @@ public AttributeValue(string key, string? value, bool isDirty = true) { /// An optional boolean indicator noting whether the collection item is a new value, and /// should thus be saved to the database when is next called. /// - /// - /// If disabled, will not call local properties on that - /// correspond to the as a means of enforcing the business logic. - /// /// /// The value that the attribute was last modified. This is intended exclusively for use when /// populating the topic graph from a persistent data store as a means of indicating the current version for each @@ -108,15 +103,13 @@ internal AttributeValue( string key, string? value, bool isDirty, - bool enforceBusinessLogic, - DateTime? lastModified = null, + DateTime? lastModified = null, bool? isExtendedAttribute = null ): this( key, value, isDirty ) { - EnforceBusinessLogic = enforceBusinessLogic; LastModified = lastModified?? DateTime.Now; IsExtendedAttribute = isExtendedAttribute; } @@ -160,34 +153,6 @@ internal AttributeValue( /// public bool IsDirty { get; set; } - /*========================================================================================================================== - | PROPERTY: ENFORCE BUSINESS LOGIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets whether business logic should be enforced when adding an to an - /// . - /// - /// - /// By default, when a user attempts to update an attribute's value by calling , or when an is added to the , the will automatically attempt to call any corresponding setters on - /// (or a derived instance) to ensure that the business logic is enforced. To avoid an infinite loop, however, this is - /// disabled when properties on call . - /// When that happens, the value is set to false to communicate to the that it should not call the local property. This value is only intended for internal - /// use. - /// - /// - /// !String.IsNullOrWhiteSpace(value) - /// - /// - /// !value.Contains(" ") - /// - internal bool EnforceBusinessLogic { get; set; } - /*========================================================================================================================== | PROPERTY: LAST MODIFIED \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 5bb57c5f..8502a857 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -440,7 +440,7 @@ internal void SetValue( else if (originalAttributeValue.Value != value) { markAsDirty = true; } - updatedAttributeValue = new AttributeValue(key, value, markAsDirty, enforceBusinessLogic, version, isExtendedAttribute); + updatedAttributeValue = new AttributeValue(key, value, markAsDirty, version, isExtendedAttribute); } /*------------------------------------------------------------------------------------------------------------------------ From 0aae414ade59b2285a0da8b333fd90432cd19232 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 17:48:07 -0800 Subject: [PATCH 034/778] Establish a local `DeletedAttributes` tracking collection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of simply tracking whether or not any attributes have been deleted, the `DeletedAttributes` collection will allow us to track _which_ attributes have been deleted specifically. In addition to making the application easier to debug by being more explicit, this will critically allow us to track the deletion of _arbitrary attributes_—i.e., attributes not defined on the `ContentTypeDescriptor`. This will fix a bug where arbitrary attributes could be deleted using `SetValue()`, since that would leave an "empty" `AttributeValue`, but not when calling `Clear()` or `Remove()`, since those would remove the `AttributeValue` entirely, thus providing no record that the arbitrary attribute had ever been present. This will be implemented in subsequent commits. For now, this establishes the framework for tracking these deletions. --- OnTopic/Collections/AttributeValueCollection.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 8502a857..df63cb3a 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -96,6 +96,23 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar /// private Dictionary BusinessLogicCache { get; } = new(); + /*========================================================================================================================== + | PROPERTY: DELETED ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// When an attribute is deleted, keep track of it so that it can be marked for deletion when the topic is saved. + /// + /// + /// As a performance enhancement, implementations will only save topics that are marked as + /// . If a is deleted, then it won't be marked as dirty. If no + /// other instances were modified, then the topic won't get saved, and that value won't be + /// deleted. Further more, the method has no way of + /// detecting the deletion of arbitrary attributes�i.e., attributes that were deleted which don't correspond to attributes + /// configured on the . By tracking any deleted attributes, we ensure both + /// scenarios can be accounted for. + /// + internal List DeletedAttributes { get; } = new(); + /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ From 54eab63a60d2716abae7e01b03e493ee21169ad1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 17:51:18 -0800 Subject: [PATCH 035/778] Track deletions via new `DeletedAttributes` collection Instead of setting the `_attributesDeleted` flag, instead add the attribute keys to the newly established `DeletedAttributes` collection (0aae414). This provides more granular and precise control over exactly what attributes have been deleted, not just whether or not any attributes have been deleted. --- OnTopic/Collections/AttributeValueCollection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index df63cb3a..840e6aaa 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -38,7 +38,6 @@ public class AttributeValueCollection : KeyedCollection \-------------------------------------------------------------------------------------------------------------------------*/ private readonly Topic _associatedTopic; private int _setCounter; - private bool _attributesDeleted; /*========================================================================================================================== | CONSTRUCTOR @@ -132,7 +131,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar /// /// True if the attribute value is marked as dirty; otherwise false. public bool IsDirty(bool excludeLastModified = false) - => _attributesDeleted || Items.Any(a => + => DeletedAttributes.Count > 0 || Items.Any(a => a.IsDirty && (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.InvariantCultureIgnoreCase)) ); @@ -176,7 +175,7 @@ public void MarkClean(DateTime? version = null) { attribute.IsDirty = false; attribute.LastModified = version?? DateTime.UtcNow; } - _attributesDeleted = false; + DeletedAttributes.Clear(); } /*========================================================================================================================== @@ -582,7 +581,8 @@ protected override void SetItem(int index, AttributeValue item) { /// s are marked as . /// protected override void RemoveItem(int index) { - _attributesDeleted = true; + var attribute = this[index]; + DeletedAttributes.Add(attribute.Key); base.RemoveItem(index); } From d0973b196c8461a160945a1beb2c2a7bbcee8639 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 18:04:09 -0800 Subject: [PATCH 036/778] Remove `DeletedAttributes` when inserting new attributes If an `AttributeValue` is removed, its key will be added to the new `DeletedAttributes` collection for tracking (54eab63). If an `AttributeValue` is subsequently added with the same `Key`, we no longer want to track that as a deletion. This is effectively what should be viewed as a replacement, even though it took place in two steps. To do that, we'll check the `DeletedAttributes` collection on `InsertItem()` and `SetItem()`. While I was at it, I also introduced missing `Contract.Requires()` for the `item`, since those methods will otherwise throw `NullReferenceException`s when attempting to dereference the `item` parameter (e.g., for `item.Key` references). --- OnTopic/Collections/AttributeValueCollection.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 840e6aaa..e3738555 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -534,9 +534,13 @@ internal void SetValue( /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." /// protected override void InsertItem(int index, AttributeValue item) { + Contract.Requires(item, nameof(item)); if (EnforceBusinessLogic(item)) { if (!Contains(item.Key)) { base.InsertItem(index, item); + if (DeletedAttributes.Contains(item.Key)) { + DeletedAttributes.Remove(item.Key); + } } else { throw new ArgumentException( @@ -564,8 +568,12 @@ protected override void InsertItem(int index, AttributeValue item) { /// The location that the should be set. /// The object which is being inserted. protected override void SetItem(int index, AttributeValue item) { + Contract.Requires(item, nameof(item)); if (EnforceBusinessLogic(item)) { base.SetItem(index, item); + if (DeletedAttributes.Contains(item.Key)) { + DeletedAttributes.Remove(item.Key); + } } } From 61570f3ca4b1291d3a3ecb3decdf2d4e1c65bc2b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 15 Dec 2020 18:16:18 -0800 Subject: [PATCH 037/778] Account for `DeletedAttributes` in `GetUnmatchedAttributes()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By calling into `DeletedAttributes` when calling `GetUnmatchedAttributes()`, we are now able to explicitly track and return _arbitrary attributes_—i.e., attributes which are on a topic, but not on the corresponding `ContentTypeDescriptor`. Previously, these could only be deleted by calling e.g. `topic.Attributes.SetValue("ArbitraryAttribute", "")`, as that would reset the value to be empty, but not delete the `AttributeValue` object. Since `GetUnmatchedAttributes()` treats empty `AttributeValues` as deletions, this would allow the arbitrary attribute to be tracked. If, however, a developer called `Remove()` or `Clear()` directly, then there was no way of tracking this deletion. The attribute would be deleted from memory, but not from the data store. This potentially causes bugs and confusion and that attribute would reappear after the application was reset. By tracking deleted attributes directly (0aae414) and factoring it into the `GetUnmatchedAttributes()`, we are now able to detect these. Technically, with this, we could likely remove the checking against the corresponding `ContentTypeDescriptor`, which represents the bulk of the logic here in `GetUnmatchedAttributes()`. We may reevaluate doing that in the future. For now, however, it doesn't add too much overhead, and adds an extra check in case there are, for some reason, disconnects between the `ContentTypeDescriptor` and the schema of the object's attributes. Whether or not that represents a serious concern is something we'll want to put careful thought into, even though there wouldn't appear to be any realistic concerns on first glance. --- OnTopic/Repositories/TopicRepositoryBase.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index ddf5148c..2711e2ee 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -697,9 +697,13 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { | attributes don't have corresponding AttributeDescriptors. To mitigate this, an ad hoc AttributeDescriptor object will be | created for each empty AttributeDescriptor. \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var attribute in topic.Attributes.Where(a => String.IsNullOrEmpty(a.Value))) { - if (!attributes.Contains(attribute.Key)) { - attributes.Add((TextAttribute)TopicFactory.Create(attribute.Key, "TextAttribute")); + var attributeKeys = topic.Attributes + .Where(a => String.IsNullOrEmpty(a.Value)) + .Select(a => a.Key) + .Union(topic.Attributes.DeletedAttributes); + foreach (var attributeKey in attributeKeys) { + if (!attributes.Contains(attributeKey)) { + attributes.Add((TextAttribute)TopicFactory.Create(attributeKey, "TextAttribute")); } } From b858cedd6ea44af940f9df1933337c1a39aacb95 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 13:18:06 -0800 Subject: [PATCH 038/778] Prefer `SetValue()` over `IsDirty` In anticipation of returning `AttributeValue` back to an immutable, read-only value object, we're preferring the use of `SetValue()` over `IsDirty`. `SetValue()` currently still writes to properties, but it will be easier to update the `SetValue()` code to handle copying and modifying the value object than it will be to decentralize that code each place where we need to modify an `AttributeValue`'s properties. Not all instances are covered here, as some need to be first addressed by the `SetValue()` code before we remove this dependency. For instance, see the setting of `IsDirty` and `LastModified` in `EnforceBusinessLogic()`. (This is also a secondary benefit of moving back to an immutable model: it helps encourage use of `SetValue()` over working with `AttributeValue` instances directly.) --- OnTopic/Collections/AttributeValueCollection.cs | 14 +++++--------- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index e3738555..e259d221 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -171,9 +171,8 @@ public bool IsDirty(string name) { /// VersionHistory"/>. /// public void MarkClean(DateTime? version = null) { - foreach (var attribute in Items.Where(a => a.IsDirty)) { - attribute.IsDirty = false; - attribute.LastModified = version?? DateTime.UtcNow; + foreach (var attribute in Items.Where(a => a.IsDirty).ToArray()) { + SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); } DeletedAttributes.Clear(); } @@ -196,10 +195,9 @@ public void MarkClean(DateTime? version = null) { /// public void MarkClean(string name, DateTime? version = null) { if (Contains(name)) { - var attributeValue = this[name]; - if (attributeValue.IsDirty) { - attributeValue.IsDirty = false; - attributeValue.LastModified = version?? DateTime.UtcNow; + var attribute = this[name]; + if (attribute.IsDirty) { + SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); } } } @@ -623,8 +621,6 @@ private bool EnforceBusinessLogic(AttributeValue originalAttribute) { ); } _typeCache.SetPropertyValue(_associatedTopic, originalAttribute.Key, originalAttribute.Value); - this[originalAttribute.Key].IsDirty = originalAttribute.IsDirty; - this[originalAttribute.Key].LastModified = originalAttribute.LastModified; _setCounter = 0; return false; } diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 2711e2ee..21a77677 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -260,7 +260,7 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var attribute in originalVersion.Attributes) { if (!topic.Attributes.Contains(attribute.Key) || topic.Attributes.GetValue(attribute.Key) != attribute.Value) { - attribute.IsDirty = true; + originalVersion.Attributes.SetValue(attribute.Key, attribute.Value, false); } } From e1beadc5f57462c7d5bc2d3becb88fa20349e1e0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 13:18:18 -0800 Subject: [PATCH 039/778] Introduced `IsExternalInit` as polyfill In C# 9.0 and the latest versions of .NET 3.1, there is an `IsExternalInit` class which permits init-only setters. We'll want to take advantage of those in order to move `AttributeValue` back to an immutable value object, as that allows its properties to be initialized without each one being represented by a constructor parameter. Since .NET Standard 2.0 (e.g., .NET Framework 4.8) doesn't have this class, however, we need to manually establish it as a type of "polyfill". That said, there's no actual functionality needed here; we just need an empty internal class to represent this type. --- OnTopic/Internal/Runtime/IsExternalInit.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 OnTopic/Internal/Runtime/IsExternalInit.cs diff --git a/OnTopic/Internal/Runtime/IsExternalInit.cs b/OnTopic/Internal/Runtime/IsExternalInit.cs new file mode 100644 index 00000000..1ba62b43 --- /dev/null +++ b/OnTopic/Internal/Runtime/IsExternalInit.cs @@ -0,0 +1,19 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace System.Runtime.CompilerServices { + + /*============================================================================================================================ + | CLASS: IS EXTERNAL INIT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. + /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. + /// + internal class IsExternalInit { + + } //Class +} //Namespace From 6283ca8d943fd12d56c11124a07352e55286c6f5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 13:18:33 -0800 Subject: [PATCH 040/778] Migrated `AttributeValue` to a new C# 9.0 `record` type The `record` type allows `AttributeValue` to fully return to an immutable, read-only value object, which will take up less memory, and help enforce a preference for using `SetValue()` instead of working directly with `AttributeValue` objects. This commit will break the few remaining places where e.g. `IsDirty` or `LastModified` are updated outside of object initialization; those will be fixed in subsequent commits. --- OnTopic/Attributes/AttributeValue.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index 779c3b5b..f4e255b8 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -38,7 +38,7 @@ namespace OnTopic.Attributes { /// method. /// /// - public class AttributeValue { + public record AttributeValue { /*========================================================================================================================== | CONSTRUCTOR @@ -128,7 +128,7 @@ internal AttributeValue( /// exception="T:System.ArgumentException"> /// !value.Contains(" ") /// - public string Key { get; } + public string Key { get; init; } /*========================================================================================================================== | PROPERTY: VALUE @@ -136,7 +136,7 @@ internal AttributeValue( /// /// Gets the current value of the attribute. /// - public string? Value { get; } + public string? Value { get; init; } /*========================================================================================================================== | PROPERTY: IS DIRTY @@ -151,7 +151,7 @@ internal AttributeValue( /// ignored, thus preventing the need to update attributes (or create new versions of attributes) whose values haven't /// changed. /// - public bool IsDirty { get; set; } + public bool IsDirty { get; init; } /*========================================================================================================================== | PROPERTY: LAST MODIFIED @@ -159,7 +159,7 @@ internal AttributeValue( /// /// Read-only reference to the last DateTime the instance was updated. /// - public DateTime LastModified { get; internal set; } = DateTime.Now; + public DateTime LastModified { get; init; } = DateTime.Now; /*========================================================================================================================== | PROPERTY: IS EXTENDED ATTRIBUTE @@ -190,7 +190,7 @@ internal AttributeValue( /// data should be stored. /// /// - public bool? IsExtendedAttribute { get; internal set; } + public bool? IsExtendedAttribute { get; init; } } //Class -} //Namespace +} //Namespace \ No newline at end of file From 6254557e01686784ebfc94f0e3becfe958485431 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 13:28:52 -0800 Subject: [PATCH 041/778] Improve `SetValue()` logic to fall back to previous value This helps ensure that any legacy values are not overwritten when updating an existing attribute, unless they are explicitly defined as parameters. This is important for maintaining e.g. `LastModified` (for import comparison) and `IsExtendedAttribute` (for identifying potential persistence store mismatches). --- OnTopic/Collections/AttributeValueCollection.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index e259d221..d447aa24 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -454,7 +454,12 @@ internal void SetValue( else if (originalAttributeValue.Value != value) { markAsDirty = true; } - updatedAttributeValue = new AttributeValue(key, value, markAsDirty, version, isExtendedAttribute); + updatedAttributeValue = originalAttributeValue with { + Value = value, + IsDirty = markAsDirty, + LastModified = version?? originalAttributeValue.LastModified, + IsExtendedAttribute = isExtendedAttribute?? originalAttributeValue.IsExtendedAttribute + }; } /*------------------------------------------------------------------------------------------------------------------------ From 129f656e3f898cdfc87208af1723550299ea3aef Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 13:37:14 -0800 Subject: [PATCH 042/778] Introduced unit test for validating `MarkClean(DateTime)` overload While we previously had unit tests for `MarkClean()`, none of them explicitly validated that seting the `version` (`DateTime`) parameter reset the `LastModified` property on any dirty `AttributeValue` instances. This is corrected with this commit. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 5ed3fcbe..bc769431 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -380,6 +380,28 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { } + /*========================================================================================================================== + | TEST: IS DIRTY: MARK CLEAN: UPDATES LAST MODIFIED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Populates the with a and then deletes it. Confirms + /// that the returns the new version after calling . + /// + [TestMethod] + public void IsDirty_MarkClean_UpdatesLastModified() { + + var topic = TopicFactory.Create("Test", "Container"); + var version = DateTime.Now.AddDays(5); + + topic.Attributes.SetValue("Baz", "Foo"); + topic.Attributes.MarkClean(version); + topic.Attributes.TryGetValue("Baz", out var cleanedAttribute); + + Assert.AreEqual(version, cleanedAttribute.LastModified); + + } + /*========================================================================================================================== | TEST: IS DIRTY: MARK CLEAN: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ From df720f82090030b47289d52b60a762e8799b8408 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 14:39:07 -0800 Subject: [PATCH 043/778] Ensured initial `AttributeValue` tracked if inserted directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When business logic is enforced, we don't expect the values on the `AttributeValue` to change—and especially those that aren't understood by the business logic, such as `IsDirty` or `LastModified`. The only goal of enforcing business logic is to validate the `value` and ensure any internal state is maintained appropriately. As such, after business logic is enforced, we expect the original `IsDirty` and `LastModified` values to be maintained. In fact, we generally expect that we'll just persist the _original_ `AttributeValue` object with whatever properties were set on it. This is what the new `BusinessLogicCache` manages, by maintaining a copy of that original `AttributeValue` instance. If a record exists in the cache, we know that it is currently doing its round trip through the business logic. This worked fine so long as an implementer called `SetValue()`. But it didn't work if the user went around `SetValue()` by e.g. adding the `AttributeValue` directly. In this case, that original value would not be set in the `BusinessLogicCache`. By accounting for this in `EnforceBusinessLogic()`, we ensure that this scenario is accounted for, in addition to calling `SetValue()` first (either directly, or through a property accessor, such as `Topic.Key`). --- OnTopic/Collections/AttributeValueCollection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index d447aa24..e3a9e1e6 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -625,6 +625,7 @@ private bool EnforceBusinessLogic(AttributeValue originalAttribute) { $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties." ); } + BusinessLogicCache.Add(originalAttribute.Key, originalAttribute); _typeCache.SetPropertyValue(_associatedTopic, originalAttribute.Key, originalAttribute.Value); _setCounter = 0; return false; From 223754401f578ccb1fa35129e7467a7ab35863c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 14:43:24 -0800 Subject: [PATCH 044/778] Introduced unit test to validate business logic honors parameters Parameters such as `IsDirty` should be honored even if the normal business logic would have changed their values. So, for instance, if I call `SetValue()` with `isDirty=false`, and then it calls the `Key` property as part of `EnforceBusinessLogic()`, the `IsDirty` property should remain false even if the `Key` property changed the value. By explicitly setting the `isDirty` parameter or `AttributeValue.IsDirty` property, we are sending clear instructions that we expect that value to be set to false for this transaction. A bug in this behavior was identified and resolved in the previous commit (df720f8), addressing a scenario where this wasn't honored if a new `AttributeValue` object was added directly to the collection. This unit test evaluates both that scenario as well as the `SetValue()` scenario to ensure that both operate as expected. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index bc769431..18e748f1 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -498,6 +498,34 @@ public void Add_InvalidAttributeValue_ThrowsException() { topic.Attributes.Add(new("Key", "# ?")); } + /*========================================================================================================================== + | TEST: REPLACE VALUE: WITH BUSINESS LOGIC: MAINTAINS ISDIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new which maps to directly to a and confirms that the original is replaced if the + /// changes. + /// + [TestMethod] + public void Add_WithBusinessLogic_MaintainsIsDirty() { + + var topic = TopicFactory.Create("Test", "Container", 1); + + topic.Attributes.TryGetValue("Key", out var originalValue); + + var index = topic.Attributes.IndexOf(originalValue); + + topic.Attributes[index] = new AttributeValue("Key", "NewValue", false); + topic.Attributes.TryGetValue("Key", out var newAttribute); + + topic.Attributes.SetValue("Key", "NewerValue", false); + topic.Attributes.TryGetValue("Key", out var newerAttribute); + + Assert.IsFalse(newAttribute.IsDirty); + Assert.IsFalse(newerAttribute.IsDirty); + + } + /*========================================================================================================================== | TEST: SET VALUE: EMPTY ATTRIBUTE VALUE: SKIPS \-------------------------------------------------------------------------------------------------------------------------*/ From 42ab7ba0527a44a3fbfa24d04ca78ec32ef6a713 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 14:50:58 -0800 Subject: [PATCH 045/778] Handled scenario where business logic changes value If `SetValue()` is called or a new `AttributeValue` is added to the `AttributeValueCollection`, and the `AttributeValue.Key` maps to a property on the associated `Topic` instance, then that property will be called in order to enforce business logic. During this process, we normally don't expect the state of the `AttributeValue` to change; i.e., we might expect the property to change its internal state, or even to throw an exception if the value is invalid, but we wouldn't expect it to change the _value_. That's probably a fine assumption. But it isn't guaranteed. And while it's not usually a recommended design choice, this update ensures that if it _does_ happen, then that change is honored. This helps avoid scenarios that, while rare, would be really difficult to isolate otherwise, and might even open up for downstream bugs. For instance, a property might clean up the `value` to remove unnecessary or even dangerous content (such as user submitted text containing JavaScript or SQL in an effort to implement an injection exploit). Usually, we'd use a method to do that prior to setting the property, or handle it as part of a model binder, but it's just one scenario where this _might_ occur. Regardless, it doesn't hurt to check, and avoid potential confusion if this scenario does occur. So we're evaluating if the new `value` has been modified after `EnforceBusinessLogic()` and, if so, updating `AttributeValue.Value` to the new value. --- OnTopic/Collections/AttributeValueCollection.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index e3a9e1e6..75faf50b 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -439,6 +439,11 @@ internal void SetValue( \-----------------------------------------------------------------------------------------------------------------------*/ if (BusinessLogicCache.ContainsKey(key)) { BusinessLogicCache.TryGetValue(key, out updatedAttributeValue); + if (updatedAttributeValue.Value != value) { + updatedAttributeValue = updatedAttributeValue with { + Value = value + }; + } } /*------------------------------------------------------------------------------------------------------------------------ From 59a77165e5813abe8ef7b30fa2ee3e29bed18981 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 15:36:01 -0800 Subject: [PATCH 046/778] Remove deprecated `ITopicRepository.Save(isDraft)` In OnTopic 2.x, we had introduced the idea of saving a draft, which was a specific version of a topic which wouldn't get published to the live site unless it was running in a special mode which loaded the draft. This was based on the idea of having a separate server for editing and previewing content, and was enabled using the legacy Web Forms `TopicsSection` configuration element. This support was removed as part of OnTopic 3.x, but the `isDraft` overload remained as a vestigal interface element. To avoid confusiong, we're removing it. This is a breaking change, though as an optional parameter, we only expect that `ITopicRepository` implementations were calling it as a passthrough in order to maintain the interface contract. This includes removing the parameter from XML Doc references as well. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 4 ++-- OnTopic.Data.Sql/SqlTopicRepository.cs | 12 +++++------- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 6 +++--- OnTopic.Tests/TopicRepositoryBaseTest.cs | 6 +++--- OnTopic/Attributes/AttributeValue.cs | 9 ++++----- .../Attributes/AttributeValueCollectionExtensions.cs | 8 ++++---- OnTopic/Collections/AttributeValueCollection.cs | 4 ++-- OnTopic/Repositories/ITopicRepository.cs | 3 +-- OnTopic/Repositories/TopicRepositoryBase.cs | 6 +++--- OnTopic/Topic.cs | 2 +- 11 files changed, 29 insertions(+), 33 deletions(-) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 7b152e61..848a15e7 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -135,8 +135,8 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) => - _dataProvider.Save(topic, isRecursive, isDraft); + public override int Save(Topic topic, bool isRecursive = false) => + _dataProvider.Save(topic, isRecursive); /*========================================================================================================================== | METHOD: MOVE diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 0f5448d4..f923f147 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -248,7 +248,7 @@ public override Topic Load(int topicId, DateTime version) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save([NotNull]Topic topic, bool isRecursive = false, bool isDraft = false) { + public override int Save([NotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Establish dependencies @@ -263,13 +263,13 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false, bool is /*------------------------------------------------------------------------------------------------------------------------ | Handle first pass \-----------------------------------------------------------------------------------------------------------------------*/ - var topicId = Save(topic, isRecursive, isDraft, connection, unresolvedTopics, version); + var topicId = Save(topic, isRecursive, connection, unresolvedTopics, version); /*------------------------------------------------------------------------------------------------------------------------ | Attempt to resolve outstanding relationships \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var unresolvedTopic in unresolvedTopics) { - Save(unresolvedTopic, false, isDraft, connection, new(), version); + Save(unresolvedTopic, false, connection, new(), version); } /*------------------------------------------------------------------------------------------------------------------------ @@ -302,13 +302,11 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false, bool is /// /// The source to save. /// Determines whether or not to recursively save . - /// Determines if the should be saved as a draft version. /// The open to use for executing s. /// A list of s with unresolved topic references. private int Save( [NotNull]Topic topic, bool isRecursive, - bool isDraft, SqlConnection connection, List unresolvedRelationships, SqlDateTime version @@ -317,7 +315,7 @@ SqlDateTime version /*------------------------------------------------------------------------------------------------------------------------ | Call base method - will trigger any events associated with the save \-----------------------------------------------------------------------------------------------------------------------*/ - base.Save(topic, isRecursive, isDraft); + base.Save(topic, isRecursive); /*------------------------------------------------------------------------------------------------------------------------ | Define variables @@ -482,7 +480,7 @@ void recurse() { if (isRecursive) { foreach (var childTopic in topic.Children) { childTopic.Attributes.SetValue("ParentID", topic.Id.ToString(CultureInfo.InvariantCulture)); - Save(childTopic, isRecursive, isDraft, connection, unresolvedRelationships, version); + Save(childTopic, isRecursive, connection, unresolvedRelationships, version); } } } diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index b4d1957e..17684c25 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -42,7 +42,7 @@ public DummyTopicRepository() : base() { } | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) => throw new NotImplementedException(); + public override int Save(Topic topic, bool isRecursive = false) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MOVE diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 62c2f5d8..2f06554d 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -105,12 +105,12 @@ public StubTopicRepository() : base() { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) { + public override int Save(Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Call base method - will trigger any events associated with the save \-----------------------------------------------------------------------------------------------------------------------*/ - base.Save(topic, isRecursive, isDraft); + base.Save(topic, isRecursive); /*------------------------------------------------------------------------------------------------------------------------ | Recurse through children @@ -124,7 +124,7 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f \-----------------------------------------------------------------------------------------------------------------------*/ if (isRecursive) { foreach (var childTopic in topic.Children) { - Save(childTopic, isRecursive, isDraft); + Save(childTopic, isRecursive); } } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index dce1031f..ae15d7b6 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -509,8 +509,8 @@ public void GetContentTypeDescriptor_GetInvalidContentType_ReturnsNull() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Loads the , then saves a new via , and ensures that - /// it is immediately reflected in the cache of s. + /// cref="ContentTypeDescriptor"/> via , and ensures that it is + /// immediately reflected in the cache of s. /// [TestMethod] public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { @@ -529,7 +529,7 @@ public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Loads the , then saves an existing via , and ensures that + /// "ContentTypeDescriptor"/> via , and ensures that /// it the cache is updated. /// [TestMethod] diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index f4e255b8..b727346f 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -54,7 +54,7 @@ public record AttributeValue { /// /// /// An optional boolean indicator noting whether the collection item is a new value, and - /// should thus be saved to the database when is next called. + /// should thus be saved to the database when is next called. /// /// @@ -87,7 +87,7 @@ public AttributeValue(string key, string? value, bool isDirty = true) { /// /// /// An optional boolean indicator noting whether the collection item is a new value, and - /// should thus be saved to the database when is next called. + /// should thus be saved to the database when is next called. /// /// /// The value that the attribute was last modified. This is intended exclusively for use when @@ -147,9 +147,8 @@ internal AttributeValue( /// /// The IsDirty property is used by the to determine whether or not /// the value has been persisted to the database. If it is set to true, the attribute's value is sent to the database - /// when is called. Otherwise, it is - /// ignored, thus preventing the need to update attributes (or create new versions of attributes) whose values haven't - /// changed. + /// when is called. Otherwise, it is ignored, thus + /// preventing the need to update attributes (or create new versions of attributes) whose values haven't changed. /// public bool IsDirty { get; init; } diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index c39e6a73..6608696b 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -186,7 +186,7 @@ out var result /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// /// The value that the attribute was last modified. This is intended exclusively for use when @@ -379,7 +379,7 @@ public void SetValue( /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 6b7d9d44..fcc8e474 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -103,13 +103,12 @@ public interface ITopicRepository { /// /// Boolean indicator nothing whether to recurse through the topic's descendants and save them as well. /// - /// Boolean indicator as to the topic's publishing status. /// The integer return value from the execution of the topics_UpdateTopic stored procedure. /// /// topic is not null /// /// topic - int Save(Topic topic, bool isRecursive = false, bool isDraft = false); + int Save(Topic topic, bool isRecursive = false); /*========================================================================================================================== | METHOD: MOVE diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 21a77677..ec2da3f4 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -95,8 +95,8 @@ public virtual ContentTypeDescriptorCollection GetContentTypeDescriptors() { /// the 's data store. There are cases, however, where it may be preferrable to instead load /// these topics from a local, in-memory source. Namely, when first instantiating a new OnTopic database, and when saving /// modifications to existing content types. As such, this protected overload is useful to call from when the topic graph being saved includes any s. + /// cref="ITopicRepository.Save(Topic, Boolean)"/> when the topic graph being saved includes any s. /// /// /// The root of a topic graph to merge into the collection for - public virtual int Save([ValidatedNotNull]Topic topic, bool isRecursive = false, bool isDraft = false) { + public virtual int Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 6f339ebd..db74cc88 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -671,7 +671,7 @@ public Topic? DerivedTopic { /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . + /// persisted to the data store on . /// /// Date: Wed, 16 Dec 2020 15:38:03 -0800 Subject: [PATCH 047/778] Marked deprecated `GetContentTypeDescriptors()` overload as obsolete The `GetContentTypeDescriptors(contentTypeDescriptor)` overload (7a5a243) was introduced to allow the content type descriptors cache to be updated at the same time as retrieving a content type by passing along a reference to a `ContentTypeDescriptor` and allowing it to override the local cache. While this built off of the familiar `GetContentTypeDescriptors()` method, it was a poorly thought through design, as the semantics don't make the behavior clear. Given that, it was deprecated and replaced with the `SetContentTypeDescriptors()` method to be more explicit about the functionality (78a9d3f). This simply moves the overload from deprecated to obsolete such that it now throws an exception if used. In a future version, we will remove this entirely. --- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 2f06554d..ad7cc1af 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -180,7 +180,7 @@ public IEnumerable GetAttributesProxy( | METHOD: GET CONTENT TYPE DESCRIPTORS (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ /// - [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", false)] + [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", true)] public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) => base.SetContentTypeDescriptors(topicGraph); diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index ec2da3f4..cb0b0096 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -104,7 +104,7 @@ public virtual ContentTypeDescriptorCollection GetContentTypeDescriptors() { /// also any descendents. /// /// - [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptors() method, which provides the same function.", false)] + [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptors() method, which provides the same function.", true)] protected virtual ContentTypeDescriptorCollection GetContentTypeDescriptors(ContentTypeDescriptor? contentTypeDescriptors) => SetContentTypeDescriptors(contentTypeDescriptors); From b013e38745c56caf71d341c923a298f9943ec327 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 16:08:38 -0800 Subject: [PATCH 048/778] Marked deprecated `ReadOnlyTopicCollection.FromList()` as obsolete This functionality is effectively satisfied by the related overload, which accepts an `IList` to prepopulate the collection. This simply moves the method from deprecated to obsolete so that it now throws an exception if used. In a future version, we will remove this entirely. --- OnTopic/Collections/ReadOnlyTopicCollection{T}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs index dae3b13e..26615350 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs @@ -59,7 +59,7 @@ public ReadOnlyTopicCollection(IList innerCollection) : base(innerCollection) /// The will be converted to a . /// /// The underlying . - [Obsolete("This is effectively satisfied by the related overload, and will be removed in OnTopic 5.0.0.", false)] + [Obsolete("This is effectively satisfied by the related overload, and will be removed in OnTopic 5.0.0.", true)] public ReadOnlyTopicCollection FromList(IList innerCollection) { Contract.Requires(innerCollection, "innerCollection should not be null"); return new(innerCollection); From 6c3a2e9656812f4e7769ee2da385ee2e8705c9a1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 16:23:06 -0800 Subject: [PATCH 049/778] Marked deprecated `Topic.Description` property as obsolete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was a convenience method intended to aid in retrieving a common—but not universal—attribute. As this is generally only needed in views, as part of page rendering, it is better served by the view model support accessed via the `ITopicMappingService`. If access to the description attribute is still needed via `Topic`, it can be accessed using `topic.Attributes.GetValue()` or `topic.Attributes.SetValue()` instead. This simply moves the property from deprecated to obsolete so that it now throws an exception if used. In a future version, we will remove this entirely. --- OnTopic/Topic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index db74cc88..152dc3f7 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -385,7 +385,7 @@ public string Title { /// /// !string.IsNullOrWhiteSpace(value) /// - [Obsolete("The Description convenience property will be removed in OnTopic Library 5.0. Use Attributes.SetValue() instead.")] + [Obsolete("The Description convenience property will be removed in OnTopic Library 5.0. Use Attributes.SetValue() instead.", true)] public string? Description { get => Attributes.GetValue("Description"); set => SetAttributeValue("Description", value); From c669055e8347565a7b00cfb72de2af4cf008253a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 16:28:18 -0800 Subject: [PATCH 050/778] Marked `GenerateSitemap()` and `AddTopic()` as private functions The `GenerateSitemap()` and `AddTopic()` methods were intended as `private` helper functions, but were inadvertantly marked as `public` methods. There was not expectation that anyone else would have called them, so this shouldn't hurt anything. To be safe, however, they have been marked as deprecated previously. They are now marked as `private` and thus entirely inaccessible. --- .../Controllers/SitemapController.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 17840af2..1d97fb19 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -13,8 +13,6 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; -#pragma warning disable CS0618 // Type or member is obsolete; supresses known issue with helper methods being moved to private - namespace OnTopic.AspNetCore.Mvc.Controllers { /*============================================================================================================================ @@ -144,8 +142,7 @@ public virtual ActionResult Index(bool indent = false, bool includeMetadata = fa /// The topic to add to the sitemap. /// Optionally enables extended metadata associated with each topic. /// A Sitemap.org sitemap. - [Obsolete("The GenerateSitemap() method should not be public. It will be marked private in OnTopic Library 5.0.")] - public virtual XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) => + private virtual XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) => new( new XElement(_sitemapNamespace + "urlset", from topic in rootTopic?.Children @@ -161,8 +158,7 @@ select AddTopic(topic, includeMetadata) /// /// The topic to add to the sitemap. /// Optionally enables extended metadata associated with each topic. - [Obsolete("The AddTopic() method should not be public. It will be marked private in OnTopic Library 5.0.")] - public IEnumerable AddTopic(Topic topic, bool includeMetadata = false) { + private IEnumerable AddTopic(Topic topic, bool includeMetadata = false) { /*------------------------------------------------------------------------------------------------------------------------ | Establish return collection @@ -241,6 +237,4 @@ from relatedTopic in topic.Relationships[relationship.Name] } } //Class -} //Namespace - -#pragma warning restore CS0618 // Type or member is obsolete \ No newline at end of file +} //Namespace \ No newline at end of file From 19612ac16dfdf107b4934bbe65451b6052a0a7fe Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 16:34:08 -0800 Subject: [PATCH 051/778] Marked `MapTopicRoute(IRouteBuilder)` as obsolete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In ASP.NET Core, there are two ways to configure a route: with `IRouteBuilder` or `IEndpointRouteBuilder`. Previously, `OnTopic.AspNetCore.Mvc` supported both, even though `IEndpointRouteBuilder` is preferred for ASP.NET Core 3+. We're now marking the `IRouteBuilder` method as obsolete. This effectively ends support for OnTopic on older versions of ASP.NET Core which don't support `IEndpointRouteBuilder`—which is fine, since the release version of `OnTopic.AspNetCore.Mvc` has only ever supported ASP.NET Core 3+. (A beta version supported ASP.NET Core 2+, but was never released.) This simply moves the property from deprecated to obsolete so that it now throws an exception if used. In a future version, we will remove this entirely. --- OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs b/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs index a309ef32..23873585 100644 --- a/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs +++ b/OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs @@ -72,7 +72,7 @@ public static IMvcBuilder AddTopicSupport(this IMvcBuilder services) { /// endpoint routing is preferred in ASP.NET Core 3. OnTopic also offers far more extension methods for endpoint routing, /// while this method is provided exclusively for backward compatibility. /// - [Obsolete("This method is deprecated and will be removed in OnTopic 5. Callers should migrate to endpoint routing.", false)] + [Obsolete("This method is deprecated and will be removed in OnTopic 5. Callers should migrate to endpoint routing.", true)] public static IRouteBuilder MapTopicRoute( this IRouteBuilder routes, string rootTopic, From 704c8defdeba585d628c3af71d8f7c2340e5d974 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 17:00:57 -0800 Subject: [PATCH 052/778] Set default value of `ITopicRepository.Delete(isRecursive)` to `false` Originally, `ITopicRepository.Delete()`'s `isRecursive` parameter was set to a default value of `false`, as it only makes sense to do a recursive delete if the user opts into it. Unfortunately, however, there was a long-standing bug where `Delete()` failed to validate whether or not there were children, and thus would _always_ do a recursive delete, regardless of whether or not `isRecursive` was set. Whoops. This was fixed in OnTopic 4.5.0 (a0cc21c). In order to avoid breaking backward compatibility, however, the default was changed to `true` so that it continued to operate as it had before (1773113); otherwise this would have been a breaking change. Having the default be more aggressive doesn't make much sense; users should opt in to the more invasive mode. As such, now that we're preparing for a major release, I'm reverting this back to its original default of `false`. Now, if a user attempts to `Delete(Topic)` without explicitly defining `isRecursive`, and that topic has children, an exception will be thrown, and the topic will not be deleted. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic/Repositories/ITopicRepository.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index f923f147..afaae4a6 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -546,7 +546,7 @@ public override void Move(Topic topic, Topic target, Topic? sibling) { | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete(Topic topic, bool isRecursive = true) { + public override void Delete(Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Delete from memory diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 17684c25..50a65edf 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -54,7 +54,7 @@ public DummyTopicRepository() : base() { } | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete(Topic topic, bool isRecursive = true) => throw new NotImplementedException(); + public override void Delete(Topic topic, bool isRecursive = false) => throw new NotImplementedException(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index ad7cc1af..8fd302dd 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -157,7 +157,7 @@ public override void Move(Topic topic, Topic target, Topic? sibling = null) { | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete(Topic topic, bool isRecursive = true) => base.Delete(topic, isRecursive); + public override void Delete(Topic topic, bool isRecursive = false) => base.Delete(topic, isRecursive); /*========================================================================================================================== | METHOD: GET ATTRIBUTES (PROXY) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index fcc8e474..7d7de8ed 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -147,13 +147,13 @@ public interface ITopicRepository { /// The topic object to delete. /// /// Boolean indicator nothing whether to recurse through the topic's descendants and delete them as well. If set to false - /// and the topic has children, including any nested topics, an exception will be thrown. + /// and the topic has children, including any nested topics, an exception will be thrown. The default is false. /// /// /// topic is not null /// /// topic - void Delete(Topic topic, bool isRecursive = true); + void Delete(Topic topic, bool isRecursive = false); } //Interface } //Namespace \ No newline at end of file From 86fd5c4855dca83dc27205644b9f238a71243443 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 17:23:54 -0800 Subject: [PATCH 053/778] Migrated view model `List<>` properties to use `Collection<>` Microsoft's design guidelines recommend that public interfaces use `Collection` instead of `List` (CA1002). The `List` type is faster, and optimized for performance, but includes a lot of low-level members that most users don't need, while `Collection` has more high-level convenience members that are more intuitive for most users. As this was a breaking change, however, we didn't implement this as part of our implementation of Microsoft's updated Code Analysis (27e6940). Now that we're preparing for a major release, we're able to make these changes which could potentially break some implementations. Notably, this will break implementations that specifically access the member as a `List` (e.g., as a cast or assignment), or which call some of the `List` specific methods, such as `AddRange()`. These calls were previously removed from elsewhere in the OnTopic library in preparation for this migration, but may still persist in some client implementations. --- .../ViewModels/CompatiblePropertyTopicViewModel.cs | 6 ++---- OnTopic/GlobalSuppressions.cs | 3 +-- OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs | 2 +- OnTopic/Topic.cs | 3 ++- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs index b4296702..b467c73c 100644 --- a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using OnTopic.Metadata; @@ -25,9 +25,7 @@ public class CompatiblePropertyTopicViewModel { public ModelType ModelType { get; set; } - #pragma warning disable CA1002 // Do not expose generic lists - public List? VersionHistory { get; set; } - #pragma warning restore CA1002 // Do not expose generic lists + public Collection? VersionHistory { get; set; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index 52eb59f6..0b09faf4 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -7,5 +7,4 @@ [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "StringComparison overload not supported by .NET Standard 2.0", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] [assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] -[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] -[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Breaking change; deferred to major version.", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index 3a719675..b5b99960 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -454,7 +454,7 @@ private static bool IsSettableType(Type sourceType, Type? targetType = null) { /// /// A list of types that are allowed to be set using . /// - public static List SettableTypes { get; } + public static Collection SettableTypes { get; } /*========================================================================================================================== | OVERRIDE: INSERT ITEM diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 152dc3f7..2cd6002b 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -644,7 +645,7 @@ public Topic? DerivedTopic { /// its derived providers). /// /// The current 's version history. - public List VersionHistory { get; } + public Collection VersionHistory { get; } #endregion From f1c0a28f04cf881d506e5671bca192b2840f8057 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 17:25:51 -0800 Subject: [PATCH 054/778] Bugfix: Removed errant `virtual` In a previous commit, I changed `GenerateSitemap()` from `public` to `private`, but missed that it was also marked as `virtual`. Private members can't be marked as virtual, but due to a local problem on my development machine I hadn't caught the compilation error (c669055). Whoops. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 1d97fb19..2b2df5fe 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -142,7 +142,7 @@ public virtual ActionResult Index(bool indent = false, bool includeMetadata = fa /// The topic to add to the sitemap. /// Optionally enables extended metadata associated with each topic. /// A Sitemap.org sitemap. - private virtual XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) => + private XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) => new( new XElement(_sitemapNamespace + "urlset", from topic in rootTopic?.Children From 7d887bbe86128201b7282ec76e2827e6f311b825 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 16 Dec 2020 17:27:31 -0800 Subject: [PATCH 055/778] Fixed broken XML Doc for `ITopicRepository.Save()` In a previous commit, I removed the optional `isDraft` overload (59a7716). As part of that, I missed an reference to the old overload in the XML Docs due to how the line break occurred. (Unfortunately, this wasn't being picked up by Visual Studio's code analysis, either.) --- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index cb0b0096..f2aca098 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -133,7 +133,7 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Topi /// preferrable to instead load these topics from a local, in-memory source. Namely, when first instantiating a new /// OnTopic database, and when saving modifications to existing content types. As such, the protected method is useful to call from when the topic graph being saved includes any new s. + /// Topic, Boolean)"/> when the topic graph being saved includes any new s. /// /// /// The root of a topic graph to merge into the collection for Date: Wed, 16 Dec 2020 17:52:32 -0800 Subject: [PATCH 056/778] Updated `ITopicRepository.Load()` to use `uniqueKey` instead of `topicKey` In a previous commit, we created a new function, `GetTopicIDByUniqueKey`, which allows a `TopicID` to be looked up based on its `UniqueKey` instead of its `TopicKey` (ea97f91). This effectively replaces the legacy `GetTopicID`, which would lookup the _first instance_ of a topic which had a given `Key`. Since multiple topics can have the same `Key`, however, this was error prone. As a result, `GetTopicIDByUniqueKey` requires that a fully qualified `UniqueKey` (e.g., from `Topic.GetUniqueKey()`) be passed along, instead of simply a `Topic.Key`. As a result, this was not implemented on `ITopicRepository.Load(topicKey)` as that would be a breaking change; any call passing a single topic key would fail, unless that topic key happened to be in the root. For this major release, we're finally updating this to use `GetTopicIDByUniqueKey`. To avoid confusion, the parameter name is also being updated from `topicKey` to `uniqueKey` to help disambiguate _which_ key is appropriate here. --- .../Components/MenuViewComponentBase{T}.cs | 2 +- OnTopic.Data.Caching/CachedTopicRepository.cs | 6 +++--- OnTopic.Data.Sql/SqlTopicRepository.cs | 10 +++++----- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 8 ++++---- .../Hierarchical/HierarchicalTopicMappingService{T}.cs | 2 +- OnTopic/Repositories/ITopicRepository.cs | 4 ++-- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index c7f35f85..2765003a 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -79,7 +79,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService var configuredRoot = CurrentTopic.Attributes.GetValue("NavigationRoot", true); if (!String.IsNullOrEmpty(configuredRoot)) { - navigationRootTopic = TopicRepository.Load(configuredRoot); + navigationRootTopic = TopicRepository.Load("Root:" + configuredRoot); } if (navigationRootTopic is null) { navigationRootTopic = HierarchicalTopicMappingService.GetHierarchicalRoot(CurrentTopic, 2, "Web"); diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 848a15e7..00adc01e 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -96,13 +96,13 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } /// - public override Topic? Load(string? topicKey = null, bool isRecursive = true) { + public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Lookup by TopicKey \-----------------------------------------------------------------------------------------------------------------------*/ - if (topicKey is not null && topicKey.Length is not 0) { - return _cache.GetByUniqueKey(topicKey); + if (uniqueKey is not null && uniqueKey.Length is not 0) { + return _cache.GetByUniqueKey(uniqueKey); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index afaae4a6..77427add 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -64,7 +64,7 @@ public SqlTopicRepository(string connectionString) : base() { | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override Topic Load(string? topicKey = null, bool isRecursive = true) { + public override Topic Load(string? uniqueKey = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Handle empty topic @@ -72,7 +72,7 @@ public override Topic Load(string? topicKey = null, bool isRecursive = true) { | If the topicKey is null, or does not contain a topic key, then assume the caller wants to return all data; in that case | call Load() with the special integer value of -1, which will load all topics from the root. \-----------------------------------------------------------------------------------------------------------------------*/ - if (String.IsNullOrEmpty(topicKey)) { + if (String.IsNullOrEmpty(uniqueKey)) { return Load(-1, isRecursive); } @@ -80,7 +80,7 @@ public override Topic Load(string? topicKey = null, bool isRecursive = true) { | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ using var connection = new SqlConnection(_connectionString); - using var command = new SqlCommand("GetTopicID", connection); + using var command = new SqlCommand("GetTopicIDByUniqueKey", connection); var topicId = -1; @@ -89,7 +89,7 @@ public override Topic Load(string? topicKey = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Establish query parameters \-----------------------------------------------------------------------------------------------------------------------*/ - command.AddParameter("TopicKey", topicKey); + command.AddParameter("UniqueKey", uniqueKey); command.AddOutputParameter(); /*------------------------------------------------------------------------------------------------------------------------ @@ -115,7 +115,7 @@ public override Topic Load(string? topicKey = null, bool isRecursive = true) { | Validate results \-----------------------------------------------------------------------------------------------------------------------*/ if (topicId < 0) { - throw new TopicNotFoundException(topicKey); + throw new TopicNotFoundException(uniqueKey); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 50a65edf..064ef57e 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -33,7 +33,7 @@ public DummyTopicRepository() : base() { } public override Topic? Load(int topicId, bool isRecursive = true) => null; /// - public override Topic? Load(string? topicKey = null, bool isRecursive = true) => null; + public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) => null; /// public override Topic? Load(int topicId, DateTime version) => throw new NotImplementedException(); diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 8fd302dd..51914828 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -53,14 +53,14 @@ public StubTopicRepository() : base() { (topicId < 0)? _cache :_cache.FindFirst(t => t.Id.Equals(topicId)); /// - public override Topic? Load(string? topicKey = null, bool isRecursive = true) { + public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Lookup by TopicKey \-----------------------------------------------------------------------------------------------------------------------*/ - if (topicKey is not null && topicKey.Length > 0) { - topicKey = topicKey.Contains(":") ? topicKey : "Root:" + topicKey; - return _cache.FindFirst(t => t.GetUniqueKey().Equals(topicKey, StringComparison.OrdinalIgnoreCase)); + if (uniqueKey is not null && uniqueKey.Length > 0) { + uniqueKey = uniqueKey.Contains(":") ? uniqueKey : "Root:" + uniqueKey; + return _cache.FindFirst(t => t.GetUniqueKey().Equals(uniqueKey, StringComparison.OrdinalIgnoreCase)); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index 263ea66c..eebe8eb0 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -74,7 +74,7 @@ ITopicMappingService topicMappingService | GET HIERARCHICAL ROOT \-------------------------------------------------------------------------------------------------------------------------*/ /// - public Topic? GetHierarchicalRoot(Topic? currentTopic, int fromRoot = 2, string defaultRoot = "Web") { + public Topic? GetHierarchicalRoot(Topic? currentTopic, int fromRoot = 2, string defaultRoot = "Root:Web") { /*------------------------------------------------------------------------------------------------------------------------ | Establish variables diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 7d7de8ed..7163e7bd 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -60,10 +60,10 @@ public interface ITopicRepository { /// /// Loads a topic (and, optionally, all of its descendants) based on the specified key name. /// - /// The topic key. + /// The fully-qualified unique topic key. /// Determines whether or not to recurse through and load a topic's children. /// A topic object. - Topic? Load(string? topicKey = null, bool isRecursive = true); + Topic? Load(string? uniqueKey = null, bool isRecursive = true); /// /// Loads a specific version of a topic based on its version. diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index f2aca098..c911d1d5 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -224,7 +224,7 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont public abstract Topic? Load(int topicId, bool isRecursive = true); /// - public abstract Topic? Load(string? topicKey = null, bool isRecursive = true); + public abstract Topic? Load(string? uniqueKey = null, bool isRecursive = true); /// public abstract Topic? Load(int topicId, DateTime version); From 7daaf78de2dae5816662e75a67846965accba2c6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 12:54:24 -0800 Subject: [PATCH 057/778] Converted collation methods to `IEnumerable` Converted return type of methods which collate topics from multiple different topics to use `IEnumerable` instead of `ReadOnlyTopicCollection`. This addresses a bug where topics with the same key cannot be added to a `ReadOnlyTopicCollection` because it's keyed by `Key`. These instances could have returned `Collection` instead, which would provide a more familiar interface, but in practice we expect that most of these only need `IEnumerable<>`. Given that, the main impact will likely be that calls to `Count` will no longer work, and will instead require LINQ's `Count()`. --- OnTopic.Tests/RelatedTopicCollectionTest.cs | 4 ++-- OnTopic.Tests/TopicQueryingTest.cs | 2 +- OnTopic/Collections/RelatedTopicCollection.cs | 14 ++++++-------- OnTopic/GlobalSuppressions.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 9 +++++---- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/RelatedTopicCollectionTest.cs index 08396cf3..1e399f34 100644 --- a/OnTopic.Tests/RelatedTopicCollectionTest.cs +++ b/OnTopic.Tests/RelatedTopicCollectionTest.cs @@ -153,7 +153,7 @@ public void GetAllTopics_ReturnsAllTopics() { Assert.AreEqual(5, relationships.Count); Assert.AreEqual("Related3", relationships.GetTopics("Relationship3").First().Key); - Assert.AreEqual(5, relationships.GetAllTopics().Count); + Assert.AreEqual(5, relationships.GetAllTopics().Count()); } @@ -175,7 +175,7 @@ public void GetAllContentTypes_ReturnsAllContentTypes() { } Assert.AreEqual(5, relationships.Count); - Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count); + Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count()); } diff --git a/OnTopic.Tests/TopicQueryingTest.cs b/OnTopic.Tests/TopicQueryingTest.cs index 9a969109..ddaf6037 100644 --- a/OnTopic.Tests/TopicQueryingTest.cs +++ b/OnTopic.Tests/TopicQueryingTest.cs @@ -64,7 +64,7 @@ public void FindAllByAttribute_ReturnsCorrectTopics() { grandNieceTopic.Attributes.SetValue("Foo", "Bar"); Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Bar").First(), grandNieceTopic); - Assert.AreEqual(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count); + Assert.AreEqual(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count()); Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Baz").First(), grandChildTopic); } diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 14efd9aa..e7125ac6 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using OnTopic.Internal.Diagnostics; @@ -66,16 +67,16 @@ public RelatedTopicCollection(Topic parent, bool isIncoming = false) : base(Stri /// /// Returns an enumerable list of objects. /// - public ReadOnlyTopicCollection GetAllTopics() { - var topics = new TopicCollection(); + public IEnumerable GetAllTopics() { + var topics = new List(); foreach (var topicCollection in this) { foreach (var topic in topicCollection) { - if (topicCollection.Contains(topic) && !topics.Contains(topic)) { + if (!topics.Contains(topic)) { topics.Add(topic); } } } - return new(topics); + return topics; } /// @@ -85,10 +86,7 @@ public ReadOnlyTopicCollection GetAllTopics() { /// /// Returns an enumerable list of objects. /// - public ReadOnlyTopicCollection GetAllTopics(string contentType) { - var topics = GetAllTopics().Where(t => t.ContentType == contentType); - return ReadOnlyTopicCollection.FromList(topics.ToList()); - } + public IEnumerable GetAllTopics(string contentType) => GetAllTopics().Where(t => t.ContentType == contentType); /*========================================================================================================================== | METHOD: GET TOPICS diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index 0b09faf4..194673b8 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -6,5 +6,5 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "StringComparison overload not supported by .NET Standard 2.0", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] -[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~OnTopic.Collections.ReadOnlyTopicCollection{OnTopic.Topic}")] +[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~System.Collections.Generic.IEnumerable{OnTopic.Topic}")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index bce33ba3..1551f940 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Collections.Generic; using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; @@ -109,7 +110,7 @@ public static class TopicExtensions { /// /// The instance of the to operate against; populated automatically by .NET. /// A collection of topics descending from the current topic. - public static ReadOnlyTopicCollection FindAll(this Topic topic) => topic.FindAll(t => true); + public static IEnumerable FindAll(this Topic topic) => topic.FindAll(t => true); /// /// Retrieves a collection of topics based on a supplied function. @@ -117,7 +118,7 @@ public static class TopicExtensions { /// The instance of the to operate against; populated automatically by .NET. /// The function to validate whether a should be included in the output. /// A collection of topics matching the input parameters. - public static ReadOnlyTopicCollection FindAll(this Topic topic, Func predicate) { + public static IEnumerable FindAll(this Topic topic, Func predicate) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -149,7 +150,7 @@ public static ReadOnlyTopicCollection FindAll(this Topic topic, Func FindAll(this Topic topic, Func /// !name.Contains(" ") /// - public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, string name, string value) { + public static IEnumerable FindAllByAttribute(this Topic topic, string name, string value) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts From 64120a9f5e8b4abb0e2022d72bd718311e577590 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 12:59:56 -0800 Subject: [PATCH 058/778] Renamed `[AttributeKey()]`'s `Value` property to `Key` This is more consistent with other attributes in this namespace, such as `[FilterByAttribute()]` and `[Relationship()]`, which use `Key` instead of `Value` or the fully-qualified (and redundant) name (e.g., `AttributeKeyAttribute.AttributeKey`). --- OnTopic/Internal/Mapping/PropertyConfiguration.cs | 4 ++-- OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs | 12 ++++++------ OnTopic/Mapping/Annotations/Relationships.cs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Internal/Mapping/PropertyConfiguration.cs index d63524d0..2406a625 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Internal/Mapping/PropertyConfiguration.cs @@ -80,7 +80,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" \-----------------------------------------------------------------------------------------------------------------------*/ GetAttributeValue(property, a => DefaultValue = a.Value); GetAttributeValue(property, a => InheritValue = true); - GetAttributeValue(property, a => AttributeKey = attributePrefix + a.Value); + GetAttributeValue(property, a => AttributeKey = attributePrefix + a.Key); GetAttributeValue(property, a => MapToParent = true); GetAttributeValue(property, a => AttributePrefix += (a.AttributePrefix?? property.Name)); GetAttributeValue(property, a => CrawlRelationships = a.Relationships); @@ -138,7 +138,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// the DTO to be aliased to a different property or attribute name on the source . /// /// - /// The property corresponds to the property. It + /// The property corresponds to the property. It /// can be assigned by decorating a DTO property with e.g. [AttributeKey("AlternateAttributeKey")]. /// /// diff --git a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs index f2873838..f6e579a2 100644 --- a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs +++ b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs @@ -30,19 +30,19 @@ public sealed class AttributeKeyAttribute : System.Attribute { /// /// Annotates a property with the class by providing a (required) attribute key. /// - /// The key value of the attribute associated with the current property. - public AttributeKeyAttribute(string value) { - TopicFactory.ValidateKey(value, false); - Value = value; + /// The key value of the attribute associated with the current property. + public AttributeKeyAttribute(string key) { + TopicFactory.ValidateKey(key, false); + Key = key; } /*========================================================================================================================== - | PROPERTY: VALUE + | PROPERTY: Key \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Gets the value of the attribute key. /// - public string Value { get; } + public string Key { get; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/Annotations/Relationships.cs b/OnTopic/Mapping/Annotations/Relationships.cs index d59ec751..1a2d5edc 100644 --- a/OnTopic/Mapping/Annotations/Relationships.cs +++ b/OnTopic/Mapping/Annotations/Relationships.cs @@ -75,7 +75,7 @@ public enum Relationships { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Map to any collection on the with either the same property name, or which corresponds to the . + /// cref="AttributeKeyAttribute.Key"/>. /// /// /// This allows mapping of custom collection, such as . @@ -90,7 +90,7 @@ public enum Relationships { /// /// /// By convention, types refer to a , , or property identifier ending in Id. + /// cref="AttributeKeyAttribute.Key"/>, or property identifier ending in Id. /// References = 1 << 5, From ac01bbfe8e341654325edf2677a0abeb354c6185 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 13:19:20 -0800 Subject: [PATCH 059/778] Moved `RelatedTopicBindingModel` to the `OnTopic.ViewModels` library Previously, `RelatedTopicBindingModel` was the only concrete model implemented in the core OnTopic library. Normally, we only need (or want) model abstractions in the main library, such as the underlying `IRelatedTopicBindingModel`. Since the core OnTopic library doesn't rely on `RelatedTopicBindingModel`, it's being moved to `OnTopic.ViewModels` under a new `BindingModels` namespace. Note: The nomenclature of `OnTopic.ViewModels.BindingModels` is a bit goofy. If we introduce more standardized binding models we might want to revisit a more invasive change of migrating to something like `OnTopic.Models.ViewModels` and `OnTopic.Models.BindingModels`. For now, however, this is a minor annoyance for an outlier. --- .../BindingModels/ContentTypeDescriptorTopicBindingModel.cs | 2 +- .../BindingModels/InvalidReferenceNameTopicBindingModel.cs | 2 +- .../InvalidRelationshipListTypeTopicBindingModel.cs | 2 +- .../BindingModels/InvalidRelationshipTypeTopicBindingModel.cs | 2 +- OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs | 2 +- .../BindingModels}/RelatedTopicBindingModel.cs | 3 ++- 6 files changed, 7 insertions(+), 6 deletions(-) rename {OnTopic/Models => OnTopic.ViewModels/BindingModels}/RelatedTopicBindingModel.cs (96%) diff --git a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs index 8c26cfbe..7e621373 100644 --- a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.Collections.ObjectModel; -using OnTopic.Models; +using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs index 706316eb..712c827c 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Models; +using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs index 69892bfb..1dc8909b 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs @@ -6,7 +6,7 @@ using System; using System.Collections; using System.Collections.Generic; -using OnTopic.Models; +using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index a091d1a9..921f188e 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -6,7 +6,7 @@ using System; using System.Collections.ObjectModel; using OnTopic.Mapping.Annotations; -using OnTopic.Models; +using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index 4dede0f8..fbbcd295 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; -using OnTopic.Models; +using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic/Models/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs similarity index 96% rename from OnTopic/Models/RelatedTopicBindingModel.cs rename to OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs index 1aa40db2..1870d665 100644 --- a/OnTopic/Models/RelatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs @@ -5,8 +5,9 @@ \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; using OnTopic.Mapping.Reverse; +using OnTopic.Models; -namespace OnTopic.Models { +namespace OnTopic.ViewModels.BindingModels { /*============================================================================================================================ | CLASS: RELATED TOPIC BINDING MODEL From 5e4047021ca1be48d1d2a8df02c3653bc85fc23f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 13:34:50 -0800 Subject: [PATCH 060/778] Moved `isIncoming` overloads on `RelatedTopicCollection` to `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `isIncoming` parameter is used to maintain referential integrity within the topic graph, and is not intended to be called by external implementors. Given this, the methods which use `isIncoming` have been marked as `internal` to avoid confusion. As part of this, the optional `isDirty` parameter for `SetTopic()` has been moved to the public overload, since that _is_ required externally—and is actively used by `ITopicRepository` implementations. Note that the (now) internal `SetTopic()` overload's parameter order has changed, to maintain consistency with the public version. As there's only one internal call to it, however, this is not expected to matter. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic/Collections/RelatedTopicCollection.cs | 25 +++++++++++++++---- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 42650cd6..3b9e07cf 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -318,7 +318,7 @@ private static void SetRelationships(this SqlDataReader reader, Dictionary instance, which the related topics are to be associated /// with. This will be used when setting incoming relationships. In addition, a may /// be set as if it is specifically intended to track incoming relationships; if this is not - /// set, then it will not allow incoming relationships to be set via the internal - /// overload. + /// set, then it will not allow incoming relationships to be set via the internal overload. /// public RelatedTopicCollection(Topic parent, bool isIncoming = false) : base(StringComparer.OrdinalIgnoreCase) { _parent = parent; @@ -124,6 +124,17 @@ public void ClearTopics(string relationshipKey) { /*========================================================================================================================== | METHOD: REMOVE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes a specific object associated with a specific relationship key. + /// + /// The key of the relationship. + /// The key of the topic to be removed. + /// + /// Returns true if the is removed; returns false if either the relationship key or the + /// cannot be found. + /// + public bool RemoveTopic(string relationshipKey, string topicKey) => RemoveTopic(relationshipKey, topicKey); + /// /// Removes a specific object associated with a specific relationship key. /// @@ -136,7 +147,7 @@ public void ClearTopics(string relationshipKey) { /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - public bool RemoveTopic(string relationshipKey, string topicKey, bool isIncoming = false) { + internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncoming = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -227,7 +238,11 @@ public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = f /// /// The key of the relationship. /// The topic to be added, if it doesn't already exist. - public void SetTopic(string relationshipKey, Topic topic) => SetTopic(relationshipKey, topic, false); + /// + /// Optionally forces the collection to a state, assuming the topic was set. + /// + public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) + => SetTopic(relationshipKey, topic, isDirty, false); /// /// Ensures that an incoming is associated with the specified relationship key. @@ -243,7 +258,7 @@ public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = f /// /// Optionally forces the collection to a state, assuming the topic was set. /// - public void SetTopic(string relationshipKey, Topic topic, bool isIncoming, bool? isDirty = null) { + internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 4d6e23ae..76510205 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -126,7 +126,7 @@ public bool DisableChildTopics { /// /// /// To add content types to the collection, use . + /// cref="RelatedTopicCollection.SetTopic(String, Topic, Boolean?)"/>. /// /// public ReadOnlyTopicCollection PermittedContentTypes { From 20f2f85dbe883cddaa7b7652d6a98d667b9026c9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 13:39:44 -0800 Subject: [PATCH 061/778] Moved `BindingModelValidator` to `OnTopic.Mapping.Reverse` namespace The `BindingModelValidator` is only used, as the name suggests, for validating binding models, which in turn are only assessed as part of the `IReverseTopicMappingService`. Given that, it has been moved from the main `OnTopic.Mapping` namespace to the `OnTopic.Mapping.Reverse` namespace. As this is an internal class, this has no external impact. (And, in fact, since it only has one caller which is already in the `OnTopic.MappingReverse` namespace, it also has no internal impact.) --- OnTopic/Mapping/{ => Reverse}/BindingModelValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename OnTopic/Mapping/{ => Reverse}/BindingModelValidator.cs (99%) diff --git a/OnTopic/Mapping/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs similarity index 99% rename from OnTopic/Mapping/BindingModelValidator.cs rename to OnTopic/Mapping/Reverse/BindingModelValidator.cs index c3b0b922..063b4d80 100644 --- a/OnTopic/Mapping/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -19,7 +19,7 @@ using OnTopic.Models; using OnTopic.Repositories; -namespace OnTopic.Mapping { +namespace OnTopic.Mapping.Reverse { /*============================================================================================================================ | CLASS: BINDING MODEL VALIDATOR From 5c8bd067406df3c227c567530f054deb90c81123 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 14:12:55 -0800 Subject: [PATCH 062/778] Operate off of `ITopicBindingModel` type, not `*TopicBindingModel` name Initially, the `DynamicTopicBindingModelLookupService` looked for all types whose names ended in `TopicBindingModel`. This is similar to how the `DynamicTopicViewModelLookupService` works. Binding models are different, however. The `ReverseTopicService` expects binding models to implement the `ITopicBindingModel` interface. As such, we needn't rely on their name to identify their purpose. Further, implementors may wish to use the same classes for view models and binding models; by forcing a naming convention that is at odds with that used with view models, we prevent that from happening. Given that, the `DyanmicTopicBindingModelLookupService` now looks for classes which implement `ITopicBindingModel`, regardless of whether or not they end with `TopicBindingModel`. In practice, today, we expect these to be identical. But this offers more flexibility to implementors. --- .../Reflection/DynamicTopicBindingModelLookupService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs b/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs index 5631b8e7..c17bdf20 100644 --- a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs +++ b/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Models; namespace OnTopic.Reflection { @@ -11,8 +12,8 @@ namespace OnTopic.Reflection { | CLASS: TOPIC BINDING MODEL LOOKUP SERVICE \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// The will search all assemblies for s that end with - /// "TopicBindingModel" + /// The will search all assemblies for s that + /// implement . /// public class DynamicTopicBindingModelLookupService : DynamicTypeLookupService { @@ -23,7 +24,7 @@ public class DynamicTopicBindingModelLookupService : DynamicTypeLookupService { /// Establishes a new instance of a . /// public DynamicTopicBindingModelLookupService() : base( - t => t.Name.EndsWith("TopicBindingModel", StringComparison.InvariantCultureIgnoreCase), + t => typeof(ITopicBindingModel).IsAssignableFrom(t), typeof(object) ) { } From 5706897fd8a29e8804ac4795e8d45fefda6acf76 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 14:26:15 -0800 Subject: [PATCH 063/778] Moved `isIncoming` overloads on `RelatedTopicCollection` to `internal` (cont.) In a previous commit, I migrated the `isIncoming` overloads to be `internal` (5e40470). In doing so, however, I missed one. Whoops! This now includes the `RemoveTopic(string, topic, bool)` overload. --- OnTopic/Collections/RelatedTopicCollection.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 7d0c78a8..00eadbcb 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -172,6 +172,18 @@ internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncomi } + /// + /// Removes a specific object associated with a specific relationship key. + /// + /// The key of the relationship. + /// The topic to be removed. + /// + /// Returns true if the is removed; returns false if either the relationship key or the + /// cannot be found. + /// + public bool RemoveTopic(string relationshipKey, Topic topic) => RemoveTopic(relationshipKey, topic); + + /// /// Removes a specific object associated with a specific relationship key. /// @@ -184,7 +196,7 @@ internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncomi /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = false) { + internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -197,10 +209,9 @@ public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = f \-----------------------------------------------------------------------------------------------------------------------*/ if (!isIncoming) { if (_isIncoming) { - throw new ArgumentException( + throw new InvalidOperationException( "You are attempting to remove an incoming relationship on a RelatedTopicCollection that is not flagged as " + - "IsIncoming", - nameof(isIncoming) + "IsIncoming" ); } topic.IncomingRelationships.RemoveTopic(relationshipKey, _parent, true); From 1f49495924010fb462763344b7ba73c7b42fc01e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:03:56 -0800 Subject: [PATCH 064/778] Updated `ReverseTopicMappingService` to use `InvalidOperationException` Previously, several validators used the `InvalidEnumArgumentException`, which makes sense if validating the argument of e.g. an `Attribute`, but these particular validators aren't doing that. Fell back to the more general `InvalidOperationException`. As these are primarily intended to validate design decisions, they aren't expected to impact any implementations. --- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index e6c1e245..7ef175bf 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -145,17 +145,17 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the content type is valid if (!_contentTypeDescriptors.Contains(source.ContentType)) { - throw new InvalidEnumArgumentException( + throw new InvalidOperationException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}'. There " + - $"no matching content type in the ITopicRepository provided. This suggests that the binding model is invalid. If " + - $"this is expected—e.g., if the content type is being added as part of this operation—then it needs to be added " + + $"are no matching content types in the ITopicRepository provided. This suggests that the binding model is invalid. " + + $"If this is expected—e.g., if the content type is being added as part of this operation—then it needs to be added " + $"to the same ITopicRepository instance prior to creating any instances of it." ); } //Ensure the content types match if (source.ContentType != target.ContentType) { - throw new InvalidEnumArgumentException( + throw new InvalidOperationException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}', while " + $"the {nameof(target)} object (with the key '{source.Key}') has a content type of '{target.ContentType}'. It is not" + $"permitted to change the topic's content type during a mapping operation, as this interferes with the validation. " + @@ -165,7 +165,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the keys match if (source.Key != target.Key && !String.IsNullOrEmpty(source.Key)) { - throw new InvalidEnumArgumentException( + throw new InvalidOperationException( $"The {nameof(source)} object has a key of '{source.Key}', while the {nameof(target)} object has a key of " + $"'{target.Key}'. It is not permitted to change the topic'key during a mapping operation, as this suggests in " + $"invalid target. If this is by design, change the key on the target topic prior to invoking MapAsync()." From 75487715265c61ad180062dbdf556f602046f80e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:04:28 -0800 Subject: [PATCH 065/778] Set default `Contract` exception to `InvalidOperationException` Previously, if `Contract.Requires(bool)` or `Contract.Assumes(bool)` were called without specifying an exception type, then an `Exception` was thrown. This is now updated to throw the more specific `InvalidOperationException`. If implementors want more control over this, they should prefer to call `Contract.Requires(bool)` or `Contract.Assumes(bool)`. As `InvalidOperationException` derives from `Exception`, this shouldn't be a breaking change, but will provide implementors more flexibility in capturing exceptions raised by this library. --- OnTopic/Internal/Diagnostics/Contract.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index e2237292..ebbc74c3 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -51,14 +51,15 @@ public static class Contract { | METHOD: REQUIRES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Will throw a if the supplied expression evaluates to false. + /// Will throw a if the supplied expression evaluates to false. /// /// An expression resulting in a boolean value indicating if an exception should be thrown. /// Optionally provides an error message in case an exception is thrown. - /// + /// /// Thrown when returns . /// - public static void Requires(bool isValid, string? errorMessage = null) => Requires(isValid, errorMessage); + public static void Requires(bool isValid, string? errorMessage = null) => + Requires(isValid, errorMessage); /// /// Will throw an if the supplied object is . @@ -118,7 +119,7 @@ or NotSupportedException | METHOD: ASSUME \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Ensures that a condition is met. If not, an is thrown. + /// Ensures that a condition is met. If not, an is thrown. /// /// /// This is virtually identical to except that, syntactically, it is expected to @@ -128,11 +129,11 @@ or NotSupportedException /// /// An expression resulting in a boolean value indicating if an exception should be thrown. /// Optionally provides an error message in case an exception is thrown. - /// + /// /// Thrown when returns . /// public static void Assume(bool isValid, string? errorMessage = null) => - Requires(isValid, errorMessage); + Requires(isValid, errorMessage); /// /// Ensures that a condition is met. If not, the provided exception is thrown. From 24e79fbb5c915c0ec35d23e819f8b4da8f208886 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:14:01 -0800 Subject: [PATCH 066/778] Ensured `ArgumentNullException` included `nameof(parameter)` Not all cases where an `ArgumentNullException` can be thrown include the parameter name. In some cases, the parameter name was included, but as a hard-coded string, or part of a more descriptive error message. In these cases, the general preference is to use `nameof(parameter)` so that the name is validated against the actual parameter name, and mismatches will be detected if the parameter name changes. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 5 +---- .../AttributeValueCollectionExtensions.cs | 8 ++++---- OnTopic/Collections/AttributeValueCollection.cs | 6 +++--- OnTopic/Collections/RelatedTopicCollection.cs | 15 ++++++++------- OnTopic/Collections/TopicCollection{T}.cs | 3 ++- .../Reflection/MemberInfoCollection{T}.cs | 2 +- OnTopic/Metadata/AttributeDescriptor.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 6 +++--- OnTopic/TopicFactory.cs | 10 +++++----- 9 files changed, 28 insertions(+), 29 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 77427add..e1db99a7 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -48,10 +48,7 @@ public SqlTopicRepository(string connectionString) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires( - !String.IsNullOrWhiteSpace(connectionString), - "The name of the connection string must be provided in order to be validated." - ); + Contract.Requires(!String.IsNullOrWhiteSpace(connectionString), nameof(connectionString)); /*------------------------------------------------------------------------------------------------------------------------ | Set private fields diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 6608696b..07a14206 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -46,7 +46,7 @@ public static bool GetBoolean( bool inheritFromDerived = true ) { Contract.Requires(attributes); - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); return Int32.TryParse( attributes.GetValue( name, @@ -84,7 +84,7 @@ public static int GetInteger( bool inheritFromDerived = true ) { Contract.Requires(attributes); - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); return Int32.TryParse( attributes.GetValue( name, @@ -122,7 +122,7 @@ public static double GetDouble( bool inheritFromDerived = true ) { Contract.Requires(attributes); - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); return Double.TryParse( attributes.GetValue( name, @@ -160,7 +160,7 @@ public static DateTime GetDateTime( bool inheritFromDerived = true ) { Contract.Requires(attributes); - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); return DateTime.TryParse( attributes.GetValue( name, diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 9186d3c6..69f6e6b5 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -232,7 +232,7 @@ public void MarkClean(string name, DateTime? version = null) { /// The string value for the Attribute. [return: NotNullIfNotNull("defaultValue")] public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromDerived = true) { - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); return GetValue(name, defaultValue, inheritFromParent, (inheritFromDerived? 5 : 0)); } @@ -269,7 +269,7 @@ public void MarkClean(string name, DateTime? version = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); TopicFactory.ValidateKey(name); @@ -418,7 +418,7 @@ internal void SetValue( /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key), "key"); + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); TopicFactory.ValidateKey(key); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 00eadbcb..85100d3d 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -100,7 +100,7 @@ public IEnumerable GetAllTopics() { /// /// The key of the relationship to be returned. public NamedTopicCollection GetTopics(string relationshipKey) { - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); if (Contains(relationshipKey)) { return this[relationshipKey]; } @@ -115,7 +115,7 @@ public NamedTopicCollection GetTopics(string relationshipKey) { /// /// The key of the relationship to be cleared. public void ClearTopics(string relationshipKey) { - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); if (Contains(relationshipKey)) { this[relationshipKey].Clear(); } @@ -152,8 +152,8 @@ internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncomi /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey)); - Contract.Requires(!String.IsNullOrWhiteSpace(topicKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(topicKey), nameof(topicKey)); /*------------------------------------------------------------------------------------------------------------------------ | Validate topic key @@ -201,7 +201,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); Contract.Requires(topic); /*------------------------------------------------------------------------------------------------------------------------ @@ -274,7 +274,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); Contract.Requires(topic); TopicFactory.ValidateKey(relationshipKey); @@ -347,7 +347,8 @@ protected override void InsertItem(int index, NamedTopicCollection item) { throw new ArgumentException( $"A {nameof(NamedTopicCollection)} with the Name '{item.Name}' already exists in this " + $"{nameof(RelatedTopicCollection)}. The existing key is '{this[item.Name].Name}'; the new item's is '{item.Name}'. " + - $"This collection is associated with the '{_parent.GetUniqueKey()}' Topic." + $"This collection is associated with the '{_parent.GetUniqueKey()}' Topic.", + nameof(item) ); } } diff --git a/OnTopic/Collections/TopicCollection{T}.cs b/OnTopic/Collections/TopicCollection{T}.cs index af66ba94..6c268ec1 100644 --- a/OnTopic/Collections/TopicCollection{T}.cs +++ b/OnTopic/Collections/TopicCollection{T}.cs @@ -86,7 +86,8 @@ protected override void InsertItem(int index, T item) { else { throw new ArgumentException( $"A {typeof(T).Name} with the Key '{item.Key}' already exists. The UniqueKey of the existing {typeof(T).Name} is " + - $"'{this[item.Key].GetUniqueKey()}'; the new item's is '{item.GetUniqueKey()}'." + $"'{this[item.Key].GetUniqueKey()}'; the new item's is '{item.GetUniqueKey()}'.", + nameof(item) ); } } diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs index f473b278..a532f9aa 100644 --- a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs +++ b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs @@ -87,7 +87,7 @@ protected override void InsertItem(int index, T item) { base.InsertItem(index, item); } else { - throw new ArgumentException($"The Type '{Type.Name}' already contains the MemberInfo '{item.Name}'"); + throw new ArgumentException($"The Type '{Type.Name}' already contains the MemberInfo '{item.Name}'", nameof(item)); } } diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index c38fa79f..cfdf4c07 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -130,7 +130,7 @@ protected AttributeDescriptor( public string? DisplayGroup { get => Attributes.GetValue("DisplayGroup", ""); set { - Contract.Requires(!String.IsNullOrWhiteSpace(value)); + Contract.Requires(!String.IsNullOrWhiteSpace(value), nameof(value)); SetAttributeValue("DisplayGroup", value); } } diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 1551f940..280f936e 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -177,9 +177,9 @@ public static IEnumerable FindAllByAttribute(this Topic topic, string nam /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(topic, "The topic parameter must be specified."); - Contract.Requires(!String.IsNullOrWhiteSpace(name), "The attribute name must be specified."); - Contract.Requires(!String.IsNullOrWhiteSpace(value), "The attribute value must be specified."); + Contract.Requires(topic, nameof(topic)); + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); + Contract.Requires(!String.IsNullOrWhiteSpace(value), nameof(value)); TopicFactory.ValidateKey(name); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 74b6548b..86aabacd 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -67,8 +67,8 @@ public static Topic Create(string key, string contentType, Topic? parent = null) /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key)); - Contract.Requires(!String.IsNullOrWhiteSpace(contentType)); + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); TopicFactory.ValidateKey(key); TopicFactory.ValidateKey(contentType); @@ -107,9 +107,9 @@ public static Topic Create(string key, string contentType, int id, Topic? parent /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key)); - Contract.Requires(!String.IsNullOrWhiteSpace(contentType)); - Contract.Requires(id > 0); + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); + Contract.Requires(id > 0, nameof(id)); TopicFactory.ValidateKey(key); TopicFactory.ValidateKey(contentType); From 9391a5d1f6acaae253f7defebb7a2b936020249a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:19:35 -0800 Subject: [PATCH 067/778] Converted `ArgumentException` to more appropriate `InvalidOperationException` Updated `RelatedTopicCollection.SetTopic()` to throw an `InvalidOperationException` instead of an `ArgumentNullException` for scenarios where `isIncoming` is set on a `RelatedTopicCollection` that isn't set to `_isIncoming`. This should never occur, as this is an `internal` overload and managed by the OnTopic library; if it does, however, it suggests a design flaw in the code. --- OnTopic/Collections/RelatedTopicCollection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 85100d3d..67967819 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -295,8 +295,8 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool \-----------------------------------------------------------------------------------------------------------------------*/ if (!isIncoming) { if (_isIncoming) { - throw new ArgumentException( - "You are attempting to set an incoming relationship on a RelatedTopicCollection that is not flagged as IsIncoming", + throw new InvalidOperationException( + "You are attempting to set an incoming relationship on a RelatedTopicCollection that is not flagged as " + nameof(isIncoming) ); } From 9069f45d078eb49fbb694795ff313e1aac6b10b5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:33:03 -0800 Subject: [PATCH 068/778] Prefer `ArgumentOutOfRangeException` for range checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a method parameter is being validated against a range—e.g., to determine if it is a positive number, or falls within a particular range of numbers—prefer `ArgumentOutOfRangeException` over the more generic `ArgumentException`. As `ArgumentOutOfRangeException` derives from `ArgumentException`, this should not be a breaking cahnge, but will provide implementors to capture a more specific exception, should they choose. --- OnTopic/Collections/AttributeValueCollection.cs | 4 ++-- OnTopic/TopicFactory.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 69f6e6b5..2704aa2c 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -270,8 +270,8 @@ public void MarkClean(string name, DateTime? version = null) { | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); - Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); + Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); + Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); TopicFactory.ValidateKey(name); string? value = null; diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 86aabacd..216274a1 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -109,7 +109,7 @@ public static Topic Create(string key, string contentType, int id, Topic? parent \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); - Contract.Requires(id > 0, nameof(id)); + Contract.Requires(id > 0, nameof(id)); TopicFactory.ValidateKey(key); TopicFactory.ValidateKey(contentType); From 25bdeb443942594372ee3d4ca7d3c2ed7fec365d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:35:21 -0800 Subject: [PATCH 069/778] Prefer `InvalidOperationException` when attempting to reset an `Id` `Topic` does not allow consumers to change the `Id`. When this happens, an exception is thrown. While this is an `ArgumentException`, and `InvalidOperationException` makes more sense here. --- OnTopic/Topic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 2cd6002b..243bb042 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -117,7 +117,7 @@ public int Id { set { Contract.Requires(value > 0, "The id is expected to be a positive value."); if (_id > 0 && !_id.Equals(value)) { - throw new ArgumentException($"The value of this topic has already been set to {_id}; it cannot be changed."); + throw new InvalidOperationException($"The value of this topic has already been set to {_id}; it cannot be changed."); } _id = value; } From 6c01ec41b3836df6242a94b8c575be6be864a101 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:37:27 -0800 Subject: [PATCH 070/778] Ensured `ArgumentNullException` included `nameof(parameter)` (cont.) Picked up a couple of `nameof()` calls for `ArgumentNullException`s, as missed in the original commit (24e79fb). --- OnTopic.ViewModels/TopicViewModelCollection.cs | 9 ++------- OnTopic/Querying/TopicExtensions.cs | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/OnTopic.ViewModels/TopicViewModelCollection.cs b/OnTopic.ViewModels/TopicViewModelCollection.cs index 0e91e4eb..94c079b6 100644 --- a/OnTopic.ViewModels/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/TopicViewModelCollection.cs @@ -56,13 +56,8 @@ public TopicViewModelCollection(IEnumerable? topics = null) : base(String /// The name of the content type to filter by. /// The filtered list of view models associated with the content type. public TopicViewModelCollection GetByContentType(string contentType) { - Contract.Requires( - !String.IsNullOrWhiteSpace(contentType), - $"A {nameof(contentType)} argument is required." - ); - return new( - Items.Where(t => t.ContentType?.Equals(contentType, StringComparison.OrdinalIgnoreCase)?? false) - ); + Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); + return new(Items.Where(t => t.ContentType?.Equals(contentType, StringComparison.OrdinalIgnoreCase)?? false)); } /*========================================================================================================================== diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 280f936e..0cb746ea 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -217,7 +217,7 @@ public static IEnumerable FindAllByAttribute(this Topic topic, string nam | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(topic, "The topic parameter must be specified."); - Contract.Requires(!String.IsNullOrWhiteSpace(uniqueKey), "The unique key must be specified."); + Contract.Requires(!String.IsNullOrWhiteSpace(uniqueKey), nameof(uniqueKey)); /*------------------------------------------------------------------------------------------------------------------------ | Find lowest common root From 7e6cb4c06dc882024bb69d44cbd7fa487fdb144c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:39:17 -0800 Subject: [PATCH 071/778] Rely on default `InvalidOperationException` When calling `Contract.Assume(bool)`, we now default to an `InvalidOperationException` (7548771). As such, we don't need to explicitly define this, and can rely on the non-generic method instead. --- OnTopic.Data.Sql/SqlCommandExtensions.cs | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql/SqlCommandExtensions.cs b/OnTopic.Data.Sql/SqlCommandExtensions.cs index 89b0395b..90ea3897 100644 --- a/OnTopic.Data.Sql/SqlCommandExtensions.cs +++ b/OnTopic.Data.Sql/SqlCommandExtensions.cs @@ -29,7 +29,7 @@ internal static class SqlCommandExtensions { /// The SQL command object. /// The name of the SQL parameter to retrieve as the return code. internal static int GetReturnCode(this SqlCommand command, string sqlParameter = "ReturnCode") { - Contract.Assume( + Contract.Assume( command.Parameters.Contains($"@{sqlParameter}"), $"The call to the {command.CommandText} stored procedure did not return the expected 'ReturnCode' parameter." ); diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index e1db99a7..7d82699d 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -437,7 +437,7 @@ SqlDateTime version topic.Id = command.GetReturnCode(); - Contract.Assume( + Contract.Assume( !topic.IsNew, "The call to the CreateTopic stored procedure did not return the expected 'Id' parameter." ); From 8ac9d24b4ed259daeba1fced61213e9a271377da Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:43:38 -0800 Subject: [PATCH 072/778] Ensured `ArgumentException` included `nameof(parameter)` Not all cases where an `ArgumentException` can be thrown include the parameter name. In these cases, the general preference is to use `nameof(parameter)` so that the name is validated against the actual parameter name, and mismatches will be detected if the parameter name changes. --- OnTopic/Collections/AttributeValueCollection.cs | 3 ++- OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs | 4 +++- OnTopic/Repositories/TopicRepositoryBase.cs | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 2704aa2c..145d130d 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -554,7 +554,8 @@ protected override void InsertItem(int index, AttributeValue item) { throw new ArgumentException( $"An {nameof(AttributeValue)} with the Key '{item.Key}' already exists. The Value of the existing item is " + $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(AttributeValue)}s are associated " + - $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'." + $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'.", + nameof(item) ); } } diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index b5b99960..ffd0b86e 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -488,7 +488,9 @@ protected override void InsertItem(int index, MemberInfoCollection item) { else { throw new ArgumentException( $"The '{nameof(TypeMemberInfoCollection)}' already contains the {nameof(MemberInfoCollection)} of the Type " + - $"'{item.Type}'."); + $"'{item.Type}'.", + nameof(item) + ); } } diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index c911d1d5..817d722c 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -315,7 +315,8 @@ public virtual int Save([ValidatedNotNull]Topic topic, bool isRecursive = false) if (contentTypeDescriptor is null) { throw new ArgumentException( $"The Content Type \"{topic.ContentType}\" referenced by \"{topic.Key}\" could not be found under " + - $"\"Configuration:ContentTypes\". There are currently {contentTypeDescriptors.Count} ContentTypes in the Repository." + $"\"Configuration:ContentTypes\". There are currently {contentTypeDescriptors.Count} ContentTypes in the Repository.", + nameof(topic) ); } From a3f39d49e69983302d3051aa3bb565b37121a00e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 15:59:26 -0800 Subject: [PATCH 073/778] Prefer `InvalidOperationException` in `SetRelationships()` for consistency While the previous `ArgumentOutOfRangeException` is defensible, all other validation errors from `ReverseTopicMappingService` throw `InvalidOperationException`, and there's no reason to think an implementor would want to capture this exception individually, compared to any other exceptions. As such, for consistency, and to simplify handling exceptions, this is now standardized to an `InvalidOperationException`. There is an argument to be made that we might want to establish a `TopicMappingException` class, potentially with more specific derived classes, to provide more granular control over error handling. We may reevaluate that in the future. For now, however, all mapping exceptions should share a common exception. --- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 7ef175bf..d39a49e8 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -436,8 +436,7 @@ PropertyConfiguration configuration foreach (IRelatedTopicBindingModel relationship in sourceList) { var targetTopic = _topicRepository.Load(relationship.UniqueKey); if (targetTopic is null) { - throw new ArgumentOutOfRangeException( - configuration.Property.Name, + throw new InvalidOperationException( $"The relationship '{relationship.UniqueKey}' mapped in the '{configuration.Property.Name}' property could not " + $"be located in the repository." ); From bb2cdf89cd63f39e5c9fb71d2a789fcb03cd37de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:07:29 -0800 Subject: [PATCH 074/778] Prefer `ReferentialIntegrityException` when validating `ContentType` If `ITopicRepository.Save()` is called with a `topic` whose `ContentType` cannot be found, an `ArgumentException` is thrown. That's fine. But, in all other cases, when there are predictable exceptions occur within an `ITopicRepository` implementation then we throw a `TopicRepositoryException` or derivative so that error handling can catch all such cases. Further, we already have a `ReferentialIntegrityException` for cases where a topic reference can't be honored. A `ContentType` is, effectively, a reference to a `ContentTypeDescriptor`, so this seems appropriate to use here. --- OnTopic/Repositories/TopicRepositoryBase.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 817d722c..483d111b 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -313,10 +313,9 @@ public virtual int Save([ValidatedNotNull]Topic topic, bool isRecursive = false) var contentTypeDescriptor = GetContentTypeDescriptor(topic); if (contentTypeDescriptor is null) { - throw new ArgumentException( + throw new ReferentialIntegrityException( $"The Content Type \"{topic.ContentType}\" referenced by \"{topic.Key}\" could not be found under " + - $"\"Configuration:ContentTypes\". There are currently {contentTypeDescriptors.Count} ContentTypes in the Repository.", - nameof(topic) + $"\"Configuration:ContentTypes\". There are currently {contentTypeDescriptors.Count} ContentTypes in the Repository." ); } From 6e9328535973c413dba7bafac4d683b449c072c1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:16:06 -0800 Subject: [PATCH 075/778] Recommend `InvalidOperationException` in `ControllerActivator` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code should not throw a generic `Exception`. Technically, it doesn't really matter in this case as this exception is normally excepted to expose a design-time issue to developers. Nevertheless, it's easy to throw a more appropriate exception—and, indeed, most implementations now thow an `InvalidOperationException` in this case. --- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 4 ++-- OnTopic.AspNetCore.Mvc/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 3126ab9f..6a63c4fa 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -104,7 +104,7 @@ public object Create(ControllerContext context) { new SitemapController(_topicRepository), nameof(RedirectController) => new RedirectController(_topicRepository), - _ => throw new Exception($"Unknown controller {type.Name}") + _ => throw new InvalidOperationException($"Unknown controller {type.Name}") }; } @@ -128,7 +128,7 @@ public object Create(ViewComponentContext context) { new MenuViewComponent(_topicRepository, _hierarchicalMappingService), nameof(PageLevelNavigationViewComponent) => new PageLevelNavigationViewComponent(_topicRepository, _hierarchicalMappingService), - _ => throw new Exception($"Unknown view component {type.Name}") + _ => throw new InvalidOperationException($"Unknown view component {type.Name}") }; } diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md index daee88d9..85ec6eee 100644 --- a/OnTopic.AspNetCore.Mvc/README.md +++ b/OnTopic.AspNetCore.Mvc/README.md @@ -145,7 +145,7 @@ return controllerType.Name switch { nameof(TopicController) => new TopicController(_topicRepository, _topicMappingService), nameof(RedirectController) => new RedirectController(_topicRepository), nameof(SitemapController) => new SitemapController(_topicRepository), - _ => throw new Exception($"Unknown controller {controllerType.Name}") + _ => throw new InvalidOperationException($"Unknown controller {controllerType.Name}") }; ``` For a complete reference template, including the ancillary controllers, view components, and a more maintainable structure, see the [`OrganizationNameActivator.cs`](https://gist.github.com/JeremyCaney/00c04b1b9f40d9743793cd45dfaaa606) Gist. Optionally, you may use a dependency injection container. From 69f300e2b317c8452ed906fcbfd2805205465ab5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:22:31 -0800 Subject: [PATCH 076/778] Use string interpolation to provide more contextual exceptions --- OnTopic/Collections/RelatedTopicCollection.cs | 2 +- .../Hierarchical/HierarchicalTopicMappingService{T}.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 67967819..7f9e436b 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -211,7 +211,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = if (_isIncoming) { throw new InvalidOperationException( "You are attempting to remove an incoming relationship on a RelatedTopicCollection that is not flagged as " + - "IsIncoming" + nameof(isIncoming) ); } topic.IncomingRelationships.RemoveTopic(relationshipKey, _parent, true); diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index eebe8eb0..9ff62e35 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -88,7 +88,7 @@ ITopicMappingService topicMappingService if (String.IsNullOrEmpty(defaultRoot)) { throw new ArgumentOutOfRangeException( nameof(defaultRoot), - "The current route could not be resolved to a topic and the defaultRoot was not set." + $"The current route could not be resolved to a topic and the {nameof(defaultRoot)} was not set." ); } navigationRootTopic = TopicRepository.Load(defaultRoot); @@ -100,7 +100,7 @@ ITopicMappingService topicMappingService if (navigationRootTopic is null) { throw new ArgumentOutOfRangeException( nameof(defaultRoot), - "Neither the current route nor the 'defaultRoot' parameter could be resolved to a topic." + $"Neither the current route nor the {nameof(defaultRoot)} parameter of {defaultRoot} could be resolved to a topic." ); } From db32581c6c7547458def0b2c94c72749aaf0915a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:33:12 -0800 Subject: [PATCH 077/778] Updated unit tests to except new default `Contract` exceptions In a previous commit, I updated `Contract.Requires(bool)` and `Contract.Assumes(bool)` to throw an `InvalidOperationException` by default (7548771). I failed, however, to update the unit tests validating the default behavior. This commit resolves that. --- OnTopic.Tests/ContractTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/ContractTest.cs b/OnTopic.Tests/ContractTest.cs index a8a253da..afda61b8 100644 --- a/OnTopic.Tests/ContractTest.cs +++ b/OnTopic.Tests/ContractTest.cs @@ -36,7 +36,7 @@ public void Requires_ConditionIsTrue_ThrowNoException() /// cref="ArgumentNullException"/>. /// [TestMethod] - [ExpectedException(typeof(Exception))] + [ExpectedException(typeof(InvalidOperationException))] public void Requires_ConditionIsFalse_ThrowArgumentNullException() => Contract.Requires(false, "The argument cannot be null"); @@ -101,7 +101,7 @@ public void Assume_ConditionIsTrue_ThrowNoException() /// cref="ArgumentNullException"/>. /// [TestMethod] - [ExpectedException(typeof(Exception))] + [ExpectedException(typeof(InvalidOperationException))] public void Assume_ConditionIsFalse_ThrowArgumentNullException() => Contract.Assume(false, "The argument cannot be null"); From 2f75e8b3ae406c13426e60bd61dbf4c3b117ff5a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:36:54 -0800 Subject: [PATCH 078/778] Updated unit tests to expect `Topic.Id` exception In a previous commit, I updated `Topic.Id` to throw an `InvalidOperationException` if a consumer attempted to change the `Id` (25bdeb4). I failed, however, to update the unit tests validating the default behavior. This commit resolves that. --- OnTopic.Tests/TopicTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 06c114c8..3b1005de 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -56,7 +56,7 @@ public void Create_ContentType_ReturnsDerivedTopic() { /// Creates a topic using the factory method, and ensures that the ID cannot be modified. /// [TestMethod] - [ExpectedException(typeof(ArgumentException), "Topic permitted the ID to be reset; this should never happen.")] + [ExpectedException(typeof(InvalidOperationException), "Topic permitted the ID to be reset; this should never happen.")] public void Id_ChangeValue_ThrowsArgumentException() { var topic = TopicFactory.Create("Test", "ContentTypeDescriptor", 123); From 4f067bddf7459190a943d185352058d58c15c90d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 16:58:09 -0800 Subject: [PATCH 079/778] Corrected overloads for `RelatedTopicCollection` In a previous set of commits, I moved the `RelatedTopicCollection` overloads which accepted `isIncoming` to be marked as `internal` (5e40470, 5706897). In setting up their public overloads, however, I inadvertantly introduced an infinite loop by a) failing to ensure they passed a value for `isIncoming`, and b) creating an ambiguous reference for the internal overload by making the `isIncoming` parameter optional. Whoops! This fixes that issue by ensuring the `public` version of `RemoveTopic()` defaults to setting `isIncoming` to `false`, and the `internal` version of `RemoveTopic()` doesn't assume a default for `isIncoming`. --- OnTopic/Collections/RelatedTopicCollection.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 7f9e436b..6b71a624 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -133,7 +133,7 @@ public void ClearTopics(string relationshipKey) { /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - public bool RemoveTopic(string relationshipKey, string topicKey) => RemoveTopic(relationshipKey, topicKey); + public bool RemoveTopic(string relationshipKey, string topicKey) => RemoveTopic(relationshipKey, topicKey, false); /// /// Removes a specific object associated with a specific relationship key. @@ -147,7 +147,7 @@ public void ClearTopics(string relationshipKey) { /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncoming = false) { + internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -181,7 +181,7 @@ internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncomi /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - public bool RemoveTopic(string relationshipKey, Topic topic) => RemoveTopic(relationshipKey, topic); + public bool RemoveTopic(string relationshipKey, Topic topic) => RemoveTopic(relationshipKey, topic, false); /// @@ -196,7 +196,7 @@ internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncomi /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming = false) { + internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -300,7 +300,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool nameof(isIncoming) ); } - topic.IncomingRelationships.SetTopic(relationshipKey, _parent, true); + topic.IncomingRelationships.SetTopic(relationshipKey, _parent, isDirty, true); } } From 422e6edb8051ec8364b1e331c1308414aa9593ee Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:19:54 -0800 Subject: [PATCH 080/778] Remove support for .NET Standard 2.0 .NET Standard 2.0 is needed to maintain support for .NET Framework (4.6.1+) as .NET Standard 2.1 only supports .NET Core 3+ and .NET 5+. Previously, we continued to target .NET Standard 2.0 in order to support the **OnTopic-MVC**, **OnTopic-WebForms**, and **OnTopic-Editor-WebForms** projects. With the release of .NET 5, and the fact that we never released an **OnTopic-Editor-MVC** project, in preference for ASP.NET Core 3+, there is no compelling reason to continue to support the .NET Framework. If any implementations continue to target .NET Framework, they can continue to use OnTopic 4.x for now. That said, we definitely encourage implementors to upgrade to .NET 5+, now that it is released. --- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index fae32cad..78cb7861 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -3,7 +3,7 @@ {206B7F91-CA25-4E9D-9576-60D2E54A2C0A} OnTopic.Data.Caching - netstandard2.0;netstandard2.1 + netstandard2.1 True False 9.0 diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 41ac0014..8d18d689 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -3,7 +3,7 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} OnTopic.Data.Sql - netstandard2.0;netstandard2.1 + netstandard2.1 9.0 enable latest diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 77ff107c..458e68c8 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 9.0 enable latest diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 93b688a8..9f4db5fa 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -3,7 +3,7 @@ {E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7} OnTopic.ViewModels - netstandard2.0;netstandard2.1 + netstandard2.1 True False 9.0 diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 88f413dc..b2443d2d 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -3,7 +3,7 @@ {B8D5B290-4451-4C3B-AE9E-0FF075958A74} OnTopic - netstandard2.0;netstandard2.1 + netstandard2.1 True False 9.0 From 3f4448a1764f8bd68446d741aa77a494b6504550 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:25:25 -0800 Subject: [PATCH 081/778] Remove legacy `ITopicRoutingService` The `ITopicRoutingService` was required by the **OnTopic-WebForms**, **OnTopic-MVC**, and **OnTopic-Editor-WebForms** projects. It was marked as deprecated in a previous **OnTopic-Library** release, and is no longer needed since **OnTopic 5.0.0** will no longer support .NET Framework (422e6ed). Since none of the ASP.NET implementations that will be supported (e.g., `OnTopic.AspNetCore.Mvc`, **OnTopic-Editor-AspNetCore**) reference this, there's no need to mark it as obsolete; it can just be removed. This includes removal of all XMLDocs that continued (mistakenly) to refer to it. Those references have either been removed entirely, or updated to refer to the `TopicViewResultExecutor`, which takes over some of the functionality once handled by the legacy `ITopicRoutingService` in **OnTopic 2.x** (though not in **OnTopic 3.x**). --- OnTopic/ITopicRoutingService.cs | 37 ----------------------- OnTopic/Metadata/ContentTypeDescriptor.cs | 3 +- OnTopic/Models/ITopicBindingModel.cs | 4 +-- OnTopic/Models/ITopicViewModel.cs | 12 +++----- OnTopic/Topic.cs | 12 +++----- 5 files changed, 12 insertions(+), 56 deletions(-) delete mode 100644 OnTopic/ITopicRoutingService.cs diff --git a/OnTopic/ITopicRoutingService.cs b/OnTopic/ITopicRoutingService.cs deleted file mode 100644 index 2e52281a..00000000 --- a/OnTopic/ITopicRoutingService.cs +++ /dev/null @@ -1,37 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; - -namespace OnTopic { - - /*============================================================================================================================ - | INTERFACE: TOPIC ROUTING SERVICE - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Given contextual information (such as URL) will determine the current Topic. - /// - /// - /// Each environment (e.g., ASP.NET MVC) will require a platform-specific to act as an - /// adapter to framework-specific libraries (e.g., HttpContext). In addition, custom versions may be created - /// in order to establish custom mappings between URL or route data and the hierarchy of topics, should the need arise. - /// - [Obsolete( - "The ITopicRoutingService is no longer used in the latest clients, and will be removed in OnTopic Library 5.0. In the " + - "latest OnTopic.AspNetCore.Mvc libraries, it is replaced with a ITopicRepository.Load() extension method which accepts " + - "RouteData as an argument, in order to lookup topics based on that library's routing conventions." - )] - public interface ITopicRoutingService { - - /*========================================================================================================================== - | METHOD: GET CURRENT TOPIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets the topic associated with the current URL. - /// - Topic? GetCurrentTopic(); - - } //Class -} //Namespace diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 76510205..4fa8c1ad 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -20,8 +20,7 @@ namespace OnTopic.Metadata { /// /// /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). The content type also determines, by default, which view - /// is rendered by the (assuming the value isn't overwritten down the pipe). + /// Editor (via the property). /// /// /// Each content type associated with a is itself a with a Content Type of diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index efb6c256..8220e507 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -46,9 +46,7 @@ public interface ITopicBindingModel { /// /// /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). The content type also determines, - /// by default, which view is rendered by the (assuming the value isn't overwritten - /// down the pipe). + /// Editor (via the property). /// [Required] string? ContentType { get; set; } diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index c414fc1a..d5a53343 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -71,9 +71,7 @@ public interface ITopicViewModel { /// /// /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). The content type also determines, - /// by default, which view is rendered by the (assuming the value isn't overwritten - /// down the pipe). + /// Editor (via the property). /// string? ContentType { get; set; } @@ -84,11 +82,11 @@ public interface ITopicViewModel { /// Gets or sets the View attribute, representing the default view to be used for the topic. /// /// - /// This value can be set via the query string (via the class), via the Accepts header - /// (also via the class), on the topic itself (via this property), or via the + /// This value can be set via the query string (via the TopicViewResultExecutor class), via the Accepts header + /// (also via the TopicViewResultExecutor class), on the topic itself (via this property), or via the /// . By default, it will be set to the name of the ; e.g., if the - /// Content Type is "Page", then the view will be "Page". This will cause the to look - /// for a view at, for instance, /Common/Templates/Page/Page.aspx. + /// Content Type is "Page", then the view will be "Page". This will cause the TopicViewResultExecutor to look + /// for a view at, for instance, /Views/Page/Page.cshtml. /// string? View { get; set; } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 243bb042..7acf1a1c 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -173,9 +173,7 @@ public Topic? Parent { /// /// /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). The content type also determines, - /// by default, which view is rendered by the (assuming the value isn't overwritten - /// down the pipe). + /// Editor (via the property). /// /// /// The key of the current 's . @@ -260,11 +258,11 @@ internal string? OriginalKey { /// Gets or sets the View attribute, representing the default view to be used for the topic. /// /// - /// This value can be set via the query string (via the class), via the Accepts header - /// (also via the class), on the topic itself (via this property). By default, it will + /// This value can be set via the query string (via the TopicViewResultExecutor class), via the Accepts header + /// (also via the TopicViewResultExecutor class), on the topic itself (via this property). By default, it will /// be set to the name of the ; e.g., if the Content Type is "Page", then the view will be - /// "Page". This will cause the to look for a view at, for instance, - /// /Common/Templates/Page/Page.aspx. + /// "Page". This will cause the TopicViewResultExecutor to look for a view at, for instance, + /// /Views/Page/Page.cshtml. /// /// /// The view, as specified by the current . From 69155f14e175ed7d512f118f0667badd865639bf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:33:33 -0800 Subject: [PATCH 082/778] Removed suppressions which were specific to .NET Standard 2.0 support There are a number of Code Analysis suggestions that are only available in .NET Standard 2.1. Those were suppressed since we were still supporting .NET Standard 2.0. Now that we're no longer targeting .NET Standard 2.0, those are no longer necessary. The warnings those expose will be addressed in subsequent commits. --- OnTopic/GlobalSuppressions.cs | 2 -- OnTopic/Properties/AssemblyInfo.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index 194673b8..f323eb98 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -5,6 +5,4 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "StringComparison overload not supported by .NET Standard 2.0", Scope = "member", Target = "~M:OnTopic.Topic.GetWebPath~System.String")] -[assembly: SuppressMessage("Usage", "CA2249:Consider using 'string.Contains' instead of 'string.IndexOf'", Justification = "StringComparison overload not supported by .NET Standard 2.0.", Scope = "member", Target = "~M:OnTopic.Querying.TopicExtensions.FindAllByAttribute(OnTopic.Topic,System.String,System.String)~System.Collections.Generic.IEnumerable{OnTopic.Topic}")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file diff --git a/OnTopic/Properties/AssemblyInfo.cs b/OnTopic/Properties/AssemblyInfo.cs index be5af7d4..2363fe73 100644 --- a/OnTopic/Properties/AssemblyInfo.cs +++ b/OnTopic/Properties/AssemblyInfo.cs @@ -15,4 +15,4 @@ [assembly: ComVisible(false)] [assembly: InternalsVisibleTo("OnTopic.Tests")] [assembly: CLSCompliant(true)] -[assembly: GuidAttribute("3CA9F6CB-B45A-4E74-AAA4-0C87CAA2704F")] +[assembly: GuidAttribute("3CA9F6CB-B45A-4E74-AAA4-0C87CAA2704F")] \ No newline at end of file From 32d6087283a960843bfe7fe24704698455f44a83 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:35:28 -0800 Subject: [PATCH 083/778] Define `StringComparison` when calling `Contains()` or `Replace()` While this has long been a best practice, these overloads weren't available in .NET Standard 2.0. Now that we're no longer supporting .NET Framework and, thus, .NET Standard 2.0, we can finally implement these. --- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic/Topic.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 51914828..e3b338f5 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -59,7 +59,7 @@ public StubTopicRepository() : base() { | Lookup by TopicKey \-----------------------------------------------------------------------------------------------------------------------*/ if (uniqueKey is not null && uniqueKey.Length > 0) { - uniqueKey = uniqueKey.Contains(":") ? uniqueKey : "Root:" + uniqueKey; + uniqueKey = uniqueKey.Contains(":", StringComparison.Ordinal) ? uniqueKey : "Root:" + uniqueKey; return _cache.FindFirst(t => t.GetUniqueKey().Equals(uniqueKey, StringComparison.OrdinalIgnoreCase)); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 7acf1a1c..6b495307 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -528,7 +528,9 @@ public string GetUniqueKey() { /// /// The HTTP-based path to the current . public string GetWebPath() { - var uniqueKey = GetUniqueKey().Replace("Root:", "/").Replace(":", "/") + "/"; + var uniqueKey = GetUniqueKey() + .Replace("Root:", "/", StringComparison.Ordinal) + .Replace(":", "/", StringComparison.Ordinal) + "/"; if (!uniqueKey.StartsWith("/", StringComparison.InvariantCulture)) { uniqueKey = $"/{uniqueKey}"; } From 59c4d7632c745f5188453ceeca13c96df912bd7e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:36:16 -0800 Subject: [PATCH 084/778] Prefer `String.Contains()` over `String.IndexOf() >= 0` While this has long been a best practice, these overloads weren't available in .NET Standard 2.0. Now that we're no longer supporting .NET Framework and, thus, .NET Standard 2.0, we can finally implement these. --- OnTopic/Querying/TopicExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 0cb746ea..386333cb 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -187,7 +187,7 @@ public static IEnumerable FindAllByAttribute(this Topic topic, string nam \-----------------------------------------------------------------------------------------------------------------------*/ return topic.FindAll(t => !String.IsNullOrEmpty(t.Attributes.GetValue(name)) && - t.Attributes.GetValue(name).IndexOf(value, StringComparison.InvariantCultureIgnoreCase) >= 0 + t.Attributes.GetValue(name).Contains(value, StringComparison.InvariantCultureIgnoreCase) ); } From 70079f7a4a1e5fe177ebe4c92e9d3134e3187aea Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:37:08 -0800 Subject: [PATCH 085/778] Prefer range operations over `Substring()` While this has long been a best practice, the range operator wasn't available in .NET Standard 2.0. Now that we're no longer supporting .NET Framework and, thus, .NET Standard 2.0, we can finally implement these. --- OnTopic/Querying/TopicExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 386333cb..803884cd 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -235,7 +235,7 @@ public static IEnumerable FindAllByAttribute(this Topic topic, string nam | Process keys \-----------------------------------------------------------------------------------------------------------------------*/ if (uniqueKey.StartsWith(currentTopic!.Key + ":", StringComparison.OrdinalIgnoreCase)) { - uniqueKey = uniqueKey.Substring(currentTopic!.Key.Length + 1); + uniqueKey = uniqueKey[(currentTopic!.Key.Length + 1)..]; } var keys = uniqueKey.Split(new char[] {':'}, StringSplitOptions.RemoveEmptyEntries); From 533e7f34c4defb9d4588468319102f79d61cd24c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:55:24 -0800 Subject: [PATCH 086/778] Removed nullability attribute polyfills .NET Framework and .NET Standard 2.0 don't have support for C# 8.0's nullability attributes, and thus we needed to introduce them as "polyfills" to maintain backward compatibility. Now that we no longer support .NET Framework and, thus, .NET Standard 2.0, however, we can remove these (422e6ed). --- .../Diagnostics/AllowNullAttribute.cs | 44 ---------------- .../Diagnostics/DisallowNullAttribute.cs | 44 ---------------- .../Internal/Diagnostics/NotNullAttribute.cs | 37 -------------- .../Diagnostics/NotNullIfNotNullAttribute.cs | 50 ------------------- 4 files changed, 175 deletions(-) delete mode 100644 OnTopic/Internal/Diagnostics/AllowNullAttribute.cs delete mode 100644 OnTopic/Internal/Diagnostics/DisallowNullAttribute.cs delete mode 100644 OnTopic/Internal/Diagnostics/NotNullAttribute.cs delete mode 100644 OnTopic/Internal/Diagnostics/NotNullIfNotNullAttribute.cs diff --git a/OnTopic/Internal/Diagnostics/AllowNullAttribute.cs b/OnTopic/Internal/Diagnostics/AllowNullAttribute.cs deleted file mode 100644 index 7c662bd1..00000000 --- a/OnTopic/Internal/Diagnostics/AllowNullAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -#if NETSTANDARD2_0 - -namespace System.Diagnostics.CodeAnalysis { - - /*============================================================================================================================ - | CLASS: ALLOW NULL (ATTRIBUTE) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Specifies that null is allowed as an input even if the corresponding type disallows it. - /// - /// - /// This class will ship with .NET Standard 3.0. Once the project is updated to support that, we'll remove this class and - /// instead allow implementers to use the out-of-the-box implementation. In the meanwhile, providing this class within the - /// correct namespace satisfies the code analysis and allows the project to move forward with implementing the nullable - /// annotation context. - /// - [AttributeUsage( - AttributeTargets.Field | - AttributeTargets.Method | - AttributeTargets.Parameter | - AttributeTargets.Property | - AttributeTargets.ReturnValue, - AllowMultiple = true - )] - public sealed class AllowNullAttribute : Attribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Instantiates a new instance of a object. - /// - public AllowNullAttribute() { } - - } //Class -} //Namespace - -#endif \ No newline at end of file diff --git a/OnTopic/Internal/Diagnostics/DisallowNullAttribute.cs b/OnTopic/Internal/Diagnostics/DisallowNullAttribute.cs deleted file mode 100644 index cc68833d..00000000 --- a/OnTopic/Internal/Diagnostics/DisallowNullAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -#if NETSTANDARD2_0 - -namespace System.Diagnostics.CodeAnalysis { - - /*============================================================================================================================ - | CLASS: DISALLOW NULL (ATTRIBUTE) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Specifies that null is disallowed as an input even if the corresponding type allows it. - /// - /// - /// This class will ship with .NET Standard 3.0. Once the project is updated to support that, we'll remove this class and - /// instead allow implementers to use the out-of-the-box implementation. In the meanwhile, providing this class within the - /// correct namespace satisfies the code analysis and allows the project to move forward with implementing the nullable - /// annotation context. - /// - [AttributeUsage( - AttributeTargets.Field | - AttributeTargets.Method | - AttributeTargets.Parameter | - AttributeTargets.Property | - AttributeTargets.ReturnValue, - AllowMultiple = true - )] - public sealed class DisallowNullAttribute: Attribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Instantiates a new instance of a object. - /// - public DisallowNullAttribute() { } - - } //Class -} //Namespace - -#endif \ No newline at end of file diff --git a/OnTopic/Internal/Diagnostics/NotNullAttribute.cs b/OnTopic/Internal/Diagnostics/NotNullAttribute.cs deleted file mode 100644 index c5792b66..00000000 --- a/OnTopic/Internal/Diagnostics/NotNullAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -#if NETSTANDARD2_0 - -namespace System.Diagnostics.CodeAnalysis { - - /*============================================================================================================================ - | CLASS: NOT NULL (ATTRIBUTE) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Marks that a method parameter is ensured not to return , even if it is submitted as such. - /// - /// - /// This class will ship with .NET Standard 3.0. Once the project is updated to support that, we'll remove this class and - /// instead allow implementers to use the out-of-the-box implementation. In the meanwhile, providing this class within the - /// correct namespace satisfies the code analysis and allows the project to move forward with implementing the nullable - /// annotation context. - /// - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)] - public sealed class NotNullAttribute: Attribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Instantiates a new instance of a object. - /// - public NotNullAttribute() { } - - } //Class -} //Namespace - -#endif \ No newline at end of file diff --git a/OnTopic/Internal/Diagnostics/NotNullIfNotNullAttribute.cs b/OnTopic/Internal/Diagnostics/NotNullIfNotNullAttribute.cs deleted file mode 100644 index c36d437d..00000000 --- a/OnTopic/Internal/Diagnostics/NotNullIfNotNullAttribute.cs +++ /dev/null @@ -1,50 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using OnTopic.Internal.Diagnostics; - -#if NETSTANDARD2_0 - -namespace System.Diagnostics.CodeAnalysis { - - /*============================================================================================================================ - | CLASS: NOT NULL IF NOT NULL (ATTRIBUTE) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Marks that a method parameter is ensured not to return if the annotated parameter is not . - /// - /// - /// This class will ship with .NET Standard 3.0. Once the project is updated to support that, we'll remove this class and - /// instead allow implementers to use the out-of-the-box implementation. In the meanwhile, providing this class within the - /// correct namespace satisfies the code analysis and allows the project to move forward with implementing the nullable - /// annotation context. - /// - [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true)] - public sealed class NotNullIfNotNullAttribute : Attribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Instantiates a new instance of a object. - /// - public NotNullIfNotNullAttribute(string parameterName) { - Contract.Requires(parameterName); - ParameterName = parameterName; - } - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Instantiates a new instance of a object. - /// - public string ParameterName { get; } - - } //Class -} //Namespace - -#endif \ No newline at end of file From 6d086d0e700b7056d9792eb8d45a1f247cc8c24a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 17:57:20 -0800 Subject: [PATCH 087/778] Removed legacy handling of `EditorType` Previously, we maintained a compiler condition to handle `EditorType` differently between .NET Standard 2.0 and .NET Standard 2.1, due to the fact that .NET Standard 2.0 didn't support using a `StringComparison` with `EditorType`. Now that we're not supporting .NET Standard 2.0, we can get rid of this. While I was at it, I migrated to the preferred `StringComparison.OrdinalIgnoreCase`. --- OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs b/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs index 7f8917b6..5e53a933 100644 --- a/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs +++ b/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs @@ -48,11 +48,7 @@ protected AttributeTypeDescriptor( | PROPERTY: EDITOR TYPE \-------------------------------------------------------------------------------------------------------------------------*/ /// - #if NETSTANDARD2_0 - public override string EditorType => GetType().Name.Replace("Attribute", ""); - #else - public override string EditorType => GetType().Name.Replace("Attribute", "", StringComparison.InvariantCultureIgnoreCase); - #endif + public override string EditorType => GetType().Name.Replace("Attribute", "", StringComparison.OrdinalIgnoreCase); } //Class } //Namespace \ No newline at end of file From 7b0027429248dba38ceffab01aade38f247ed570 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 17 Dec 2020 18:21:22 -0800 Subject: [PATCH 088/778] Merged `AttributeDescriptor` and `AttributeTypeDescriptor` OnTopic Editor 4.0.0 formalized the idea of attribute types as content types, thus allowing each attribute type to have its own data model in the Editor. Prior to this, attribute type specific configuration values had to be configured via a single text attribute which was parsed and applied by each `{AttributeType}ViewModel` (or, prior to that, `{AttributeType}.ascx`). To continue supporting **OnTopic-Editor-WebForms**. the `AttributeDescriptor`'s `EditorType` and `ModelType` necessited complex logic, which was moved to a `ConfigurableAttributeDescriptor` in the **OnTopic-WebForms** project. As a result, the `AttributeDescriptor` acted as an abstract base class for both `ConfigurableAttributeDescriptor` as well as a simplified `AttributeTypeDescriptor` for the **OnTopic-Editor-AspNetCore** classes. Since we're no longer supporting .NET Framework (422e6ed), there's no need to maintain this abstraction. Instead, we can merge the simplified `AttributeTypeDescriptor` logic into `AttributeDescriptor` and use that as the base class for all of the `OnTopic.Metadata.AttributeTypes` base clases (which represent the strongly typed attribute descriptors, and map to their corresponding content types). --- .../ViewModels/Metadata/TextAttributeTopicViewModel.cs | 3 ++- .../Metadata/TopicReferenceAttributeTopicViewModel.cs | 3 ++- OnTopic/Metadata/AttributeDescriptor.cs | 4 ++-- OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/FileListAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs | 2 +- .../Metadata/AttributeTypes/IncomingRelationshipAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/NumberAttribute.cs | 2 +- .../Metadata/AttributeTypes/QueryableTopicListAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/TextAttribute.cs | 2 +- OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs | 2 +- 18 files changed, 21 insertions(+), 19 deletions(-) diff --git a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs index 002b7319..420bb160 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; using OnTopic.Metadata.AttributeTypes; namespace OnTopic.Tests.ViewModels.Metadata { @@ -11,7 +12,7 @@ namespace OnTopic.Tests.ViewModels.Metadata { | VIEW MODEL: TEXT ATTRIBUTE (DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a dummy implementation of a view model for an view model, in order to + /// Provides a dummy implementation of a view model for an view model, in order to /// allow the dynamic resolution of mapping topics to view models. /// /// diff --git a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs index 28c722eb..0744ab1a 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; using OnTopic.Metadata.AttributeTypes; namespace OnTopic.Tests.ViewModels.Metadata { @@ -11,7 +12,7 @@ namespace OnTopic.Tests.ViewModels.Metadata { | VIEW MODEL: TOPIC REFERENCE ATTRIBUTE (DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a dummy implementation of a view model for an view model, in order to + /// Provides a dummy implementation of a view model for an view model, in order to /// allow the dynamic resolution of mapping topics to view models. /// /// diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index cfdf4c07..647edc14 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -88,7 +88,7 @@ protected AttributeDescriptor( /// reduces these down into a single type based on how they're exposed in the Topic Library, not based on how they're /// exposed in the editor. /// - public abstract ModelType ModelType { get; } + public virtual ModelType ModelType => ModelType.ScalarValue; /*========================================================================================================================== | PROPERTY: EDITOR TYPE @@ -109,7 +109,7 @@ protected AttributeDescriptor( /// !value.Contains(" ") && !value.Contains("/") /// [AttributeSetter] - public abstract string? EditorType { get; } + public virtual string EditorType => GetType().Name.Replace("Attribute", "", StringComparison.OrdinalIgnoreCase); /*========================================================================================================================== | PROPERTY: DISPLAY GROUP diff --git a/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs b/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs index 13c17362..72ca8584 100644 --- a/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class BooleanAttribute : AttributeTypeDescriptor { + public class BooleanAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs b/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs index e45d1284..5775eac2 100644 --- a/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class DateTimeAttribute : AttributeTypeDescriptor { + public class DateTimeAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs b/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs index 9f37f929..36da5ac0 100644 --- a/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class FileListAttribute : AttributeTypeDescriptor { + public class FileListAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs b/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs index 0e53b2d5..41be86ee 100644 --- a/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class FilePathAttribute : AttributeTypeDescriptor { + public class FilePathAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs b/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs index 9428966e..6675f0f4 100644 --- a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class HtmlAttribute : AttributeTypeDescriptor { + public class HtmlAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs b/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs index c06ce249..e6cc2156 100644 --- a/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class IncomingRelationshipAttribute : AttributeTypeDescriptor { + public class IncomingRelationshipAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs b/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs index a9bbe1c2..d2944298 100644 --- a/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs @@ -16,7 +16,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class InstructionAttribute : AttributeTypeDescriptor { + public class InstructionAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs b/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs index 68938e14..a10e80c9 100644 --- a/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class LastModifiedAttribute : AttributeTypeDescriptor { + public class LastModifiedAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs b/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs index 4ee61896..6a128979 100644 --- a/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class LastModifiedByAttribute : AttributeTypeDescriptor { + public class LastModifiedByAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs index 4d05afa0..a77f68e1 100644 --- a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class NestedTopicListAttribute : AttributeTypeDescriptor { + public class NestedTopicListAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs b/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs index 21f58aca..880c66ae 100644 --- a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class NumberAttribute : AttributeTypeDescriptor { + public class NumberAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs index cd302b11..e8248649 100644 --- a/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class QueryableTopicListAttribute : AttributeTypeDescriptor { + public class QueryableTopicListAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs b/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs index 97481201..efc03230 100644 --- a/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class TextAreaAttribute : AttributeTypeDescriptor { + public class TextAreaAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/TextAttribute.cs b/OnTopic/Metadata/AttributeTypes/TextAttribute.cs index 7f07c508..2a0d2b97 100644 --- a/OnTopic/Metadata/AttributeTypes/TextAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TextAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class TextAttribute : AttributeTypeDescriptor { + public class TextAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs b/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs index a073a3eb..5082b713 100644 --- a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs @@ -17,7 +17,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class TopicReferenceAttribute : AttributeTypeDescriptor { + public class TopicReferenceAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR From 07a9ee3997b847bf6505e86a7a200684e1b2453b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 14:37:24 -0800 Subject: [PATCH 089/778] Fixed titles of preexisting `ITypeLookupService` implementations --- OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs | 2 +- OnTopic/Reflection/DynamicTopicLookupService.cs | 2 +- OnTopic/Reflection/DynamicTopicViewModelLookupService.cs | 2 +- OnTopic/Reflection/DynamicTypeLookupService.cs | 2 +- OnTopic/StaticTypeLookupService.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs b/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs index c17bdf20..fa46e1d4 100644 --- a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs +++ b/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs @@ -9,7 +9,7 @@ namespace OnTopic.Reflection { /*============================================================================================================================ - | CLASS: TOPIC BINDING MODEL LOOKUP SERVICE + | CLASS: DYNAMIC TOPIC BINDING MODEL LOOKUP SERVICE \---------------------------------------------------------------------------------------------------------------------------*/ /// /// The will search all assemblies for s that diff --git a/OnTopic/Reflection/DynamicTopicLookupService.cs b/OnTopic/Reflection/DynamicTopicLookupService.cs index 28f956b1..1bfbecaa 100644 --- a/OnTopic/Reflection/DynamicTopicLookupService.cs +++ b/OnTopic/Reflection/DynamicTopicLookupService.cs @@ -8,7 +8,7 @@ namespace OnTopic.Reflection { /*============================================================================================================================ - | CLASS: TOPIC LOOKUP SERVICE + | CLASS: DYNAMIC TOPIC LOOKUP SERVICE \---------------------------------------------------------------------------------------------------------------------------*/ /// /// The will search all assemblies for s that derive from /// The will search all assemblies for s that end with diff --git a/OnTopic/Reflection/DynamicTypeLookupService.cs b/OnTopic/Reflection/DynamicTypeLookupService.cs index 82f9af46..6a2eb34c 100644 --- a/OnTopic/Reflection/DynamicTypeLookupService.cs +++ b/OnTopic/Reflection/DynamicTypeLookupService.cs @@ -9,7 +9,7 @@ namespace OnTopic.Reflection { /*============================================================================================================================ - | CLASS: TYPE INDEX + | CLASS: DYNAMIC TYPE LOOKUP SERVICE \---------------------------------------------------------------------------------------------------------------------------*/ /// /// The will search all assemblies for instances that match a diff --git a/OnTopic/StaticTypeLookupService.cs b/OnTopic/StaticTypeLookupService.cs index 1b7570b0..791757c1 100644 --- a/OnTopic/StaticTypeLookupService.cs +++ b/OnTopic/StaticTypeLookupService.cs @@ -12,7 +12,7 @@ namespace OnTopic { /*============================================================================================================================ - | CLASS: TYPE INDEX + | CLASS: STATIC TYPE LOOKUP SERVICE \---------------------------------------------------------------------------------------------------------------------------*/ /// /// The can be configured to provide a lookup of classes based on From 5cd5de6c56661c31361c2f3014adbe2bf6c7a47b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 15:43:34 -0800 Subject: [PATCH 090/778] Introduced the `CompositeTypeLookupService` As the name suggests, the `CompositeTypeLookupService` allows multiple `ITypeLookupService`s to be combined under a single interface such that multiple implementations can be queried at the same time. The `Lookup()` interface starts with the _last_ `ITypeLookupService` and crawls backwards until it receives a valid response. --- OnTopic/CompositeTypeLookupService.cs | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 OnTopic/CompositeTypeLookupService.cs diff --git a/OnTopic/CompositeTypeLookupService.cs b/OnTopic/CompositeTypeLookupService.cs new file mode 100644 index 00000000..18e099c4 --- /dev/null +++ b/OnTopic/CompositeTypeLookupService.cs @@ -0,0 +1,68 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace OnTopic { + + /*============================================================================================================================ + | CLASS: COMPOSITE TYPE LOOKUP SERVICE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The allows s from multiple s + /// to be merged into a single . + /// + /// + /// Different libraries—such as the OnTopic View Models or OnTopic Editors—may have their own for managing s that are specific to that library. The + /// allows those to be combined into a single by passing each of them to the constructor. + /// If conflicts occur, then the last entered will take precidence, overriding any initial definitions. + /// + public class CompositeTypeLookupService: ITypeLookupService { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly List _typeLookupServices = new(); + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new instance of a . Accepts any number of instanced to be passed to the constructor. + /// + /// + /// The list of instances to expose as part of this service. + /// + public CompositeTypeLookupService(params ITypeLookupService[] typeLookupServices) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add types to internal collection + \-----------------------------------------------------------------------------------------------------------------------*/ + _typeLookupServices.AddRange(typeLookupServices.Reverse()); + + } + + /*========================================================================================================================== + | METHOD: LOOKUP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public Type? Lookup(string typeName) { + var type = typeof(Object); + foreach (var typeLookupService in _typeLookupServices) { + type = typeLookupService.Lookup(typeName); + if (type is not null && type.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)) { + return type; + } + } + //Default to default return type of last query + return type; + } + + } //Class +} //Namespace \ No newline at end of file From 4303402b53008a1710215bf971dc6f4fffa32e17 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 15:47:01 -0800 Subject: [PATCH 091/778] Implements a unit test for validating the new `CompositeTypeLookupService` The test deliberately places the `FakeViewModelLookupService` first, as it inherits from `TopicViewModelLookupService`. As such, the `CompositeTypeLookupService` will first query `TopicViewModelLookupService`, then fall back to the more comprehensive `FakeViewModelLookupService`. --- OnTopic.Tests/ITypeLookupServiceTest.cs | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 OnTopic.Tests/ITypeLookupServiceTest.cs diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs new file mode 100644 index 00000000..c9548fbc --- /dev/null +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -0,0 +1,46 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Reflection; +using OnTopic.Tests.TestDoubles; +using OnTopic.Tests.ViewModels; +using OnTopic.ViewModels; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: TYPE LOOKUP SERVICE TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the interface and its implementations, including the + /// and . + /// + [TestClass] + public class ITypeLookupServiceTest { + + /*========================================================================================================================== + | TEST: COMPOSITE: LOOKUP VALID TYPE: RETURNS TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new with two instances of a and + /// confirms that it returns the expected for a query. + /// + [TestMethod] + public void Assume_ObjectIsNull_ThrowInvalidOperationException() { + + var lookupServiceA = new FakeViewModelLookupService(); + var lookupServiceB = new TopicViewModelLookupService(); + var compositeLookup = new CompositeTypeLookupService(lookupServiceA, lookupServiceB); + + Assert.AreEqual(typeof(SlideshowTopicViewModel), compositeLookup.Lookup(nameof(SlideshowTopicViewModel))); + Assert.AreEqual(typeof(MapToParentTopicViewModel), compositeLookup.Lookup(nameof(MapToParentTopicViewModel))); + Assert.AreEqual(typeof(Object), compositeLookup.Lookup(nameof(Topic))); + + } + + } //Class +} //Namespace \ No newline at end of file From 7b02cafecb5a8201fbdf0f336a36ad818ce3692a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 16:24:42 -0800 Subject: [PATCH 092/778] Updated `TopicMappingService` unit tests to use `CompositeTypeLookupService` --- OnTopic.Tests/TopicMappingServiceTest.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index ed449504..65526c6b 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -36,6 +36,7 @@ public class TopicMappingServiceTest { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ + readonly ITypeLookupService _typeLookupService; readonly ITopicRepository _topicRepository; readonly ITopicMappingService _mappingService; @@ -52,8 +53,21 @@ public class TopicMappingServiceTest { /// crawling the object graph. /// public TopicMappingServiceTest() { - _topicRepository = new CachedTopicRepository(new StubTopicRepository()); - _mappingService = new TopicMappingService(new DummyTopicRepository(), new FakeViewModelLookupService()); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create composite topic lookup service + \-----------------------------------------------------------------------------------------------------------------------*/ + _typeLookupService = new CompositeTypeLookupService( + new TopicViewModelLookupService(), + new FakeViewModelLookupService() + ); + + /*------------------------------------------------------------------------------------------------------------------------ + | Assemble dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + _topicRepository = new CachedTopicRepository(new StubTopicRepository()); + _mappingService = new TopicMappingService(new DummyTopicRepository(), _typeLookupService); + } /*========================================================================================================================== @@ -451,7 +465,7 @@ public async Task Map_MapToParent_ReturnsMappedModel() { [TestMethod] public async Task Map_TopicReferences_ReturnsMappedModel() { - var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService()); + var mappingService = new TopicMappingService(_topicRepository, _typeLookupService); var topicReference = _topicRepository.Load(11111); var topic = TopicFactory.Create("Test", "TopicReference"); @@ -589,7 +603,7 @@ Topic getRelatedTopic(RelatedEntityTopicViewModel topic, string key) [TestMethod] public async Task Map_MetadataLookup_ReturnsLookupItems() { - var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService()); + var mappingService = new TopicMappingService(_topicRepository, _typeLookupService); var topic = TopicFactory.Create("Test", "MetadataLookup"); var target = (MetadataLookupTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); From a9541d6b3b6579841fc66434194985051327620c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 16:30:48 -0800 Subject: [PATCH 093/778] Decoupled `FakeViewModelLookupService` and `TopicViewModelLookupService` Previously, the `FakeViewModelLookupService` derived from `TopicViewModelLookupService`, such that it aggregated the types from both. That's a fine approach and continues to be supported. At this point, however, we additionally have the new `CompositeTypeLookupService` which allows multiple different `ITypeLookupService`s to be joined, even if they don't derive from one another. While using the `CompositeTypeLookupService` isn't strictly necessary here, using it makes it easier to test it, while also better representing real world scenarios. For instance, in the next release of OnTopic, we'll be separating the `EditorViewModelLookupService` so that it doesn't inheriting from the `TopicViewModelLookupService`, thus allowing implementers the flexibility of whether or not they want to use the `OnTopic.ViewModels`. --- OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index eb6e9414..b0a51ea4 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; -using OnTopic.ViewModels; namespace OnTopic.Tests.TestDoubles { @@ -18,7 +17,7 @@ namespace OnTopic.Tests.TestDoubles { /// /// Allows testing of services that depend on without using expensive reflection. /// - public class FakeViewModelLookupService: TopicViewModelLookupService { + public class FakeViewModelLookupService: StaticTypeLookupService { /*========================================================================================================================== | CONSTRUCTOR From e5708aa6e18fa8fb87f2ac6dd1429656621b4abe Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 18 Dec 2020 16:33:11 -0800 Subject: [PATCH 094/778] Corrected name of `CompositeTypeLookupService` unit test --- OnTopic.Tests/ITypeLookupServiceTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index c9548fbc..86f0d9b0 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -30,7 +30,7 @@ public class ITypeLookupServiceTest { /// confirms that it returns the expected for a query. /// [TestMethod] - public void Assume_ObjectIsNull_ThrowInvalidOperationException() { + public void Composite_LookupValidType_ReturnsType() { var lookupServiceA = new FakeViewModelLookupService(); var lookupServiceB = new TopicViewModelLookupService(); From 8ba3514acc2b0d1e1ba3dd46bda8a1c39734c83c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 12:09:41 -0800 Subject: [PATCH 095/778] Deleted unused `Stack_Top` column Previously, the `Stack_Top` column was used by the `GenerateNestedSet` stored procedure when regenerating the hierarchy. In a previous commit, this dependency was removed by updating `GenerateNestedSet` to operate off of a temporary table instead (dc72be2e). Given that, there's no need to maintain this dependency anymore. --- OnTopic.Data.Sql.Database/Tables/Topics.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index 26ec012e..60894f46 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -6,7 +6,6 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE TABLE [dbo].[Topics] ( - [Stack_Top] INT NULL, [TopicID] INT IDENTITY (1, 1) NOT NULL, [RangeLeft] INT NOT NULL, [RangeRight] INT NOT NULL, From 2b981f20612298e1e806e80798fe6ae2a21d8c76 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 12:13:50 -0800 Subject: [PATCH 096/778] Deleted unused `DateModified` column The `DateModified` column provides a timestamp of when a record was created. This predated the `Version` column. Originally, the `Version` column was an `INT` and served a different purpose than `DateModified`. Later, however, `Version` was migrated to a `DATETIME` and effectively serves the same purpose as `DateModified`. Given this, `DateModified` is being removed. It's worth noting that no SQL or C# code relied on this, so this isn't expected to be a breaking change, unless implementers were using it for some reason. --- OnTopic.Data.Sql.Database/Tables/Attributes.sql | 2 -- .../Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql | 2 -- 2 files changed, 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql index 4afab276..6d66b1c8 100644 --- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql @@ -11,8 +11,6 @@ TABLE [dbo].[Attributes] ( [TopicID] INT NOT NULL, [AttributeKey] VARCHAR (128) NOT NULL, [AttributeValue] NVARCHAR (255) NOT NULL, - [DateModified] DATETIME - CONSTRAINT [DF_Attributes_DateModified] DEFAULT (GetDate()) NOT NULL, [Version] DATETIME CONSTRAINT [DF_Attributes_Version] DEFAULT (GetDate()) NOT NULL, CONSTRAINT [PK_Attributes] PRIMARY KEY diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql index 9b9ff760..20349b76 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql @@ -31,7 +31,6 @@ WITH GroupedValues AS ( SELECT TopicID, AttributeKey, AttributeValue, - DateModified, Version, ValueGroup = ROW_NUMBER() OVER(PARTITION BY TopicID, AttributeKey ORDER BY TopicID, AttributeKey, Version) - ROW_NUMBER() OVER(PARTITION BY TopicID, AttributeKey, AttributeValue ORDER BY TopicID, AttributeKey, Version) @@ -45,7 +44,6 @@ RankedValues AS ( SELECT TopicID, AttributeKey, AttributeValue, - DateModified, Version, ValueGroup, ValueRank = ROW_NUMBER() OVER(PARTITION BY ValueGroup, TopicID, AttributeKey, AttributeValue ORDER BY TopicID, AttributeKey, Version) From 6c4952440362e841235283429a379730507df81f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 12:17:32 -0800 Subject: [PATCH 097/778] Set `Version` default to use UTC date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typically, the `Version` is set by the `CreateTopic` or `UpdateTopic` stored procedure. In a previous update, the `SqlTopicRepository.Save()` method was updated to ensure that the `Version` was set using `DateTime.UtcNow` so that the timestamp wouldn't vary based on the server's location or time settings. Attributes that rely on the default—such as those created manually in SQL without explicitly setting the `Version`—aren't able to take advantage of that. To ensure that the logic is synchronized between the client library and the SQL defaults, the default constraint for `Version` is now set to `GETUTCDATE()`. --- OnTopic.Data.Sql.Database/Tables/Attributes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql index 6d66b1c8..674940e5 100644 --- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql @@ -12,7 +12,7 @@ TABLE [dbo].[Attributes] ( [AttributeKey] VARCHAR (128) NOT NULL, [AttributeValue] NVARCHAR (255) NOT NULL, [Version] DATETIME - CONSTRAINT [DF_Attributes_Version] DEFAULT (GetDate()) NOT NULL, + CONSTRAINT [DF_Attributes_Version] DEFAULT (GETUTCDATE()) NOT NULL, CONSTRAINT [PK_Attributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [AttributeKey] ASC, From a0f1a325136c27008ee3fcd1c3084136f77feb98 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 13:47:24 -0800 Subject: [PATCH 098/778] Update `GetTopics` to use new(er) `GetTopicIDByUniqueKey` function In a previous update, `SqlTopicRepository` was updated to use the new(er) `GetTopicIDByUniqueKey` function over the legacy `GetTopicID` function, thus operating off the `UniqueKey` instead of just finding the first instance of a key (4c3d30f). In that commit, we also ensured that `ITopicRepository.Load(string)` calls relied on a fully qualified path (e.g., `Root:Configuration:ContentTypes`). In this update, we extend that to by the default when calling the `GetTopics` stored procedure directly, as well. As with the previous commit, the associated parameter is renamed from `@TopicKey` to `@UniqueKey` to ensure this expected value is clear. --- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 4846db5e..aad4d8f0 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -8,20 +8,20 @@ CREATE PROCEDURE [dbo].[GetTopics] @TopicID int = -1, @DeepLoad bit = 1, - @TopicKey nvarchar(255) = null + @UniqueKey nvarchar(255) = null AS -------------------------------------------------------------------------------------------------------------------------------- -- GET TOPIC ID IF UNKNOWN. -------------------------------------------------------------------------------------------------------------------------------- -IF @TopicKey IS NOT NULL +IF @UniqueKey IS NOT NULL BEGIN - SET @TopicID = dbo.GetTopicID(@TopicKey) + SET @TopicID = dbo.GetTopicIDByUniqueKey(@UniqueKey) END IF @TopicID < 0 BEGIN - SET @TopicID = dbo.GetTopicID('Root') + SET @TopicID = dbo.GetTopicIDByUniqueKey('Root') END -------------------------------------------------------------------------------------------------------------------------------- From ff58a710317c67b386bc926c08aa7c8b3500d61a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 13:54:48 -0800 Subject: [PATCH 099/778] Delete legacy `GetTopicID` function In a previous update, `SqlTopicRepository` was updated to use the new(er) `GetTopicIDByUniqueKey` function over the legacy `GetTopicID` function, thus operating off the `UniqueKey` instead of just finding the first instance of a key (4c3d30f). More recently, the same was applied to the `GetTopics` stored procedure (5706897). With this, the legacy `GetTopicID` function is no longer needed; the `GetTopicIDByUniqueKey` function should always be used instead. This addresses flakey behavior in `GetTopicID` since it didn't allow callers to specify a unique topic, and could thus lead to bugs. --- .../Functions/GetTopicID.sql | 45 ------------------- .../OnTopic.Data.Sql.Database.sqlproj | 1 - 2 files changed, 46 deletions(-) delete mode 100644 OnTopic.Data.Sql.Database/Functions/GetTopicID.sql diff --git a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql deleted file mode 100644 index dd5a559a..00000000 --- a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql +++ /dev/null @@ -1,45 +0,0 @@ --------------------------------------------------------------------------------------------------------------------------------- --- GET TOPIC ID --------------------------------------------------------------------------------------------------------------------------------- --- Given a particular topic key, finds the FIRST instance of the TopicID associated with that key. Be aware that since keys are --- not guaranteed to be unique, this may yield unexpected results if multiple topics share the same key; in that case, the first --- key in the hierarchy will be returned. --------------------------------------------------------------------------------------------------------------------------------- - -CREATE -FUNCTION [dbo].[GetTopicID] ( - @TopicKey NVARCHAR(255) -) -RETURNS INT -AS - -BEGIN - - ------------------------------------------------------------------------------------------------------------------------------ - -- DECLARE AND DEFINE VARIABLES - ------------------------------------------------------------------------------------------------------------------------------ - DECLARE @TopicID INT = -1 - - ------------------------------------------------------------------------------------------------------------------------------ - -- GET TOPIC ID BASED ON TOPIC KEY - ------------------------------------------------------------------------------------------------------------------------------ - SELECT TOP 1 - @TopicID = Topics.TopicID - FROM Attributes Attributes - JOIN Topics Topics - ON Attributes.TopicID = Topics.TopicID - WHERE AttributeKey = 'Key' - AND AttributeValue = @TopicKey - ORDER BY RangeLeft DESC - OPTION ( - OPTIMIZE - FOR ( @TopicKey = 'Root' - ) - ) - - ------------------------------------------------------------------------------------------------------------------------------ - -- RETURN TOPIC ID - ------------------------------------------------------------------------------------------------------------------------------ - RETURN @TopicID - -END \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 1cab94a5..cb72b10d 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -103,7 +103,6 @@ - From 10a9c0c99fd69ad2bae44c9e67c15d2a1655b2b5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 14:52:18 -0800 Subject: [PATCH 100/778] Rename `GetTopicIDByUniqueKey` to `GetTopicID` Now that we've deleted the legacy (and flakey!) `GetTopicID` function in preference for the new(er) `GetTopicIDByUniqueKey` function, we can rename the `GetTopicIDByUniqueKey` to the unqualified name `GetTopicID`. This is a more intuitive name now that there's no longer a `GetTopicID` function (ff58a71). As part of this, updated the reference in `SqlTopicRepository`, the `GetTopics` stored procedure, as well as the `OnTopic.Data.Sql.Database` documentation (`README.md`). --- .../Functions/{GetTopicIDByUniqueKey.sql => GetTopicID.sql} | 4 ++-- OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj | 2 +- OnTopic.Data.Sql.Database/README.md | 3 +-- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 4 ++-- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) rename OnTopic.Data.Sql.Database/Functions/{GetTopicIDByUniqueKey.sql => GetTopicID.sql} (97%) diff --git a/OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql similarity index 97% rename from OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql rename to OnTopic.Data.Sql.Database/Functions/GetTopicID.sql index e753134b..4de49e63 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql @@ -1,12 +1,12 @@ -------------------------------------------------------------------------------------------------------------------------------- --- GET TOPIC ID BY UNIQUE KEY +-- GET TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- -- Given a fully-qualified unique key, finds the TopicID associated with that key. Unlike [GetTopicID], this is guaranteed to -- return an exclusive instance. -------------------------------------------------------------------------------------------------------------------------------- CREATE -FUNCTION [dbo].[GetTopicIDByUniqueKey] +FUNCTION [dbo].[GetTopicID] ( @UniqueKey NVARCHAR(2500) ) diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index cb72b10d..761c1e56 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -83,9 +83,9 @@ - + diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index bc1dc861..bbb56eff 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -36,8 +36,7 @@ The following is a summary of the most relevant stored procedures. - **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). ## Functions -- **[`GetTopicID`](Functions/GetTopicID.sql)**: Retrieves a topic's `TopicId` based on a corresponding `@TopicKey`. -- **[`GetTopicIDByUniqueKey`](Functions/GetTopicIDByUniqueKey.sql)**: Retrieves a topic's `TopicId` based on a corresponding `@UniqueKey` (e.g., `Root:Configuration`). +- **[`GetTopicID`](Functions/GetTopicID.sql)**: Retrieves a topic's `TopicId` based on a corresponding `@UniqueKey` (e.g., `Root:Configuration`). - **[`GetUniqueKey`](Functions/GetUniqueKey.sql)**: Retrieves a topic's `UniqueKey` based on a corresponding `@TopicID`. - **[`GetParentID`](Functions/GetParentID.sql)**: Retrieves a topic's parent's `TopicID` based the child's `@TopicID`. - **[`GetAttributes`](functions/GetAttributes.sql)**: Given a `@TopicID`, provides the latest version of each attribute value from both `Attributes` and `ExtendedAttributes`, excluding key attributes (i.e., `Key`, `ContentType`, and `ParentID`). diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index aad4d8f0..e8e5d382 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -16,12 +16,12 @@ AS -------------------------------------------------------------------------------------------------------------------------------- IF @UniqueKey IS NOT NULL BEGIN - SET @TopicID = dbo.GetTopicIDByUniqueKey(@UniqueKey) + SET @TopicID = dbo.GetTopicID(@UniqueKey) END IF @TopicID < 0 BEGIN - SET @TopicID = dbo.GetTopicIDByUniqueKey('Root') + SET @TopicID = dbo.GetTopicID('Root') END -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 7d82699d..b6e949ee 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -77,7 +77,7 @@ public override Topic Load(string? uniqueKey = null, bool isRecursive = true) { | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ using var connection = new SqlConnection(_connectionString); - using var command = new SqlCommand("GetTopicIDByUniqueKey", connection); + using var command = new SqlCommand("GetTopicID", connection); var topicId = -1; From ba4112ddccf2ac3eedfb891c60bec69cb69aa552 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 21 Dec 2020 15:21:06 -0800 Subject: [PATCH 101/778] Include key attributes in `AttributeIndex` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When we return attributes from `GetTopics`, we don't include the key attributes—i.e., `Key`, `ContentType`, and `ParentID`, which are required to construct each `Topic`. That's because those are separately extracted from the `Attributes` table from the `TopicIndex` view, and thus not needed. In practice, this often causes confusion, as it looks like they're missing when querying `AttributeIndex`. Given this, I'm updating the logic to return the key attributes, but then explicitly exclude them from `GetTopics`. That makes the logic a bit more intuitive and clear, instead of making `AttributeIndex` overoptimized for the business logic of `GetTopics`. In the future, we may move key attributes to the `Topics` table at which point this will be irrelevant, as the separation will be enforced based on the data storage. Until then, however, this improves upon the current behavior. --- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 5 +++++ OnTopic.Data.Sql.Database/Views/AttributeIndex.sql | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index e8e5d382..5f8c6e27 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -107,6 +107,11 @@ SELECT Attributes.TopicID, FROM AttributeIndex Attributes JOIN #Topics AS Storage ON Storage.TopicID = Attributes.TopicID +WHERE AttributeKey +NOT IN ( 'Key', + 'ParentID', + 'ContentType' +) -------------------------------------------------------------------------------------------------------------------------------- -- SELECT EXTENDED ATTRIBUTES diff --git a/OnTopic.Data.Sql.Database/Views/AttributeIndex.sql b/OnTopic.Data.Sql.Database/Views/AttributeIndex.sql index fe265d6f..9ab1bb24 100644 --- a/OnTopic.Data.Sql.Database/Views/AttributeIndex.sql +++ b/OnTopic.Data.Sql.Database/Views/AttributeIndex.sql @@ -20,11 +20,6 @@ WITH Attributes AS ( ORDER BY Version DESC ) FROM [dbo].[Attributes] - WHERE AttributeKey - NOT IN ( 'Key', - 'ParentID', - 'ContentType' - ) ) SELECT Attributes.TopicID, Attributes.AttributeKey, From 5ad799cd0de0b94248df2d3e15c0f11dc75b12d0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 11:36:07 -0800 Subject: [PATCH 102/778] Sorted and grouped SQL scripts This has no functional impact, but will make it easier to read and keep organized going forward. --- .../OnTopic.Data.Sql.Database.sqlproj | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 761c1e56..124e4fd3 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -62,57 +62,63 @@ - - - + + + + + + + + + - + + - - - - - - - - - + + + - + - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -121,11 +127,4 @@ master - - - - - - - \ No newline at end of file From c50f44d03222ab3cab1b72b01ec5e2eb4c03c779 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 13:56:34 -0800 Subject: [PATCH 103/778] Add key attributes to the `Topics` table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Key`, `ContentType`, and `ParentID` have historically been stored in `Attributes` as indexed attributes—but they've always been treated as exceptions. They've mostly been filtered out of `AttributeIndex`—or, more recently, at the `GetTopics` stored procedure level (ba4112d). There is a frequent need to extract them (and only them) from `Attributes` for the sake of joins. For reporting and maintenance, they've the most commonly queried fields. And, generally, not only are they not versioned, but it doesn't necessarily make sense to version them—for instance, versioning `ParentID` would cause problems since it would conflic with the actual nested set hierarchy. And, of course, there's no referential integrity for the `ParentID`. Given this, I've migrated them to be columns on the `Topics` table. This makes them first class citizens that are much easier to query and join against, and also provides better data integrity since their requirement can be enforced—as can their referential integrity in terms of `ParentID`. This just establishes the schema. Future commits will need to migrate the data, and then update the various SQL calls to reflect that change. Until those are entirely complete, this change will continue to exist in a limbo, with the data being maintained and pulled from both locations. --- OnTopic.Data.Sql.Database/Tables/Topics.sql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index 60894f46..d94b8c4d 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -9,9 +9,15 @@ TABLE [dbo].[Topics] ( [TopicID] INT IDENTITY (1, 1) NOT NULL, [RangeLeft] INT NOT NULL, [RangeRight] INT NOT NULL, - CONSTRAINT [PK_Topics] PRIMARY KEY + [TopicKey] VARCHAR(128) NOT NULL, + [ContentType] VARCHAR(128) NOT NULL, + [ParentID] INT NULL + CONSTRAINT [PK_Topics] PRIMARY KEY CLUSTERED ( [TopicID] ASC - ) + ), + CONSTRAINT [FK_Topics_Topics] + FOREIGN KEY ( [ParentID] ) + REFERENCES [Topics]([TopicID]) ); GO From 112444335b46ea70d5429ed54807440950d620fd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 14:01:47 -0800 Subject: [PATCH 104/778] Establish basic pre-deployment migration script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Many of the changes we're introducing can rely on a basic schema comparison. Adding and populating non-nullable fields, however, requires a custom script. This script will add these three core fields—but will temporarily allow `NULL` values—and will populate them with data from the `Attributes` field. This should be run before the schema comparison. The schema comparison will then mark the `TopicKey` and `ContentType` fields as non-nullable, allowing the schema comparison to run without problems. --- .../OnTopic.Data.Sql.Database.sqlproj | 2 + .../Upgrade from OnTopic 4 to OnTopic 5.sql | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 124e4fd3..335ac42b 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -64,6 +64,7 @@ + @@ -71,6 +72,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql new file mode 100644 index 00000000..ebbc90d1 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -0,0 +1,50 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- UPGRADE FROM ONTOPIC 4.x TO ONTOPIC 5.x +-------------------------------------------------------------------------------------------------------------------------------- +-- There are a few data schema differences that cannot be handled as part of the schema comparison. These should be executed +-- prior to running migrations. +-------------------------------------------------------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------------------------------------------------------- +-- MIGRATE CORE ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +-- In OnTopic 5, core attributes that don't utilize versioning have been moved from the Attributes table to the Topics table. +-- This includes Key, ContentType, and ParentID. Previously, these required a lot of workaround since they frequently utilized +-- in a way that's inconsistent with other attributes. By moving them to Topic, we better acknowledge their unique status. +-------------------------------------------------------------------------------------------------------------------------------- + +ALTER TABLE [dbo].[Topics] + ADD [TopicKey] VARCHAR (128) NULL, + [ContentType] VARCHAR (128) NULL, + [ParentID] INT NULL; + +WITH KeyAttributes AS ( + SELECT TopicID, + AttributeKey, + AttributeValue, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY TopicID, + AttributeKey + ORDER BY Version DESC + ) + FROM [dbo].[Attributes] + WHERE AttributeKey + IN ( 'Key', + 'ContentType', + 'ParentID' + ) +) +UPDATE Topics +SET Topics.TopicKey = Pvt.[Key], + Topics.ContentType = Pvt.ContentType, + Topics.ParentID = Pvt.ParentID +FROM KeyAttributes +PIVOT ( MIN(AttributeValue) + FOR AttributeKey IN ( + [Key], + [ContentType], + [ParentID] + ) +) AS Pvt +WHERE RowNumber = 1 +AND Topics.TopicID = Pvt.TopicID From a74389d00727247a86da3e81c865d6bafdda6a99 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 14:11:24 -0800 Subject: [PATCH 105/778] Add `@TopicKey` and `@ContentType` to `CreateTopic`, `UpdateTopic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a previous commit, the key attributes of `Key`, `ContentType`, and `ParentID` were migrated from `Attributes` to `Topics`. This updates the `CreateTopic` and `UpdateTopic` stored procedure—as well as its call from `SqlTopicRepository`—to include `@TopicKey` and `@ContentType`. (`@ParentID` was already required for `CreateTopic`, and continues to be handled via `MoveTopic` for updates.) Technically, the `UpdateTopic` stored procedure only requires the `@TopicKey` or `@ContentType` parameters if they have changed; it's smart enough to fall back to the existing value if they aren't passed. That said, it doesn't hurt anything to hard code them in absence of library support for tracking their state change. These three attributes continue to be saved in the `Attributes` table for now, and are thus maintained in two locations. That will be removed in subsequent commits. --- .../Stored Procedures/CreateTopic.sql | 14 ++++++++--- .../Stored Procedures/UpdateTopic.sql | 24 +++++++++++++++++++ OnTopic.Data.Sql/SqlTopicRepository.cs | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 6d836ccd..58f27739 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -5,7 +5,9 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[CreateTopic] - @ParentID int = -1, + @Key VARCHAR(128) , + @ContentType VARCHAR(128) , + @ParentID INT = -1, @Attributes AttributeValues READONLY, @ExtendedAttributes Xml = null, @Version datetime = null @@ -53,11 +55,17 @@ IF (@ParentID > -1) -------------------------------------------------------------------------------------------------------------------------------- INSERT INTO Topics ( RangeLeft, - RangeRight + RangeRight, + TopicKey, + ContentType, + ParentID ) Values ( @RangeRight, - @RangeRight + 1 + @RangeRight + 1, + @Key, + @ContentType, + @ParentID ) DECLARE @TopicID INT diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 7172a7c0..4d8a4eca 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -6,6 +6,8 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @TopicID INT = -1 , + @Key VARCHAR(128) = NULL , + @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , @ExtendedAttributes XML = null , @Version DATETIME = null , @@ -18,6 +20,28 @@ AS IF @Version IS NULL SET @Version = GetDate() +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE KEY ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- + +IF @Key IS NOT NULL OR @ContentType IS NOT NULL + BEGIN + UPDATE Topics + SET TopicKey = + CASE + WHEN @Key IS NULL + THEN TopicKey + ELSE @Key + END, + ContentType = + CASE + WHEN @ContentType IS NULL + THEN TopicKey + ELSE @ContentType + END + WHERE TopicID = @TopicID + END + -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NEW ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index b6e949ee..03f964f2 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -423,6 +423,8 @@ SqlDateTime version else if (topic.Parent is not null) { command.AddParameter("ParentID", topic.Parent.Id); } + command.AddParameter("Key", topic.Key); + command.AddParameter("ContentType", topic.ContentType); command.AddParameter("Version", version.Value); command.AddParameter("ExtendedAttributes", extendedAttributes); command.AddParameter("Attributes", attributeValues); From 2dc4fb27388ebdd1dccedd1d428c0d52c0e613a7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 14:52:06 -0800 Subject: [PATCH 106/778] Updated SQL code to get key attributes from `Topics` The `Key`, `ContentType`, and `ParentID` are currently being maintained in both `Attributes` (as they've historically been) and `Topics` (c50f44d, 1124443, a74389d). Pulling data from `Topics` is dramatically easier since a) the data is in columns, b) the columns are strongly typed, and c) there is no versioning to deduplicate. This is important as the key attributes are frequently relied upon by other functions and procedures as part of joins. Given this, we can now simplify those queries to instead rely upon the `Topics` table instead of doing a complex query against `Attributes` or a join against `TopicIndex`. For now, the `Key`, `ContentType`, and `ParentID` attributes continue to be stored in `Attributes`; in a future commit, we'll be removing that as well. --- .../Functions/FindTopicIDs.sql | 2 +- .../Functions/GetTopicID.sql | 19 ++++++------------- .../Functions/GetUniqueKey.sql | 8 -------- .../Stored Procedures/GetTopics.sql | 12 ++++++------ 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql index a5697fff..e83980fe 100644 --- a/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql +++ b/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql @@ -40,7 +40,7 @@ BEGIN INSERT INTO @Topics SELECT TopicID - FROM TopicIndex + FROM Topics WHERE ( @AttributeKey = 'Key' AND TopicKey = @AttributeValue OR @AttributeKey = 'ContentType' diff --git a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql index 4de49e63..7b3e14da 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql @@ -26,27 +26,20 @@ BEGIN ;WITH RCTE AS ( SELECT TopicID, - CAST(NULL AS NVARCHAR(255)) AS ParentID, + ParentID, CAST('Root' AS NVARCHAR(255)) AS UniqueKey FROM Topics root WHERE root.TopicID = 1 UNION ALL - SELECT p.TopicID, - p.AttributeValue AS ParentID, + SELECT Topics.TopicID, + Topics.ParentID, CAST(recursive.UniqueKey + ':' + TopicKey AS NVARCHAR(255)) AS UniqueKey - FROM Attributes p - CROSS APPLY ( - SELECT AttributeValue AS TopicKey - FROM [dbo].[Attributes] k - WHERE k.TopicID = p.TopicID - AND k.AttributeKey = 'Key' - ) TopicKey + FROM Topics INNER JOIN RCTE recursive - ON p.AttributeValue = CAST(recursive.TopicID AS NVARCHAR(10)) - WHERE p.AttributeKey = 'ParentID' - AND @UniqueKey LIKE CAST(recursive.UniqueKey + ':' + TopicKey AS NVARCHAR(255)) + '%' + ON Topics.ParentID = recursive.TopicID + WHERE @UniqueKey LIKE CAST(recursive.UniqueKey + ':' + TopicKey AS NVARCHAR(255)) + '%' ) SELECT @TopicID = TopicID FROM RCTE AS hierarchy diff --git a/OnTopic.Data.Sql.Database/Functions/GetUniqueKey.sql b/OnTopic.Data.Sql.Database/Functions/GetUniqueKey.sql index a6186322..5854def1 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetUniqueKey.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetUniqueKey.sql @@ -34,14 +34,6 @@ BEGIN ------------------------------------------------------------------------------------------------------------------------------ SELECT @UniqueKey = COALESCE(@UniqueKey + ':' + TopicKey, TopicKey) FROM Topics - CROSS APPLY ( - SELECT TOP 1 - AttributeValue AS TopicKey - FROM [dbo].[Attributes] - WHERE Attributes.TopicID = Topics.TopicID - AND Attributes.AttributeKey = 'Key' - ORDER BY Version DESC - ) TopicKey WHERE RangeLeft <= @RangeLeft AND RangeRight >= @RangeRight ORDER BY RangeLeft diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 5f8c6e27..d37257a1 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -87,14 +87,14 @@ ELSE -------------------------------------------------------------------------------------------------------------------------------- -- SELECT KEY ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- -SELECT TopicIndex.TopicID, - TopicIndex.ContentType, - TopicIndex.ParentID, - TopicIndex.TopicKey, +SELECT Topics.TopicID, + Topics.ContentType, + Topics.ParentID, + Topics.TopicKey, Storage.SortOrder -FROM TopicIndex AS TopicIndex +FROM Topics AS Topics JOIN #Topics AS Storage - ON Storage.TopicID = TopicIndex.TopicID + ON Storage.TopicID = Topics.TopicID ORDER BY SortOrder -------------------------------------------------------------------------------------------------------------------------------- From 7c738b1f4ad0fccd52a681304560c143847e331f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:07:09 -0800 Subject: [PATCH 107/778] Ensured SQL keywords are capitalized for consistency Ignia's styleguide calls for capitalizing SQL keywords. This is generally followed, but there were some exceptions I've noticed as part of the current round up updates. Taking a moment to consistently apply those. --- .../Stored Procedures/CreateTopic.sql | 10 +++++----- .../Stored Procedures/GetTopicVersion.sql | 4 ++-- .../Stored Procedures/MoveTopic.sql | 6 +++--- .../Stored Procedures/UpdateTopic.sql | 6 +++--- .../Tables/ExtendedAttributes.sql | 2 +- .../Stored Procedures/DeleteConsecutiveAttributes.sql | 5 ++--- .../DeleteConsecutiveExtendedAttributes.sql | 4 ++-- .../DeleteOrphanedLastModifiedAttributes.sql | 8 ++++---- .../Utilities/Tables/AdjacencyList.sql | 4 ++-- .../Utilities/Views/UniqueKeyIndex.sql | 4 ++-- 10 files changed, 26 insertions(+), 27 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 58f27739..d34d73e6 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -9,20 +9,20 @@ CREATE PROCEDURE [dbo].[CreateTopic] @ContentType VARCHAR(128) , @ParentID INT = -1, @Attributes AttributeValues READONLY, - @ExtendedAttributes Xml = null, - @Version datetime = null + @ExtendedAttributes XML = NULL, + @Version DATETIME = NULL AS -------------------------------------------------------------------------------------------------------------------------------- -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = getdate() +SET @Version = GETUTCDATE() -------------------------------------------------------------------------------------------------------------------------------- -- DECLARE AND SET VARIABLES -------------------------------------------------------------------------------------------------------------------------------- -DECLARE @RangeRight Integer --Right Most Sibling +DECLARE @RangeRight INT --Right Most Sibling SET @RangeRight = 0 -------------------------------------------------------------------------------------------------------------------------------- @@ -92,7 +92,7 @@ WHERE AttributeKey != 'ParentID' -------------------------------------------------------------------------------------------------------------------------------- -- ADD EXTENDED ATTRIBUTES (XML) -------------------------------------------------------------------------------------------------------------------------------- -IF @ExtendedAttributes is not null +IF @ExtendedAttributes IS NOT NULL BEGIN INSERT INTO ExtendedAttributes ( diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index bbc099c3..d9848ed2 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -5,8 +5,8 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[GetTopicVersion] - @TopicID int = -1, - @Version datetime = null + @TopicID INT = -1, + @Version DATETIME = NULL AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index 27064b92..4535bdee 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -92,7 +92,7 @@ ELSE -- target location (@InsertionPoint) is not within the scope of the source tree (@TargetID); a tree cannot be moved to a child -- of itself. -------------------------------------------------------------------------------------------------------------------------------- -IF @TopicID is null or @OriginalLeft is null or @OriginalRight is null +IF @TopicID IS NULL OR @OriginalLeft IS NULL OR @OriginalRight IS NULL BEGIN RAISERROR ( N'The topic ("%n") could not be found.', @@ -103,7 +103,7 @@ IF @TopicID is null or @OriginalLeft is null or @OriginalRight is null RETURN END -IF @ParentID is null or @InsertionPoint is null +IF @ParentID IS NULL OR @InsertionPoint IS NULL BEGIN RAISERROR ( N'The parent ("%n") could not be found.', @@ -300,7 +300,7 @@ END -- UPDATE PARENT ID -------------------------------------------------------------------------------------------------------------------------------- UPDATE Attributes -SET AttributeValue = CONVERT(NVarChar(255), @ParentID) +SET AttributeValue = CONVERT(NVARCHAR(255), @ParentID) WHERE TopicID = @TopicID AND AttributeKey = 'ParentID' diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 4d8a4eca..94f68fb0 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -9,9 +9,9 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @Key VARCHAR(128) = NULL , @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , - @ExtendedAttributes XML = null , - @Version DATETIME = null , @DeleteRelationships BIT = 0 + @ExtendedAttributes XML = NULL , + @Version DATETIME = NULL AS -------------------------------------------------------------------------------------------------------------------------------- @@ -120,7 +120,7 @@ CROSS APPLY ( AND AttributeKey = New.AttributeKey ORDER BY Version DESC ) Existing -WHERE IsNull(AttributeValue, '') = '' +WHERE ISNULL(AttributeValue, '') = '' AND ExistingValue != '' -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql index 15ffe748..d68a9844 100644 --- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql @@ -8,7 +8,7 @@ CREATE TABLE [dbo].[ExtendedAttributes] ( [TopicID] INT NOT NULL, - [AttributesXml] XML NOT NULL, + [AttributesXml] XML NOT NULL, [DateModified] DATETIME CONSTRAINT [DF_ExtendedAttributes_DateModified] DEFAULT ( GetDate() diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql index 20349b76..fd927dcd 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql @@ -19,7 +19,7 @@ SET NOCOUNT ON; -------------------------------------------------------------------------------------------------------------------------------- DECLARE @Count INT -SELECT @Count = Count(TopicID) +SELECT @Count = COUNT(TopicID) FROM Attributes Print('Initial Count: ' + CAST(@Count AS VARCHAR) + ' Attributes in the database.'); @@ -63,5 +63,4 @@ WHERE ValueRank > 1; SELECT @Count = @Count - Count(TopicID) FROM Attributes -Print('Final Count: ' + CAST(@Count AS VARCHAR) + ' Attributes were identified and deleted.') - +Print('Final Count: ' + CAST(@Count AS VARCHAR) + ' Attributes were identified and deleted.') \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql index 3c5674f1..35c7ed40 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql @@ -22,7 +22,7 @@ SET NOCOUNT ON; -------------------------------------------------------------------------------------------------------------------------------- DECLARE @Count INT -SELECT @Count = Count(TopicID) +SELECT @Count = COUNT(TopicID) FROM ExtendedAttributes Print('Initial Count: ' + CAST(@Count AS VARCHAR) + ' Extended Attributes in the database.'); @@ -65,7 +65,7 @@ PRINT('Concurrent duplicates have been deleted.') -------------------------------------------------------------------------------------------------------------------------------- -- CHECK FINAL VALUES -------------------------------------------------------------------------------------------------------------------------------- -SELECT @Count = @Count - Count(TopicID) +SELECT @Count = @Count - COUNT(TopicID) FROM ExtendedAttributes Print('Final Count: ' + CAST(@Count AS VARCHAR) + ' duplicate Extended Attributes were identified and deleted.') diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteOrphanedLastModifiedAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteOrphanedLastModifiedAttributes.sql index 065bfef1..127f6c37 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteOrphanedLastModifiedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteOrphanedLastModifiedAttributes.sql @@ -23,7 +23,7 @@ SET NOCOUNT ON; -------------------------------------------------------------------------------------------------------------------------------- DECLARE @Count INT -SELECT @Count = Count(TopicID) +SELECT @Count = COUNT(TopicID) FROM Attributes WHERE AttributeKey = 'LastModified' @@ -41,14 +41,14 @@ LEFT JOIN Attributes Unmatched LEFT JOIN ExtendedAttributes UnmatchedExtended ON Attributes.TopicID = UnmatchedExtended.TopicID AND Attributes.Version = UnmatchedExtended.Version -WHERE Unmatched.AttributeKey is null - AND UnmatchedExtended.TopicID is null +WHERE Unmatched.AttributeKey IS NULL + AND UnmatchedExtended.TopicID IS NULL AND Attributes.AttributeKey = 'LastModified' -------------------------------------------------------------------------------------------------------------------------------- -- CHECK FINAL VALUES -------------------------------------------------------------------------------------------------------------------------------- -SELECT @Count = @Count - Count(TopicID) +SELECT @Count = @Count - COUNT(TopicID) FROM Attributes WHERE AttributeKey = 'LastModified' diff --git a/OnTopic.Data.Sql.Database/Utilities/Tables/AdjacencyList.sql b/OnTopic.Data.Sql.Database/Utilities/Tables/AdjacencyList.sql index cb50e58e..2ee49f27 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Tables/AdjacencyList.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Tables/AdjacencyList.sql @@ -9,8 +9,8 @@ CREATE TABLE [Utilities].[AdjacencyList] ( [TopicID] INT NOT NULL, [Parent_TopicID] INT NULL, - [SortOrder] INT NOT NULL, - CONSTRAINT [PK_Hierarchy] PRIMARY KEY + [SortOrder] INT NOT NULL, + CONSTRAINT [PK_Hierarchy] PRIMARY KEY CLUSTERED ( [TopicID] ASC ) ); \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database/Utilities/Views/UniqueKeyIndex.sql b/OnTopic.Data.Sql.Database/Utilities/Views/UniqueKeyIndex.sql index 1d98fcd9..8fbe9277 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Views/UniqueKeyIndex.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Views/UniqueKeyIndex.sql @@ -9,10 +9,10 @@ WITH SCHEMABINDING AS SELECT Tree.TopicID, - Path = Replace(Path, '>', ':') + Path = REPLACE(Path, '>', ':') FROM [dbo].[Topics] Tree CROSS APPLY ( - SELECT Path = Stuff(( + SELECT Path = STUFF(( SELECT '>' + AttributeValue FROM ( SELECT RangeLeft, From 06b2cd9745ef57714e9fe3edd22c60fd64f60c72 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:09:26 -0800 Subject: [PATCH 108/778] Removed legacy `@DeleteRelationships` parameter The need for `@DeleteRelationships` was removed in a previous update, but the parameter was maintained for backward compatibility. Now that we're on a major release, I'm deleting the parameter entirely, as it's no longer used. --- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 1 - OnTopic.Data.Sql/SqlTopicRepository.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 94f68fb0..0f81e697 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -9,7 +9,6 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @Key VARCHAR(128) = NULL , @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , - @DeleteRelationships BIT = 0 @ExtendedAttributes XML = NULL , @Version DATETIME = NULL AS diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 03f964f2..edadf30a 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -418,7 +418,6 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ if (!topic.IsNew) { command.AddParameter("TopicID", topic.Id); - command.AddParameter("DeleteRelationships", areReferencesResolved && areRelationshipsDirty); } else if (topic.Parent is not null) { command.AddParameter("ParentID", topic.Parent.Id); From ea25ed3d62fbf44a4840bf0a88c96d79c1bce12b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:21:08 -0800 Subject: [PATCH 109/778] Stop persisting key attributes in `Topics.Attributes` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As part of the migration away from storing the key attributes—i.e., `Key`, `ContentType`, and `ParentId`—as attributes, their corresponding properties now pull and write the values as local variables, instead of storing them in the `AttributeValueCollection` of `Topic.Attributes`. This means subsequent updates to these will no longer be updated in `Attributes`—though those values will continue to persist for the time being. The `Topics` table will now be the canonical reference for core parameters. --- OnTopic/Topic.cs | 50 ++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 6b495307..112c5f79 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -29,6 +29,7 @@ public class Topic { | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private string _key; + private string _contentType; private int _id = -1; private string? _originalKey; private Topic? _parent; @@ -67,6 +68,13 @@ public Topic(string key, string contentType, Topic parent, int id = -1) { Relationships = new(this, false); VersionHistory = new(); + /*------------------------------------------------------------------------------------------------------------------------ + | Set entity identifier, if present + \-----------------------------------------------------------------------------------------------------------------------*/ + if (id >= 0) { + Id = id; + } + /*------------------------------------------------------------------------------------------------------------------------ | Set core properties \-----------------------------------------------------------------------------------------------------------------------*/ @@ -75,23 +83,13 @@ public Topic(string key, string contentType, Topic parent, int id = -1) { Parent = parent; /*------------------------------------------------------------------------------------------------------------------------ - | Initialize key + | Initialize key fields \-----------------------------------------------------------------------------------------------------------------------*/ - //###HACK JJC20190924: The local backing field _key is always initialized at this point. But Roslyn's flow analysis - //isn't smart enough to detect this. As such, the following effectively sets _key to itself. - _key = Key; - - /*------------------------------------------------------------------------------------------------------------------------ - | If ID is set, ensure attributes are not marked as IsDirty - \-----------------------------------------------------------------------------------------------------------------------*/ - if (id >= 0) { - Id = id; - Attributes.SetValue("Key", key, false, false); - Attributes.SetValue("ContentType", contentType, false, false); - if (parent is not null) { - Attributes.SetValue("ParentId", parent.Id.ToString(CultureInfo.InvariantCulture), false, false); - } - } + //###HACK JJC20190924: The local backing fields _key and _contentType are always initialized at this point. But Roslyn's + //flow analysis isn't smart enough to detect this. As such, the following effectively sets _key and _contentType to + //themselves. + _key = Key; + _contentType = ContentType; } @@ -179,8 +177,14 @@ public Topic? Parent { /// The key of the current 's . /// public string ContentType { - get => Attributes.GetValue("ContentType")?? ""; - set => SetAttributeValue("ContentType", value); + get => _contentType; + set { + TopicFactory.ValidateKey(value); + if (_contentType == value) { + return; + } + _contentType = value; + } } /*========================================================================================================================== @@ -201,20 +205,21 @@ public string ContentType { /// > /// !value.Contains(" ") /// - [AttributeSetter] public string Key { get => _key; set { TopicFactory.ValidateKey(value); + if (_key == value) { + return; + } if (_originalKey is null) { - _originalKey = Attributes.GetValue("Key", _key, false, false); + _originalKey = _key; } //If an established key value is changed, the parent's index must be manually updated; this won't happen automatically. if (_originalKey is not null && !value.Equals(_key, StringComparison.OrdinalIgnoreCase) && Parent is not null) { Parent.Children.ChangeKey(this, value); } - SetAttributeValue("Key", value); - _key = value; + _key = value; } } @@ -475,7 +480,6 @@ public void SetParent(Topic parent, Topic? sibling = null) { \-----------------------------------------------------------------------------------------------------------------------*/ if (_parent != parent) { _parent = parent; - SetAttributeValue("ParentID", parent.Id.ToString(CultureInfo.InvariantCulture)); } From a8feb6de9d80c064aa11baf27519b68a1fd1073c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:32:36 -0800 Subject: [PATCH 110/778] Introduce `Topic.IsDirty()` for state tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In previous commits, we introduced `AttributeValueCollection.IsDirty()` (1560a46) and `RelatedTopicCollection.IsDirty()` (2adef58) for tracking changes. This allowed us to intelligently determine whether or not a `Topic` needed to be saved as part of a recursive `ITopicRepository.Save()`, thus speeding up bulk imports and updates (afa1e9b). As we've now moved `Key`, `ContentType`, and `Parent` out of `Attributes` and to `Topic`, we need an additional level of state tracking, as otherwise we won't be able to detect if e.g. the `Key` and/or `ContentType` have changed unless other attributes or relationships have _also_ changed. To do this, we've introduced a `Topic.IsDirty()`. Given its centralized position, however, we've also added an overload to `Topic.IsDirty()` to `checkCollections`—which will call `Topic.Attributes.IsDirty()` and `Topic.Relationships.IsDirty()`—and to optionally `excludeLastModified`, which relays that parameter to `Topic.Attributes.IsDirty()`. This logic is integrated with `SqlTopicRepository` to ensure that it will save a topic even if the only change is the `Key` or `ContentType`. (There was already handling for `Parent` under `ITopicRepository.Move()`.) Finally, three unit tests have been introduced for verifying three different states of `Topic.IsDirty()`—i.e., that a new topic, or a topic whose `Key` or `ContentType` properties are changed is flagged as `IsDirty()`, while an existing topic (with an `Id`) is not. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 ++ OnTopic.Tests/TopicTest.cs | 48 ++++++++++++++++++++++++++ OnTopic/Topic.cs | 33 +++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index edadf30a..ff544c5f 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -318,6 +318,7 @@ SqlDateTime version | Define variables \-----------------------------------------------------------------------------------------------------------------------*/ var areReferencesResolved = true; + var isTopicDirty = topic.IsDirty(); var areRelationshipsDirty = topic.Relationships.IsDirty(); var areAttributesDirty = topic.Attributes.IsDirty(excludeLastModified: !areRelationshipsDirty); var extendedAttributeList = GetAttributes(topic, isExtendedAttribute: true); @@ -337,6 +338,7 @@ SqlDateTime version | as a quick fix to reduce the overhead of recursive saves. \-----------------------------------------------------------------------------------------------------------------------*/ var isDirty = + isTopicDirty || areRelationshipsDirty || areAttributesDirty || indexedAttributeList.Any() || diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 3b1005de..4826f38c 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -346,5 +346,53 @@ public void DerivedTopic_ResavedValue_ReturnsExpectedValue() { } + /*========================================================================================================================== + | IS DIRTY: NEW TOPIC: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates a new topic, and confirms that returns true. + /// + [TestMethod] + public void IsDirty_NewTopic_ReturnsTrue() { + + var topic = TopicFactory.Create("Topic", "Page"); + + Assert.IsTrue(topic.IsDirty()); + + } + + /*========================================================================================================================== + | IS DIRTY: EXISTING TOPIC: RETURNS FALSE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates an existing topic, and confirms that returns false. + /// + [TestMethod] + public void IsDirty_ExistingTopic_ReturnsFalse() { + + var topic = TopicFactory.Create("Topic", "Page", 1); + + Assert.IsFalse(topic.IsDirty()); + + } + + /*========================================================================================================================== + | IS DIRTY: CHANGE KEY: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates an existing topic, changes the , and confirms that returns true. + /// + [TestMethod] + public void IsDirty_ChangeKey_ReturnsTrue() { + + var topic = TopicFactory.Create("Topic", "Page", 1); + + topic.Key = "NewTopic"; + + Assert.IsTrue(topic.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 112c5f79..46d91560 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -34,6 +33,7 @@ public class Topic { private string? _originalKey; private Topic? _parent; private Topic? _derivedTopic; + private bool _isDirty; /*========================================================================================================================== | CONSTRUCTOR @@ -183,6 +183,9 @@ public string ContentType { if (_contentType == value) { return; } + else if (_contentType is not null || IsNew) { + _isDirty = true; + } _contentType = value; } } @@ -212,6 +215,9 @@ public string Key { if (_key == value) { return; } + else if (_key is not null || IsNew) { + _isDirty = true; + } if (_originalKey is null) { _originalKey = _key; } @@ -541,6 +547,31 @@ public string GetWebPath() { return uniqueKey; } + /*========================================================================================================================== + | METHOD: IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if the topic is dirty, optionally checking and . + /// + /// + /// Determines if and should be checked. + /// + /// + /// Optionally excludes s whose keys start with LastModified. This is useful for + /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically + /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. + /// + /// + public bool IsDirty(bool checkCollections = false, bool excludeLastModified = false) { + if (!_isDirty && checkCollections) { + _isDirty = Relationships.IsDirty(); + } + if (!_isDirty && checkCollections) { + _isDirty = Attributes.IsDirty(excludeLastModified); + } + return _isDirty; + } + #endregion #region Relationship and Collection Properties From 07c938bd85200e136e027b7983d19b86671b0b9d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:36:03 -0800 Subject: [PATCH 111/778] Delete core attributes from `Attributes` as part of pre-deployment script In a previous commit, a basic pre-deployment migration script was established which created the `TopicKey`, `ContentType`, and `ParentID` columns in the `Topic` table and copied the core attributes from `Attributes` to `Topics` (1124443). Now that the migration is mostly complete, and all functions and stored procedures (2dc4fb2) and business objects (ea25ed3) are correctly saving that data to `Topics`, we can safely delete the legacy data from `Attributes`. --- .../Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index ebbc90d1..3cd0ffd2 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -48,3 +48,11 @@ PIVOT ( MIN(AttributeValue) ) AS Pvt WHERE RowNumber = 1 AND Topics.TopicID = Pvt.TopicID + +DELETE +FROM Attributes +WHERE AttributeKey +IN ( 'Key', + 'ContentType', + 'ParentID' +) \ No newline at end of file From f6e7222d0096728fa0410c121a645912335eabf0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:37:18 -0800 Subject: [PATCH 112/778] Stop caching `ParentID` in `Attributes` With the `ParentID` now fully migrated over to the `Topic` table, there's no need to continue "caching" it in the `Attributes` table when calling the `CreateTopic` stored procedure. --- .../Stored Procedures/CreateTopic.sql | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index d34d73e6..1f7f1ac4 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -106,21 +106,6 @@ IF @ExtendedAttributes IS NOT NULL ) END --------------------------------------------------------------------------------------------------------------------------------- --- CACHE PARENT ID FOR DATA INTEGRITY PURPOSES --------------------------------------------------------------------------------------------------------------------------------- -INSERT INTO Attributes ( - TopicID , - AttributeKey , - AttributeValue , - Version -) -VALUES ( @TopicID , - 'ParentID' , - CONVERT(NVarChar(255), @ParentID), - @Version -) - -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- From da9813b019363a6c48e950f3736ce3cf5e303281 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:42:05 -0800 Subject: [PATCH 113/778] Remove conditional exclusions of core attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The core attributes—`Key`, `ContentType`, and `ParentID`—were always treated as exceptions to other attributes and, as such, were excluded from a number of queries on e.g. `@Attributes` parameters in the `CreatTopic` and `UpdateTopic` stored procedures, as well as the attribute queries in the `GetTopics` and `GetTopicVersion` stored procedures. Now that those have been deleted from `Attributes` (07c938b) and are now instead pulled from `Topics` (2dc4fb2), there's no need to exclude these from queries of the `Attributes` table; all `Attributes` can now be treated as equal. --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 3 +-- .../Stored Procedures/GetTopicVersion.sql | 5 ----- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 5 ----- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 3 +-- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 1f7f1ac4..f6795b02 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -86,8 +86,7 @@ SELECT @TopicID, AttributeValue, @Version FROM @Attributes -WHERE AttributeKey != 'ParentID' - AND IsNull(AttributeValue, '') != '' +WHERE ISNULL(AttributeValue, '') != '' -------------------------------------------------------------------------------------------------------------------------------- -- ADD EXTENDED ATTRIBUTES (XML) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index d9848ed2..b02429f3 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -66,11 +66,6 @@ AS ( FROM Attributes WHERE TopicID = @TopicID AND Version <= @Version - AND AttributeKey - NOT IN ( 'Key', - 'ParentID', - 'ContentType' - ) ) SELECT TopicID, AttributeKey, diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index d37257a1..8064f7bb 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -107,11 +107,6 @@ SELECT Attributes.TopicID, FROM AttributeIndex Attributes JOIN #Topics AS Storage ON Storage.TopicID = Attributes.TopicID -WHERE AttributeKey -NOT IN ( 'Key', - 'ParentID', - 'ContentType' -) -------------------------------------------------------------------------------------------------------------------------------- -- SELECT EXTENDED ATTRIBUTES diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 0f81e697..bc0ce6cb 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -64,8 +64,7 @@ OUTER APPLY ( AND AttributeKey = New.AttributeKey ORDER BY Version DESC ) Existing -WHERE AttributeKey != 'ParentId' - AND ISNULL(AttributeValue, '') != '' +WHERE ISNULL(AttributeValue, '') != '' AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') -------------------------------------------------------------------------------------------------------------------------------- From a4a28d68af789a10b758134ff77015964c1280c8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:45:08 -0800 Subject: [PATCH 114/778] Removed now-obsolete `TopicIndex` view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicIndex` extracted the latest version of the key attributes—i.e., `Key`, `ContentType`, and `ParentID`—from the `Attributes` table and pivoted them to columns. That complexity can now be handled by a straight query of the `Topics` table, as has already been implemented in the dependency functions and stored procedures (2dc4fb2). As such, the `TopicIndex` view is no longer used, and no longer serves a purpose, and can be safely removed. --- .../OnTopic.Data.Sql.Database.sqlproj | 2 - OnTopic.Data.Sql.Database/README.md | 1 - .../Views/TopicIndex.sql | 42 ------------------- 3 files changed, 45 deletions(-) delete mode 100644 OnTopic.Data.Sql.Database/Views/TopicIndex.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 335ac42b..a39fc9a8 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -89,7 +89,6 @@ - @@ -119,7 +118,6 @@ - diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index bbb56eff..5a3caa42 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -45,7 +45,6 @@ The following is a summary of the most relevant stored procedures. ## Views The majority of the views provide records corresponding to the latest version of records for each topic. These include: -- **[`TopicIndex`](Views/TopicIndex.sql)**: Includes the core topic attributes, `topicId`, `Key`, `ParentId`, and `ContentType`. - **[`AttributeIndex`](Views/AttributeIndex.sql)**: Includes the `TopicId`, `AttributeKey` and `AttributeValue`. - **[`ExtendedAttributesIndex`](Views/ExtendedAttributeIndex.sql)**: Includes the `TopicId` and `AttributeXml`. - **[`VersionHistoryIndex`](Views/VersionHistoryIndex.sql)**: Includes up to the last five `Version` records for every `TopicId`. diff --git a/OnTopic.Data.Sql.Database/Views/TopicIndex.sql b/OnTopic.Data.Sql.Database/Views/TopicIndex.sql deleted file mode 100644 index 82e76749..00000000 --- a/OnTopic.Data.Sql.Database/Views/TopicIndex.sql +++ /dev/null @@ -1,42 +0,0 @@ --------------------------------------------------------------------------------------------------------------------------------- --- TOPIC (INDEX) --------------------------------------------------------------------------------------------------------------------------------- --- Retrieves the latest version of the key attributes for each topic and pivots them into a single record for each topic. When --- loading or reporting topics, it's often useful to start with the Key, ContentType, and ParentID; once those are established, --- other attributes and relationships can be pulled. This helps in that process by making all of those items available in a --- single query. --------------------------------------------------------------------------------------------------------------------------------- -CREATE -VIEW [dbo].[TopicIndex] -WITH SCHEMABINDING -AS - -WITH KeyAttributes AS ( - SELECT TopicID, - AttributeKey, - AttributeValue, - RowNumber = ROW_NUMBER() OVER ( - PARTITION BY TopicID, - AttributeKey - ORDER BY Version DESC - ) - FROM [dbo].[Attributes] - WHERE AttributeKey - IN ( 'Key', - 'ParentID', - 'ContentType' - ) -) -SELECT TopicID, - ContentType, - ParentID, - [Key] AS 'TopicKey' -FROM KeyAttributes -PIVOT ( MIN(AttributeValue) - FOR AttributeKey IN ( - [Key], - [ParentID], - [ContentType] - ) -) AS Pvt -WHERE RowNumber = 1 \ No newline at end of file From c1c6b7116a1083694094b292ad2104ff2afb110a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:50:07 -0800 Subject: [PATCH 115/778] Updated `AttributeValueCollection` tests to use `View` for business logic tests The `AttributeValueCollection.EnforceBusinessLogic()` method looks for corresponding properties on the associated `Topic` instance which may have a property with the same name as an `AttributeValue` being modified. If so, it routes the user through that property to ensure that any business logic and state management is maintained. Previously, the `AttributeValueCollection`'s unit tests relied on the `Topic.Key` property as the quentissential example of this, since it was not only an `[AttributeSetter]`, but also enforced business logic, by ensuring that the value was a proper key. With the `Key` now operating off of a local field (ea25ed3), those tests are no longer valid. As such, they've been updated to instead use the `Topic.View` field which enforces similar logic, and remains used today. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 18e748f1..ef95411a 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -25,12 +25,13 @@ public class AttributeValueCollectionTest { | TEST: GET VALUE: CORRECT VALUE: IS RETURNED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Creates a new topic and ensures that the key can be returned as an attribute. + /// Creates a new attribute via an [AttributeSetter] and ensures that the attribute can be returned. /// [TestMethod] public void GetValue_CorrectValue_IsReturned() { var topic = TopicFactory.Create("Test", "Container"); - Assert.AreEqual("Test", topic.Attributes.GetValue("Key")); + topic.View = "Test"; + Assert.AreEqual("Test", topic.Attributes.GetValue("View")); } /*========================================================================================================================== @@ -455,11 +456,11 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { [TestMethod] [ExpectedException( typeof(TargetInvocationException), - "The topic allowed a key to be set via a back door, without routing it through the Key property." + "The topic allowed a view to be set via a back door, without routing it through the View property." )] public void SetValue_InvalidValue_ThrowsException() { var topic = TopicFactory.Create("Test", "Container"); - topic.Attributes.SetValue("Key", "# ?"); + topic.Attributes.SetValue("View", "# ?"); } /*========================================================================================================================== @@ -474,10 +475,9 @@ public void Add_ValidAttributeValue_IsReturned() { var topic = TopicFactory.Create("Test", "Container"); - topic.Attributes.Remove("Key"); - topic.Attributes.Add(new("Key", "NewKey", false)); + topic.Attributes.Add(new("View", "NewKey", false)); - Assert.AreEqual("NewKey", topic.Key); + Assert.AreEqual("NewKey", topic.View); } @@ -494,8 +494,7 @@ public void Add_ValidAttributeValue_IsReturned() { )] public void Add_InvalidAttributeValue_ThrowsException() { var topic = TopicFactory.Create("Test", "Container"); - topic.Attributes.Remove("Key"); - topic.Attributes.Add(new("Key", "# ?")); + topic.Attributes.Add(new("View", "# ?")); } /*========================================================================================================================== @@ -511,15 +510,16 @@ public void Add_WithBusinessLogic_MaintainsIsDirty() { var topic = TopicFactory.Create("Test", "Container", 1); - topic.Attributes.TryGetValue("Key", out var originalValue); + topic.View = "Test"; + topic.Attributes.TryGetValue("View", out var originalValue); var index = topic.Attributes.IndexOf(originalValue); - topic.Attributes[index] = new AttributeValue("Key", "NewValue", false); - topic.Attributes.TryGetValue("Key", out var newAttribute); + topic.Attributes[index] = new AttributeValue("View", "NewValue", false); + topic.Attributes.TryGetValue("View", out var newAttribute); - topic.Attributes.SetValue("Key", "NewerValue", false); - topic.Attributes.TryGetValue("Key", out var newerAttribute); + topic.Attributes.SetValue("View", "NewerValue", false); + topic.Attributes.TryGetValue("View", out var newerAttribute); Assert.IsFalse(newAttribute.IsDirty); Assert.IsFalse(newerAttribute.IsDirty); From c0ce94f96a9930804b5700db57dfc2db6d444af9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:52:49 -0800 Subject: [PATCH 116/778] Reduced expected count of attributes by two in unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the `TopicRepositoryBase` unit tests, multiple tests validated the number of returned attributes. These were consistently two too high, since they expected all topics to have a `Key` and `ContentType` attribute. Now that those have been removed (ea25ed3) those unit tests fail, and so the count needs to be removed by two to account for that change. Note: As these are not established as existing entities—i.e., they don't have their `Id` set—there isn't a `ParentId` attribute, and thus the reduction is only two, not three. --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index ae15d7b6..3fededf3 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -208,7 +208,7 @@ public void GetAttributes_AnyAttributes_ReturnsAllAttributes() { var attributes = _topicRepository.GetAttributesProxy(topic, null); - Assert.AreEqual(3, attributes.Count()); + Assert.AreEqual(1, attributes.Count()); } @@ -250,7 +250,7 @@ public void GetAttributes_IndexedAttributes_ReturnsIndexedAttributes() { var attributes = _topicRepository.GetAttributesProxy(topic, false); - Assert.AreEqual(2, attributes.Count()); + Assert.AreEqual(0, attributes.Count()); } @@ -313,8 +313,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsNothing() { var attributes = _topicRepository.GetAttributesProxy(topic, false, true); - //Expect Key and ContentType, but not Title - Assert.AreEqual(2, attributes.Count()); + Assert.AreEqual(0, attributes.Count()); } @@ -335,8 +334,7 @@ public void GetAttributes_ExcludeLastModified_ReturnsOtherAttributes() { var attributes = _topicRepository.GetAttributesProxy(topic, null, excludeLastModified: true); - //Expected to return Key and ContentType, butnot LastModified or LastModifiedBy - Assert.AreEqual(2, attributes.Count()); + Assert.AreEqual(0, attributes.Count()); } From 07911715db0e5c438b4eedab9983cd21b44bc597 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:56:01 -0800 Subject: [PATCH 117/778] Called core attributes as `Topic` properties not `Attributes` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the core attributes—i.e., `Key`, `ContentType`, and `ParentId`—have been moved exclusively to local fields on `Topic` instead of persisted to the `Attributes` collection (ea25ed3), they should no longer be retrieved from the `AttributeValueCollection`. --- OnTopic.Tests/TopicTest.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 4826f38c..3bfba022 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -32,7 +32,7 @@ public void Create_ReturnsTopic() { var topic = TopicFactory.Create("Test", "Page"); Assert.IsNotNull(topic); Assert.AreEqual(topic.Key, "Test"); - Assert.AreEqual(topic.Attributes.GetValue("ContentType"), "Page"); + Assert.AreEqual(topic.ContentType, "Page"); } /*========================================================================================================================== @@ -123,10 +123,7 @@ public void Parent_SetValue_UpdatesParentTopic() { childTopic.Parent = parentTopic; Assert.ReferenceEquals(parentTopic.Children["Child"], childTopic); - Assert.AreEqual( - 5, - Int32.Parse(childTopic.Attributes.GetValue("ParentId", "0"), NumberStyles.Integer, CultureInfo.InvariantCulture) - ); + Assert.AreEqual(5, childTopic.Parent.Id); } @@ -150,10 +147,7 @@ public void Parent_ChangeValue_UpdatesParentTopic() { Assert.ReferenceEquals(targetParent.Children["ChildTopic"], childTopic); Assert.IsFalse(sourceParent.Children.Contains("ChildTopic")); - Assert.AreEqual( - 10, - Int32.Parse(childTopic.Attributes.GetValue("ParentId", "0"), NumberStyles.Integer, CultureInfo.InvariantCulture) - ); + Assert.AreEqual(10, childTopic.Parent.Id); } From c2eb7b03f47ac90b08aa5e9a139614e15897cfc1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 15:57:23 -0800 Subject: [PATCH 118/778] Removed redundant `Assert` statements The `Id_ChangeValue_ThrowsArgumentException` test expected an `InvalidOperationException` operation to occur. As such, the `Assert` statements should never be evaluated and are unnecessary. --- OnTopic.Tests/TopicTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 3bfba022..f6da25f3 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -62,9 +62,6 @@ public void Id_ChangeValue_ThrowsArgumentException() { var topic = TopicFactory.Create("Test", "ContentTypeDescriptor", 123); topic.Id = 124; - Assert.AreEqual(123, topic.Id); - Assert.AreNotEqual(124, topic.Id); - } /*========================================================================================================================== From 0ecc68bf7e72771cbfe69a0579c31421641a1819 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:02:15 -0800 Subject: [PATCH 119/778] Simplified `isDirty` tracking in `SqlTopicRepository.Save()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the call to `Topic.Attributes.IsDirty()` was set to conditionally set `excludeLastModified` based on the state of `areRelationshipsDirty`. There's some logic to that. But because both `areAttributesDirty` and `areRelationshipsDirty` (and, now, `isTopicDirty`) are all evaluated as part of the determination of whether or not to save the topic, there's no real need to add the complexity of a conditional `excludeLastModified`—we can always set that to `true`. This is much easier to reason through that the weird double-negative created by `excludeLastModified: !areRelationshipsDirty`, and ends up with the same end result. I.e., we also only care if `Attributes` are dirty if `LastModified` and `LastModifiedBy` aren't the _only_ attributes that were modified. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index ff544c5f..f819568d 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -320,7 +320,7 @@ SqlDateTime version var areReferencesResolved = true; var isTopicDirty = topic.IsDirty(); var areRelationshipsDirty = topic.Relationships.IsDirty(); - var areAttributesDirty = topic.Attributes.IsDirty(excludeLastModified: !areRelationshipsDirty); + var areAttributesDirty = topic.Attributes.IsDirty(true); var extendedAttributeList = GetAttributes(topic, isExtendedAttribute: true); var indexedAttributeList = GetAttributes( topic : topic, From 4a9f5b7740a56276cf3e533e2765df5bca6b4668 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:05:40 -0800 Subject: [PATCH 120/778] Update `[FilterByAttribute()]` tests to not use `ContentType` Since the `ContentType` is no longer stored as an `Attribute`, the `[FilterByAttribute()]` unit test fails, as the `FilteredTopicViewModel` was filtering by `ContentType`. To mitigate that, the unit test is updated to filter by two arbitrary attributes instead. In the future, we'll need to reestablish a way to filter by `ContentType`, as that's actually the most common use case for the `[FilterByAttribute()]` attribute. But, for now, this ensures all of our unit tests are passing, and fully accounting for the change of location for `ContentType`. --- OnTopic.Tests/TopicMappingServiceTest.cs | 8 +++++++- OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 65526c6b..9c9445dc 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -807,7 +807,13 @@ public async Task Map_FilterByAttribute_ReturnsFilteredCollection() { childTopic1.Attributes.SetValue("SomeAttribute", "ValueA"); childTopic2.Attributes.SetValue("SomeAttribute", "ValueA"); childTopic3.Attributes.SetValue("SomeAttribute", "ValueA"); - childTopic4.Attributes.SetValue("SomeAttribute", "ValueB"); + childTopic4.Attributes.SetValue("SomeAttribute", "ValueA"); + + childTopic1.Attributes.SetValue("SomeOtherAttribute", "ValueB"); + childTopic2.Attributes.SetValue("SomeOtherAttribute", "ValueB"); + childTopic3.Attributes.SetValue("SomeOtherAttribute", "ValueA"); + childTopic4.Attributes.SetValue("SomeOtherAttribute", "ValueA"); + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); diff --git a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs index 1d6ce229..4b87fc24 100644 --- a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs @@ -20,8 +20,8 @@ namespace OnTopic.Tests.ViewModels { /// public class FilteredTopicViewModel { - [FilterByAttribute("ContentType", "Page")] [FilterByAttribute("SomeAttribute", "ValueA")] + [FilterByAttribute("SomeOtherAttribute", "ValueB")] public TopicViewModelCollection Children { get; } = new(); } //Class From 105cfdde7d67bbba642b4280fcf95bb47f78bb41 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:18:35 -0800 Subject: [PATCH 121/778] 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.) --- OnTopic.Tests/TopicMappingServiceTest.cs | 27 +++++++++++- .../FilteredContentTypeTopicViewModel.cs | 27 ++++++++++++ .../Internal/Mapping/PropertyConfiguration.cs | 22 ++++++++++ .../Annotations/FilterByContentType.cs | 44 +++++++++++++++++++ OnTopic/Mapping/TopicMappingService.cs | 7 +++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs create mode 100644 OnTopic/Mapping/Annotations/FilterByContentType.cs diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 9c9445dc..5f0793cc 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -632,14 +632,14 @@ public async Task Map_CircularReference_ReturnsCachedParent() { } /*========================================================================================================================== - | TEST: MAP: FILTER BY CONTENT TYPE: RETURNS FILTERED COLLECTION + | TEST: MAP: FILTER BY COLLECTION TYPE: RETURNS FILTERED COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Establishes a and tests whether the resulting object's property can be filtered by . /// [TestMethod] - public async Task Map_FilterByContentType_ReturnsFilteredCollection() { + public async Task Map_FilterByCollectionType_ReturnsFilteredCollection() { var topic = TopicFactory.Create("Test", "Descendent"); var childTopic1 = TopicFactory.Create("ChildTopic1", "Descendent", topic); @@ -821,6 +821,29 @@ public async Task Map_FilterByAttribute_ReturnsFilteredCollection() { } + /*========================================================================================================================== + | TEST: MAP: FILTER BY CONTENT TYPE: RETURNS FILTERED COLLECTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests whether the resulting object's property can be filtered using a + /// instances. + /// + [TestMethod] + public async Task Map_FilterByContentType_ReturnsFilteredCollection() { + + var topic = TopicFactory.Create("Test", "Filtered"); + var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", topic); + var childTopic2 = TopicFactory.Create("ChildTopic2", "Index", topic); + var childTopic3 = TopicFactory.Create("ChildTopic3", "Page", topic); + var childTopic4 = TopicFactory.Create("ChildTopic4", "Page", childTopic3); + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.AreEqual(3, target.Children.Count); + + } + /*========================================================================================================================== | TEST: MAP: FLATTEN ATTRIBUTE: RETURNS FLAT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs new file mode 100644 index 00000000..0c613841 --- /dev/null +++ b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs @@ -0,0 +1,27 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping.Annotations; +using OnTopic.ViewModels; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: FILTERED CONTENT TYPE TOPIC + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed data transfer object for testing views properties annotated with the . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class FilteredContentTypeTopicViewModel { + + [FilterByContentType("Page")] + public TopicViewModelCollection Children { get; } = new(); + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Internal/Mapping/PropertyConfiguration.cs index 2406a625..6de2d2eb 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Internal/Mapping/PropertyConfiguration.cs @@ -87,6 +87,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue(property, a => FlattenChildren = true); GetAttributeValue(property, a => MetadataKey = a.Key); GetAttributeValue(property, a => DisableMapping = true); + GetAttributeValue(property, a => ContentTypeFilter = a.ContentType); /*------------------------------------------------------------------------------------------------------------------------ | Attributes: Determine relationship key and type @@ -364,6 +365,27 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// public bool DisableMapping { get; set; } + /*========================================================================================================================== + | PROPERTY: CONTENT TYPE FILTER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a ContentType which can optionally be used to filter a collection. + /// + /// + /// + /// By default, all s in a source collection (e.g., ) will be included in + /// a corresponding collection on the DTO (assuming the mapped DTO is compatible with the collection type). If the + /// is set, however, then each will be evaluated to confirm that + /// it is of that content type. + /// + /// + /// The property corresponds to the property. It can be assigned by decorating a DTO property with e.g. [FilterByContentType("Page")] + /// . + /// + /// + public string? ContentTypeFilter { get; set; } + /*========================================================================================================================== | PROPERTY: ATTRIBUTE FILTERS \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/Annotations/FilterByContentType.cs b/OnTopic/Mapping/Annotations/FilterByContentType.cs new file mode 100644 index 00000000..5d93d4dc --- /dev/null +++ b/OnTopic/Mapping/Annotations/FilterByContentType.cs @@ -0,0 +1,44 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace OnTopic.Mapping.Annotations { + + /*============================================================================================================================ + | ATTRIBUTE: FILTER BY CONTENT TYPE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Flags that a collection property should be filtered by a specified ContentType. + /// + /// + /// By default, will add any corresponding relationships to a collection, assuming they + /// are assignable to the collection's base type. With the [FilterByContentType(contentType)] attribute, the + /// collection will instead be filtered to only those topics that have the specified content type. + /// + [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=true, Inherited=true)] + public sealed class FilterByContentTypeAttribute : System.Attribute { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Annotates a property with the class by providing a (required) content type. + /// + /// The content type to filter by. + public FilterByContentTypeAttribute(string contentType) { + TopicFactory.ValidateKey(contentType, false); + ContentType = contentType; + } + + /*========================================================================================================================== + | PROPERTY: CONTENT TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the content type. + /// + public string ContentType { get; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 3b5fc6ad..7b08bb72 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -625,6 +625,13 @@ ConcurrentDictionary cache continue; } + if ( + configuration.ContentTypeFilter is not null && + childTopic.ContentType.Equals(configuration.ContentTypeFilter, StringComparison.OrdinalIgnoreCase) + ) { + continue; + } + //Skip nested topics; those should be explicitly mapped to their own collection or topic reference if (childTopic.ContentType.Equals("List", StringComparison.OrdinalIgnoreCase)) { continue; From b5b4829382d524df8ef9872e0df5efbaec6f3673 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:21:07 -0800 Subject: [PATCH 122/778] Enforce dropping columns as part of pre-deployment script Now that we have established a pre-deployment script (1124443) we can use it to address issues not effectively addressed by schema comparison. Notably, schema comparison won't drop columns that have data in them. As such, we can drop the legacy `Topics.Stack_Top` (8ba3514) and `Attributes.DateModified` (2b981f2) columns, which were previously removed from the schema. --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 3cd0ffd2..025af10e 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -5,6 +5,29 @@ -- prior to running migrations. -------------------------------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------------------------------- +-- DROP COLUMNS +-------------------------------------------------------------------------------------------------------------------------------- +-- Migrations won't drop columns that have data in them. The following drop columns that are no longer needed. This also drops +-- stored procedures that reference those columns—with the knowledge that their replacements will be recreated by the +-- migrations. +-------------------------------------------------------------------------------------------------------------------------------- + +ALTER +TABLE Topics +DROP +COLUMN Stack_Top; + +ALTER +TABLE Attributes +DROP +CONSTRAINT DF_Attributes_DateModified; + +ALTER +TABLE Attributes +DROP +COLUMN DateModified; + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE CORE ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- From d777c3daa1a24cc8aa812e5b5cfdc2b20211965f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:21:53 -0800 Subject: [PATCH 123/778] Removed inadvertent trailing space Whoops. --- OnTopic/Querying/TopicExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 803884cd..9caa4fc1 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -289,4 +289,4 @@ public static IEnumerable FindAllByAttribute(this Topic topic, string nam } } //Class -} //Namespace +} //Namespace \ No newline at end of file From 303cee2e971f34eaa2a648d41e1e7e406b071844 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:27:11 -0800 Subject: [PATCH 124/778] Establish basic pre-deployment migration script for 4.0.0 This is a bit late, but as we've established a pre-deployment script for migrating from 4.0.0 to 5.0.0, I'm also committing our legacy pre-deployment script for migrating from 3.0.0 to 4.0.0. This was maintained independently by Ignia, but really makes more sense as part of the `OnTopic.Data.Sql.Database` project. This one is most notably needed to migrate the legacy generic `AttributeDescriptor` references to their specific `AttributeDescriptor` descendents. E.g., a text attribute no longer uses the generic `AttributeDescriptor` content type, but instead uses the strongly typed `TextAttribute` content type. As part of this, it also ensures the `ContentType` is explicitly set on each `Topic`, whereas previously these values could be inherited from a `DerivedTopic`. While technically fine, that introduces confusion with reporting and updates like this one, so it's easier all around to ensure those are repeated. (This business preference is firmly enforced in OnTopic 5.0.0 due to the `ContentType` being moved to the `Topics` table.) --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + .../Upgrade from OnTopic 3 to OnTopic 4.sql | 82 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index a39fc9a8..458f66d4 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -64,6 +64,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql new file mode 100644 index 00000000..c6e2162c --- /dev/null +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql @@ -0,0 +1,82 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- UPGRADE FROM ONTOPIC 3.x TO ONTOPIC 4.x +-------------------------------------------------------------------------------------------------------------------------------- +-- There are a few data schema differences that cannot be handled as part of the schema comparison. These should be executed +-- prior to running migrations. +-------------------------------------------------------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------------------------------------------------------- +-- DROP COLUMNS +-------------------------------------------------------------------------------------------------------------------------------- +-- Migrations won't drop columns that have data in them. The following drop columns that are no longer needed. This also drops +-- stored procedures that reference those columns—with the knowledge that their replacements will be recreated by the +-- migrations. +-------------------------------------------------------------------------------------------------------------------------------- + +ALTER +TABLE topics_TopicAttributes +DROP +COLUMN AttributeID + +-------------------------------------------------------------------------------------------------------------------------------- +-- INHERIT TYPES +-------------------------------------------------------------------------------------------------------------------------------- +-- Attribute Descriptors will be converted to more specific content types based on their legacy attribute type. Attribute types +-- may be inherited from derived topics, however. This script identifies any cases where the attribute type is inherited, and +-- updates the target topic with the derived value. This may need to be run multiple times if there are multiple layers of +-- inheritance (e.g., an attribute derives from an attribute which derives from another attribute). +-------------------------------------------------------------------------------------------------------------------------------- + +INSERT +INTO topics_TopicAttributes +SELECT SourceTopicID, + 'Type', + AttributeTypes.AttributeValue, + GETDATE() +FROM ( + SELECT TopicID AS SourceTopicID, + AttributeKey, + AttributeValue + FROM Topics_TopicAttributes +) Attributes +PIVOT ( MAX(AttributeValue) + FOR AttributeKey IN ( + [Type], + [ContentType], + [TopicID] + ) +) AS Attributes +JOIN Topics_TopicAttributes AttributeTypes + ON Attributes.TopicID = CAST(AttributeTypes.TopicID AS VARCHAR(10)) + AND AttributeTypes.AttributeKey = 'Type' +WHERE ContentType = 'AttributeDescriptor' +AND Type IS NULL +AND ISNULL(Attributes.TopicID, -1) > 0 +ORDER BY SourceTopicID, + ContentType + +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE CONTENT TYPES +-------------------------------------------------------------------------------------------------------------------------------- +-- Based on the attribute types, update the content type to the corresponding attribute descriptor. In some cases, the names +-- have changed, and so an explicit mapping is required. +-------------------------------------------------------------------------------------------------------------------------------- + +UPDATE ContentTypes +SET ContentTypes.AttributeValue = + CASE + WHEN AttributeTypes.AttributeValue LIKE 'DateTime%' THEN 'DateTimeAttribute' + WHEN AttributeTypes.AttributeValue = 'File.ascx' THEN 'FileListAttribute' + WHEN AttributeTypes.AttributeValue = 'FormField.ascx' THEN 'TextAttribute' + WHEN AttributeTypes.AttributeValue = 'Relationships.ascx' THEN 'RelationshipAttribute' + WHEN AttributeTypes.AttributeValue = 'TopicList.ascx' THEN 'NestedTopicListAttribute' + WHEN AttributeTypes.AttributeValue = 'TopicLookup.ascx' THEN 'TopicListAttribute' + WHEN AttributeTypes.AttributeValue = 'TopicPointer.ascx' THEN 'TopicReferenceAttribute' + WHEN AttributeTypes.AttributeValue = 'WYSIWYG.ascx' THEN 'HtmlAttribute' + ELSE REPLACE(AttributeTypes.AttributeValue, '.ascx', '') + 'Attribute' + END +FROM topics_TopicAttributes ContentTypes +INNER JOIN topics_TopicAttributes AttributeTypes + ON AttributeTypes.TopicID = ContentTypes.TopicID + AND AttributeTypes.AttributeKey = 'Type' +WHERE ContentTypes.AttributeKey = 'ContentType' \ No newline at end of file From ecddea8706b372ceed697a6d347818d1f2f026b4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 16:40:41 -0800 Subject: [PATCH 125/778] Fixed bug in `[FilterByContentType()]` logic In a previous commit, I introduced a new `[FilterByContentType()]` attribute (105cfdd). As part of that, however, I inadvertantly introduced a bug in how the logic was evaluated. This fixes that logic, as well as the unit test to correctly account for the expected number of filtered topics. Whoops! --- OnTopic.Tests/TopicMappingServiceTest.cs | 2 +- OnTopic/Mapping/TopicMappingService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 5f0793cc..59a3380e 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -840,7 +840,7 @@ public async Task Map_FilterByContentType_ReturnsFilteredCollection() { var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); - Assert.AreEqual(3, target.Children.Count); + Assert.AreEqual(2, target.Children.Count); } diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 7b08bb72..487b7431 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -627,7 +627,7 @@ ConcurrentDictionary cache if ( configuration.ContentTypeFilter is not null && - childTopic.ContentType.Equals(configuration.ContentTypeFilter, StringComparison.OrdinalIgnoreCase) + !childTopic.ContentType.Equals(configuration.ContentTypeFilter, StringComparison.OrdinalIgnoreCase) ) { continue; } From bc137131834f1233ef79c5f838291befa463a98b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 17:56:52 -0800 Subject: [PATCH 126/778] Introduced unit test for ensuring invalid attributes are skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our previous unit test for `SitemapController` didn't confirm that attributes in the `ExcludeAttributes` list are properly excluded—and vice versa. This unit test addresses that scenario. --- .../TopicControllerTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index 22155839..ac15aa63 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -148,5 +148,43 @@ public void SitemapController_Index_ReturnsSitemapXml() { } + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it properly + /// excludes the Body and IsHidden attributes. + /// + [TestMethod] + public void SitemapController_Index_ExcludesAttributes() { + + var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1:Web_0_1_1")!; + + topic.Attributes.SetValue("Title", "Title"); + topic.Attributes.SetValue("LastModified", "December 23, 1918"); + topic.Attributes.SetValue("Body", "Body"); + topic.Attributes.SetValue("IsHidden", "0"); + + var actionContext = new ActionContext { + HttpContext = new DefaultHttpContext(), + RouteData = new(), + ActionDescriptor = new ControllerActionDescriptor() + }; + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(actionContext) + }; + var result = controller.Index(false, true) as ContentResult; + var model = result.Content as string; + + controller.Dispose(); + + Assert.IsNotNull(model); + Assert.IsTrue(model.Contains("")); + Assert.IsTrue(model.Contains("")); + Assert.IsFalse(model.Contains("")); + Assert.IsFalse(model.Contains("")); + + } + } //Class } //Namespace \ No newline at end of file From bf1685dcac823e5f5a33fc6faeb2e6a55e0b9a73 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 17:59:53 -0800 Subject: [PATCH 127/778] Introduced unit test for ensuring invalid content types are skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Our previous unit test for `SitemapController` didn't confirm that content types in the `ExcludeContentTypes` list (such as `List`) or which are explicitly skipped (such as `Container`) are properly excluded—and vice versa. This unit test addresses that scenario. --- .../TopicControllerTest.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index ac15aa63..e3235f9c 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -148,6 +148,44 @@ public void SitemapController_Index_ReturnsSitemapXml() { } + /*========================================================================================================================== + | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTENT TYPES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Triggers the index action of the action and verifies that it properly + /// excludes List content types, and skips over Container and PageGroup. + /// + [TestMethod] + public void SitemapController_Index_ExcludesContentTypes() { + + var hiddenTopic1 = _topicRepository.Load("Root:Web:Web_1:Web_1_0")!; + var hiddenTopic2 = _topicRepository.Load("Root:Web:Web_1:Web_1_1")!; + + hiddenTopic1.ContentType = "List"; + hiddenTopic2.ContentType = "Container"; + + var actionContext = new ActionContext { + HttpContext = new DefaultHttpContext(), + RouteData = new(), + ActionDescriptor = new ControllerActionDescriptor() + }; + var controller = new SitemapController(_topicRepository) { + ControllerContext = new(actionContext) + }; + var result = controller.Index(false, true) as ContentResult; + var model = result.Content as string; + + controller.Dispose(); + + Assert.IsNotNull(model); + Assert.IsTrue(model.Contains("")); + Assert.IsFalse(model.Contains("")); + Assert.IsFalse(model.Contains("")); + Assert.IsTrue(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/")); + Assert.IsFalse(model.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/")); + + } + /*========================================================================================================================== | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ From a90fe9fa407a9981c3edcf5b491b23ac075b2fb0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 18:07:41 -0800 Subject: [PATCH 128/778] Skip including of `PageGroup` as part of the `SitemapController` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When writing the sitemap, the `SitemapController` skips over any `Container` content types—but still includes their descendents. We want to add `PageGroup` to that list, as well as any topics with a `Url` attribute, since those will always redirect the request to another page, and thus shouldn't be in the sitemap. To facilitate this, I refactored the `Container` and `PageGroup` into a new `SkippedContentTypes` list, similar to the existing `ExcludeContentTypes` and `ExcludeAttributes` lists. That will make it easier in case we want to add additional content types to skip in the future. Finally, updated the unit tests to account for this new scenario. --- .../TopicControllerTest.cs | 8 ++++++++ .../Controllers/SitemapController.cs | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index e3235f9c..ba0ae272 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -160,9 +160,13 @@ public void SitemapController_Index_ExcludesContentTypes() { var hiddenTopic1 = _topicRepository.Load("Root:Web:Web_1:Web_1_0")!; var hiddenTopic2 = _topicRepository.Load("Root:Web:Web_1:Web_1_1")!; + var hiddenTopic3 = _topicRepository.Load("Root:Web:Web_1:Web_1_1:Web_1_1_1")!; + var hiddenTopic4 = _topicRepository.Load("Root:Web:Web_0:Web_0_0")!; hiddenTopic1.ContentType = "List"; hiddenTopic2.ContentType = "Container"; + hiddenTopic3.ContentType = "PageGroup"; + hiddenTopic4.Attributes.SetValue("Url", "https://www.microsoft.com/"); var actionContext = new ActionContext { HttpContext = new DefaultHttpContext(), @@ -181,8 +185,12 @@ public void SitemapController_Index_ExcludesContentTypes() { Assert.IsTrue(model.Contains("")); Assert.IsFalse(model.Contains("")); Assert.IsFalse(model.Contains("")); + Assert.IsFalse(model.Contains("")); + Assert.IsTrue(model.Contains("/Web/Web_0/Web_0_0/Web_0_0_1/")); Assert.IsTrue(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/")); Assert.IsFalse(model.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/")); + Assert.IsFalse(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); + Assert.IsFalse(model.Contains("/Web/Web_0/Web_0_0/")); } diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 2b2df5fe..d3d30505 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -39,10 +39,18 @@ public class SitemapController : Controller { | EXCLUDE CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Specifies what content types should not be listed in the sitemap. + /// Specifies what content types should not be listed in the sitemap, including any descendents. /// private static string[] ExcludeContentTypes { get; } = { "List" }; + /*========================================================================================================================== + | SKIPPED CONTENT TYPES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Specifies what content types should not be listed in the sitemap—but whose descendents should still be evaluated. + /// + private static string[] SkippedContentTypes { get; } = { "PageGroup", "Container" }; + /*========================================================================================================================== | EXCLUDE ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ @@ -195,7 +203,10 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false getRelationships() ) : null ); - if (!topic.ContentType!.Equals("Container", StringComparison.OrdinalIgnoreCase)) { + if ( + !SkippedContentTypes.Any(c => topic.ContentType?.Equals(c, StringComparison.OrdinalIgnoreCase)?? false) && + String.IsNullOrWhiteSpace(topic.Attributes.GetValue("Url")) + ) { topics.Add(topicElement); } From 4067e1f456a3d7175c1a3e2d27c86a27cdb5409d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 23 Dec 2020 20:45:08 -0800 Subject: [PATCH 129/778] Unhide unnecessary attributes from `SitemapController` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `IsActive` is not used, and should not ever be present—at least as a global attribute. `Url` is used, but a new feature skips any topics that have it defined (a90fe9f), and so we'd never expect it to be present. Technically, `IsDisabled` and `NoIndex` shouldn't show up, as they're also excluded from the `SitemapController`, but they might show up as a `0` if they're explicitly disabled, and so they're being maintained. I'm not sure this is necessary, but it doesn't hurt anything, and helps reduce the amount of clutter. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index d3d30505..4d1355b8 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -59,13 +59,11 @@ public class SitemapController : Controller { /// private static string[] ExcludeAttributes { get; } = { "Body", - "IsActive", "IsDisabled", "ParentID", "TopicID", "IsHidden", "NoIndex", - "URL", "SortOrder" }; From 6e26ac9420fe958c6c842f143ad2408cea29382e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 24 Dec 2020 11:54:02 -0800 Subject: [PATCH 130/778] Established a new `TopicMappingException` class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mapping services—such as `ITopicMappingService`, `IReverseTopicMappingService`, &c.—should generally return exceptions (which derive) from a specific type. This will allow callers to catch mapping exceptions more easily, while potentially allowing more specialized handling of particular exceptions if we identify particular scenarios. For now, we're just establishing a single base class—`TopicMappingException`—which can be used within the concrete implementations of each interface. --- OnTopic/Mapping/TopicMappingException.cs | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 OnTopic/Mapping/TopicMappingException.cs diff --git a/OnTopic/Mapping/TopicMappingException.cs b/OnTopic/Mapping/TopicMappingException.cs new file mode 100644 index 00000000..ac3343aa --- /dev/null +++ b/OnTopic/Mapping/TopicMappingException.cs @@ -0,0 +1,59 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.Serialization; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Mapping { + + /*============================================================================================================================ + | CLASS: TOPIC MAPPING EXCEPTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The provides a general exception that can be thrown for any errors that arise from + /// concrete implementations of as well as other mapping service interfaces. + /// + /// + /// Having one (base) class used for all expected exceptions from the and other mapping + /// service interfaces allows implementors to capture all exceptions—while, potentially, catching more specific exceptions + /// based on derived classes, if we discover the need for more specific exceptions. + /// + [Serializable] + public class TopicMappingException : Exception { + + /*========================================================================================================================== + | CONSTRUCTOR: TOPIC MAPPING EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance. + /// + public TopicMappingException() : base() { } + + /// + /// Initializes a new instance with a specific error message. + /// + /// The message to display for this exception. + public TopicMappingException(string message) : base(message) { } + + /// + /// Initializes a new instance with a specific error message and nested exception. + /// + /// The message to display for this exception. + /// The reference to the original, underlying exception. + public TopicMappingException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Instantiates a new instance for serialization. + /// + /// A instance with details about the serialization requirements. + /// A instance with details about the request context. + /// A new instance. + protected TopicMappingException(SerializationInfo info, StreamingContext context) : base(info, context) { + Contract.Requires(info); + } + + } //Class +} //Namespace \ No newline at end of file From f8c3b6ae4b37fa7a08d0d8e24abd80fa01564084 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 24 Dec 2020 11:54:41 -0800 Subject: [PATCH 131/778] Replaced existing mapping exceptions with `TopicMappingException` --- .../HierarchicalTopicMappingService{T}.cs | 6 ++---- .../Mapping/Reverse/BindingModelValidator.cs | 19 +++++++++---------- .../Reverse/ReverseTopicMappingService.cs | 12 ++++++------ OnTopic/Mapping/TopicMappingService.cs | 2 +- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index 9ff62e35..c7d2b6c3 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -86,8 +86,7 @@ ITopicMappingService topicMappingService \-----------------------------------------------------------------------------------------------------------------------*/ if (navigationRootTopic is null) { if (String.IsNullOrEmpty(defaultRoot)) { - throw new ArgumentOutOfRangeException( - nameof(defaultRoot), + throw new TopicMappingException( $"The current route could not be resolved to a topic and the {nameof(defaultRoot)} was not set." ); } @@ -98,8 +97,7 @@ ITopicMappingService topicMappingService | Handle error state \-----------------------------------------------------------------------------------------------------------------------*/ if (navigationRootTopic is null) { - throw new ArgumentOutOfRangeException( - nameof(defaultRoot), + throw new TopicMappingException( $"Neither the current route nor the {nameof(defaultRoot)} parameter of {defaultRoot} could be resolved to a topic." ); } diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 063b4d80..5588fd31 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -14,7 +14,6 @@ using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; using OnTopic.Mapping.Annotations; -using OnTopic.Mapping.Reverse; using OnTopic.Metadata; using OnTopic.Models; using OnTopic.Repositories; @@ -199,7 +198,7 @@ static internal void ValidateProperty( | Handle children \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.RelationshipType is RelationshipType.Children) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The {nameof(ReverseTopicMappingService)} does not support mapping child topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + $"it from being evaluated by the {nameof(ReverseTopicMappingService)}. If children must be mapped, then the " + @@ -212,7 +211,7 @@ static internal void ValidateProperty( | Handle parent \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.AttributeKey is "Parent") { - throw new InvalidOperationException( + throw new TopicMappingException( $"The {nameof(ReverseTopicMappingService)} does not support mapping Parent topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + $"it from being evaluated by the {nameof(ReverseTopicMappingService)}." @@ -223,7 +222,7 @@ static internal void ValidateProperty( | Validate attribute type \-----------------------------------------------------------------------------------------------------------------------*/ if (attributeDescriptor is null) { - throw new InvalidOperationException( + throw new TopicMappingException( $"A '{nameof(sourceType)}' object was provided with a content type set to '{contentTypeDescriptor.Key}'. This " + $"content type does not contain an attribute named '{compositeAttributeKey}', as requested by the " + $"'{configuration.Property.Name}' property. If this property is not intended to be mapped by the " + @@ -246,7 +245,7 @@ attributeDescriptor.ModelType is ModelType.NestedTopic && !typeof(ITopicBindingModel).IsAssignableFrom(listType) && listType is not null ) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.RelationshipType}, but the generic type '{listType.Name}' does not implement the " + $"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " + @@ -263,7 +262,7 @@ listType is not null attributeDescriptor.ModelType is ModelType.Reference && !typeof(IRelatedTopicBindingModel).IsAssignableFrom(propertyType) ) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " + $"{nameof(IRelatedTopicBindingModel)} interface. This is required for references. If this property is not intended " + @@ -280,7 +279,7 @@ attributeDescriptor.ModelType is ModelType.Reference && attributeDescriptor.ModelType is ModelType.Reference && !configuration.AttributeKey.EndsWith("Id", StringComparison.InvariantCulture) ) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a topic reference, but " + $"the generic type '{compositeAttributeKey}' does not end in Id. By convention, all topic reference are " + $"expected to end in Id. To keep the property name set to '{propertyType.Name}', use the " + @@ -334,7 +333,7 @@ [AllowNull]Type listType | Validate list \-----------------------------------------------------------------------------------------------------------------------*/ if (!typeof(IList).IsAssignableFrom(property.PropertyType)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but does not implement {nameof(IList)}. Relationships must implement " + $"{nameof(IList)} or derive from a collection that does." @@ -345,7 +344,7 @@ [AllowNull]Type listType | Validate relationship type \-----------------------------------------------------------------------------------------------------------------------*/ if (!new[] { RelationshipType.Any, RelationshipType.Relationship }.Contains(configuration.RelationshipType)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but is configured as a {configuration.RelationshipType}. The property should be " + $"flagged as either {nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}." @@ -356,7 +355,7 @@ [AllowNull]Type listType | Validate the correct base class for relationships \-----------------------------------------------------------------------------------------------------------------------*/ if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.RelationshipType}, but the generic type '{listType?.Name}' does not implement the " + $"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index d39a49e8..a7f38662 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -145,7 +145,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the content type is valid if (!_contentTypeDescriptors.Contains(source.ContentType)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}'. There " + $"are no matching content types in the ITopicRepository provided. This suggests that the binding model is invalid. " + $"If this is expected—e.g., if the content type is being added as part of this operation—then it needs to be added " + @@ -155,7 +155,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the content types match if (source.ContentType != target.ContentType) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}', while " + $"the {nameof(target)} object (with the key '{source.Key}') has a content type of '{target.ContentType}'. It is not" + $"permitted to change the topic's content type during a mapping operation, as this interferes with the validation. " + @@ -165,7 +165,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the keys match if (source.Key != target.Key && !String.IsNullOrEmpty(source.Key)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The {nameof(source)} object has a key of '{source.Key}', while the {nameof(target)} object has a key of " + $"'{target.Key}'. It is not permitted to change the topic'key during a mapping operation, as this suggests in " + $"invalid target. If this is by design, change the key on the target topic prior to invoking MapAsync()." @@ -293,7 +293,7 @@ await MapAsync( var attributeType = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey); if (attributeType is null) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The attribute '{configuration.AttributeKey}' mapped by the {source.GetType()} could not be found on the " + $"'{contentTypeDescriptor.Key}' content type."); } @@ -436,7 +436,7 @@ PropertyConfiguration configuration foreach (IRelatedTopicBindingModel relationship in sourceList) { var targetTopic = _topicRepository.Load(relationship.UniqueKey); if (targetTopic is null) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The relationship '{relationship.UniqueKey}' mapped in the '{configuration.Property.Name}' property could not " + $"be located in the repository." ); @@ -542,7 +542,7 @@ PropertyConfiguration configuration | Provide error handling \-----------------------------------------------------------------------------------------------------------------------*/ if (topicReference is null) { - throw new InvalidOperationException( + throw new TopicMappingException( $"The topic '{modelReference.UniqueKey}' referenced by the '{source.GetType()}' model's " + $"'{configuration.Property.Name}' property could not be found." ); diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index ad1048ef..3fcbcc30 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -123,7 +123,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); if (viewModelType is null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { - throw new InvalidOperationException( + throw new TopicMappingException( $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + $"to map the topic '{topic.GetUniqueKey()}'." ); From 3859d5e988c09822644af634fa48f67001f1cce2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 14:57:04 -0800 Subject: [PATCH 132/778] Implemented `TypeLoadException` if target DTO class is invalid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the `ITypeLookupClass` can't find a type associated with a particular content type, then it used to throw an `InvalidOperationException`, followed recently by a `TopicMappingException`. The problem with these is that they're ambiguous, and could represent a number of _other_ issues. Since this exception represents a potentially serious design-time error, I'm replacing it with the more specific `TypeLoadException`. As part of this, I'm also updating the `catch` statement in `SetTopicReferenceAsync()`, which is specifically looking for these errors with a previously imprecise catch statement. In the future, we _may_ want to replace this error with a specific type—e.g., `MappingTypeException`—that is a subclass of the new `TopicMappingException`. That would be more consistent with the original concept of that class. We'll reevaluate that as part of our broader reassessment of exceptions. For now, however, this at least provides a specific exception that can be caught regarding this exact error. --- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 3fcbcc30..0cae879f 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -123,7 +123,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); if (viewModelType is null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { - throw new TopicMappingException( + throw new TypeLoadException( $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + $"to map the topic '{topic.GetUniqueKey()}'." ); @@ -744,7 +744,7 @@ MappedTopicCache cache try { topicDto = await MapAsync(source, configuration.CrawlRelationships, cache).ConfigureAwait(false); } - catch (InvalidOperationException) { + catch (TypeLoadException) { //Disregard errors caused by unmapped view models; those are functionally equivalent to IsAssignableFrom() mismatches } if (topicDto is not null && configuration.Property.PropertyType.IsAssignableFrom(topicDto.GetType())) { From ce2ecd41b50662720be9df6eeeb497a1212b5562 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 14:58:11 -0800 Subject: [PATCH 133/778] Corrected exception types expected in unit tests This aligns the unit tests with the changes introduced in f8c3b6a, where `TopicMappingException` was thrown for validation errors in `ReverseTopicMappingService`. --- OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 26a73f71..a3960e96 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -11,6 +11,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; using OnTopic.Data.Caching; +using OnTopic.Mapping; using OnTopic.Mapping.Annotations; using OnTopic.Mapping.Reverse; using OnTopic.Metadata; @@ -370,7 +371,7 @@ public async Task Map_ExceedsMinimumValue_ThrowsValidationException() { /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -388,7 +389,7 @@ public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException() /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -409,7 +410,7 @@ public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() { /// . /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -427,7 +428,7 @@ public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() { /// is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -447,7 +448,7 @@ public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationExceptio /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -466,7 +467,7 @@ public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException() /// cref="IList"/>. This is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -484,7 +485,7 @@ public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationExceptio /// . /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidTopicReferenceName_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -503,7 +504,7 @@ public async Task Map_InvalidTopicReferenceName_ThrowsInvalidOperationException( /// cref="IRelatedTopicBindingModel"/>. This is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] + [ExpectedException(typeof(TopicMappingException))] public async Task Map_InvalidTopicReferenceType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); From bc9b7bc2c7b38e07d7f6a9561663c69c4ec04c02 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 14:58:40 -0800 Subject: [PATCH 134/778] Improved error messages Identified and corrected a ouple of minor errors in the exception messages thrown in the `ReverseTopicMappingService`. --- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index a7f38662..aafae580 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -157,7 +157,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { if (source.ContentType != target.ContentType) { throw new TopicMappingException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}', while " + - $"the {nameof(target)} object (with the key '{source.Key}') has a content type of '{target.ContentType}'. It is not" + + $"the {nameof(target)} object (with the key '{target.Key}') has a content type of '{target.ContentType}'. It is not" + $"permitted to change the topic's content type during a mapping operation, as this interferes with the validation. " + $"If this is by design, change the content type on the target topic prior to invoking MapAsync()." ); @@ -167,7 +167,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { if (source.Key != target.Key && !String.IsNullOrEmpty(source.Key)) { throw new TopicMappingException( $"The {nameof(source)} object has a key of '{source.Key}', while the {nameof(target)} object has a key of " + - $"'{target.Key}'. It is not permitted to change the topic'key during a mapping operation, as this suggests in " + + $"'{target.Key}'. It is not permitted to change the topic's key during a mapping operation, as this suggests an " + $"invalid target. If this is by design, change the key on the target topic prior to invoking MapAsync()." ); } From 911ace89eb38fbdeceffb44c53d64ab97d5588e8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 15:05:54 -0800 Subject: [PATCH 135/778] Implemented argument exceptions for argument errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a previous commit, I replaced most exceptions in the mapping services with the new `MappingServiceException`, with the idea that implementors could easily catch any expected exception with one specific class. That rationale generally makes sense, but there is some room for nuance. For example, the exception thrown by the `HierarchicalTopicMappingService`'s `GetHierarchicalRoot()` method is thrown in direct response to invalid data being passed to that method. In that case, an `ArgumentNullException` (if the parameter was explicitly nulled out) or an `ArgumentOutOfRangeException` (if it refers to an invalid topic path) make more sense. We _might_ consider replacing the latter with a subclass of `MappingServiceException` in the future to take advantage of the original rationale, but it should definitely be a class that sends a clear signal to the caller that the parameter value that they either passed, or the default value, is invalid. It's worth noting that this scenario should be rare. It only calls `defaultRoot` as a fallback for scenarios where a page doesn't have a topic associated with it—such as a 404 page—and even then, the default of `Root:Web` is expected to be valid for _most_ implementations. --- .../Hierarchical/HierarchicalTopicMappingService{T}.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index c7d2b6c3..c8868432 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -86,7 +86,7 @@ ITopicMappingService topicMappingService \-----------------------------------------------------------------------------------------------------------------------*/ if (navigationRootTopic is null) { if (String.IsNullOrEmpty(defaultRoot)) { - throw new TopicMappingException( + throw new ArgumentNullException( $"The current route could not be resolved to a topic and the {nameof(defaultRoot)} was not set." ); } @@ -97,7 +97,7 @@ ITopicMappingService topicMappingService | Handle error state \-----------------------------------------------------------------------------------------------------------------------*/ if (navigationRootTopic is null) { - throw new TopicMappingException( + throw new ArgumentOutOfRangeException( $"Neither the current route nor the {nameof(defaultRoot)} parameter of {defaultRoot} could be resolved to a topic." ); } From 5472f195595e0092d4b8dd44f0dcfb0867d2172c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 15:28:59 -0800 Subject: [PATCH 136/778] Established unit test to evaluate mapping records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historically, all DTOs used by the `ITopicMappingService` have been `class`es. This unit test establishes a DTO based on the new C# 9.0 `record` type. Initially, this fails. The goal of this branch is to resolve the issues with the underlying services—and, notably, the `TypeMemberInfoCollection`—that currently prevent records from being mapped. --- .../TestDoubles/FakeViewModelLookupService.cs | 1 + OnTopic.Tests/TopicMappingServiceTest.cs | 18 +++++++++++ .../ViewModels/RecordTopicViewModel.cs | 30 +++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 OnTopic.Tests/ViewModels/RecordTopicViewModel.cs diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index b0a51ea4..665bd718 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -48,6 +48,7 @@ public FakeViewModelLookupService() : base(null, typeof(object)) { Add(typeof(MinimumLengthPropertyTopicViewModel)); Add(typeof(NestedTopicViewModel)); Add(typeof(PropertyAliasTopicViewModel)); + Add(typeof(RecordTopicViewModel)); Add(typeof(RelatedEntityTopicViewModel)); Add(typeof(RelationTopicViewModel)); Add(typeof(RelationWithChildrenTopicViewModel)); diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 54aaf521..f684b94a 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -94,6 +94,24 @@ public async Task Map_Generic_ReturnsNewModel() { } + /*========================================================================================================================== + | TEST: MAP: GENERIC: RETURNS NEW RECORD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and confirms that a basic record type can be mapped by + /// explicitly setting defining the target type. + /// + [TestMethod] + public async Task Map_Generic_ReturnsNewRecord() { + + var topic = TopicFactory.Create("Test", "Page"); + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.AreEqual(topic.Key, target.Key); + + } + /*========================================================================================================================== | TEST: MAP: DYNAMIC: RETURNS NEW MODEL \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs b/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs new file mode 100644 index 00000000..eae8b3b8 --- /dev/null +++ b/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs @@ -0,0 +1,30 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: RECORD + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a simple view model with a single property (), implemented as a record instead of a + /// class. + /// + /// + /// + /// Intended to validate that a record can be mapped using the . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + /// + public record RecordTopicViewModel { + + public string? Key { get; set; } + + } //Class +} //Namespace \ No newline at end of file From 694688f3d7e9c80010fab1d35aaf1a15041281f4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 15:35:53 -0800 Subject: [PATCH 137/778] Handle duplicate members in `MemberInfoCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historically, the `MemberInfoCollection` couldn't support duplicate members. This would occur if there was an overload or a override of a member. This typically isn't usually a major issue with mapping view models, since they rarely have overloads or overrides. But it's an issue with C# 9.0's new `record` type, since the compiler dynamically generates an override of the `Equals()` operator for records. A quick fix for this is to operate on a first-come-first-serve basis. If there's a duplicate key, that member will be ignored. As far as the `MemberInfoCollection` is concerned—and, thus, the `TopicMappingService` which depends upon it!—only the first of the overloads or the most derived of the overrides will count. There might be some issues with overloads where this assumption doesn't make sense—but to address those, we'd need a much more involved workaround, such as an attribute for flagging the priority overrload. That's not an immediate need. In the meanwhile, this satisfies most scenarios, avoids an exception when there's an overload or override, and allows us to map C# 9.0 records. --- OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs index a532f9aa..9e890eaa 100644 --- a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs +++ b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs @@ -40,7 +40,9 @@ in type.GetMembers( BindingFlags.Public ).Where(m => typeof(T).IsAssignableFrom(m.GetType())) ) { - Add((T)member); + if (!Contains(member.Name)) { + Add((T)member); + } } } From 7f3110216115bb5b77715aa5a1fd6b490f41ffc5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 29 Dec 2020 15:40:28 -0800 Subject: [PATCH 138/778] Updated `RecordTopicViewModel` to use `init` setter This better represents one of the unique challenges of a `record` type, since records are readonly. Technically, this means that properties should only be set during initialization. A special exception exists for reflection, however, thus allowing the `TopicMappingService` to set these properties after initialization. --- OnTopic.Tests/ViewModels/RecordTopicViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs b/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs index eae8b3b8..50285f5e 100644 --- a/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs @@ -24,7 +24,7 @@ namespace OnTopic.Tests.ViewModels { /// public record RecordTopicViewModel { - public string? Key { get; set; } + public string? Key { get; init; } } //Class } //Namespace \ No newline at end of file From fade4d0a80f2d2055e626c413a98eb44aec93400 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 12:02:02 -0800 Subject: [PATCH 139/778] Introduced the new `TopicReferences` table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicReferences` table is functionally identical to the existing `Relationships` table, except that it enforces a 1:1 relationship, instead of an 1:n relationship. Notably, with relationships, a topic can have multiple topics related via a relationship key, whereas with reference, a topic can only have a single topic related via a reference key. (Note: A topic can still have multiple references—just only one reference per key. In that way, the 1:1 vs 1:n terminology gets a bit confusing. If each reference were modeled as a hard-coded column, instead of as a row with an arbitrary `ReferenceKey`, the relationship would be clearer.) --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + .../Tables/TopicReferences.sql | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Tables/TopicReferences.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 458f66d4..b5be4653 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -101,6 +101,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql new file mode 100644 index 00000000..963a1360 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql @@ -0,0 +1,27 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- TOPIC REFERENCES (TABLE) +-------------------------------------------------------------------------------------------------------------------------------- +-- Represents 1:1 relationship between topics, grouped together by namespaces ("ReferenceKey"). +-------------------------------------------------------------------------------------------------------------------------------- +CREATE +TABLE [dbo].[TopicReferences] ( + [Source_TopicID] INT NOT NULL, + [ReferenceKey] VARCHAR(128) NOT NULL, + [Target_TopicID] INT NOT NULL, + CONSTRAINT [PK_TopicReferences] PRIMARY KEY + CLUSTERED ( [Source_TopicID] ASC, + [ReferenceKey] ASC + ), + CONSTRAINT [FK_TopicReferences_Source] + FOREIGN KEY ( [Source_TopicID] + ) + REFERENCES [dbo].[Topics] ( + [TopicID] + ), + CONSTRAINT [FK_TopicReferences_Target] + FOREIGN KEY ( [Target_TopicID] + ) + REFERENCES [dbo].[Topics] ( + [TopicID] + ) +); \ No newline at end of file From e0aacc508422f8b416dc7095460d259a8e45082f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:15:25 -0800 Subject: [PATCH 140/778] Capitalized SQL keywords These were missed as part of the previous sweep (7c738b1). --- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 6 +++--- .../Stored Procedures/UpdateRelationships.sql | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 8064f7bb..a3c7cbf3 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -6,9 +6,9 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[GetTopics] - @TopicID int = -1, - @DeepLoad bit = 1, - @UniqueKey nvarchar(255) = null + @TopicID INT = -1, + @DeepLoad BIT = 1, + @UniqueKey NVARCHAR(255) = NULL AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index cd53dfcb..f35ef79e 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -34,7 +34,7 @@ WHERE Target_TopicID IS NULL -------------------------------------------------------------------------------------------------------------------------------- IF @DeleteUnmatched = 1 BEGIN - DELETE EXISTING + DELETE Existing FROM @RelatedTopics Relationships RIGHT JOIN Relationships Existing ON Target_TopicID = TopicID From 919a6ab95a2f03a06b02c625c34bcd5f0a056354 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:17:37 -0800 Subject: [PATCH 141/778] Removed unnecessary prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `GetTopics` stored procedure had a number of redundant prefixes. This is a stylistic preference, and there's an argument for keeping them—it's more explicit, and it ensures variables are lined up. That said, with longer column names, it also breaks up the value alignment. Generally, in other plaes, we only use them when necessary, so I'm maintaining that here. --- .../Stored Procedures/GetTopics.sql | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index a3c7cbf3..28e797fb 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -88,10 +88,10 @@ ELSE -- SELECT KEY ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- SELECT Topics.TopicID, - Topics.ContentType, - Topics.ParentID, - Topics.TopicKey, - Storage.SortOrder + ContentType, + ParentID, + TopicKey, + SortOrder FROM Topics AS Topics JOIN #Topics AS Storage ON Storage.TopicID = Topics.TopicID @@ -101,9 +101,9 @@ ORDER BY SortOrder -- SELECT TOPIC ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- SELECT Attributes.TopicID, - Attributes.AttributeKey, - Attributes.AttributeValue, - Attributes.Version + AttributeKey, + AttributeValue, + Version FROM AttributeIndex Attributes JOIN #Topics AS Storage ON Storage.TopicID = Attributes.TopicID @@ -112,8 +112,8 @@ JOIN #Topics AS Storage -- SELECT EXTENDED ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- SELECT Attributes.TopicID, - Attributes.AttributesXml, - Attributes.Version + AttributesXml, + Version FROM ExtendedAttributeIndex AS Attributes JOIN #Topics AS Storage ON Storage.TopicID = Attributes.TopicID @@ -121,9 +121,9 @@ JOIN #Topics AS Storage -------------------------------------------------------------------------------------------------------------------------------- -- SELECT RELATIONSHIPS -------------------------------------------------------------------------------------------------------------------------------- -SELECT Relationships.Source_TopicID, - Relationships.RelationshipKey, - Relationships.Target_TopicID +SELECT Source_TopicID, + RelationshipKey, + Target_TopicID FROM Relationships Relationships JOIN #Topics AS Storage ON Storage.TopicID = Relationships.Source_TopicID @@ -131,8 +131,8 @@ JOIN #Topics AS Storage -------------------------------------------------------------------------------------------------------------------------------- -- SELECT HISTORY -------------------------------------------------------------------------------------------------------------------------------- -SELECT VersionHistory.TopicID, - VersionHistory.Version -FROM VersionHistoryIndex VersionHistory +SELECT History.TopicID, + Version +FROM VersionHistoryIndex History JOIN #Topics AS Storage - ON Storage.TopicID = VersionHistory.TopicID; \ No newline at end of file + ON Storage.TopicID = History.TopicID; \ No newline at end of file From 0db107dc314bb6817b769b3bd30d6cd244e68b2d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:20:34 -0800 Subject: [PATCH 142/778] Set default for `@DeleteUnmatched` to `0` (false) Destructive options should be disabled by default, and opted into by callers if appropriate. As part of this, ensured that the call to `UpdateRelationships` explicitly sets the `@DeleteUnmatched` parameter when calling `UnpdateRelationships`. --- .../Stored Procedures/UpdateRelationships.sql | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index f35ef79e..4ad26ca4 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -8,7 +8,7 @@ CREATE PROCEDURE [dbo].[UpdateRelationships] @TopicID INT = -1, @RelationshipKey VARCHAR(255) = 'related', @RelatedTopics TopicList READONLY, - @DeleteUnmatched BIT = 1 + @DeleteUnmatched BIT = 0 AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index f819568d..42f68d30 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -628,6 +628,7 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { command.AddParameter("TopicID", topicId); command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); + command.AddParameter("DeleteUnmatched", true); command.ExecuteNonQuery(); From 5716723d5caea4b4516f24b7271784e7b9ad89b9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:31:21 -0800 Subject: [PATCH 143/778] Establish a `TopicReferences` user-defined table types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By establishing a custom `TopicReferences` user-defined table types—and corresponding `TopicReferencesDataTable` in C#—we'll be able to pass key/value pairs for topic references to stored procedures, similar to how we pass `AttributeValues` and `TopicList`s today. This is more elegant and less error-prone than e.g. passing a delimited string and attempting to parse it in SQL. --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + .../Types/TopicReferences.sql | 13 ++++ .../Models/TopicReferencesDataTable.cs | 75 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Types/TopicReferences.sql create mode 100644 OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index b5be4653..50adfb6d 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -105,6 +105,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Types/TopicReferences.sql b/OnTopic.Data.Sql.Database/Types/TopicReferences.sql new file mode 100644 index 00000000..4d19e0ce --- /dev/null +++ b/OnTopic.Data.Sql.Database/Types/TopicReferences.sql @@ -0,0 +1,13 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- TOPIC REFERENCES TYPE +-------------------------------------------------------------------------------------------------------------------------------- +-- Represents a list of reference keys associated with TopicIDs. Useful for relaying a list of topics instead of needing to e.g. +-- pass and parse a delimited string. +-------------------------------------------------------------------------------------------------------------------------------- +CREATE +TYPE [dbo].[TopicReferences] +AS TABLE ( + ReferenceKey VARCHAR(128) NOT NULL, + TopicID INT NOT NULL + PRIMARY KEY ( ReferenceKey ) +) \ No newline at end of file diff --git a/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs b/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs new file mode 100644 index 00000000..d2632a41 --- /dev/null +++ b/OnTopic.Data.Sql/Models/TopicReferencesDataTable.cs @@ -0,0 +1,75 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Data; + +namespace OnTopic.Data.Sql.Models { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCES (DATA TABLE) + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Extends to model the schema for the TopicReferences user-defined table type. + /// + internal class TopicReferencesDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new with the appropriate schema for the TopicReferences user-defined + /// table type. + /// + internal TopicReferencesDataTable() { + + /*------------------------------------------------------------------------------------------------------------------------ + | COLUMN: Reference Key + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add( + new DataColumn("ReferenceKey") { + MaxLength = 128 + } + ); + + /*------------------------------------------------------------------------------------------------------------------------ + | COLUMN: Topic ID + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add( + new DataColumn("TopicID", typeof(int)) + ); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a convenience method for adding a new based on the expected column values. + /// + /// The key to associated the referenced with. + /// The of the related . + internal DataRow AddRow(string referenceKey, int topicId) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Define record + \-----------------------------------------------------------------------------------------------------------------------*/ + var record = NewRow(); + record["ReferenceKey"] = referenceKey; + record["TopicID"] = topicId; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add record + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(record); + + /*------------------------------------------------------------------------------------------------------------------------ + | Return record + \-----------------------------------------------------------------------------------------------------------------------*/ + return record; + + } + + } //Class +} //Namespaces \ No newline at end of file From f5ef96b5198bf060f1378669f281fe5ec66ce158 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:37:00 -0800 Subject: [PATCH 144/778] Removed defaults for required parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `UpdateTopic` and `UpdateRelationships` stored procedures both had a default value for `@TopicID` of `-1`, with the latter also having a default value for `@RelationshipKey` of `related`. These should be considered required fields. There is never expected to be a (legitimate) `TopicID` of `-1` in the database; attempting to execute this would yield an exception. And there's no reasonable need for a default relationship—that should be defined by the content type descriptor. In practice, we always pass these values; by not including defaults, we better communicate that these are strictly required. --- .../Stored Procedures/UpdateRelationships.sql | 4 ++-- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index 4ad26ca4..ee86bee6 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -5,8 +5,8 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[UpdateRelationships] - @TopicID INT = -1, - @RelationshipKey VARCHAR(255) = 'related', + @TopicID INT, + @RelationshipKey VARCHAR(255), @RelatedTopics TopicList READONLY, @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index bc0ce6cb..9de0b50e 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -5,7 +5,7 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[UpdateTopic] - @TopicID INT = -1 , + @TopicID INT , @Key VARCHAR(128) = NULL , @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , From 79016bd2e6434f9cf26f519c2358da412c7b3fe7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 16:41:06 -0800 Subject: [PATCH 145/778] Pass existing `unresolvedTopics` cache when resolving topics When picking up unresolved relationships in the `SqlTopicRepository.Save()` method, `isRecursive` is always set to `false` and, therefore, the `unresolvedTopics` cache doesn't come into play, as there's no subsequent opportunity for any unresolved relationships to be recovered (since they definitely exist outside the scope of the current operation). As the private `Save()` overload requires a unresolved relationships cache, however, we were previously constructing a new one for each call. This is a cheap operation. But as we already have one, and it doesn't matter what it contains, we might as well reuse it. This is a minor optimization. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 42f68d30..2682a0d8 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -266,7 +266,7 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false) { | Attempt to resolve outstanding relationships \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var unresolvedTopic in unresolvedTopics) { - Save(unresolvedTopic, false, connection, new(), version); + Save(unresolvedTopic, false, connection, unresolvedTopics, version); } /*------------------------------------------------------------------------------------------------------------------------ From b736df1f1e9e3ebd5840722250ad5a3d8fadb6d0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 17:51:42 -0800 Subject: [PATCH 146/778] Fixed bug in `UpdateRelationships` handling of multiple relationships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `UpdateRelationships` stored procedure contains logic to prevent inserting duplicate values—and to optionally delete unmatched values. Unfortunately, that logic failed to account for the `RelationshipKey`. This isn't generally a problem, as _most_ topics only have _one_ relationship. But it is very much a potential problem if a relationship contains _multiple_ relationships. To prevent this scenario,the `@RelationshipKey` is now factored into these queries. --- .../Stored Procedures/UpdateRelationships.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index ee86bee6..09257028 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -27,6 +27,7 @@ FROM @RelatedTopics Target LEFT JOIN Relationships Existing ON Target_TopicID = TopicID AND Source_TopicID = @TopicID + AND RelationshipKey = @RelationshipKey WHERE Target_TopicID IS NULL -------------------------------------------------------------------------------------------------------------------------------- @@ -40,6 +41,7 @@ IF @DeleteUnmatched = 1 ON Target_TopicID = TopicID WHERE Source_TopicID = @TopicID AND ISNULL(TopicID, '') = '' + AND RelationshipKey = @RelationshipKey END -------------------------------------------------------------------------------------------------------------------------------- From 1a62c23f2bacc8e8c756cb1f5abe67bfba50b870 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 31 Dec 2020 18:26:59 -0800 Subject: [PATCH 147/778] Introduced new `UpdateReferences` stored procedure Technically, since this is a 1:n relationship, and not an n:n relationship, this logic could be built into e.g. the `UpdateTopic` stored procedure. To maintain consistency with `UpdateRelationships`, however, and to allow it to be tested independently, I'm setting it up as its own stored procedure. That said, given that there will only be one of these per update, we'll be adding a `@References` parameter to the `CreateTopic` and `UpdateTopic` stored procedures later, so they don't need to call this separately. --- .../Stored Procedures/UpdateReferences.sql | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql new file mode 100644 index 00000000..bf0c01a1 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -0,0 +1,62 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +-- Saves the 1:1 mappings for referenced topics. +-------------------------------------------------------------------------------------------------------------------------------- + +CREATE PROCEDURE [dbo].[UpdateReferences] + @TopicID INT, + @ReferencedTopics TopicReferences READONLY, + @DeleteUnmatched BIT = 0 +AS + +-------------------------------------------------------------------------------------------------------------------------------- +-- INSERT NOVEL VALUES +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID + ) +SELECT @TopicID, + Target.ReferenceKey, + Target.TopicID +FROM @ReferencedTopics Target +LEFT JOIN TopicReferences Existing + ON Source_TopicID = @TopicID + AND Existing.ReferenceKey = Target.ReferenceKey +WHERE ISNULL(Source_TopicID, '') = '' + AND Target.TopicID > 0 + +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE EXISTING VALUES +-------------------------------------------------------------------------------------------------------------------------------- +UPDATE Existing +SET Target_TopicID = TopicID +FROM @ReferencedTopics Target +LEFT JOIN TopicReferences Existing + ON Source_TopicID = @TopicID + AND Existing.ReferenceKey = Target.ReferenceKey +WHERE Source_TopicID IS NOT NULL + AND Target.TopicID != Target_TopicID + AND Target.TopicID > 0 + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE UNMATCHED VALUES +-------------------------------------------------------------------------------------------------------------------------------- +IF @DeleteUnmatched = 1 + BEGIN + DELETE Existing + FROM @ReferencedTopics New + RIGHT JOIN TopicReferences Existing + ON Source_TopicID = @TopicID + AND Existing.ReferenceKey = New.ReferenceKey + WHERE Source_TopicID = @TopicID + AND ISNULL(TopicID, '') = '' + END + +-------------------------------------------------------------------------------------------------------------------------------- +-- RETURN TOPIC ID +-------------------------------------------------------------------------------------------------------------------------------- +RETURN @TopicID; \ No newline at end of file From 8a7d5d1a1c8b99c5627b4b872d735c91824803c6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 12:51:48 -0800 Subject: [PATCH 148/778] Incorporated `UpdateReferences` into `CreateTopic` and `UpdateTopic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Incorporated the new `UpdateReferences` stored procedure (1a62c23) into the `CreateTopic` and `UpdateTopic` stored procedures by introducing a new `@References` parameter and relaying it to the `UpdateReferences` stored procedure in the body, alongside the `@TopicID`. In addition, for the `UpdateTopic` stored procedure, I exposed a new, optional `@DeleteUnmatched` parameter, which maps to the optional `@DeleteUnmatched` parameter on the `UpdateReferences` stored procedure. This allows callers of `UpdateTopic` to delete any unmatched references, which is useful if the `@References` collection is expected to be completed, as opposed to just being a difference. Finally, the `SqlTopicRepository` has been updated to pass these parameters along in order to ensure these calls remain valid. That said, the `Save()` method is not yet injecting data into this. Further, we may update this later to be handled as a secondary call—similar to `PersistRelationships()`—just because it works better with `areReferencesResolved`. Regardless, we'll maintain the _ability_ for the stored procedures to insert references, even if we don't take advantage of it via `SqlTopicRepository.Save()`. --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + .../Stored Procedures/CreateTopic.sql | 8 ++++++++ .../Stored Procedures/UpdateTopic.sql | 11 ++++++++++- OnTopic.Data.Sql/SqlTopicRepository.cs | 7 +++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 50adfb6d..ca49050a 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -96,6 +96,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index f6795b02..ea9ebd21 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -10,6 +10,7 @@ CREATE PROCEDURE [dbo].[CreateTopic] @ParentID INT = -1, @Attributes AttributeValues READONLY, @ExtendedAttributes XML = NULL, + @References TopicReferences READONLY, @Version DATETIME = NULL AS @@ -105,6 +106,13 @@ IF @ExtendedAttributes IS NOT NULL ) END +-------------------------------------------------------------------------------------------------------------------------------- +-- ADD REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +EXEC UpdateReferences @TopicID, + @References, + 1 + -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 9de0b50e..9c42a140 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -10,7 +10,9 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , @ExtendedAttributes XML = NULL , - @Version DATETIME = NULL + @References TopicReferences READONLY , + @Version DATETIME = NULL, + @DeleteUnmatched BIT = 0 AS -------------------------------------------------------------------------------------------------------------------------------- @@ -121,6 +123,13 @@ CROSS APPLY ( WHERE ISNULL(AttributeValue, '') = '' AND ExistingValue != '' +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +EXEC UpdateReferences @TopicID, + @References, + @DeleteUnmatched + -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 2682a0d8..7b5e929f 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -394,6 +394,11 @@ SqlDateTime version attributeValues.AddRow(attribute.Key); } + /*------------------------------------------------------------------------------------------------------------------------ + | Add topic references + \-----------------------------------------------------------------------------------------------------------------------*/ + using var topicReferences = new TopicReferencesDataTable(); + /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ @@ -420,6 +425,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ if (!topic.IsNew) { command.AddParameter("TopicID", topic.Id); + command.AddParameter("DeleteUnmatched", true); } else if (topic.Parent is not null) { command.AddParameter("ParentID", topic.Parent.Id); @@ -429,6 +435,7 @@ SqlDateTime version command.AddParameter("Version", version.Value); command.AddParameter("ExtendedAttributes", extendedAttributes); command.AddParameter("Attributes", attributeValues); + command.AddParameter("References", topicReferences); command.AddOutputParameter(); /*------------------------------------------------------------------------------------------------------------------------ From 5a739e84b2843a55dd49db611ebb07ecb47ae32c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 12:52:45 -0800 Subject: [PATCH 149/778] Fix comment label in `CreateTopic` The label "Create attributes from string` predates the use of the `AttributeValues` table-valued type, and alludes to the previous method of parsing a string to extract key/value pairs. --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index ea9ebd21..d42ecb53 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -74,7 +74,7 @@ DECLARE @TopicID INT SELECT @TopicID = SCOPE_IDENTITY() -------------------------------------------------------------------------------------------------------------------------------- --- CREATE ATTRIBUTES FROM STRING +-- ADD INDEXED ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- INSERT INTO Attributes ( TopicID , From e523e09173f14fb236c13dc29f4abb5466c63e09 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:02:37 -0800 Subject: [PATCH 150/778] Established `TopicReferenceDictionary` for tracking topic references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dictionary makes the most sense here since there will be a 1:1 relationship between a `referenceKey` and a `Topic`—but the `referenceKey` can't be a property of the `Topic` since the same `Topic` may be referenced from multiple other `Topic`s. Unfortunately, none of the out-of-the-box implementations of `IDictionary` allow write operations to be intercepted in order to e.g. track `IsDirty` or handle incoming relationships, as we were able to do with `KeyedCollection`. In fact, even if we wanted to intercept these at each possible entry point, we can't becomes none of the members are marked as `virtual`—and not just on `Dictionary`, but on all other out-of-the-box implementations. To mitigate this, I've created a `TopicReferenceDictionary` façade which implements `IDictionary`, and then relays those calls to a private `Dictionary` backing field. This allows us to intercept these calls without needing to recreate the under-the-hood logic of a dictionary. This initial implementation is barebones and doesn't yet include full support for `IsDirty()` tracking, reciprocol relationships, or optimized methods for setting and retrieving topics by key. Those will come in subsequent commits. For now, this establishes the basic framework for the façade and, importantly, frames out the interception points we'll need for e.g. `Add()`, `Remove()`, and `Clear()`. --- .../Collections/TopicReferenceDictionary.cs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 OnTopic/Collections/TopicReferenceDictionary.cs diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs new file mode 100644 index 00000000..2c2b7b79 --- /dev/null +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -0,0 +1,188 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections; +using System.Collections.Generic; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCE DICTIONARY + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects associated with particular reference keys. + /// + public class TopicReferenceDictionary : IDictionary { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + readonly Topic _parent; + readonly IDictionary _storage; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the . + /// + public TopicReferenceDictionary(Topic parent) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(parent, nameof(parent)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize backing fields + \-----------------------------------------------------------------------------------------------------------------------*/ + _parent = parent; + _storage = new Dictionary(); + + } + + /*========================================================================================================================== + | COUNT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public int Count => _storage.Count; + + /*========================================================================================================================== + | IsReadOnly + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool IsReadOnly => false; + + /*========================================================================================================================== + | ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public Topic this[string referenceKey] { + get => _storage[referenceKey]; + set { + _storage[referenceKey] = value; + } + } + + /*========================================================================================================================== + | KEYS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public ICollection Keys => _storage.Keys; + + /*========================================================================================================================== + | VALUES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public ICollection Values => _storage.Values; + + /*========================================================================================================================== + | ADD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + void ICollection>.Add(KeyValuePair item) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(item, nameof(item)); + + TopicFactory.ValidateKey(item.Key); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add item + \-----------------------------------------------------------------------------------------------------------------------*/ + _storage.Add(item); + + } + + /// + public void Add(string key, Topic value) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(key, nameof(key)); + Contract.Requires(value, nameof(value)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add item + \-----------------------------------------------------------------------------------------------------------------------*/ + _storage.Add(new(key, value)); + + } + + /*========================================================================================================================== + | CLEAR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void Clear() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Call base method + \-----------------------------------------------------------------------------------------------------------------------*/ + _storage.Clear(); + + } + + /*========================================================================================================================== + | CONTAINS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool Contains(KeyValuePair item) => _storage.Contains(item); + + /*========================================================================================================================== + | CONTAINS KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool ContainsKey(string key) => _storage.ContainsKey(key); + + /*========================================================================================================================== + | COPY TO + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _storage.CopyTo(array, arrayIndex); + + /*========================================================================================================================== + | GET ENUMERATOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + IEnumerator IEnumerable.GetEnumerator() => _storage.GetEnumerator(); + + /// + public IEnumerator> GetEnumerator() => _storage.GetEnumerator(); + + /*========================================================================================================================== + | REMOVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + bool ICollection>.Remove(KeyValuePair item) => + Contains(item) && Remove(item.Key); + + /// + public bool Remove(string key) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(key, nameof(key)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Call base method + \-----------------------------------------------------------------------------------------------------------------------*/ + return _storage.Remove(key); + + } + + /*========================================================================================================================== + | TRY/GET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value); + + } //Class +} //Namespace \ No newline at end of file From ba2e9a4d75e325e59b3285f21d024d99000ccd57 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:04:38 -0800 Subject: [PATCH 151/778] Prevent a topic from referencing itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should generally be a valid assumption, and prevents obvious scenarios that will lead to circular loops. With existing topic references—such as `DerivedTopic`—this is the main piece of business logic we need, and thus staves off the need to handle business logic via reflection, as we need to do with the `AttributeValueCollection`. (I imagine we'll eventually want to handle business logic in a similar way, but it's a fairly complex addition, and so not something I want to add until it's necessary.) --- OnTopic/Collections/TopicReferenceDictionary.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 2c2b7b79..07e030d0 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -64,6 +64,10 @@ public TopicReferenceDictionary(Topic parent) { public Topic this[string referenceKey] { get => _storage[referenceKey]; set { + Contract.Requires( + value != _parent, + "A topic reference may not point to itself." + ); _storage[referenceKey] = value; } } @@ -93,6 +97,11 @@ void ICollection>.Add(KeyValuePair it TopicFactory.ValidateKey(item.Key); + Contract.Requires( + item.Value != _parent, + "A topic reference may not point to itself." + ); + /*------------------------------------------------------------------------------------------------------------------------ | Add item \-----------------------------------------------------------------------------------------------------------------------*/ From 66c65d4fb36f73b5a44ac9b80c7b6b8996fbe8c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:09:47 -0800 Subject: [PATCH 152/778] Incorporate `IsDirty` tracking into `TopicReferenceDictionary` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is handled as a basic `IsDirty` property, similar to the `NamedTopicCollection`, which is the `TopicReferenceDictionary`'s closest analog. This takes advantage of the intercept points established as part of the `TopicReferenceDictionary` façade (ba2e9a4). --- .../Collections/TopicReferenceDictionary.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 07e030d0..6a527f31 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -68,6 +68,9 @@ public Topic this[string referenceKey] { value != _parent, "A topic reference may not point to itself." ); + if (!_storage.TryGetValue(referenceKey, out var existing) || existing != value) { + IsDirty = true; + } _storage[referenceKey] = value; } } @@ -102,6 +105,13 @@ void ICollection>.Add(KeyValuePair it "A topic reference may not point to itself." ); + /*------------------------------------------------------------------------------------------------------------------------ + | Mark dirty + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!_storage.TryGetValue(item.Key, out var existing) || existing != item.Value) { + IsDirty = true; + } + /*------------------------------------------------------------------------------------------------------------------------ | Add item \-----------------------------------------------------------------------------------------------------------------------*/ @@ -131,6 +141,13 @@ public void Add(string key, Topic value) { /// public void Clear() { + /*------------------------------------------------------------------------------------------------------------------------ + | Mark dirty + \-----------------------------------------------------------------------------------------------------------------------*/ + if (Count > 0) { + IsDirty = true; + } + /*------------------------------------------------------------------------------------------------------------------------ | Call base method \-----------------------------------------------------------------------------------------------------------------------*/ @@ -180,6 +197,13 @@ public bool Remove(string key) { \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(key, nameof(key)); + /*------------------------------------------------------------------------------------------------------------------------ + | Handle existing + \-----------------------------------------------------------------------------------------------------------------------*/ + if (TryGetValue(key, out var existing)) { + IsDirty = true; + } + /*------------------------------------------------------------------------------------------------------------------------ | Call base method \-----------------------------------------------------------------------------------------------------------------------*/ @@ -193,5 +217,14 @@ public bool Remove(string key) { /// public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value); + /*========================================================================================================================== + | IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if the dictionary has been modified. This value is set to true any time a new item is inserted or + /// removed from the dictionary. + /// + public bool IsDirty { get; set; } + } //Class } //Namespace \ No newline at end of file From 1af282eb428aa94478b0d17f13b1cb64a9281350 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:16:14 -0800 Subject: [PATCH 153/778] Incorporate `IncomingRelationships` into `TopicReferenceDictionary` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We stored relationships in both `Topic.Relationships` as well as `Topic.IncomingRelationships` in order to maintain a reciprocal relationship. We'll want to do something similar with `Topic.References` via the `TopicReferenceDictionary`. If a `Topic` is targeted by multiple references, however, that reciprocol relationship can't be modeled via a `TopicReferenceDictionary` since it models a 1:1 relationship per `referenceKey`. That said, we can treat the incoming references as a relationship and store those in the same `Topic.IncomingRelationships` property used for normal relationships, which _does_ support a 1:n relationship per key. And since `IncomingRelationships` is only maintained for convenience, and not persisted, this doesn't interfere with the backend data modeling—unless, of course, there is a relationship and a reference with the same key. Semantically, that probably doesn't make much sense usually, and is something we should avoid regardless—but if it happens, it will work, but result in an ambiguous `IncomingRelationships` collection for that key. (It's worth noting that this is already an potential design concern with `IncomingRelationships` since two `ContentTypeDescriptors` can have the same `RelationshipKey`, even if they refer to potentially distinct concepts. Designer beware!) --- OnTopic/Collections/TopicReferenceDictionary.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 6a527f31..bcbc3c06 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -112,6 +112,11 @@ void ICollection>.Add(KeyValuePair it IsDirty = true; } + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + item.Value.IncomingRelationships.SetTopic(item.Key, item.Value); + /*------------------------------------------------------------------------------------------------------------------------ | Add item \-----------------------------------------------------------------------------------------------------------------------*/ @@ -148,6 +153,13 @@ public void Clear() { IsDirty = true; } + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var item in _storage) { + item.Value.IncomingRelationships.RemoveTopic(item.Key, _parent); + } + /*------------------------------------------------------------------------------------------------------------------------ | Call base method \-----------------------------------------------------------------------------------------------------------------------*/ @@ -201,6 +213,7 @@ public bool Remove(string key) { | Handle existing \-----------------------------------------------------------------------------------------------------------------------*/ if (TryGetValue(key, out var existing)) { + existing.IncomingRelationships.RemoveTopic(key, _parent); IsDirty = true; } From 145916bef8048272654bb0677be12caf023cf18e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:18:49 -0800 Subject: [PATCH 154/778] Introduce `SetTopic()` method on `TopicReferenceDictionary` The `SetTopic()` will `Add()` a new topic, set an existing topic, or remove a `null` topic reference. It also allows `IsDirty` to be disabled as part of the write, which is important for establishing references on existing entities (e.g., as part of `ITopicRepository.Load()`). Callers can still use e.g. `Add()` or `this[]`, obviously, but I expect `SetTopic()` to be the most convenient entry point. --- .../Collections/TopicReferenceDictionary.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index bcbc3c06..5b8e7e78 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -140,6 +140,28 @@ public void Add(string key, Topic value) { } + /*========================================================================================================================== + | SET TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is + /// removed. + /// + public void SetTopic(string key, Topic? value, bool? isDirty = null) { + var wasDirty = IsDirty; + if (value is null) { + if (ContainsKey("key")) { + Remove(key); + } + } + else { + this[key] = value; + } + if (wasDirty is false && isDirty is false) { + IsDirty = false; + } + } + /*========================================================================================================================== | CLEAR \-------------------------------------------------------------------------------------------------------------------------*/ From a313a18f796d58446ceb3a0ef090c4f7f5ffed2c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:20:00 -0800 Subject: [PATCH 155/778] Introduce `GetTopic()` method on `TopicReferenceDictionary` Implementors can always call `TryGetValue()`, which remains a useful entry point, but `GetTopic()` streamlines that to return the topic if it exists, and otherwise return `null`. It's a convenience method to handle the most frequent scenario for pulling a topic reference. --- OnTopic/Collections/TopicReferenceDictionary.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 5b8e7e78..d122c5ac 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -252,6 +252,14 @@ public bool Remove(string key) { /// public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value); + /*========================================================================================================================== + | GET TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Attempts to retrieve a topic reference based on its ; if it doesn't exist, returns null. + /// + public Topic? GetTopic(string key) => TryGetValue(key, out var existing)? existing : null; + /*========================================================================================================================== | IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ From 573a80065cb390dff910b8ad194af56b0db2b773 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:21:17 -0800 Subject: [PATCH 156/778] Exposed `TopicReferenceDictionary` as a `Topic.References` property This will be the primary implementation of `TopicReferenceDictionary`, and the most common entry point for working with references. --- OnTopic/Topic.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 46d91560..c16d623e 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -654,6 +654,20 @@ public Topic? DerivedTopic { /// The current 's relationships. public RelatedTopicCollection Relationships { get; } + + /*========================================================================================================================== + | PROPERTY: REFERENCES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// A fa�ade for accessing references topics based on a reference key; can be used for derived topics, etc. + /// + /// + /// The references property exposes a with child topics representing named references (e.g., + /// "DerivedTopic" for a derived topic). + /// + /// The current 's relationships. + public TopicReferenceDictionary References { get; } + /*=========================================================================================================================== | PROPERTY: INCOMING RELATIONSHIPS \--------------------------------------------------------------------------------------------------------------------------*/ From 9524870359eb45f6e851db20d8ec6554a2aac7e2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:23:35 -0800 Subject: [PATCH 157/778] Persist topic references as part of `SqlTopicRepository.Save()` The logic for this was scaffolded in a previous commit (8a7d5d1), but the data couldn't be populated until we had established `Topic.References` to pull from. Now that that is complete (573a800), we're able to close that loop. Note: We'll likely move this to be treated as an ancillary call, similar to `PersistRelationships()`, in a later commit. For now, however, this should function. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 7b5e929f..6d5c8f17 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -399,6 +399,10 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ using var topicReferences = new TopicReferencesDataTable(); + foreach (var topicReference in topic.References) { + topicReferences.AddRow(topicReference.Key, topicReference.Value.Id); + } + /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ From fbe35910b97542ecaa55a2b360cda622c1298eac Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:25:29 -0800 Subject: [PATCH 158/778] Account for `References` in `unresolvedRelationships` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As with relationships, if any references point to unresolved topics—i.e., topics that haven't yet been saved—then we want to attempt to save them again later, once a recursive save has completed. Unlike relationships, this will result in the `References` being saved twice, since they're persisted as part of the call to the `UpdateTopic` or `CreateTopic` stored procedures. We'll likely change that in a subsequent commit. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 6d5c8f17..c5dfaf2a 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -419,7 +419,12 @@ SqlDateTime version | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The | same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship. \-----------------------------------------------------------------------------------------------------------------------*/ - if (isRecursive && (topic.DerivedTopic?.Id < 0 || topic.Relationships.Any(r => r.Any(t => t.Id < 0)))) { + if ( + isRecursive && + ( topic.Relationships.Any(r => r.Any(t => t.Id < 0)) || + topic.References.Values.Any(t => t.Id < 0) + ) + ) { unresolvedRelationships.Add(topic); areReferencesResolved = false; } From 2059fa0ec33ae9326952dd396a2cca94d1b25161 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 13:28:16 -0800 Subject: [PATCH 159/778] Pull `TopicReferences` with `GetTopics`, `GetTopicVersion` When loading topics via either the `GetTopics` or `GetTopicVersion` stored procedures, retrieve `TopicReferences` alongside `Attributes`, `ExtendedAttributes`, and `Relationships`. In addition, incorporate this into the `SqlDataReader` extension methods so that they're mapped to topics in the topic graph on `SqlTopicRepository.Load()`. --- .../Stored Procedures/GetTopicVersion.sql | 9 +++ .../Stored Procedures/GetTopics.sql | 10 ++++ OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 55 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index b02429f3..9ccb6a15 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -102,6 +102,15 @@ WHERE RowNumber = 1 FROM Relationships WHERE Source_TopicID = @TopicID +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT ReferenceKey, + Source_TopicID, + Target_TopicID +FROM TopicReferences TopicReferences +WHERE Source_TopicID = @TopicID + -------------------------------------------------------------------------------------------------------------------------------- -- SELECT HISTORY -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 28e797fb..729152b1 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -128,6 +128,16 @@ FROM Relationships Relationships JOIN #Topics AS Storage ON Storage.TopicID = Relationships.Source_TopicID +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT ReferenceKey, + Source_TopicID, + Target_TopicID +FROM TopicReferences TopicReferences +JOIN #Topics AS Storage + ON Storage.TopicID = TopicReferences.Source_TopicID + -------------------------------------------------------------------------------------------------------------------------------- -- SELECT HISTORY -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 3b9e07cf..993a1251 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -100,6 +100,19 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte } } + /*---------------------------------------------------------------------------------------------------------------------- + | Read referenced items + \---------------------------------------------------------------------------------------------------------------------*/ + Debug.WriteLine("SqlTopicRepository.Load(): SetReferences() [" + DateTime.Now + "]"); + + // Move to the version history dataset + reader.NextResult(); + + // Loop through each version; multiple records may exist per topic + while (reader.Read()) { + reader.SetReferences(topics); + } + /*---------------------------------------------------------------------------------------------------------------------- | Read version history \---------------------------------------------------------------------------------------------------------------------*/ @@ -322,6 +335,48 @@ private static void SetRelationships(this SqlDataReader reader, Dictionary + /// Adds topic references to their associated topics. + /// + /// + /// Topics can be cross-referenced with each other topics via a one-to-one relationships. Once the topics are populated in + /// memory, loop through the data to create these associations. + /// + /// The with output from the GetTopics stored procedure. + /// A of topics to be loaded. + private static void SetReferences(this SqlDataReader reader, Dictionary topics) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify attributes + \-----------------------------------------------------------------------------------------------------------------------*/ + var sourceTopicId = reader.GetTopicId("Source_TopicID"); + var relationshipKey = reader.GetString("RelationshipKey"); + var targetTopicId = reader.GetTopicId("Target_TopicID"); + + /*------------------------------------------------------------------------------------------------------------------------ + | Identify affected topics + \-----------------------------------------------------------------------------------------------------------------------*/ + var current = topics[sourceTopicId]; + var referenced = (Topic?)null; + + // Fetch the related topic + if (topics.Keys.Contains(targetTopicId)) { + referenced = topics[targetTopicId]; + } + + // Bypass if either of the objects are missing + if (referenced is null) return; + + /*------------------------------------------------------------------------------------------------------------------------ + | Set relationship on object + \-----------------------------------------------------------------------------------------------------------------------*/ + current.References.SetTopic(relationshipKey, referenced, isDirty: false); + + } + /*========================================================================================================================== | METHOD: SET VERSION HISTORY \-------------------------------------------------------------------------------------------------------------------------*/ From db355705ddb055fcf04a29ad8953d0f2d8fc54f1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:02:20 -0800 Subject: [PATCH 160/778] Extended upgrade script to migrate existing relationships This updates the script to create a barebones version of the `TopicReferences` table (without any of the constraints), identifies likely topic references in the `Attributes` table, and inserts them into the `TopicReferences` table, while stripping the `Id` suffix used by convention in the `Attributes` table. It then renames the `Topic` references (previously `TopicID`) to `DerivedTopic`, to maintain consistency with the `Topic.DerivedTopic` property. --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 025af10e..8a7def3f 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -78,4 +78,36 @@ WHERE AttributeKey IN ( 'Key', 'ContentType', 'ParentID' -) \ No newline at end of file +) + +-------------------------------------------------------------------------------------------------------------------------------- +-- MIGRATE TOPIC REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +-- In OnTopic 5, references to other topics—such as `DerivedTopic`—have been moved from the Attributes table to a new +-- TopicReferences table, where they act more like relationships. This allows referential integrity to be enforced through +-- foreign key constraints, and formalizes the relationship so we don't need to rely on hacks in e.g. the Topic Data Transer +-- service to infer which attributes represent relationships in order to translate their values from `TopicID` to `UniqueKey`. +-------------------------------------------------------------------------------------------------------------------------------- + +CREATE +TABLE [dbo].[TopicReferences] ( + [Source_TopicID] INT NOT NULL, + [ReferenceKey] VARCHAR(128) NOT NULL, + [Target_TopicID] INT NOT NULL +); + +INSERT +INTO TopicReferences +SELECT AttributeIndex.TopicID, + SUBSTRING(AttributeKey, 0, LEN(AttributeKey)-1), + AttributeValue +FROM AttributeIndex +JOIN Topics + ON Topics.TopicID = CONVERT(INT, AttributeValue) +WHERE AttributeKey LIKE '%ID' + AND ISNUMERIC(AttributeValue) = 1 + AND Topics.TopicID IS NOT NULL + +UPDATE TopicReferences +SET ReferenceKey = 'DerivedTopic' +WHERE ReferenceKey = 'Topic' \ No newline at end of file From 818f09352b4469877e7fcf9a01151cb270b20e27 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:02:38 -0800 Subject: [PATCH 161/778] Ensure `References` property is properly initialized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This should have been done alongside 573a800—whoops! --- OnTopic/Topic.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index c16d623e..42ac8c3c 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -66,6 +66,7 @@ public Topic(string key, string contentType, Topic parent, int id = -1) { Attributes = new(this); IncomingRelationships = new(this, true); Relationships = new(this, false); + References = new(this); VersionHistory = new(); /*------------------------------------------------------------------------------------------------------------------------ From 0ec079c68abb00dfc78f9e2bf8aad4b5ecd072a4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:02:46 -0800 Subject: [PATCH 162/778] Update `DerivedTopic` to pull from `References` instead of `Attributes` --- OnTopic/Topic.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 42ac8c3c..1f62c0f8 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -32,7 +32,6 @@ public class Topic { private int _id = -1; private string? _originalKey; private Topic? _parent; - private Topic? _derivedTopic; private bool _isDirty; /*========================================================================================================================== @@ -609,19 +608,13 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa /// value != this /// public Topic? DerivedTopic { - get => _derivedTopic; + get => References.GetTopic("DerivedTopic"); set { Contract.Requires( value != this, "A topic may not derive from itself." ); - _derivedTopic = value; - if (value is not null && value.Id > 0) { - SetAttributeValue("TopicID", value.Id.ToString(CultureInfo.InvariantCulture)); - } - else { - Attributes.Remove("TopicID"); - } + References.SetTopic("DerivedTopic", value); } } @@ -660,7 +653,7 @@ public Topic? DerivedTopic { | PROPERTY: REFERENCES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// A fa�ade for accessing references topics based on a reference key; can be used for derived topics, etc. + /// A façade for accessing references topics based on a reference key; can be used for derived topics, etc. /// /// /// The references property exposes a with child topics representing named references (e.g., From e01aa835a4b1d9cf75d7f5bb243fb7289e718273 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:03:49 -0800 Subject: [PATCH 163/778] Correct order of `TopicReferences` in `GetTopics` This doesn't strictly matter, but it's an easy change, maintains consistency with both the table and `Relationships`, and avoids potential bugs depending on how implementors work with the data. --- OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 729152b1..43f93be2 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -131,8 +131,8 @@ JOIN #Topics AS Storage -------------------------------------------------------------------------------------------------------------------------------- -- SELECT REFERENCES -------------------------------------------------------------------------------------------------------------------------------- -SELECT ReferenceKey, - Source_TopicID, +SELECT Source_TopicID, + ReferenceKey, Target_TopicID FROM TopicReferences TopicReferences JOIN #Topics AS Storage From 3ab0318ee296f7c1fc2ca406d85e2fe904d78cf9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:07:00 -0800 Subject: [PATCH 164/778] Fixed reference to `ReferenceKey` in `SetReferences()` In the `SetReferences()` method, the call to the `ReferenceKey` mistakenly was calling the `RelationshipKey`. (Wrong data set!) --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 993a1251..dfc78852 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -353,7 +353,7 @@ private static void SetReferences(this SqlDataReader reader, Dictionary Date: Fri, 1 Jan 2021 14:40:17 -0800 Subject: [PATCH 165/778] Factor `topic.References.IsDirty` into `isDirty` calculation Previously, the `SqlTopicRepository.Save()` method's `isDirty` calculation failed to take into account `topic.References.IsDirty`, thus potentially bypassing topic references if no other values had changed. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index c5dfaf2a..163c4f4f 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -320,6 +320,7 @@ SqlDateTime version var areReferencesResolved = true; var isTopicDirty = topic.IsDirty(); var areRelationshipsDirty = topic.Relationships.IsDirty(); + var areReferencesDirty = topic.References.IsDirty; var areAttributesDirty = topic.Attributes.IsDirty(true); var extendedAttributeList = GetAttributes(topic, isExtendedAttribute: true); var indexedAttributeList = GetAttributes( @@ -340,6 +341,7 @@ SqlDateTime version var isDirty = isTopicDirty || areRelationshipsDirty || + areReferencesDirty || areAttributesDirty || indexedAttributeList.Any() || extendedAttributeList.Any(a => a.IsExtendedAttribute == false); From 06c07fc3ac664ecd46c5f6cb3b0f1fd77efe3b08 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 14:44:38 -0800 Subject: [PATCH 166/778] Established `PersistReferences()` helper method While the `CreateTopic` and `UpdateTopic` stored procedures accept a `@TopicReferences` parameter, it makes more sense to save topic references as a separate query so that we don't need to do a second save in the case that there are unresolved references. This logic works very similar to the existing `PersisRelations()` method. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 68 ++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 163c4f4f..a73ed2b6 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -396,15 +396,6 @@ SqlDateTime version attributeValues.AddRow(attribute.Key); } - /*------------------------------------------------------------------------------------------------------------------------ - | Add topic references - \-----------------------------------------------------------------------------------------------------------------------*/ - using var topicReferences = new TopicReferencesDataTable(); - - foreach (var topicReference in topic.References) { - topicReferences.AddRow(topicReference.Key, topicReference.Value.Id); - } - /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ @@ -446,7 +437,6 @@ SqlDateTime version command.AddParameter("Version", version.Value); command.AddParameter("ExtendedAttributes", extendedAttributes); command.AddParameter("Attributes", attributeValues); - command.AddParameter("References", topicReferences); command.AddOutputParameter(); /*------------------------------------------------------------------------------------------------------------------------ @@ -467,6 +457,10 @@ SqlDateTime version PersistRelations(topic, connection); } + if (areReferencesResolved && areReferencesDirty) { + PersistReferences(topic, connection); + } + if (!topic.VersionHistory.Contains(version.Value)) { topic.VersionHistory.Insert(0, version.Value); } @@ -674,5 +668,59 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { } + /*========================================================================================================================== + | METHOD: PERSIST REFERENCES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Internal method that saves topic references to the 1:n mapping table in SQL. + /// + /// The topic object whose references should be persisted. + /// The SQL connection. + private static void PersistReferences(Topic topic, SqlConnection connection) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Persist relations to database + \-----------------------------------------------------------------------------------------------------------------------*/ + try { + + var topicId = topic.Id.ToString(CultureInfo.InvariantCulture); + using var references = new TopicReferencesDataTable(); + using var command = new SqlCommand("UpdateReferences", connection) { + CommandType = CommandType.StoredProcedure + }; + + foreach (var relatedTopic in topic.References.Where(t => !t.Value.IsNew)) { + references.AddRow(relatedTopic.Key, relatedTopic.Value.Id); + } + + // Add Parameters + command.AddParameter("TopicID", topicId); + command.AddParameter("ReferencedTopics", references); + command.AddParameter("DeleteUnmatched", true); + + command.ExecuteNonQuery(); + + //Reset isDirty, assuming there aren't any unresolved references + topic.References.IsDirty = references.Rows.Count < topic.References.Count; + + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Catch exception + \-----------------------------------------------------------------------------------------------------------------------*/ + catch (SqlException exception) { + throw new TopicRepositoryException( + $"Failed to persist references for Topic '{topic.Key}' ({topic.Id}): '{exception.Message}'", + exception + ); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Return + \-----------------------------------------------------------------------------------------------------------------------*/ + return; + + } + } //Class } //Namespace \ No newline at end of file From f468bb891355f40e747cf0649fbc4827132ac69d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 15:06:33 -0800 Subject: [PATCH 167/778] Conditionally exclude references, if not supplied If `@DeleteUnmatched` is true (`1`) and `@References` is null or empty, then `UpdateReferences` will delete all references. As we conditionally remove references, this potentially causes a problem. By putting in this condition we introduce other potential issues since we now can't delete all references. I need to think through this some more. In the meanwhile, this prevents accidental deletion of all references. --- .../Stored Procedures/CreateTopic.sql | 9 ++++++++- .../Stored Procedures/UpdateTopic.sql | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index d42ecb53..ffff5706 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -109,9 +109,16 @@ IF @ExtendedAttributes IS NOT NULL -------------------------------------------------------------------------------------------------------------------------------- -- ADD REFERENCES -------------------------------------------------------------------------------------------------------------------------------- -EXEC UpdateReferences @TopicID, +DECLARE @ReferenceCount INT +SELECT @ReferenceCount = COUNT(ReferenceKey) +FROM @References + +IF @ReferenceCount > 0 + BEGIN + EXEC UpdateReferences @TopicID, @References, 1 + END -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 9c42a140..5bcb9208 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -126,9 +126,16 @@ WHERE ISNULL(AttributeValue, '') = '' -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE REFERENCES -------------------------------------------------------------------------------------------------------------------------------- -EXEC UpdateReferences @TopicID, +DECLARE @ReferenceCount INT +SELECT @ReferenceCount = COUNT(ReferenceKey) +FROM @References + +IF @ReferenceCount > 0 + BEGIN + EXEC UpdateReferences @TopicID, @References, @DeleteUnmatched + END -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID From db74b712dc553df977a15c1b55c2aae7216de989 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 16:34:54 -0800 Subject: [PATCH 168/778] Introduce support for `@DeleteUnmatched` attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Technically, this does the same thing that `TopicRepositoryBase.GetUnmatchedAttributes()` does—but with a lot less code. Given that, it may be worth relying _exclusively_ on `@DeleteUnmatched` instead of merging `GetUnmatchedAttributes()` into the `@Attributes` parameter. I'm still considering that. In the meanwhile, this provides the flexibility of deleting unmatched attributes without needing to explicitly enumerate them as part of the `@Attributes` collection. This also reinforces the challenge with `@DeleteUnmatched` and the optional `@References` parameter, as discussed in a previous commit (f468bb8)—that's still something that I'm thinking through. --- .../Stored Procedures/UpdateTopic.sql | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 5bcb9208..be0a2402 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -123,6 +123,25 @@ CROSS APPLY ( WHERE ISNULL(AttributeValue, '') = '' AND ExistingValue != '' +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE UNMATCHED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +IF @DeleteUnmatched = 1 + BEGIN + INSERT + INTO Attributes + SELECT @TopicID, + Existing.AttributeKey, + '', + @Version + FROM AttributeIndex Existing + LEFT JOIN @Attributes New + ON Existing.TopicID = @TopicID + AND Existing.AttributeKey = New.AttributeKey + WHERE ISNULL(New.AttributeKey, '') = '' + AND Existing.TopicID = @TopicID + END + -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE REFERENCES -------------------------------------------------------------------------------------------------------------------------------- From 164498aec50b2c49778ac426b4f8e88ac1bca4f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 16:40:01 -0800 Subject: [PATCH 169/778] Added `References.IsDirty` check to `Topic.IsDirty(true)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures that a topic is marked as `IsDirty` if the references collection is dirty, even if nothing else has changed. This wasn't necessary for `SqlTopicRepository.Save()`—which assesses each collection individually—but it maintains the expectations of what results will be returned when the `checkCollections` parameter is enabled. --- OnTopic/Topic.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 1f62c0f8..7d8ec39d 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -566,6 +566,9 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa if (!_isDirty && checkCollections) { _isDirty = Relationships.IsDirty(); } + if (!_isDirty && checkCollections) { + _isDirty = References.IsDirty; + } if (!_isDirty && checkCollections) { _isDirty = Attributes.IsDirty(excludeLastModified); } From 1379d2aa20cf192548a35d7a813aef613b132f8c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 16:57:54 -0800 Subject: [PATCH 170/778] Refactor `isDirty` handling to bypass attributes if `!areAttributesDirty` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We previously defined `areAttributesDirty`, and factored it into the calculation of `isDirty`. Now, the check for mismatched extended attributes is moved from `isDirty` to instead factor into `areAttributesDirty`. That doesn't change the logic of `isDirty`, since it still relies on `areAttributesDirty`. But it allows us to conditionally exclude the `@Attributes` and `@ExtendedAttributes` from `UpdateTopics` if no attributes have been modified, and there aren't any mismatches to persist. As part of this, `DeleteUnmatched` is reverted back to `false`, as we don't want to delete any unmatched `@Attributes`—nevertheless `@References`!—if the collection isn't passed. (Unfortunately, SQL doesn't provide a way to differentiate between a null and empty user-defined, table-valued type.) --- OnTopic.Data.Sql/SqlTopicRepository.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index a73ed2b6..5aec9628 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -338,13 +338,13 @@ SqlDateTime version | current command. A more aggressive version of this would wrap much of the below logic in this, but this is just meant | as a quick fix to reduce the overhead of recursive saves. \-----------------------------------------------------------------------------------------------------------------------*/ + areAttributesDirty = areAttributesDirty || extendedAttributeList.Any(a => a.IsExtendedAttribute == false); + var isDirty = isTopicDirty || areRelationshipsDirty || areReferencesDirty || - areAttributesDirty || - indexedAttributeList.Any() || - extendedAttributeList.Any(a => a.IsExtendedAttribute == false); + areAttributesDirty; /*------------------------------------------------------------------------------------------------------------------------ | Bypass is not dirty @@ -427,7 +427,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ if (!topic.IsNew) { command.AddParameter("TopicID", topic.Id); - command.AddParameter("DeleteUnmatched", true); + command.AddParameter("DeleteUnmatched", false); } else if (topic.Parent is not null) { command.AddParameter("ParentID", topic.Parent.Id); @@ -435,8 +435,10 @@ SqlDateTime version command.AddParameter("Key", topic.Key); command.AddParameter("ContentType", topic.ContentType); command.AddParameter("Version", version.Value); - command.AddParameter("ExtendedAttributes", extendedAttributes); - command.AddParameter("Attributes", attributeValues); + if (areAttributesDirty) { + command.AddParameter("Attributes", attributeValues); + command.AddParameter("ExtendedAttributes", extendedAttributes); + } command.AddOutputParameter(); /*------------------------------------------------------------------------------------------------------------------------ From 2f4bf9c77ffc02b3807f38b9c68f96bcd60a511a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 17:03:07 -0800 Subject: [PATCH 171/778] Conditionally exclude `Key` and `ContentType` unless they've changed In a previous commit, I updated `UpdateTopic` so that it would only save `@Key` or `@ContentType` if they were provided, thus maintaining consistency with the rest of `UpdateTopic`, which allows omitting values if they haven't changed (a74389d). In this update, I've made `SqlTopicRepository` aware of this change by conditionally excluding the `@Key` and `@ContentType` parameters unless `Topic.IsDirty()` or `Topic.IsNew`. (If a topic is new, the core attributes should _always_ be marked as dirty, so the logic is a bit redundant, but it helps reinforce that the logic.) --- OnTopic.Data.Sql/SqlTopicRepository.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 5aec9628..845452cf 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -432,8 +432,10 @@ SqlDateTime version else if (topic.Parent is not null) { command.AddParameter("ParentID", topic.Parent.Id); } - command.AddParameter("Key", topic.Key); - command.AddParameter("ContentType", topic.ContentType); + if (isTopicDirty || topic.IsNew) { + command.AddParameter("Key", topic.Key); + command.AddParameter("ContentType", topic.ContentType); + } command.AddParameter("Version", version.Value); if (areAttributesDirty) { command.AddParameter("Attributes", attributeValues); From 983207ee398998d81f5e53a93b11f550272f09d9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 17:09:32 -0800 Subject: [PATCH 172/778] Don't execute `UpdateTopic` if neither the topics nor attributes are dirty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, we skip `Save()` entirely if `isDirty` isn't true. And we now exclude `@Attributes` and `@ExtendedAttributes` if `areAttributesDirty` is false, and `@Key` and `ContentType` if `isTopicDirty` is false. But if both `isTopicDirty` and `areAttributesDirty` are false, but `isDirty` isn't—e.g., due to `areRelationshipsDirty` or `areReferencesDirty`—then there's no need to execute `UpdateTopic` at all. This update bypasses the `ExecuteNonQuery()` in this scenario. This should offer a marginal performance improvement when dealing with recursive saves of a large topic graph, as is often the case when importing topics. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 845452cf..451a6ce0 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -448,9 +448,10 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ try { - command.ExecuteNonQuery(); - - topic.Id = command.GetReturnCode(); + if (topic.IsNew || isTopicDirty || areAttributesDirty) { + command.ExecuteNonQuery(); + topic.Id = command.GetReturnCode(); + } Contract.Assume( !topic.IsNew, From de6f3a68ab2b92d3dd86258e63ebb211a855746f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 17:23:37 -0800 Subject: [PATCH 173/778] Don't construct `attributeValues`, `extendedAttributes` if `!areAttributesDirty` If `areAttributesDirty` is false, then neither `@Attributes` nor `@ExtendedAttributes` will be sent to the `UpdateTopic` stored procedure, so there's no purpose to populating the `attributeValues` or `extendedAttributes` collections. As part of this, I consolidated the population of `attributeValues` so that it was broken up by the population of `extendedAttributes`. (There are two parts to populating `attributeValues`: adding the attributes that have changed, then adding the attributes that have either not changed or are missing.) This provides a very marginal performance benefit by excluding the population of unused collections. The collections are still initialized, however, since conditionally excluding them introduces additional complexities with null checking down the road. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 49 ++++++++++++++------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 451a6ce0..33a82180 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -356,11 +356,21 @@ SqlDateTime version /*------------------------------------------------------------------------------------------------------------------------ | Add indexed attributes that are dirty + >------------------------------------------------------------------------------------------------------------------------- + | Loop through the content type's supported attributes and add attribute to null attributes if topic does not contain it. \-----------------------------------------------------------------------------------------------------------------------*/ using var attributeValues = new AttributeValuesDataTable(); - foreach (var attributeValue in indexedAttributeList) { - attributeValues.AddRow(attributeValue.Key, attributeValue.Value); + if (!areAttributesDirty) { + + foreach (var attributeValue in indexedAttributeList) { + attributeValues.AddRow(attributeValue.Key, attributeValue.Value); + } + + foreach (var attribute in GetUnmatchedAttributes(topic)) { + attributeValues.AddRow(attribute.Key); + } + } /*------------------------------------------------------------------------------------------------------------------------ @@ -368,32 +378,27 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ var extendedAttributes = new StringBuilder(); - extendedAttributes.Append(""); + if (!areAttributesDirty) { - foreach (var attributeValue in extendedAttributeList) { + extendedAttributes.Append(""); - extendedAttributes.Append( - "" - ); + foreach (var attributeValue in extendedAttributeList) { - //###NOTE JJC20200502: By treating extended attributes as unmatched, we ensure that any indexed attributes with the same - //value are overwritten with an empty attribute. This is useful for cases where an indexed attribute is moved to an - //extended attribute, as it persists that version history, while removing ambiguity over which record is authoritative. - //This is also useful for supporting arbitrary attribute values, since they may be moved from indexed to extended - //attributes if their length exceeds 255. - attributeValues.AddRow(attributeValue.Key); + extendedAttributes.Append( + "" + ); - } + //###NOTE JJC20200502: By treating extended attributes as unmatched, we ensure that any indexed attributes with the same + //value are overwritten with an empty attribute. This is useful for cases where an indexed attribute is moved to an + //extended attribute, as it persists that version history, while removing ambiguity over which record is authoritative. + //This is also useful for supporting arbitrary attribute values, since they may be moved from indexed to extended + //attributes if their length exceeds 255. + attributeValues.AddRow(attributeValue.Key); - extendedAttributes.Append(""); + } + + extendedAttributes.Append(""); - /*------------------------------------------------------------------------------------------------------------------------ - | Add unmatched attributes - >------------------------------------------------------------------------------------------------------------------------- - | Loop through the content type's supported attributes and add attribute to null attributes if topic does not contain it - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var attribute in GetUnmatchedAttributes(topic)) { - attributeValues.AddRow(attribute.Key); } /*------------------------------------------------------------------------------------------------------------------------ From 268f018baf8d8c9c175502cb7929af1d8d45f381 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 17:46:46 -0800 Subject: [PATCH 174/778] Corrected boolean logic for `areAttributesDirty` In my previous commit (de6f3a6), I got the logic backwards for the `areAttributesDirty` check. Whoops! --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 33a82180..a4198ac8 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -361,7 +361,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ using var attributeValues = new AttributeValuesDataTable(); - if (!areAttributesDirty) { + if (areAttributesDirty) { foreach (var attributeValue in indexedAttributeList) { attributeValues.AddRow(attributeValue.Key, attributeValue.Value); @@ -378,7 +378,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ var extendedAttributes = new StringBuilder(); - if (!areAttributesDirty) { + if (areAttributesDirty) { extendedAttributes.Append(""); From c1b27a749697197bcf2eecc4b26fcec1f12b0362 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 18:46:20 -0800 Subject: [PATCH 175/778] Updated unit tests for `DerivedTopic` The previous unit tests for `DerivedTopic` evaluated the `Attributes` as a backing store to ensure the value was properly reflected there. As `DerivedTopic` is now stored in `Topic.References` instead of `Topic.Attributes` (0ec079c), these tests needed to be updated. Note: These tests may not be as meaningful at this point since we'd expect these to reference the same object, instead of requiring a serialization of the value. We may want to revisit if these provide value later. --- OnTopic.Tests/TopicTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index f6da25f3..e039ea5a 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -290,7 +290,7 @@ public void DerivedTopic_UpdateValue_ReturnsExpectedValue() { topic.DerivedTopic = finalDerivedTopic; Assert.ReferenceEquals(topic.DerivedTopic, finalDerivedTopic); - Assert.AreEqual(2, topic.Attributes.GetInteger("TopicID", 0)); + Assert.AreEqual(2, topic.References.GetTopic("DerivedTopic").Id); } @@ -333,7 +333,7 @@ public void DerivedTopic_ResavedValue_ReturnsExpectedValue() { topic.DerivedTopic = derivedTopic; Assert.ReferenceEquals(topic.DerivedTopic, derivedTopic); - Assert.AreEqual(5, topic.Attributes.GetInteger("TopicID", -2)); + Assert.AreEqual(5, topic.References.GetTopic("DerivedTopic").Id); } From b33cd6480faded01ab02421dc7d78abcd4df932a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 19:20:18 -0800 Subject: [PATCH 176/778] Call local `Add(KeyValuePair)` from `Add(string, Topic)` Previously, the `Add(string, Topic)` inadvertantly called `_storage.Add()` directly, instead of routing through the local `Add(KeyValuePair)`. The latter includes the core business logic for the `Add()` method, and thus is necessary to route through. --- OnTopic/Collections/TopicReferenceDictionary.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index d122c5ac..9f428a8a 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -136,7 +136,8 @@ public void Add(string key, Topic value) { /*------------------------------------------------------------------------------------------------------------------------ | Add item \-----------------------------------------------------------------------------------------------------------------------*/ - _storage.Add(new(key, value)); + var self = this as ICollection>; + self.Add(new(key, value)); } From 5bbc800a9fc84b05d4731c543b647c03123b99b0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 19:27:16 -0800 Subject: [PATCH 177/778] Explicitly identify `IncomingRelationships` as `incomingRelationship` This is necessary to acknowledge that this is a deliberate action, and not an accidental intent of setting `Relationships`. The overload only exists internally, and is intended for internal plumbing such as this type of call. --- OnTopic/Collections/TopicReferenceDictionary.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 9f428a8a..e5620768 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -115,7 +115,7 @@ void ICollection>.Add(KeyValuePair it /*------------------------------------------------------------------------------------------------------------------------ | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ - item.Value.IncomingRelationships.SetTopic(item.Key, item.Value); + item.Value.IncomingRelationships.SetTopic(item.Key, item.Value, null, true); /*------------------------------------------------------------------------------------------------------------------------ | Add item @@ -180,7 +180,7 @@ public void Clear() { | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var item in _storage) { - item.Value.IncomingRelationships.RemoveTopic(item.Key, _parent); + item.Value.IncomingRelationships.RemoveTopic(item.Key, _parent, true); } /*------------------------------------------------------------------------------------------------------------------------ @@ -236,7 +236,7 @@ public bool Remove(string key) { | Handle existing \-----------------------------------------------------------------------------------------------------------------------*/ if (TryGetValue(key, out var existing)) { - existing.IncomingRelationships.RemoveTopic(key, _parent); + existing.IncomingRelationships.RemoveTopic(key, _parent, true); IsDirty = true; } From 2306302d690379f5046ab4efd513c7cb519a1f9b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 19:29:47 -0800 Subject: [PATCH 178/778] Corrected `IncomingRelationships` reference to point to `_parent` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `Add()` method was inadvertantly setting the `IncomingRelationships` reference back to the referenced topic—i.e., the topic that contains the `IncomingRelationships`. Whoops. --- OnTopic/Collections/TopicReferenceDictionary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index e5620768..f706ea2f 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -115,7 +115,7 @@ void ICollection>.Add(KeyValuePair it /*------------------------------------------------------------------------------------------------------------------------ | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ - item.Value.IncomingRelationships.SetTopic(item.Key, item.Value, null, true); + item.Value.IncomingRelationships.SetTopic(item.Key, _parent, null, true); /*------------------------------------------------------------------------------------------------------------------------ | Add item From d4c8f8982b76be6866fbb2466d299b16509a5bd6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 19:32:26 -0800 Subject: [PATCH 179/778] Ensured that `SetTopic()` correctly calls the `key` parameter Previously, this was mistakenly set to look for a hard-coded `referenceKey` named `"key"`. Whoops! --- OnTopic/Collections/TopicReferenceDictionary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index f706ea2f..962c45a4 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -151,7 +151,7 @@ public void Add(string key, Topic value) { public void SetTopic(string key, Topic? value, bool? isDirty = null) { var wasDirty = IsDirty; if (value is null) { - if (ContainsKey("key")) { + if (ContainsKey(key)) { Remove(key); } } From 9a17fea956dfe3fae4390c88e1a040d04f10eb77 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 19:42:39 -0800 Subject: [PATCH 180/778] Introduced unit tests for core functionality of `TopicReferenceDictionary` These test the `IsDirty`, `SetValue()`, `GetValue()`, `Clear()`, and `topic.IncomingRelationships` functionality of the `IDictionary` interceptors to ensure that the business logic is all being correctly adhered to. --- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 OnTopic.Tests/TopicReferenceDictionaryTest.cs diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs new file mode 100644 index 00000000..7f6bb8d1 --- /dev/null +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -0,0 +1,254 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Collections; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCE DICTIONARY TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the , with a particular emphasis on the custom features + /// such as , , , and the cross-referencing of reciprocal values in the + /// property. + /// + [TestClass] + public class TopicReferenceDictionaryTest { + + /*========================================================================================================================== + | TEST: ADD: NEW REFERENCE: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference, and confirms that + /// is correctly set. + /// + [TestMethod] + public void Add_NewReference_IsDirty() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + + Assert.AreEqual(1, topic.References.Count); + Assert.IsTrue(topic.References.IsDirty); + + } + + /*========================================================================================================================== + | TEST: SET TOPIC: NEW REFERENCE: NOT DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , and confirms that is not set. + /// + [TestMethod] + public void SetTopic_NewReference_NotDirty() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.SetTopic("Reference", reference, false); + + Assert.AreEqual(1, topic.References.Count); + Assert.IsFalse(topic.References.IsDirty); + + } + + /*========================================================================================================================== + | TEST: REMOVE: EXISTING REFERENCE: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new with a topic reference, removes that reference using , and confirms that is set. + /// + [TestMethod] + public void Remove_ExistingReference_IsDirty() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.SetTopic("Reference", reference, false); + topic.References.Remove("Reference"); + + Assert.AreEqual(0, topic.References.Count); + Assert.IsTrue(topic.References.IsDirty); + + } + + /*========================================================================================================================== + | TEST: CLEAR: EXISTING REFERENCES: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , calls and confirms that is set. + /// + [TestMethod] + public void Clear_ExistingReferences_IsDirty() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.SetTopic("Reference", reference, false); + topic.References.Clear(); + + Assert.AreEqual(0, topic.References.Count); + Assert.IsTrue(topic.References.IsDirty); + + } + + /*========================================================================================================================== + | TEST: ADD: NEW REFERENCE: INCOMING RELATIONSHIP SET + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , and confirms that + /// reference is correctly set. + /// + [TestMethod] + public void Add_NewReference_IncomingRelationshipSet() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + + Assert.AreEqual(1, reference.IncomingRelationships.GetTopics("Reference").Count); + + } + + /*========================================================================================================================== + | TEST: REMOVE: EXISTING REFERENCE: INCOMING RELATIONSHIP REMOVED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , removes the reference using , and confirms that the reference is correctly removed as + /// well. + /// + [TestMethod] + public void Remove_ExistingReference_IncomingRelationshipRemoved() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + topic.References.Remove("Reference"); + + Assert.AreEqual(0, reference.IncomingRelationships.GetTopics("Reference").Count); + + } + + /*========================================================================================================================== + | TEST: SET TOPIC: EXISTING REFERENCE: TOPIC UPDATED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , updates the reference using , and confirms that the reference is correctly updated. + /// + [TestMethod] + public void SetTopic_ExistingReference_TopicUpdated() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + var newReference = TopicFactory.Create("NewReference", "Page"); + + topic.References.Add("Reference", reference); + topic.References.SetTopic("Reference", newReference); + + Assert.AreEqual(newReference, topic.References.GetTopic("Reference")); + + } + + /*========================================================================================================================== + | TEST: SET TOPIC: NULL REFERENCE: TOPIC REMOVED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference using , updates the reference with a null value using , and confirms that the reference + /// is correctly removed. + /// + [TestMethod] + public void SetTopic_NullReference_TopicRemoved() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + topic.References.SetTopic("Reference", null); + + Assert.AreEqual(0, topic.References.Count); + + } + + /*========================================================================================================================== + | TEST: ADD: NEW REFERENCE: TOPIC IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference, and confirms that + /// is correctly set. + /// + [TestMethod] + public void Add_NewReference_TopicIsDirty() { + + var topic = TopicFactory.Create("Topic", "Page", 1); + var reference = TopicFactory.Create("Reference", "Page", 2); + + topic.References.Add("Reference", reference); + + Assert.IsTrue(topic.IsDirty(true)); + Assert.IsFalse(reference.IsDirty(true)); + + } + + /*========================================================================================================================== + | TEST: GET TOPIC: EXISTING REFERENCE: RETURNS TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference, and confirms that + /// correctly returns the . + /// + [TestMethod] + public void GetTopic_ExistingReference_ReturnsTopic() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + + Assert.AreEqual(reference, topic.References.GetTopic("Reference")); + + } + + /*========================================================================================================================== + | TEST: GET TOPIC: MISSING REFERENCE: RETURNS NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new , adds a new reference, and confirms that + /// correctly returns null if an incorrect + /// referencedKey is entered. + /// + [TestMethod] + public void GetTopic_MissingReference_ReturnsNull() { + + var topic = TopicFactory.Create("Topic", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("Reference", reference); + + Assert.IsNull(topic.References.GetTopic("MissingReference")); + + } + + } //Class +} //Namespace \ No newline at end of file From bfac5669b9a498c4573a7801665c38d4bd610cca Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 1 Jan 2021 20:01:33 -0800 Subject: [PATCH 181/778] Allow topic references to inherit from derived topics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This maintains familiar functionality from `topic.Attributes.GetValue()`, and ensures that any topic references which previously relied upon inheritance when they were stored as attributes continue to work now that they're stored as `TopicReferences`. (This excludes calls to `Topic.DerivedTopic`, of course, as that would introduce an infinite loop!) This includes three new unit tests to ensure that inheritance works as expected—and can be optionally disabled. --- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 70 ++++++++++++++++++- .../Collections/TopicReferenceDictionary.cs | 11 ++- OnTopic/Topic.cs | 2 +- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index 7f6bb8d1..b568353c 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -235,7 +235,7 @@ public void GetTopic_ExistingReference_ReturnsTopic() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// correctly returns null if an incorrect + /// correctly returns null if an incorrect /// referencedKey is entered. /// [TestMethod] @@ -250,5 +250,73 @@ public void GetTopic_MissingReference_ReturnsNull() { } + /*========================================================================================================================== + | TEST: GET TOPIC: DERIVED REFERENCE: RETURNS TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns the related topic reference. + /// + [TestMethod] + public void GetTopic_DerivedReference_ReturnsTopic() { + + var topic = TopicFactory.Create("Topic", "Page"); + var derived = TopicFactory.Create("Derived", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.DerivedTopic = derived; + derived.References.Add("Reference", reference); + + Assert.AreEqual(reference, topic.References.GetTopic("Reference")); + + } + + /*========================================================================================================================== + | TEST: GET TOPIC: DERIVED REFERENCE: RETURNS NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if an incorrect referencedKey + /// is entered. + /// + [TestMethod] + public void GetTopic_DerivedReference_ReturnsNull() { + + var topic = TopicFactory.Create("Topic", "Page"); + var derived = TopicFactory.Create("Derived", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.DerivedTopic = derived; + derived.References.Add("Reference", reference); + + Assert.IsNull(topic.References.GetTopic("MissingReference")); + + } + + /*========================================================================================================================== + | TEST: GET TOPIC: DERIVED REFERENCE WITHOUT INHERIT: RETURNS NULL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromDerived is + /// set to false. + /// + [TestMethod] + public void GetTopic_DerivedReferenceWithoutInherit_ReturnsNull() { + + var topic = TopicFactory.Create("Topic", "Page"); + var derived = TopicFactory.Create("Derived", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.DerivedTopic = derived; + derived.References.Add("Reference", reference); + + Assert.IsNull(topic.References.GetTopic("Reference", false)); + + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index 962c45a4..cac63072 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using OnTopic.Internal.Diagnostics; +using OnTopic.Querying; namespace OnTopic.Collections { @@ -259,7 +260,15 @@ public bool Remove(string key) { /// /// Attempts to retrieve a topic reference based on its ; if it doesn't exist, returns null. /// - public Topic? GetTopic(string key) => TryGetValue(key, out var existing)? existing : null; + public Topic? GetTopic(string key, bool inheritFromDerived = true) { + if (TryGetValue(key, out var existing)) { + return existing; + } + else if (inheritFromDerived) { + return _parent.DerivedTopic?.References.GetTopic(key); + } + return null; + } /*========================================================================================================================== | IS DIRTY? diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 7d8ec39d..42fc6ab2 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -611,7 +611,7 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa /// value != this /// public Topic? DerivedTopic { - get => References.GetTopic("DerivedTopic"); + get => References.GetTopic("DerivedTopic", false); set { Contract.Requires( value != this, From e5b87eb0907615939885313dd3a7a4762be91430 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 12:27:28 -0800 Subject: [PATCH 182/778] Introduced support for `TopicReferences` in `TopicMappingService` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicMappingService` already has support for topic references—but it previously assumed that they would be stored as attributes ending with `Id`. That support is maintained (for now) for backward compatibility. In addition, however, the `SetPropertyAsync()` method now _also_ checks the `Topic.References` collection for an exact match and, if found, prefers that over the attribute-based topic reference. --- OnTopic.Tests/TopicMappingServiceTest.cs | 26 +++++++++++++++++++++++- OnTopic/Mapping/TopicMappingService.cs | 10 +++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index f684b94a..da3fc689 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -499,6 +499,30 @@ public async Task Map_MapToParent_ReturnsMappedModel() { } + /*========================================================================================================================== + | TEST: MAP: TOPIC REFERENCES AS ATTRIBUTE: RETURNS MAPPED MODEL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests whether it successfully maps referenced topics stored in + /// . + /// + [TestMethod] + public async Task Map_TopicReferencesAsAttribute_ReturnsMappedModel() { + + var mappingService = new TopicMappingService(_topicRepository, _typeLookupService); + var topicReference = _topicRepository.Load(11111); + + var topic = TopicFactory.Create("Test", "TopicReference"); + + topic.Attributes.SetInteger("TopicReferenceId", topicReference.Id); + + var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.IsNotNull(target.TopicReference); + Assert.AreEqual(topicReference.Key, target.TopicReference.Key); + + } + /*========================================================================================================================== | TEST: MAP: TOPIC REFERENCES: RETURNS MAPPED MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -513,7 +537,7 @@ public async Task Map_TopicReferences_ReturnsMappedModel() { var topic = TopicFactory.Create("Test", "TopicReference"); - topic.Attributes.SetInteger("TopicReferenceId", topicReference.Id); + topic.References.SetTopic("TopicReference", topicReference); var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 0cae879f..d6fb608b 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -279,7 +279,7 @@ protected async Task SetPropertyAsync( var configuration = new PropertyConfiguration(property, attributePrefix); var topicReferenceId = source.Attributes.GetInteger($"{configuration.AttributeKey}Id", 0); - if (topicReferenceId == 0 && configuration.AttributeKey.EndsWith("Id", StringComparison.InvariantCultureIgnoreCase)) { + if (topicReferenceId == 0 && configuration.AttributeKey.EndsWith("Id", StringComparison.OrdinalIgnoreCase)) { topicReferenceId = source.Attributes.GetInteger(configuration.AttributeKey, 0); } @@ -310,8 +310,14 @@ protected async Task SetPropertyAsync( await SetTopicReferenceAsync(source.Parent, target, configuration, cache).ConfigureAwait(false); } } + else if ( + source.References.TryGetValue(configuration.AttributeKey, out var topicReference) && + relationships.HasFlag(Relationships.References) + ) { + await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); + } else if (topicReferenceId > 0 && relationships.HasFlag(Relationships.References)) { - var topicReference = _topicRepository.Load(topicReferenceId); + topicReference = _topicRepository.Load(topicReferenceId); if (topicReference is not null) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); } From 21b8562a1b14aaae0c32566e0d346483f76df404 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 12:51:56 -0800 Subject: [PATCH 183/778] Corrected name of `ReverseTopicMappingServiceTest` class It was inadvertantly set to `ReverseReverseTopicMappingServiceTest`. Which is amusing, but innacurate. --- OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index a3960e96..86b60d67 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -31,7 +31,7 @@ namespace OnTopic.Tests { /// Provides unit tests for the using local DTOs. /// [TestClass] - public class ReverseReverseTopicMappingServiceTest { + public class ReverseTopicMappingServiceTest { /*========================================================================================================================== | PRIVATE VARIABLES @@ -42,7 +42,7 @@ public class ReverseReverseTopicMappingServiceTest { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the with shared resources. + /// Initializes a new instance of the with shared resources. /// /// /// This uses the to provide data, and then to @@ -50,7 +50,7 @@ public class ReverseReverseTopicMappingServiceTest { /// relatively lightweight façade to any , and prevents the need to duplicate logic for /// crawling the object graph. /// - public ReverseReverseTopicMappingServiceTest() { + public ReverseTopicMappingServiceTest() { _topicRepository = new CachedTopicRepository(new StubTopicRepository()); } From 85a9657207e39eff56e8f6d824be271c92ae8ed7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 13:16:58 -0800 Subject: [PATCH 184/778] Introduced support for `TopicReferences` in `ReverseTopicMappingService` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was already support for `ModelType.Reference`, but it was based on storing the referenced topic's `Id` as an attribute. As part of that, it enforced that the attribute name must, by convention, end with an `Id`. That validation is now removed, and the topic reference is now stored in `Topic.References` instead of `Topic.Attributes`. As with the corresponding `TopicMappingService` implementation (e5b87eb), the `ReverseTopicService` implementation maintains backward compatibility with legacy attribute-based topic references. It does so by relying on the legacy `{ReferenceKey}Id` convention: If the `AttributeKey` ends with `Id`, then it will be stored in `Topic.Attributes`; otherwise, it will be stored in `Topic.References`. Note that the `Id` check is case-sensitive. We don't expect the `ReferenceKey` used in `Topic.References` to end in `Id`—and, in fact, remove the `Id` as part of the migration script (db35570). But it is possible they they will end in an `id` (e.g., `Aid` or `Android`). By keeping this case sensitive, we help avoid false positives. --- .../Mapping/Reverse/BindingModelValidator.cs | 18 ------------------ .../Reverse/ReverseTopicMappingService.cs | 7 ++++++- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 5588fd31..eb8d31dc 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -272,24 +272,6 @@ attributeDescriptor.ModelType is ModelType.Reference && ); } - /*------------------------------------------------------------------------------------------------------------------------ - | Validate that references end in "Id" - \-----------------------------------------------------------------------------------------------------------------------*/ - if ( - attributeDescriptor.ModelType is ModelType.Reference && - !configuration.AttributeKey.EndsWith("Id", StringComparison.InvariantCulture) - ) { - throw new TopicMappingException( - $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a topic reference, but " + - $"the generic type '{compositeAttributeKey}' does not end in Id. By convention, all topic reference are " + - $"expected to end in Id. To keep the property name set to '{propertyType.Name}', use the " + - $"{nameof(AttributeKeyAttribute)} to specify the name of the topic reference this should map to. If this property " + - $"is not intended to be mapped at all, include the {nameof(DisableMappingAttribute)}. If the " + - $"'{contentTypeDescriptor.Key}' defines a topic reference attribute that doesn't follow this convention, then it " + - $"should be updated." - ); - } - } /*========================================================================================================================== diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index aafae580..2394897f 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -551,7 +551,12 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Set target attribute \-----------------------------------------------------------------------------------------------------------------------*/ - target.Attributes.SetInteger(configuration.AttributeKey, topicReference.Id); + if (configuration.AttributeKey.EndsWith("Id", StringComparison.Ordinal)) { + target.Attributes.SetInteger(configuration.AttributeKey, topicReference.Id); + } + else { + target.References.SetTopic(configuration.AttributeKey, topicReference); + } } From 69dee7d454703580f22a82b944c58531fad770b9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 13:18:12 -0800 Subject: [PATCH 185/778] Removed unit tests for validating that topic references end in `Id` As this is no longer a requirement, these unit tests will no longer fail (as was originally expected). --- .../InvalidReferenceNameTopicBindingModel.cs | 28 ------------------- .../ReverseTopicMappingServiceTest.cs | 18 ------------ 2 files changed, 46 deletions(-) delete mode 100644 OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs deleted file mode 100644 index 712c827c..00000000 --- a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs +++ /dev/null @@ -1,28 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using OnTopic.ViewModels.BindingModels; - -namespace OnTopic.Tests.BindingModels { - - /*============================================================================================================================ - | BINDING MODEL: REFERENCE NAME TOPIC (INVALID) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a custom binding model with an invalid reference name—i.e., one that doesn't end in Id. An should be thrown when it is mapped. - /// - /// - /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. - /// - public class InvalidReferenceNameTopicBindingModel : BasicTopicBindingModel { - - public InvalidReferenceNameTopicBindingModel(string? key = null) : base(key, "Page") { } - - public RelatedTopicBindingModel TopicReference { get; } = new(); - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 86b60d67..2dcb8b0d 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -477,24 +477,6 @@ public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationExceptio } - /*========================================================================================================================== - | TEST: MAP: INVALID TOPIC REFERENCE NAME: THROWS INVALID OPERATION EXCEPTION - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Maps a content type that has a reference that does not end in Id. This is invalid, and expected to throw an - /// . - /// - [TestMethod] - [ExpectedException(typeof(TopicMappingException))] - public async Task Map_InvalidTopicReferenceName_ThrowsInvalidOperationException() { - - var mappingService = new ReverseTopicMappingService(_topicRepository); - var bindingModel = new InvalidReferenceNameTopicBindingModel("Test"); - - var target = await mappingService.MapAsync(bindingModel).ConfigureAwait(false); - - } - /*========================================================================================================================== | TEST: MAP: INVALID TOPIC REFERENCE TYPE: THROWS INVALID OPERATION EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ From b5dbaeff88ad05ceecef7c2800a8339619799c2d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 13:23:28 -0800 Subject: [PATCH 186/778] Updated unit tests to use `DerivedTopic` from `Topic.References` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This included removing the explicit `[AttributeKey()]` setting it to use `TopicId` (instead of the default of `DerivedTopic`), changing the `AttributeDescriptor` to be named `DerivedTopic` instead of `TopicId`, and, finally, removing the explicit setting of the `DerivedTopic` reference in the unit test based on the `Topic.Attributes` value. While I was at it, I extended the unit test to confirm that the `DerivedTopic` has the correct key. Note: This does _not_ test the backward compatible support for `{ReferenceKey}Id` names that _should_ be stored in `Topic.Attributes`. I may reintroduce a unit test for that again in the future. For now, this isn't an immediate priority, and especially as this functionality will likely be removed in a future release—possibly even before the next release. --- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs | 1 - OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 4 +--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index e3b338f5..dd158822 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -221,7 +221,7 @@ private Topic CreateFakeData() { addAttribute(contentTypes, "Key", "TextAttribute", false, true); addAttribute(contentTypes, "ContentType", "TextAttribute", false, true); addAttribute(contentTypes, "Title", "TextAttribute", true, true); - addAttribute(contentTypes, "TopicId", "TopicReferenceAttribute", false); + addAttribute(contentTypes, "DerivedTopic", "TopicReferenceAttribute", false); var contentTypeDescriptor = TopicFactory.Create("ContentTypeDescriptor", "ContentTypeDescriptor", contentTypes); diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index fbbcd295..88bf5b57 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -22,7 +22,6 @@ public class ReferenceTopicBindingModel : BasicTopicBindingModel { public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttribute") { } - [AttributeKey("TopicId")] public RelatedTopicBindingModel? DerivedTopic { get; set; } } //Class diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 2dcb8b0d..a90b6f69 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -9,7 +9,6 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Attributes; using OnTopic.Data.Caching; using OnTopic.Mapping; using OnTopic.Mapping.Annotations; @@ -298,9 +297,8 @@ public async Task Map_TopicReferences_ReturnsMappedTopic() { var target = (TopicReferenceAttribute?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); - target.DerivedTopic = _topicRepository.Load(target.Attributes.GetInteger("TopicId", -5)); - Assert.IsNotNull(target.DerivedTopic); + Assert.AreEqual("Title", target.DerivedTopic.Key); Assert.AreEqual("TopicReference", target.EditorType); } From 7ae64fe55304d3850d198ce9a69691099fa78c39 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 13:43:38 -0800 Subject: [PATCH 187/778] Group indexed attribute operations Previously, the handling of extended attributes (`@ExtendedAttributes`) was placed in between different operations related to indexed attributes (`@Attributes`). A more logical order would group these based on the parameter and/or table they're operating against. This acheived that, without modifying any of the logic. --- .../Stored Procedures/UpdateTopic.sql | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index be0a2402..cb7262df 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -11,7 +11,7 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @Attributes AttributeValues READONLY , @ExtendedAttributes XML = NULL , @References TopicReferences READONLY , - @Version DATETIME = NULL, + @Version DATETIME = NULL , @DeleteUnmatched BIT = 0 AS @@ -24,7 +24,6 @@ SET @Version = GetDate() -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE KEY ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- - IF @Key IS NOT NULL OR @ContentType IS NOT NULL BEGIN UPDATE Topics @@ -69,34 +68,6 @@ OUTER APPLY ( WHERE ISNULL(AttributeValue, '') != '' AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') --------------------------------------------------------------------------------------------------------------------------------- --- PULL PREVIOUS EXTENDED ATTRIBUTES --------------------------------------------------------------------------------------------------------------------------------- -DECLARE @PreviousExtendedAttributes XML - -SELECT TOP 1 - @PreviousExtendedAttributes = AttributesXml -FROM ExtendedAttributes -WHERE TopicID = @TopicID -ORDER BY Version DESC - --------------------------------------------------------------------------------------------------------------------------------- --- ADD EXTENDED ATTRIBUTES, IF CHANGED --------------------------------------------------------------------------------------------------------------------------------- -IF CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) - BEGIN - INSERT - INTO ExtendedAttributes ( - TopicID , - AttributesXml , - Version - ) - VALUES ( - @TopicID , - @ExtendedAttributes , - @Version - ) - END -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NULL ATTRIBUTES @@ -142,6 +113,35 @@ IF @DeleteUnmatched = 1 AND Existing.TopicID = @TopicID END +-------------------------------------------------------------------------------------------------------------------------------- +-- PULL PREVIOUS EXTENDED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @PreviousExtendedAttributes XML + +SELECT TOP 1 + @PreviousExtendedAttributes = AttributesXml +FROM ExtendedAttributes +WHERE TopicID = @TopicID +ORDER BY Version DESC + +-------------------------------------------------------------------------------------------------------------------------------- +-- ADD EXTENDED ATTRIBUTES, IF CHANGED +-------------------------------------------------------------------------------------------------------------------------------- +IF CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) + BEGIN + INSERT + INTO ExtendedAttributes ( + TopicID , + AttributesXml , + Version + ) + VALUES ( + @TopicID , + @ExtendedAttributes , + @Version + ) + END + -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE REFERENCES -------------------------------------------------------------------------------------------------------------------------------- From 7f6ebb0a6b6069063deb994f52685e28c8d25179 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 2 Jan 2021 13:53:35 -0800 Subject: [PATCH 188/778] Updated operations against table-valued parameters to be conditional Previously, even if a user-defined, table-valued type parameter (e.g., `@Attributes`, `@References`) was empty, the operation would still run. In most cases, this wouldn't hurt anything, as a join against an empty table is a fast operation. Nevertheless, it's an unnecessary operation for cases where there aren't any `@Attributes` or `@References`. An easy fix is to wrap them in conditionals, so they're only executed if values are supplied. With the removal of core attributes from the `@Attributes` parameter (and `Attributes` table), there are far more cases where a topic won't have any corresponding attributes. And it's pretty common that topics won't have any `@References`. As such, this might have some marginal performance benefit for recursive saves over large topic graphs. --- .../Stored Procedures/UpdateTopic.sql | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index cb7262df..433f3860 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -45,54 +45,60 @@ IF @Key IS NOT NULL OR @ContentType IS NOT NULL -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NEW ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- -INSERT -INTO Attributes ( +IF EXISTS (SELECT TOP 1 NULL FROM @Attributes) + BEGIN + INSERT + INTO Attributes ( TopicID , AttributeKey , AttributeValue , Version ) -SELECT @TopicID, + SELECT @TopicID, AttributeKey, AttributeValue, @Version -FROM @Attributes New -OUTER APPLY ( - SELECT TOP 1 + FROM @Attributes New + OUTER APPLY ( + SELECT TOP 1 AttributeValue AS ExistingValue - FROM Attributes - WHERE TopicID = @TopicID - AND AttributeKey = New.AttributeKey - ORDER BY Version DESC - ) Existing -WHERE ISNULL(AttributeValue, '') != '' - AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') - + FROM Attributes + WHERE TopicID = @TopicID + AND AttributeKey = New.AttributeKey + ORDER BY Version DESC + ) Existing + WHERE ISNULL(AttributeValue, '') != '' + AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') + END -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NULL ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- -INSERT INTO Attributes ( +IF EXISTS (SELECT TOP 1 NULL FROM @Attributes) + BEGIN + INSERT + INTO Attributes ( TopicID , AttributeKey , AttributeValue , Version ) -SELECT @TopicID, + SELECT @TopicID, AttributeKey, '', @Version -FROM @Attributes New -CROSS APPLY ( - SELECT TOP 1 + FROM @Attributes New + CROSS APPLY ( + SELECT TOP 1 AttributeValue AS ExistingValue - FROM Attributes - WHERE TopicID = @TopicID - AND AttributeKey = New.AttributeKey - ORDER BY Version DESC -) Existing -WHERE ISNULL(AttributeValue, '') = '' - AND ExistingValue != '' + FROM Attributes + WHERE TopicID = @TopicID + AND AttributeKey = New.AttributeKey + ORDER BY Version DESC + ) Existing + WHERE ISNULL(AttributeValue, '') = '' + AND ExistingValue != '' + END -------------------------------------------------------------------------------------------------------------------------------- -- DELETE UNMATCHED ATTRIBUTES @@ -127,7 +133,7 @@ ORDER BY Version DESC -------------------------------------------------------------------------------------------------------------------------------- -- ADD EXTENDED ATTRIBUTES, IF CHANGED -------------------------------------------------------------------------------------------------------------------------------- -IF CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) +IF @ExtendedAttributes IS NOT NULL AND CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) BEGIN INSERT INTO ExtendedAttributes ( @@ -145,11 +151,7 @@ IF CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttribute -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE REFERENCES -------------------------------------------------------------------------------------------------------------------------------- -DECLARE @ReferenceCount INT -SELECT @ReferenceCount = COUNT(ReferenceKey) -FROM @References - -IF @ReferenceCount > 0 +IF EXISTS (SELECT TOP 1 NULL FROM @References) BEGIN EXEC UpdateReferences @TopicID, @References, From 9847b6ee29e5d8cb2a375f5e45f4de74f67d3813 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 3 Jan 2021 12:26:29 -0800 Subject: [PATCH 189/778] Removed `TopicReferences` support from `UpdateTopic` Previously, the `UpdateTopic` stored procedure had been updated to accept an "optional" `@References` parameter which, if present, would be relayed to `UpdateReferences` (8a7d5d1). Unfortunately, this introduces confusion as there's no way to differentiate between a `null` or empty table-valued typed parameter. As such, this leads to one of two scenarios: a) if processing is skipped if `@References` is empty, then there's no way to delete all topic references, but alternatively b) if no `@References` parameter is defined, but `@DeleteUnmatched` is, then all topic references would be deleted, assuming we didn't check for empty. Ideally, we'd be able to differentiate between null and empty here, and there wouldn't be a problem. In absence of that, however, we also don't want to force callers to include all topic references in order to use `@DeleteUnmatched`, assuming they only want to update e.g. the attributes. This is especially true since, in `SqlTopicRepository.Save()`, we don't pass `@References` since it may contain unresolved references; in those cases, the save is deferred. To mitigate that, the `@References` parameter has been removed. This means updates to `@References` must be made via the `UpdateReferences` stored procedure. Note: These conflicts don't exist on the `CreateTopic` stored procedure, since `@DeleteUnmatched` has no relevance assuming the topic is new. As such, I'm maintaining the `@References` parameter on `CreateTopic`, thus giving it a bit more flexibility. The `SqlTopicRepository.Save()` won't use this, but it may be useful for other calls, and especially utilities or maintenance scripts. That said, I'm a bit conflicted by the (further) inconsistency between the `CreateTopic` and `UpdateTopic` signatures, and may revisit this later, yet again. --- .../Stored Procedures/UpdateTopic.sql | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 433f3860..f77c25cf 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -10,7 +10,6 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , @ExtendedAttributes XML = NULL , - @References TopicReferences READONLY , @Version DATETIME = NULL , @DeleteUnmatched BIT = 0 AS @@ -148,16 +147,6 @@ IF @ExtendedAttributes IS NOT NULL AND CAST(@ExtendedAttributes AS NVARCHAR(MAX) ) END --------------------------------------------------------------------------------------------------------------------------------- --- UPDATE REFERENCES --------------------------------------------------------------------------------------------------------------------------------- -IF EXISTS (SELECT TOP 1 NULL FROM @References) - BEGIN - EXEC UpdateReferences @TopicID, - @References, - @DeleteUnmatched - END - -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- From 2580002487236c4b89cbfb0e804f23983a3adf96 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 3 Jan 2021 13:08:54 -0800 Subject: [PATCH 190/778] Ensured topic references are deleted during `DeleteTopic` Because of the referential integrity constraints on `TopicReferences`, a call to `DeleteTopic` would fail if any corresponding references in `TopicReferences` were not first deleted. And, of course, we don't want orphaned references to phantom topics! --- .../Stored Procedures/DeleteTopic.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/DeleteTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/DeleteTopic.sql index 349f79ed..46ef16a8 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/DeleteTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/DeleteTopic.sql @@ -95,6 +95,16 @@ FROM ExtendedAttributes ExtendedAttributes INNER JOIN @Topics Topics ON Topics.TopicId = ExtendedAttributes.TopicID +DELETE TopicReferences +FROM TopicReferences TopicReferences +INNER JOIN @Topics Topics + ON Topics.TopicId = TopicReferences.Source_TopicID + +DELETE TopicReferences +FROM TopicReferences TopicReferences +INNER JOIN @Topics Topics + ON Topics.TopicId = TopicReferences.Target_TopicID + DELETE Relationships FROM Relationships Relationships INNER JOIN @Topics Topics From 634857b7ce52aad48f63877d812f4c2f33e33743 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 3 Jan 2021 13:30:12 -0800 Subject: [PATCH 191/778] Introduced `UpdateAttributes` stored procedure Moves the logic from `UpdateTopic` into a standalone stored procedure. This is useful since the logic for this is becoming fairly complex. It also allow the `Attributes` table to be easily updated independent of other tables. (Technically, the `UpdateTopic` stored procedure also allowed this, but this makes it more explicit and intuitive.) --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + OnTopic.Data.Sql.Database/README.md | 1 + .../Stored Procedures/UpdateAttributes.sql | 89 +++++++++++++++++++ .../Stored Procedures/UpdateTopic.sql | 78 ++-------------- 4 files changed, 97 insertions(+), 72 deletions(-) create mode 100644 OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index ca49050a..e42e6233 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -123,6 +123,7 @@ + diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 5a3caa42..88cf9f93 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -33,6 +33,7 @@ The following is a summary of the most relevant stored procedures. - **[`DeleteTopic`](Stored%20Procedures/DeleteTopic.sql)**: Deletes an existing topic based on a `@TopicId`. - **[`MoveTopic`](Stored%20Procedures/MoveTopic.sql)**: Moves an existing topic based on a `@TopicId`, `@ParentId`, and `@SiblingId`. - **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Optionally deletes all relationships; these will need to be re-added using `UpdateRelationships`. Old attributes are persisted as previous versions. + - **[`UpdateAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the indexed attributes, optionally removing any whose values aren't matched in the provided `@Attributes` parameter. - **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). ## Functions diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql new file mode 100644 index 00000000..602c05d9 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql @@ -0,0 +1,89 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +-- Saves a set of AttributeValues to the Attributes table, while optionally accounting for deleted or unmatched attributes. +-- Optionally update ExtendedAttributes values if XML is included. +-------------------------------------------------------------------------------------------------------------------------------- + +CREATE PROCEDURE [dbo].[UpdateAttributes] + @TopicID INT, + @Attributes AttributeValues READONLY , + @Version DATETIME = NULL , + @DeleteUnmatched BIT = 0 +AS + +-------------------------------------------------------------------------------------------------------------------------------- +-- INSERT NEW ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO Attributes ( + TopicID , + AttributeKey , + AttributeValue , + Version + ) +SELECT @TopicID, + AttributeKey, + AttributeValue, + @Version +FROM @Attributes New +OUTER APPLY ( + SELECT TOP 1 + AttributeValue AS ExistingValue + FROM Attributes + WHERE TopicID = @TopicID + AND AttributeKey = New.AttributeKey + ORDER BY Version DESC +) Existing +WHERE ISNULL(AttributeValue, '') != '' + AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') + +-------------------------------------------------------------------------------------------------------------------------------- +-- INSERT NULL ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO Attributes ( + TopicID , + AttributeKey , + AttributeValue , + Version + ) +SELECT @TopicID, + AttributeKey, + '', + @Version +FROM @Attributes New +CROSS APPLY ( + SELECT TOP 1 + AttributeValue AS ExistingValue + FROM Attributes + WHERE TopicID = @TopicID + AND AttributeKey = New.AttributeKey + ORDER BY Version DESC +) Existing +WHERE ISNULL(AttributeValue, '') = '' + AND ExistingValue != '' + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE UNMATCHED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +IF @DeleteUnmatched = 1 + BEGIN + INSERT + INTO Attributes + SELECT @TopicID, + Existing.AttributeKey, + '', + @Version + FROM AttributeIndex Existing + LEFT JOIN @Attributes New + ON Existing.TopicID = @TopicID + AND Existing.AttributeKey = New.AttributeKey + WHERE ISNULL(New.AttributeKey, '') = '' + AND Existing.TopicID = @TopicID + END + +-------------------------------------------------------------------------------------------------------------------------------- +-- RETURN TOPIC ID +-------------------------------------------------------------------------------------------------------------------------------- +RETURN @TopicID; \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index f77c25cf..e335485d 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -1,7 +1,7 @@ -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE TOPIC -------------------------------------------------------------------------------------------------------------------------------- --- Used to update the attributes of a provided node +-- Used to update the attributes of a provided topic, including core, indexed, and extended attributes. -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[UpdateTopic] @@ -42,80 +42,14 @@ IF @Key IS NOT NULL OR @ContentType IS NOT NULL END -------------------------------------------------------------------------------------------------------------------------------- --- INSERT NEW ATTRIBUTES +-- UPDATE ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- IF EXISTS (SELECT TOP 1 NULL FROM @Attributes) BEGIN - INSERT - INTO Attributes ( - TopicID , - AttributeKey , - AttributeValue , - Version - ) - SELECT @TopicID, - AttributeKey, - AttributeValue, - @Version - FROM @Attributes New - OUTER APPLY ( - SELECT TOP 1 - AttributeValue AS ExistingValue - FROM Attributes - WHERE TopicID = @TopicID - AND AttributeKey = New.AttributeKey - ORDER BY Version DESC - ) Existing - WHERE ISNULL(AttributeValue, '') != '' - AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') - END - --------------------------------------------------------------------------------------------------------------------------------- --- INSERT NULL ATTRIBUTES --------------------------------------------------------------------------------------------------------------------------------- -IF EXISTS (SELECT TOP 1 NULL FROM @Attributes) - BEGIN - INSERT - INTO Attributes ( - TopicID , - AttributeKey , - AttributeValue , - Version - ) - SELECT @TopicID, - AttributeKey, - '', - @Version - FROM @Attributes New - CROSS APPLY ( - SELECT TOP 1 - AttributeValue AS ExistingValue - FROM Attributes - WHERE TopicID = @TopicID - AND AttributeKey = New.AttributeKey - ORDER BY Version DESC - ) Existing - WHERE ISNULL(AttributeValue, '') = '' - AND ExistingValue != '' - END - --------------------------------------------------------------------------------------------------------------------------------- --- DELETE UNMATCHED ATTRIBUTES --------------------------------------------------------------------------------------------------------------------------------- -IF @DeleteUnmatched = 1 - BEGIN - INSERT - INTO Attributes - SELECT @TopicID, - Existing.AttributeKey, - '', - @Version - FROM AttributeIndex Existing - LEFT JOIN @Attributes New - ON Existing.TopicID = @TopicID - AND Existing.AttributeKey = New.AttributeKey - WHERE ISNULL(New.AttributeKey, '') = '' - AND Existing.TopicID = @TopicID + EXEC UpdateAttributes @TopicID, + @Attributes, + @Version, + @DeleteUnmatched END -------------------------------------------------------------------------------------------------------------------------------- From 3664b2b99f9b882eb113ecd81224e19a1b2c3e28 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 3 Jan 2021 13:31:23 -0800 Subject: [PATCH 192/778] Introduced `UpdateExtendedAttributes` stored procedure Moves the logic from `UpdateTopic` into a standalone stored procedure. This is useful since the logic for this is a bit complex. It also allow the `ExtendedAttributes` table to be easily updated independent of other tables. (Technically, the `UpdateTopic` stored procedure also allowed this, but this makes it more explicit and intuitive.) --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + OnTopic.Data.Sql.Database/README.md | 1 + .../UpdateExtendedAttributes.sql | 46 +++++++++++++++++++ .../Stored Procedures/UpdateTopic.sql | 27 ++--------- 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index e42e6233..cbcf4a2a 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -124,6 +124,7 @@ + diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 88cf9f93..0d7797f8 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -34,6 +34,7 @@ The following is a summary of the most relevant stored procedures. - **[`MoveTopic`](Stored%20Procedures/MoveTopic.sql)**: Moves an existing topic based on a `@TopicId`, `@ParentId`, and `@SiblingId`. - **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Optionally deletes all relationships; these will need to be re-added using `UpdateRelationships`. Old attributes are persisted as previous versions. - **[`UpdateAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the indexed attributes, optionally removing any whose values aren't matched in the provided `@Attributes` parameter. + - **[`UpdateExtendedAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the extended attributes, assuming the `@ExtendedAttributes` parameter doesn't match the previous value. - **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). ## Functions diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql new file mode 100644 index 00000000..c6dcbab9 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql @@ -0,0 +1,46 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE EXTENDED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +-- Saves ExtendedAttributes values if XML is included. +-------------------------------------------------------------------------------------------------------------------------------- + +CREATE PROCEDURE [dbo].[UpdateExtendedAttributes] + @TopicID INT, + @ExtendedAttributes XML = NULL , + @Version DATETIME = NULL , + @DeleteUnmatched BIT = 0 +AS + +-------------------------------------------------------------------------------------------------------------------------------- +-- PULL PREVIOUS EXTENDED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @PreviousExtendedAttributes XML + +SELECT TOP 1 + @PreviousExtendedAttributes = AttributesXml +FROM ExtendedAttributes +WHERE TopicID = @TopicID +ORDER BY Version DESC + +-------------------------------------------------------------------------------------------------------------------------------- +-- ADD EXTENDED ATTRIBUTES, IF CHANGED +-------------------------------------------------------------------------------------------------------------------------------- +IF CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) + BEGIN + INSERT + INTO ExtendedAttributes ( + TopicID , + AttributesXml , + Version + ) + VALUES ( + @TopicID , + @ExtendedAttributes , + @Version + ) + END + +-------------------------------------------------------------------------------------------------------------------------------- +-- RETURN TOPIC ID +-------------------------------------------------------------------------------------------------------------------------------- +RETURN @TopicID; \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index e335485d..b35f7e03 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -52,33 +52,14 @@ IF EXISTS (SELECT TOP 1 NULL FROM @Attributes) @DeleteUnmatched END --------------------------------------------------------------------------------------------------------------------------------- --- PULL PREVIOUS EXTENDED ATTRIBUTES --------------------------------------------------------------------------------------------------------------------------------- -DECLARE @PreviousExtendedAttributes XML - -SELECT TOP 1 - @PreviousExtendedAttributes = AttributesXml -FROM ExtendedAttributes -WHERE TopicID = @TopicID -ORDER BY Version DESC - -------------------------------------------------------------------------------------------------------------------------------- -- ADD EXTENDED ATTRIBUTES, IF CHANGED -------------------------------------------------------------------------------------------------------------------------------- -IF @ExtendedAttributes IS NOT NULL AND CAST(@ExtendedAttributes AS NVARCHAR(MAX)) != CAST(@PreviousExtendedAttributes AS NVARCHAR(MAX)) +IF @ExtendedAttributes IS NOT NULL BEGIN - INSERT - INTO ExtendedAttributes ( - TopicID , - AttributesXml , - Version - ) - VALUES ( - @TopicID , - @ExtendedAttributes , - @Version - ) + EXEC UpdateExtendedAttributes @TopicID, + @ExtendedAttributes, + @Version END -------------------------------------------------------------------------------------------------------------------------------- From d6bb4b8872e9f143fd2e1ad6a1aadc3dffb00fc8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 3 Jan 2021 13:32:14 -0800 Subject: [PATCH 193/778] Updated documentation based on previous updates With the introduction of `TopicReferences` (31144cc), there were quite a few updates to the database, but these weren't correctly updated in the documentation. This is corrected here. --- OnTopic.Data.Sql.Database/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 0d7797f8..7034f754 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -17,7 +17,8 @@ The following is a summary of the most relevant tables. - **[`Topics`](Tables/Topics.sql)**: Represents the core hierarchy of topics, encoded using a nested set model. - **[`Attributes`](Tables/Attributes.sql)**: Represents key/value pairs of topic attributes, including historical versions. - **[`ExtendedAttributes`](Tables/ExtendedAttributes.sql)**: Represents an XML-based representation of non-indexed attributes, which are too long for `Attributes`. -- **[`Relationships`](Tables/Relationships.sql)**: Represents relationships between topics, segmented by a `RelationshipKey`. +- **[`TopicReferences`](Tables/TopicReferences.sql)**: Represents (1:1) references between topics, segmented by a `ReferenceKey`. +- **[`Relationships`](Tables/Relationships.sql)**: Represents (1:n) relationships between topics, segmented by a `RelationshipKey`. > *Note:* Neither `Topics` nor `Relationships` are subject to tracking versions. Changes to these records are permanent. @@ -35,6 +36,7 @@ The following is a summary of the most relevant stored procedures. - **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Optionally deletes all relationships; these will need to be re-added using `UpdateRelationships`. Old attributes are persisted as previous versions. - **[`UpdateAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the indexed attributes, optionally removing any whose values aren't matched in the provided `@Attributes` parameter. - **[`UpdateExtendedAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the extended attributes, assuming the `@ExtendedAttributes` parameter doesn't match the previous value. +- **[`UpdateReferences`](Stored%20Procedures/UpdateReferences.sql)**: Associates a reference with a topic based on a `@TopicId` and a `TopicReferences` array of `@ReferencKey`s and `@Target_TopicId`s. - **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). ## Functions @@ -42,6 +44,7 @@ The following is a summary of the most relevant stored procedures. - **[`GetUniqueKey`](Functions/GetUniqueKey.sql)**: Retrieves a topic's `UniqueKey` based on a corresponding `@TopicID`. - **[`GetParentID`](Functions/GetParentID.sql)**: Retrieves a topic's parent's `TopicID` based the child's `@TopicID`. - **[`GetAttributes`](functions/GetAttributes.sql)**: Given a `@TopicID`, provides the latest version of each attribute value from both `Attributes` and `ExtendedAttributes`, excluding key attributes (i.e., `Key`, `ContentType`, and `ParentID`). +- **[`GetChildTopicIDs`](functions/GetChildTopicIDs.sql)**: Given a `@TopicID`, returns a list of `TopicID`s that are immediate children. - **[`GetExtendedAttribute`](Functions/GetExtendedAttribute.sql)**: Retrieves an individual attribute from a topic's latest `ExtendedAttributes` record. - **[`FindTopicIDs`](Functions/FindTopicIDs.sql)**: Retrieves all `TopicID`s under a given `@TopicID` that match the `@AttributeKey` and `@AttributeValue`. Accepts `@IsExtendedAttribute` and `@UsePartialMatch`. @@ -54,4 +57,5 @@ The majority of the views provide records corresponding to the latest version of ## Types User-defined table valued types are used to relay arrays of information to (and between) the stored procedures. These can be mimicked in C# using e.g. a `DataTable`. These include: - **[`AttributeValues`](Types/AttributeValues.sql)**: Defines a table with an `AttributeKey` `Varchar(128)` and `AttributeValue` `Varchar(255)` columns. -- **[`TopicList`](Types/TopicList.sql)**: Defines a table with a single `TopicId` `Int` column for passing lists of topics. \ No newline at end of file +- **[`TopicList`](Types/TopicList.sql)**: Defines a table with a single `TopicId` `Int` column for passing lists of topics. +- **[`TopicReferences`](Types/TopicReferences.sql)**: Defines a table with a `ReferenceKey` `Varchar(128)` and a `Target_TopicId` `Int` column for passing lists of topic references. \ No newline at end of file From 718586282d6e3f6ac663663784037480756d937c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 13:39:56 -0800 Subject: [PATCH 194/778] Improve order of `IsDirty()` check on `Topic` Most topics won't have relationships or references, but will have attributes. And those attributes are more likely to change per edit. As such, even though the `Attributes.IsDirty()` check is a bit more expensive than e.g. `References.IsDirty()`, it's probably more optimal to check the attributes first, thus potentially preventing the need to check relationships or references if the attributes have already been determined to be dirty. --- OnTopic/Topic.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 42fc6ab2..c7b55c2e 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -554,23 +554,25 @@ public string GetWebPath() { /// Determines if the topic is dirty, optionally checking and . /// /// - /// Determines if and should be checked. + /// Determines if , , and should be checked. /// /// /// Optionally excludes s whose keys start with LastModified. This is useful for /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. /// - /// + /// + /// Returns true if the , , or, optionally, any collections have been modified. + /// public bool IsDirty(bool checkCollections = false, bool excludeLastModified = false) { if (!_isDirty && checkCollections) { - _isDirty = Relationships.IsDirty(); + _isDirty = Attributes.IsDirty(excludeLastModified); } if (!_isDirty && checkCollections) { - _isDirty = References.IsDirty; + _isDirty = Relationships.IsDirty(); } if (!_isDirty && checkCollections) { - _isDirty = Attributes.IsDirty(excludeLastModified); + _isDirty = References.IsDirty; } return _isDirty; } From 265e2e56e3984b06f87489e76a8007a43591c5f2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 13:48:21 -0800 Subject: [PATCH 195/778] Convert `References.IsDirty` from property to method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is more consistent with other collections—such as `AttributeValueCollection` and `RelatedTopicCollection`—as well as `Topic` itself. Technically, this is just operating as a bit, and could continue to be a property, but treating it as a method gives us more flexibility in the future to e.g., add optional parameters, if needed, or track individual references, as we do with attributes. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 6 +++-- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 8 +++---- .../Collections/TopicReferenceDictionary.cs | 24 ++++++++++++------- OnTopic/Topic.cs | 2 +- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index a4198ac8..35637dd7 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -320,7 +320,7 @@ SqlDateTime version var areReferencesResolved = true; var isTopicDirty = topic.IsDirty(); var areRelationshipsDirty = topic.Relationships.IsDirty(); - var areReferencesDirty = topic.References.IsDirty; + var areReferencesDirty = topic.References.IsDirty(); var areAttributesDirty = topic.Attributes.IsDirty(true); var extendedAttributeList = GetAttributes(topic, isExtendedAttribute: true); var indexedAttributeList = GetAttributes( @@ -711,7 +711,9 @@ private static void PersistReferences(Topic topic, SqlConnection connection) { command.ExecuteNonQuery(); //Reset isDirty, assuming there aren't any unresolved references - topic.References.IsDirty = references.Rows.Count < topic.References.Count; + if (references.Rows.Count == topic.References.Count) { + topic.References.MarkClean(); + } } diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index b568353c..7abce93d 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -37,7 +37,7 @@ public void Add_NewReference_IsDirty() { topic.References.Add("Reference", reference); Assert.AreEqual(1, topic.References.Count); - Assert.IsTrue(topic.References.IsDirty); + Assert.IsTrue(topic.References.IsDirty()); } @@ -58,7 +58,7 @@ public void SetTopic_NewReference_NotDirty() { topic.References.SetTopic("Reference", reference, false); Assert.AreEqual(1, topic.References.Count); - Assert.IsFalse(topic.References.IsDirty); + Assert.IsFalse(topic.References.IsDirty()); } @@ -79,7 +79,7 @@ public void Remove_ExistingReference_IsDirty() { topic.References.Remove("Reference"); Assert.AreEqual(0, topic.References.Count); - Assert.IsTrue(topic.References.IsDirty); + Assert.IsTrue(topic.References.IsDirty()); } @@ -101,7 +101,7 @@ public void Clear_ExistingReferences_IsDirty() { topic.References.Clear(); Assert.AreEqual(0, topic.References.Count); - Assert.IsTrue(topic.References.IsDirty); + Assert.IsTrue(topic.References.IsDirty()); } diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index cac63072..a9f91900 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -7,7 +7,6 @@ using System.Collections; using System.Collections.Generic; using OnTopic.Internal.Diagnostics; -using OnTopic.Querying; namespace OnTopic.Collections { @@ -24,6 +23,7 @@ public class TopicReferenceDictionary : IDictionary { \-------------------------------------------------------------------------------------------------------------------------*/ readonly Topic _parent; readonly IDictionary _storage; + private bool _isDirty; /*========================================================================================================================== | CONSTRUCTOR @@ -70,7 +70,7 @@ public Topic this[string referenceKey] { "A topic reference may not point to itself." ); if (!_storage.TryGetValue(referenceKey, out var existing) || existing != value) { - IsDirty = true; + _isDirty = true; } _storage[referenceKey] = value; } @@ -110,7 +110,7 @@ void ICollection>.Add(KeyValuePair it | Mark dirty \-----------------------------------------------------------------------------------------------------------------------*/ if (!_storage.TryGetValue(item.Key, out var existing) || existing != item.Value) { - IsDirty = true; + _isDirty = true; } /*------------------------------------------------------------------------------------------------------------------------ @@ -150,7 +150,7 @@ public void Add(string key, Topic value) { /// removed. /// public void SetTopic(string key, Topic? value, bool? isDirty = null) { - var wasDirty = IsDirty; + var wasDirty = _isDirty; if (value is null) { if (ContainsKey(key)) { Remove(key); @@ -160,7 +160,7 @@ public void SetTopic(string key, Topic? value, bool? isDirty = null) { this[key] = value; } if (wasDirty is false && isDirty is false) { - IsDirty = false; + _isDirty = false; } } @@ -174,7 +174,7 @@ public void Clear() { | Mark dirty \-----------------------------------------------------------------------------------------------------------------------*/ if (Count > 0) { - IsDirty = true; + _isDirty = true; } /*------------------------------------------------------------------------------------------------------------------------ @@ -238,7 +238,7 @@ public bool Remove(string key) { \-----------------------------------------------------------------------------------------------------------------------*/ if (TryGetValue(key, out var existing)) { existing.IncomingRelationships.RemoveTopic(key, _parent, true); - IsDirty = true; + _isDirty = true; } /*------------------------------------------------------------------------------------------------------------------------ @@ -277,7 +277,15 @@ public bool Remove(string key) { /// Determines if the dictionary has been modified. This value is set to true any time a new item is inserted or /// removed from the dictionary. /// - public bool IsDirty { get; set; } + public bool IsDirty() => _isDirty; + + /*========================================================================================================================== + | MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Resets the status of the . + /// + public void MarkClean() => _isDirty = false; } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index c7b55c2e..784614cb 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -572,7 +572,7 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa _isDirty = Relationships.IsDirty(); } if (!_isDirty && checkCollections) { - _isDirty = References.IsDirty; + _isDirty = References.IsDirty(); } return _isDirty; } From c37934b6c771f83c045476ad5cc2e816ca8b411a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 13:51:23 -0800 Subject: [PATCH 196/778] Added a `MarkClean()` method to `RelatedTopicCollection` There was previously no way to mark all relationships as clean without looping through the list of `NamedTopicCollections` and flipping the `IsDirty` value of each. That's all the new `MarkClean()` method does as well, but it centralizes the logic in the `RelatedTopicCollection`, and offers consistency with other collections that use the `IsDirty()`/`MarkClean()` convention (e.g., `AttributeValueCollection`). --- OnTopic/Collections/RelatedTopicCollection.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index 6b71a624..a868ef49 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -314,6 +314,19 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool /// public bool IsDirty() => Items.Any(r => r.IsDirty); + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets the property of every in this to false. + /// + public void MarkClean() { + foreach (var relationship in Items) { + relationship.IsDirty = false; + } + } + /*========================================================================================================================== | OVERRIDE: INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ From 28ea6ea0d27ddc8f0a73a74e777ab985560d76a1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 13:54:02 -0800 Subject: [PATCH 197/778] =?UTF-8?q?Allow=20topics=20to=20be=20marked=20as?= =?UTF-8?q?=20clean=E2=80=94and=20optionally=20include=20collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This provides consistency with other classes that implement `IsDirty()` (e.g., `AttributeValueCollection`, `RelatedTopicCollection` (c37934b), and `TopicReferenceDictionary` (265e2e5). By default, it only covers the local topic—i.e., `Key` and `ContentType`—but it will also call `MarkClean()` on downstream collections if the optional `includeCollections` parameter is set. --- OnTopic/Topic.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 784614cb..375f1927 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -577,6 +577,29 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa return _isDirty; } + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Resets the status of the —and, optionally, that of all collections, using the + /// parameter. + /// + /// + /// Determines if , , and should be included. + /// + /// + /// The value that the attributes were last saved. This corresponds to the . + /// + public void MarkClean(bool includeCollections = false, DateTime? version = null) { + _isDirty = false; + if (includeCollections) { + Attributes.MarkClean(version); + Relationships.MarkClean(); + References.MarkClean(); + } + } + #endregion #region Relationship and Collection Properties From 8facaec41de1f4ec6843a2508b26b14899b193f8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 13:54:54 -0800 Subject: [PATCH 198/778] Fixed parameters in XML doc refrences These were not properly aligned with the latest method signatures they call. --- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index 7abce93d..e8c97057 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -14,9 +14,9 @@ namespace OnTopic.Tests { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the , with a particular emphasis on the custom features - /// such as , , , and the cross-referencing of reciprocal values in the - /// property. + /// such as , , , and the cross-referencing of reciprocal + /// values in the property. /// [TestClass] public class TopicReferenceDictionaryTest { @@ -67,7 +67,7 @@ public void SetTopic_NewReference_NotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a topic reference, removes that reference using , and confirms that is set. + /// "TopicReferenceDictionary.Remove(String)"/> , and confirms that is set. /// [TestMethod] public void Remove_ExistingReference_IsDirty() { @@ -216,7 +216,7 @@ public void Add_NewReference_TopicIsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// correctly returns the . + /// correctly returns the . /// [TestMethod] public void GetTopic_ExistingReference_ReturnsTopic() { From 7a9bbafd4767f4921d6dda3ebfaca48f7873aa25 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 14:19:22 -0800 Subject: [PATCH 199/778] Convert `NamedTopicCollection.IsDirty` from property to method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is more consistent with other collections—such as `AttributeValueCollection`, `RelatedTopicCollection`, and `ReferenceTopicDictionary`—as well as `Topic` itself. Technically, this is just operating as a bit, and could continue to be a property, but treating it as a method gives us more flexibility in the future to e.g., add optional parameters, if needed, or track individual references, as we do with attributes. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++- OnTopic.Tests/NamedTopicCollection.cs | 22 ++++++------- OnTopic/Collections/NamedTopicCollection.cs | 31 +++++++++++++------ OnTopic/Collections/RelatedTopicCollection.cs | 8 +++-- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 35637dd7..3556db3b 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -655,7 +655,9 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { command.ExecuteNonQuery(); //Reset isDirty, assuming there aren't any unresolved references - relatedTopics.IsDirty = savedTopics.Count() < relatedTopics.Count; + if (savedTopics.Count() == relatedTopics.Count) { + relatedTopics.MarkClean(); + } } diff --git a/OnTopic.Tests/NamedTopicCollection.cs b/OnTopic.Tests/NamedTopicCollection.cs index 2f70eccb..a7465755 100644 --- a/OnTopic.Tests/NamedTopicCollection.cs +++ b/OnTopic.Tests/NamedTopicCollection.cs @@ -33,7 +33,7 @@ public void AddTopic_IsDirty() { relationships.Add(related); - Assert.IsTrue(relationships.IsDirty); + Assert.IsTrue(relationships.IsDirty()); } @@ -52,7 +52,7 @@ public void AddTopic_IsDuplicate_IsNotDirty() { var related2 = TopicFactory.Create("Topic", "Page"); relationships.Add(related1); - relationships.IsDirty = false; + relationships.MarkClean(); try { relationships.Add(related2); @@ -61,7 +61,7 @@ public void AddTopic_IsDuplicate_IsNotDirty() { //Expected due to duplicate key } - Assert.IsFalse(relationships.IsDirty); + Assert.IsFalse(relationships.IsDirty()); } @@ -88,7 +88,7 @@ public void AddTopic_IsDuplicate_StaysDirty() { //Expected due to duplicate key } - Assert.IsTrue(relationships.IsDirty); + Assert.IsTrue(relationships.IsDirty()); } @@ -107,10 +107,10 @@ public void RemoveTopic_IsDirty() { var related = TopicFactory.Create("Topic", "Page"); relationships.Add(related); - relationships.IsDirty = false; + relationships.MarkClean(); relationships.Remove(related); - Assert.IsTrue(relationships.IsDirty); + Assert.IsTrue(relationships.IsDirty()); } @@ -129,7 +129,7 @@ public void RemoveTopic_MissingTopic_IsNotDirty() { relationships.Remove(related); - Assert.IsFalse(relationships.IsDirty); + Assert.IsFalse(relationships.IsDirty()); } @@ -150,7 +150,7 @@ public void RemoveTopic_MissingTopic_StaysDirty() { relationships.Add(related); relationships.Remove(missing); - Assert.IsTrue(relationships.IsDirty); + Assert.IsTrue(relationships.IsDirty()); } @@ -168,10 +168,10 @@ public void Clear_ExistingTopics_IsDirty() { var related = TopicFactory.Create("Topic", "Page"); relationships.Add(related); - relationships.IsDirty = false; + relationships.MarkClean(); relationships.Clear(); - Assert.IsTrue(relationships.IsDirty); + Assert.IsTrue(relationships.IsDirty()); } @@ -189,7 +189,7 @@ public void Clear_NoTopics_IsNotDirty() { relationships.Clear(); - Assert.IsFalse(relationships.IsDirty); + Assert.IsFalse(relationships.IsDirty()); } diff --git a/OnTopic/Collections/NamedTopicCollection.cs b/OnTopic/Collections/NamedTopicCollection.cs index b32c65fc..098dec11 100644 --- a/OnTopic/Collections/NamedTopicCollection.cs +++ b/OnTopic/Collections/NamedTopicCollection.cs @@ -19,6 +19,11 @@ namespace OnTopic.Collections { /// public class NamedTopicCollection: TopicCollection { + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private bool _isDirty; + /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ @@ -41,19 +46,27 @@ public NamedTopicCollection(string name = "", IEnumerable? topics = null) /// Determines if the collection has been modified. This value is set to true any time a new item is inserted or /// removed from the collection. /// - public bool IsDirty { get; set; } + public bool IsDirty() => _isDirty; + + /*========================================================================================================================== + | MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Resets the status of the . + /// + public void MarkClean() => _isDirty = false; /*========================================================================================================================== | INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// /// When inserting an item, determine if it will change the collection; if it will, mark the collection as . + /// cref="_isDirty"/>. /// protected override void InsertItem(int index, Topic item) { Contract.Requires(index, nameof(index)); Contract.Requires(item, nameof(item)); - IsDirty = IsDirty || !Contains(item.Key); + _isDirty = _isDirty || !Contains(item.Key); base.InsertItem(index, item); } @@ -62,12 +75,12 @@ protected override void InsertItem(int index, Topic item) { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// When updating an existing item, determine if it will change the collection; if it will, mark the collection as . + /// cref="_isDirty"/>. /// protected override void SetItem(int index, Topic item) { Contract.Requires(index, nameof(index)); Contract.Requires(item, nameof(item)); - IsDirty = IsDirty || !Contains(item.Key); + _isDirty = _isDirty || !Contains(item.Key); base.SetItem(index, item); } @@ -75,11 +88,11 @@ protected override void SetItem(int index, Topic item) { | REMOVE ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// When removing an item from the collection, mark the collection as . + /// When removing an item from the collection, mark the collection as . /// protected override void RemoveItem(int index) { Contract.Requires(index, nameof(index)); - IsDirty = IsDirty || index < Count; + _isDirty = _isDirty || index < Count; base.RemoveItem(index); } @@ -87,10 +100,10 @@ protected override void RemoveItem(int index) { | CLEAR ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// When clearing the collection, mark the collection as if it had items in it. + /// When clearing the collection, mark the collection as if it had items in it. /// protected override void ClearItems() { - IsDirty = IsDirty || Count > 0; + _isDirty = _isDirty || Count > 0; base.ClearItems(); } diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/Collections/RelatedTopicCollection.cs index a868ef49..8cce0e15 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/Collections/RelatedTopicCollection.cs @@ -287,7 +287,9 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool var topics = this[relationshipKey]; if (!topics.Contains(topic.Key)) { topics.Add(topic); - topics.IsDirty = isDirty?? topics.IsDirty; + if (!(isDirty?? topics.IsDirty())) { + topics.MarkClean(); + } } /*------------------------------------------------------------------------------------------------------------------------ @@ -312,7 +314,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool /// Evaluates each of the child s to determine if any of them are set to . If they are, returns true. /// - public bool IsDirty() => Items.Any(r => r.IsDirty); + public bool IsDirty() => Items.Any(r => r.IsDirty()); /*========================================================================================================================== | METHOD: MARK CLEAN @@ -323,7 +325,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool /// public void MarkClean() { foreach (var relationship in Items) { - relationship.IsDirty = false; + relationship.MarkClean(); } } From 6bbc573d91f6c88d3ed4126282cfd314322bd54a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 14:33:18 -0800 Subject: [PATCH 200/778] Introduced unit test for `Topic.IsDirty(checkCollections)` --- OnTopic.Tests/TopicTest.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index e039ea5a..d68af4a9 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -385,5 +385,27 @@ public void IsDirty_ChangeKey_ReturnsTrue() { } + /*========================================================================================================================== + | IS DIRTY: CHANGE COLLECTIONS: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates an existing topic, changes the , , and collections, and confirms that returns + /// true. + /// + [TestMethod] + public void IsDirty_ChangeCollections_ReturnsTrue() { + + var topic = TopicFactory.Create("Topic", "Page", 1); + var related = TopicFactory.Create("Related", "Page", 2); + + topic.Attributes.SetValue("Related", related.Key); + topic.References.SetTopic("Related", related); + topic.Relationships.SetTopic("Related", related); + + Assert.IsTrue(topic.IsDirty(true)); + + } + } //Class } //Namespace \ No newline at end of file From 48e8ed47ce1d5977a128c0c8456b7e1cb4bd418d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 14:33:38 -0800 Subject: [PATCH 201/778] Introduced unit test for `Topic.MarkClean(includeCollections)` --- OnTopic.Tests/TopicTest.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index d68af4a9..97d1bc6f 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -407,5 +407,29 @@ public void IsDirty_ChangeCollections_ReturnsTrue() { } + /*========================================================================================================================== + | MARK CLEAN: CHANGE COLLECTION: RESETS IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates an existing topic, changes the , , and collections, and confirms that resets the + /// value of . + /// + [TestMethod] + public void MarkClean_ChangeCollection_ResetIsDirty() { + + var topic = TopicFactory.Create("Topic", "Page"); + var related = TopicFactory.Create("Related", "Page"); + + topic.Attributes.SetValue("Related", related.Key); + topic.References.SetTopic("Related", related); + topic.Relationships.SetTopic("Related", related); + + topic.MarkClean(true); + + Assert.IsFalse(topic.IsDirty(true)); + + } + } //Class } //Namespace \ No newline at end of file From c2a8fe8714619e3b682f0c82e70a86d63755109f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 4 Jan 2021 14:49:49 -0800 Subject: [PATCH 202/778] Changed return type for `ISqlTopicRepository.Save()` to `void` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `ISqlTopicRepository.Save()` method—and, thus, all of its implementations—expected a return type of `int`, representing the `Topic.Id`. This is most valuable when saving a new topic, which wouldn't have had an `Id` previously assigned. In most cases, however, this is never expected or needed. And, in the few cases where it is, it can easily be retrieved from `Topic.Id` instead—which is always available, since it's a required parameter of `Save()`. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 16 +++++++--------- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 7 +------ OnTopic/Repositories/ITopicRepository.cs | 3 +-- OnTopic/Repositories/TopicRepositoryBase.cs | 3 +-- 6 files changed, 12 insertions(+), 21 deletions(-) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 00adc01e..7fcf94ec 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -135,7 +135,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false) => + public override void Save(Topic topic, bool isRecursive = false) => _dataProvider.Save(topic, isRecursive); /*========================================================================================================================== diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 3556db3b..97a6c11e 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -245,7 +245,7 @@ public override Topic Load(int topicId, DateTime version) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save([NotNull]Topic topic, bool isRecursive = false) { + public override void Save([NotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Establish dependencies @@ -260,7 +260,7 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Handle first pass \-----------------------------------------------------------------------------------------------------------------------*/ - var topicId = Save(topic, isRecursive, connection, unresolvedTopics, version); + Save(topic, isRecursive, connection, unresolvedTopics, version); /*------------------------------------------------------------------------------------------------------------------------ | Attempt to resolve outstanding relationships @@ -270,10 +270,9 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false) { } /*------------------------------------------------------------------------------------------------------------------------ - | Return value + | Close shared connection \-----------------------------------------------------------------------------------------------------------------------*/ connection.Close(); - return topicId; } @@ -301,7 +300,7 @@ public override int Save([NotNull]Topic topic, bool isRecursive = false) { /// Determines whether or not to recursively save . /// The open to use for executing s. /// A list of s with unresolved topic references. - private int Save( + private void Save( [NotNull]Topic topic, bool isRecursive, SqlConnection connection, @@ -351,7 +350,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ if (!isDirty) { recurse(); - return topic.Id; + return; } /*------------------------------------------------------------------------------------------------------------------------ @@ -490,13 +489,12 @@ SqlDateTime version } /*------------------------------------------------------------------------------------------------------------------------ - | Return value + | Recuse over any children \-----------------------------------------------------------------------------------------------------------------------*/ recurse(); - return topic.Id; /*------------------------------------------------------------------------------------------------------------------------ - | Recurse + | Function: Recurse \-----------------------------------------------------------------------------------------------------------------------*/ void recurse() { if (isRecursive) { diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 064ef57e..d40cd7b5 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -42,7 +42,7 @@ public DummyTopicRepository() : base() { } | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false) => throw new NotImplementedException(); + public override void Save(Topic topic, bool isRecursive = false) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MOVE diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index dd158822..21e7b9f4 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -105,7 +105,7 @@ public StubTopicRepository() : base() { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override int Save(Topic topic, bool isRecursive = false) { + public override void Save(Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Call base method - will trigger any events associated with the save @@ -128,11 +128,6 @@ public override int Save(Topic topic, bool isRecursive = false) { } } - /*------------------------------------------------------------------------------------------------------------------------ - | Return identity - \-----------------------------------------------------------------------------------------------------------------------*/ - return topic.Id; - } /*========================================================================================================================== diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 7163e7bd..01887282 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -103,12 +103,11 @@ public interface ITopicRepository { /// /// Boolean indicator nothing whether to recurse through the topic's descendants and save them as well. /// - /// The integer return value from the execution of the topics_UpdateTopic stored procedure. /// /// topic is not null /// /// topic - int Save(Topic topic, bool isRecursive = false); + void Save(Topic topic, bool isRecursive = false); /*========================================================================================================================== | METHOD: MOVE diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 483d111b..e1d0e308 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -299,7 +299,7 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual int Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { + public virtual void Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -386,7 +386,6 @@ _contentTypeDescriptors is not null && | Reset original key \-----------------------------------------------------------------------------------------------------------------------*/ topic.OriginalKey = null; - return -1; } From 9074c88d888dd270947e6d0fd20e2e3fe40f3024 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 5 Jan 2021 16:12:59 -0800 Subject: [PATCH 203/778] Update `Topic` constructor to default to a null `parent` The root topic will have a `null` parent, as well many topics used in unit tests. The constructor should _expect_ that the parent _might_ be `null`. Further, it should default to `null`, if it's not provided. While I was at it, updated `Topic` derivatives, including `AttributeDescriptor` and `ContentTypeDescriptor`. --- OnTopic/Metadata/AttributeDescriptor.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/Topic.cs | 7 +++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 647edc14..8455ea06 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -65,7 +65,7 @@ public abstract class AttributeDescriptor : Topic { protected AttributeDescriptor( string key, string contentType, - Topic parent, + Topic? parent = null, int id = -1 ) : base( key, diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 4fa8c1ad..ba2166d8 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -66,7 +66,7 @@ public class ContentTypeDescriptor : Topic { public ContentTypeDescriptor( string key, string contentType, - Topic parent, + Topic? parent = null, int id = -1 ) : base( key, diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 375f1927..5cd251a8 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -56,7 +56,7 @@ public class Topic { /// Thrown when the class representing the content type is found, but doesn't derive from . /// /// A strongly-typed instance of the class based on the target content type. - public Topic(string key, string contentType, Topic parent, int id = -1) { + public Topic(string key, string contentType, Topic? parent = null, int id = -1) { /*------------------------------------------------------------------------------------------------------------------------ | Set relationships @@ -80,7 +80,10 @@ public Topic(string key, string contentType, Topic parent, int id = -1) { \-----------------------------------------------------------------------------------------------------------------------*/ Key = key; ContentType = contentType; - Parent = parent; + + if (parent is not null) { + Parent = parent; + } /*------------------------------------------------------------------------------------------------------------------------ | Initialize key fields From 574ed1753cc7a597e47f24d538488f84c5214248 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 5 Jan 2021 16:21:01 -0800 Subject: [PATCH 204/778] Introduced a custom `Topic` derivative for test purposes The `CustomTopic` class has a number of properties of different data types (`string`, `int`, `bool`, and `DateTime`), all of which are `[AttributeSetter]`s, and some of which enforce special business logic (e.g., `DateTimeAttribute` must be greater than 2000, `NumericAttribute` must be a positive number). These can be used to evaluate some of the corner cases of the business logic validation in the `AttributeValueCollection` class to ensure that different data types are correctly routed to their corresponding properties, and that business logic is correctly enforced. --- OnTopic.Tests/Entities/CustomTopic.cs | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 OnTopic.Tests/Entities/CustomTopic.cs diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs new file mode 100644 index 00000000..0b0482c4 --- /dev/null +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -0,0 +1,90 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Globalization; +using OnTopic.Attributes; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Entities { + + /*============================================================================================================================ + | TOPIC ENTITY: CUSTOM + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a derived version of with additional properties for evaluating the enforcement of business + /// logic. + /// + public class CustomTopic: Topic { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public CustomTopic(string key, string contentType, Topic? parent = null, int id = -1): base(key, contentType, parent, id) { + } + + /*========================================================================================================================== + | TEXT ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a text property which is intended to be mapped to a text attribute. + /// + [AttributeSetter] + public string TextAttribute { + get => Attributes.GetValue("TextAttribute"); + set => SetAttributeValue("TextAttribute", value); + } + + /*========================================================================================================================== + | BOOLEAN ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a Boolean property which is intended to be mapped to a Boolean attribute. + /// + [AttributeSetter] + public bool BooleanAttribute { + get => Attributes.GetBoolean("BooleanAttribute", false); + set => SetAttributeValue("BooleanAttribute", value? "1" : "0"); + } + + /*========================================================================================================================== + | NUMERIC ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a numeric property which is intended to be mapped to a numeric attribute. + /// + [AttributeSetter] + public int NumericAttribute { + get => Attributes.GetInteger("NumericAttribute", 0); + set { + Contract.Requires( + value >= 0, + $"{nameof(NumericAttribute)} expects a positive value." + ); + SetAttributeValue("NumericAttribute", value.ToString(CultureInfo.InvariantCulture)); + } + } + + /*========================================================================================================================== + | DATE/TIME ATTRIBUTE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a date/time property which is intended to be mapped to a date/time attribute. + /// + [AttributeSetter] + public DateTime DateTimeAttribute { + get => Attributes.GetDateTime("DateTimeAttribute", DateTime.MinValue); + set { + Contract.Requires( + value.Year > 2000, + $"{nameof(DateTimeAttribute)} expects a date after 2000." + ); + SetAttributeValue("DateTimeAttribute", value.ToString(CultureInfo.InvariantCulture)); + } + } + + } //Class +} //Namespace \ No newline at end of file From afb6728dce301e1f71f3e3afe17d456df673353d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 5 Jan 2021 16:21:44 -0800 Subject: [PATCH 205/778] Established more comprehensive business logic tests With the new `CustomTopic` entity (574ed17), we're able to establish a more comprehensive and meaningful set of tests against `AttributeValueCollection.EnforceBusinessLogic()` to ensure that a) different data types are properly serialized when calling their respective properties, and b) any business logic enforced by those properties is correctly adhered to. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index ef95411a..bdef42f8 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; using OnTopic.Collections; +using OnTopic.Tests.Entities; namespace OnTopic.Tests { @@ -481,6 +482,99 @@ public void Add_ValidAttributeValue_IsReturned() { } + /*========================================================================================================================== + | TEST: ADD: NUMERIC VALUE WITH BUSINESS LOGIC: IS RETURNED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a numeric attribute on a topic instance; ensures it is routed through the corresponding property and correctly + /// retrieved. + /// + [TestMethod] + public void Add_NumericValueWithBusinessLogic_IsReturned() { + + var topic = new CustomTopic("Test", "Page"); + + topic.Attributes.SetInteger("NumericAttribute", 1); + + Assert.AreEqual(1, topic.NumericAttribute); + + } + + /*========================================================================================================================== + | TEST: ADD: BOOLEAN VALUE WITH BUSINESS LOGIC: IS RETURNED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a Boolean attribute on a topic instance; ensures it is routed through the corresponding property and correctly + /// retrieved. + /// + [TestMethod] + public void Add_BooleanValueWithBusinessLogic_IsReturned() { + + var topic = new CustomTopic("Test", "Page"); + + topic.Attributes.SetBoolean("BooleanAttribute", true); + + Assert.IsTrue(topic.BooleanAttribute); + + } + + /*========================================================================================================================== + | TEST: ADD: NUMERIC VALUE WITH BUSINESS LOGIC: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a numeric attribute on a topic instance with an invalid value; ensures an exception is thrown. + /// + [TestMethod] + [ExpectedException( + typeof(TargetInvocationException), + "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." + )] + public void Add_NumericValueWithBusinessLogic_ThrowsException() { + + var topic = new CustomTopic("Test", "Page"); + + topic.Attributes.SetInteger("NumericAttribute", -1); + + } + + /*========================================================================================================================== + | TEST: ADD: DATE/TIME VALUE WITH BUSINESS LOGIC: IS RETURNED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a Date/Time attribute on a topic instance; ensures it is routed through the corresponding property and correctly + /// retrieved. + /// + [TestMethod] + public void Add_DateTimeValueWithBusinessLogic_IsReturned() { + + var topic = new CustomTopic("Test", "Page"); + var dateTime = new DateTime(2021, 1, 5); + + topic.Attributes.SetDateTime("DateTimeAttribute", dateTime); + + Assert.AreEqual(dateTime, topic.DateTimeAttribute); + + } + + /*========================================================================================================================== + | TEST: ADD: DATE/TIME VALUE WITH BUSINESS LOGIC: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a Date/Time attribute on a topic instance with an invalid value; ensures an exception is thrown. + /// + [TestMethod] + [ExpectedException( + typeof(TargetInvocationException), + "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." + )] + public void Add_DateTimeValueWithBusinessLogic_ThrowsException() { + + var topic = new CustomTopic("Test", "Page"); + + topic.Attributes.SetDateTime("DateTimeAttribute", DateTime.MinValue); + + } + /*========================================================================================================================== | TEST: SET VALUE: INSERT INVALID ATTRIBUTE VALUE: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ @@ -490,7 +584,7 @@ public void Add_ValidAttributeValue_IsReturned() { [TestMethod] [ExpectedException( typeof(TargetInvocationException), - "The topic allowed a key to be set via a back door, without routing it through the Key property." + "The topic allowed a key to be set via a back door, without routing it through the View property." )] public void Add_InvalidAttributeValue_ThrowsException() { var topic = TopicFactory.Create("Test", "Container"); From 0705587f91bd5880238cb6b8eabfc26b3d4a72b4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 14:08:41 -0800 Subject: [PATCH 206/778] Introduced `targetType` parameter for the `HasSettable*()` methods The `IsSettableType()` method has an optional `targetType` parameter which, if passed, will return `true` if the return type of the target property or the first (and only) parameter of a method call can be assigned from `targetType`. This allows support for complex object types that the `TypeMemberInfoCollection` doesn't otherwise know how to serialize from a string representation by relaying them directly to the property or method parameter. While this was supported on `IsSettableType()`, it wasn't supported by either `HasSettableProperty()` or `HasSettableMethod()` methods. By exposing this as an optional parameter that can be relayed to `IsSettableType()`, we allow those to return `true` if the `targetType` is innately compatible with the member, even if `SetPropertyValue()` or `SetMemberValue()` don't know how to serialize the reference. --- .../Internal/Reflection/TypeMemberInfoCollection.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index ffd0b86e..f94d8545 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -150,11 +150,12 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// /// The on which the property is defined. /// The name of the property to assess. - public bool HasSettableProperty(Type type, string name) { + /// Optional, the expected. + public bool HasSettableProperty(Type type, string name, Type? targetType = null) { var property = GetMember(type, name); return ( property is not null and { CanWrite: true } && - IsSettableType(property.PropertyType) && + IsSettableType(property.PropertyType, targetType) && (_attributeFlag is null || System.Attribute.IsDefined(property, _attributeFlag)) ); } @@ -263,12 +264,13 @@ public bool HasGettableProperty(Type type, string name, Type? targetType = null) /// /// The on which the method is defined. /// The name of the method to assess. - public bool HasSettableMethod(Type type, string name) { + /// Optional, the expected. + public bool HasSettableMethod(Type type, string name, Type? targetType = null) { var method = GetMember(type, name); return ( method is not null && method.GetParameters().Length is 1 && - IsSettableType(method.GetParameters().First().ParameterType) && + IsSettableType(method.GetParameters().First().ParameterType, targetType) && (_attributeFlag is null || System.Attribute.IsDefined(method, _attributeFlag)) ); } From c7fdf5d91cc04d31f63d234f5ce72ee159299387 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 14:14:36 -0800 Subject: [PATCH 207/778] Introduced `object?` overloads for `Set*Value()` methods With the introduction of `targetType` on `HasSettableProperty()` and `HasSettableMethod()` (0705587), we can now add an overload to `SetPropertyValue() and `SetMethodValue()` that accepts an `object?` and attempt to set it directly to a given member without making any effort to convert it into the appropriate data type, as the original `SetPropertyValue()` and `SetMethodValue()` overloads do. This allows potentially _any_ complex object to be set to a property or the first (and only) parameter of a method using the `TypeMemberInfoCollection`, where as previously, only strings that could be converted to `string`, `bool`, `int`, `double`, or `DateTime` were supported. This greatly expands the possible range of use cases that it can be used to support. Notable, this is being put in place to allow `Topic` valued properties on `Topic` to be supported, as needed to allow enforcing of business logic for the `Topic.References` collection. For instance, if a `Topic` is added to `Topic.References` with a `referenceKey` of `DerivedTopic`, it should be set using `Topic.DerivedTopic`. To do that, however, `SetPropertyValue()` needs to be able to accept a `Topic` object instance, and be able to set it to a `Topic` valued property. This enables that, as well as many other possible scenarios. --- .../Reflection/TypeMemberInfoCollection.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index f94d8545..d218b401 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -194,6 +194,35 @@ public bool SetPropertyValue(object target, string name, string? value) { } + /// + /// Uses reflection to call a property, assuming that the property value is compatible with the + /// type. + /// + /// The object on which the property is defined. + /// The name of the property to assess. + /// The value to set on the property. + public bool SetPropertyValue(object target, string name, object? value) { + + Contract.Requires(target, nameof(target)); + Contract.Requires(name, nameof(name)); + + if (!HasSettableProperty(target.GetType(), name, value?.GetType())) { + return false; + } + + var property = GetMember(target.GetType(), name); + + Contract.Assume(property, $"The {name} property could not be retrieved."); + + if (value is null) { + return false; + } + + property.SetValue(target, value); + return true; + + } + /*========================================================================================================================== | METHOD: HAS GETTABLE PROPERTY \-------------------------------------------------------------------------------------------------------------------------*/ @@ -323,6 +352,49 @@ public bool SetMethodValue(object target, string name, string? value) { } + /// + /// Uses reflection to call a method, assuming that the parameter value is compatible with the + /// type. + /// + /// + /// Be aware that this will only succeed if the method has a single parameter of a settable type. If additional parameters + /// are present it will return false, even if those additional parameters are optional. + /// + /// The object instance on which the method is defined. + /// The name of the method to assess. + /// The value to set the method to. + public bool SetMethodValue(object target, string name, object? value) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(target, nameof(target)); + Contract.Requires(name, nameof(name)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate member type + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!HasSettableMethod(target.GetType(), name, value?.GetType())) { + return false; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set value + \-----------------------------------------------------------------------------------------------------------------------*/ + var method = GetMember(target.GetType(), name); + + Contract.Assume(method, $"The {name}() method could not be retrieved."); + + if (value is null) { + return false; + } + + method.Invoke(target, new object[] { value }); + + return true; + + } + /*========================================================================================================================== | METHOD: HAS GETTABLE METHOD \-------------------------------------------------------------------------------------------------------------------------*/ From d7b63a8da5a40ade0c9d8b844f14b9d1975d59f8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 14:28:16 -0800 Subject: [PATCH 208/778] Established a reusable `TopicPropertyDispatcher` class When an `AttributeValue` is added to the `AttributeValueCollection`, it checks to see if a corresponding property on the associated `Topic` exists based on the `AttributeKey` (which is expected to match the property name) and the presence of an `[AttributeSetter]` attribute. If it does, the value is set using that property, instead of being added directly to the collection. This prevents the collection from bypassing the business logic of a property setter, which could prevent data validation from occurring, or interfere with internal state tracking of the `Topic` instance. The new (`internal`) `TopicPropertyDispatcher` class extracts the code for accomplishing this, and places it it in a reusable utility that can be applied to other collections, thus preventing this from being used _exclusively_ by `AttributeValueCollection`. This has the additional benefit of allowing us to simplify the code for the `AttributeValueCollection`, while moving the logic for enforcing business to a class that's focused entirely on that task. While I was at it, I greatly improved the documentation for this functionality, so it's hopefully a bit clearer. (The abstraction makes this already complicated logic even more difficult to reason through, but hopefully the added remarks aid in that.) --- .../Collections/AttributeValueCollection.cs | 111 +------ .../Reflection/TopicPropertyDispatcher.cs | 277 ++++++++++++++++++ 2 files changed, 292 insertions(+), 96 deletions(-) create mode 100644 OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Collections/AttributeValueCollection.cs index 145d130d..9fd6638b 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Collections/AttributeValueCollection.cs @@ -29,15 +29,14 @@ namespace OnTopic.Collections { public class AttributeValueCollection : KeyedCollection { /*========================================================================================================================== - | STATIC VARIABLES + | DISPATCHER \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly TypeMemberInfoCollection _typeCache = new(typeof(AttributeSetterAttribute)); + private readonly TopicPropertyDispatcher _topicPropertyDispatcher; /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private readonly Topic _associatedTopic; - private int _setCounter; /*========================================================================================================================== | CONSTRUCTOR @@ -52,49 +51,9 @@ public class AttributeValueCollection : KeyedCollection /// A reference to the topic that the current attribute collection is bound to. internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.InvariantCultureIgnoreCase) { _associatedTopic = parentTopic; + _topicPropertyDispatcher = new(parentTopic); } - /*========================================================================================================================== - | PROPERTY: BUSINESS LOGIC CACHE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a local cache of objects, keyed by their , prior - /// to them having their business logic enforced. - /// - /// - /// - /// By default, there is no business logic enforced for objects. This can be mitigate by - /// implementing properties that correspond to the attribute names on or a derivative class. - /// - /// - /// The enforces this business logic by forcing updates to go through that - /// property if it exists. To ensure this is enforced at all entry points, this is handled via the and methods. This ensures that the - /// business logic is enforced even if implementors bypass the method, and instead use e.g. 's indexer or underlying methods - /// such as . - /// - /// - /// Since neither the or - /// methods, nor the properties that calls, accept the optional - /// parameters from , however, that means that - /// parameter values corresponding to e.g. and will get lost in the process. In addition, there needs to be a way to track whether - /// the call to e.g., is being triggered by a direct call, or as a round- - /// trip through one of these property setters. - /// - /// - /// The addresses this issue by providing a cache of the original instances, indexed by their , for attributes currently being - /// routed through their corresponding property setter. If a record exists for the current attribute, the method knows it should not enforce business logic again—as that would result - /// in an infinite loop—and should instead persist the record to the collection. Further, because the includes the original , the original parameters such as the are not lost, and can be applied to the final object. - /// - /// - private Dictionary BusinessLogicCache { get; } = new(); - /*========================================================================================================================== | PROPERTY: DELETED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ @@ -425,7 +384,6 @@ internal void SetValue( | Retrieve original attribute \-----------------------------------------------------------------------------------------------------------------------*/ AttributeValue? originalAttributeValue = null; - AttributeValue? updatedAttributeValue = null; if (Contains(key)) { originalAttributeValue = this[key]; @@ -437,8 +395,7 @@ internal void SetValue( | If the original values have already been applied, and SetValue() is being triggered a second time after enforcing | business logic, then use the original values, while applying any change in the value triggered by the business logic. \-----------------------------------------------------------------------------------------------------------------------*/ - if (BusinessLogicCache.ContainsKey(key)) { - BusinessLogicCache.TryGetValue(key, out updatedAttributeValue); + if (_topicPropertyDispatcher.IsRegistered(key, out var updatedAttributeValue)) { if (updatedAttributeValue.Value != value) { updatedAttributeValue = updatedAttributeValue with { Value = value @@ -485,19 +442,17 @@ internal void SetValue( } /*------------------------------------------------------------------------------------------------------------------------ - | Establish secret handshake for later enforcement of properties + | Register that business logic has already been enforced >------------------------------------------------------------------------------------------------------------------------- - | ###HACK JJC100617: We want to ensure that any attempt to set attributes that have corresponding (writable) properties - | use those properties, thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by - | KeyedCollection, and not just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) will look for the - | EnforceBusinessLogic property. If it is set to false, they assume the property set the value (e.g., by calling the - | protected SetValue method with enforceBusinessLogic set to false). Otherwise, the corresponding property will be called. - | The EnforceBusinessLogic thus avoids a redirect loop in this scenario. This, of course, assumes that properties are - | correctly written to call the enforceBusinessLogic parameter. + | We want to ensure that any attempt to set references that have corresponding (writable) properties use those properties, + | thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by ICollection, and not + | just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) call the Enforce() method. If it returns false, + | they assume the property set the value (e.g., by calling the internal SetValue method with enforceBusinessLogic set to + | false). Otherwise, the corresponding property will be called. The Register() method thus avoids a redirect loop in this + | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. \-----------------------------------------------------------------------------------------------------------------------*/ - enforceBusinessLogic = !enforceBusinessLogic && _typeCache.HasSettableProperty(_associatedTopic.GetType(), key); - if (enforceBusinessLogic && !BusinessLogicCache.ContainsKey(key)) { - BusinessLogicCache.Add(key, updatedAttributeValue); + if (!enforceBusinessLogic) { + _topicPropertyDispatcher.Register(key, updatedAttributeValue); } /*------------------------------------------------------------------------------------------------------------------------ @@ -543,7 +498,7 @@ internal void SetValue( /// protected override void InsertItem(int index, AttributeValue item) { Contract.Requires(item, nameof(item)); - if (EnforceBusinessLogic(item)) { + if (_topicPropertyDispatcher.Enforce(item.Key, item)) { if (!Contains(item.Key)) { base.InsertItem(index, item); if (DeletedAttributes.Contains(item.Key)) { @@ -578,7 +533,7 @@ protected override void InsertItem(int index, AttributeValue item) { /// The object which is being inserted. protected override void SetItem(int index, AttributeValue item) { Contract.Requires(item, nameof(item)); - if (EnforceBusinessLogic(item)) { + if (_topicPropertyDispatcher.Enforce(item.Key, item)) { base.SetItem(index, item); if (DeletedAttributes.Contains(item.Key)) { DeletedAttributes.Remove(item.Key); @@ -603,42 +558,6 @@ protected override void RemoveItem(int index) { base.RemoveItem(index); } - /*========================================================================================================================== - | METHOD: ENFORCE BUSINESS LOGIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Inspects a provided to determine if the value should be routed through local business - /// logic. - /// - /// - /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is - /// set by the 's - /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. - /// - /// The object which is being inserted. - /// The with the business logic applied. - private bool EnforceBusinessLogic(AttributeValue originalAttribute) { - if (BusinessLogicCache.ContainsKey(originalAttribute.Key)) { - BusinessLogicCache.Remove(originalAttribute.Key); - return true; - } - else if (_typeCache.HasSettableProperty(_associatedTopic.GetType(), originalAttribute.Key)) { - _setCounter++; - if (_setCounter > 3) { - throw new InvalidOperationException( - $"An infinite loop has occurred when setting '{originalAttribute.Key}'; be sure that you are referencing " + - $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties." - ); - } - BusinessLogicCache.Add(originalAttribute.Key, originalAttribute); - _typeCache.SetPropertyValue(_associatedTopic, originalAttribute.Key, originalAttribute.Value); - _setCounter = 0; - return false; - } - return true; - } - /*========================================================================================================================== | OVERRIDE: GET KEY FOR ITEM \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs new file mode 100644 index 00000000..471f50c7 --- /dev/null +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -0,0 +1,277 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using OnTopic.Attributes; +using OnTopic.Collections; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: TOPIC PROPERTY DISPATCHER + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The allows a collection on a + /// entity to optionally route requests through properties on the corresponding which correspond to the + /// item key, thus ensuring local state and any business logic enforced by the property setter are maintained. + /// + /// + /// + /// Collections on , such as and , aren't + /// well-positioned to enforce attribute-specific business logic when adding or setting items in the collection. Instead, + /// this logic is typically handled by property setters on , such as or . This introduces a potential backdoor, as updates made directly to the collection can + /// bypass any business logic—such as data validation or local state management—handled by those property setters. The + /// class addresses this by allowing those collections + /// to route requests through appropriately decorated properties on prior to adding or setting a + /// value. + /// + /// + /// The requires two type arguments. represents an attribute which must be present on each property setter. This helps avoid potential + /// ambiguities. For instance, if both and have the same + /// key, and that key maps to the identify of a property, the usage will be restricted based on + /// whether the expected attribute is used to decorate the property—for instance, the or . In practice, this is an unexpected situation since a) individual content + /// types cannot use the same key for both and , and b) even + /// if they did, these properties support different data types, and thus are not intercompatible. Nevertheless, these + /// attributes provide an additional level of explicitness to avoid any ambiguity, and provide both developers as well as + /// the hints about what a property + /// is intended for. + /// + /// + /// The represents the value that is stored in the corresponding collection. This value + /// is saved as part of either or , + /// and can optionally be retrieved via . This is useful in case there + /// is data from the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as + /// , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we + /// allow the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as + /// critical for e.g. since the will be the same that's sent to the corresponding property, and thus is expected to be the same as the value set by the + /// property itself. + /// + /// + /// In a typical workflow, the method will end up getting once or twice. The + /// first occurs when a caller attempts to insert an item directly into a collection; if a corresponding property setter + /// is requested, then will call , + /// trigger the call to the corresponding property, and return false—indicating that the item should not be added + /// to the collection. In this case, the property setter will run its own business logic, then attempt to add or set the + /// item into the collection again. This time, the call to will prevent the + /// second call to from enforcing the business logic, thus preventing an + /// infinite loop. + /// + /// + /// One caveat to this are cases where the caller attempts to set the value via the property directly, + /// instead of adding the item directly to the corresponding collection—e.g., they call instead + /// of e.g. the method from . In that case, + /// the business logic will already have been enforced, but the method will + /// not have been called. To mitigate the property setter getting called twice, collection implementors are advised to + /// offer an internal overload that allows an item to be added to the collection while bypassing the business logic. For + /// instance, this can be done using or ; in each + /// case, the internally accessible enforceBusinessLogic parameter allows a property setter to disable business + /// logic. Internally, this is done by calling , thus assuring that the business logic has already occurred. + /// + /// + internal class TopicPropertyDispatcher + where TAttributeType: Attribute + where TValueType: class + { + + /*========================================================================================================================== + | STATIC VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + static readonly TypeMemberInfoCollection _typeCache = new(typeof(TAttributeType)); + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly Topic _associatedTopic; + private int _setCounter; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class associated + /// with a specific . + /// + /// The whose properties should be called, when appropriate. + internal TopicPropertyDispatcher(Topic associatedTopic) { + _associatedTopic = associatedTopic; + } + + /*========================================================================================================================== + | PROPERTY: PROPERTY CACHE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a local cache of objects, keyed by an associated itemKey, prior + /// to having their business logic enforced. + /// + /// + /// + /// Whenever a property setter has been called, a record in the is added containing a) the + /// key associated with the item (and, therefore, property), and b) the original that + /// was to be added to the collection. This registers that the business logic for that property has been enforced. + /// + /// + /// There are two ways that a record is created in the . The typical way is to call , which will check to see if there is a corresponding property setter and, if there + /// is, will add a record to the cache, and call the property. The second way is to call to directly register that the property has already been executed. This is typically done by special + /// internal methods, called exclusively by the property setters themselves, with a enforceBusinessLogic + /// parameter that is set to false; that prevents calls made directly to the property setter to bypass the + /// dispatcher. + /// + /// + /// There is only one way to remove an item from the once it's been created. This happens + /// when is called, and a record already exists. When this occurs, the record + /// is removed, and returns true without any further action. This + /// instructs the caller—i.e., a method on a collection responsible for adding or setting an item—that it can complete + /// the request. + /// + /// + private Dictionary PropertyCache { get; } = new(); + + /*========================================================================================================================== + | REGISTER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instructs the that the business logic for a + /// corresponding property has been set, and does not need to be executed again. + /// + /// + /// + /// The method is called by right + /// before it triggers a call to a corresponding property setter. This allows it to track that the business logic has + /// been enforced, and it doesn't need to make the call again on a round trip. + /// + /// + /// The method can also be called directly by a collection to tell the + /// that business logic should not be enforced when + /// adding or setting an item. The typical use case for this is an internal method which allows the property setters + /// themselves to bypass business logic, thus preventing them from being called twice. These methods should be marked + /// internal to prevent external actors from bypassing the business logic; the purpose is to confirm that the business + /// logic has already been enforced, not to make the business logic optional. Two examples of this are the internal + /// enforceBusinessLogic parameters on and . + /// + /// + /// It's worth noting that any calls to are invalidated the next time is called. As such, is not a way + /// to permanently disable calling a property setter. (The correct way to do that is to remove the property setter, or + /// at least its corresponding .) Instead, it only disables the next attempt to add + /// an item corresponding to that key—which, if correctly implemented, will be when the current is added to the collection. + /// + /// + /// + /// The key of the , which potentially corresponds to a property. + /// + /// The object which is being inserted. + internal bool Register(string itemKey, TValueType? initialValue) { + var type = initialValue?.GetType(); + if (typeof(AttributeValue).IsAssignableFrom(type)) { + type = null; + } + if ( + _typeCache.HasSettableProperty(_associatedTopic.GetType(), itemKey, type) && + !PropertyCache.ContainsKey(itemKey) + ) { + PropertyCache.Add(itemKey, initialValue); + return true; + } + return false; + } + + /*========================================================================================================================== + | IS REGISTERED? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Identifies whether the given has been registered—and, thus, has already had its business + /// logic enforced. + /// + /// + /// The key of the , which potentially corresponds to a property. + /// + /// Returns true if the has been registered, otherwise false. + internal bool IsRegistered(string itemKey) => IsRegistered(itemKey, out var _); + + /// + /// Identifies whether the given has been registered—and, thus, has already had its business + /// logic enforced. Returns the that was registered as an out parameter. + /// + /// + /// The key of the , which potentially corresponds to a property. + /// + /// The object which is being inserted. + /// Returns true if the has been registered, otherwise false. + internal bool IsRegistered(string itemKey, [NotNullWhen(true)] out TValueType? initialObject) => + PropertyCache.TryGetValue(itemKey, out initialObject); + + /*========================================================================================================================== + | METHOD: ENFORCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Inspects the requested to determine if the corresponding should be routed through the associated in order to enforce business logic. + /// + /// + /// + /// If a settable property is available on the associated corresponding to the , the call should be routed through that property to ensure that local business logic is enforced. This + /// is determined by looking for attribute, which confirms that a property with a + /// matching name is aware of and intended to operate with a given collection. + /// + /// + /// The method should be called from an implementing collection prior to + /// committing an add, insert, or set operation. That operation should only be completed if returns true; otherwise, the request will be routed through the corresponding property on + /// in order to enforce any business logic, after which the property will attempt to add the + /// property to the collection again. When is called a second time for the + /// same , it won't enforce the business logic, and will instead return true. + /// + /// + /// + /// The key of the , which potentially corresponds to a property + /// setter. + /// + /// The object which is being inserted. + /// Returns true if the business logic has been enfored; otherwise false. + internal bool Enforce(string itemKey, TValueType? initialObject) { + if (PropertyCache.ContainsKey(itemKey)) { + PropertyCache.Remove(itemKey); + return true; + } + else if (Register(itemKey, initialObject)) { + _setCounter++; + if (_setCounter > 3) { + throw new InvalidOperationException( + $"An infinite loop has occurred when setting '{itemKey}'; be sure that you are referencing " + + $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties." + ); + } + var attribute = initialObject as AttributeValue; + if (attribute is not null) { + _typeCache.SetPropertyValue(_associatedTopic, itemKey, attribute.Value); + } + else { + _typeCache.SetPropertyValue(_associatedTopic, itemKey, initialObject); + } + _setCounter = 0; + return false; + } + return true; + } + + } //Class +} //Namespace \ No newline at end of file From 31442353e38606e4a97d01fd9a349fe4268c7007 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 14:35:18 -0800 Subject: [PATCH 209/778] Add support for enforcing business logic on `TopicReferenceDictionary` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `AttributeValueCollection`—i.e., `Topic.Attributes`—prevented bypassing any business logic enforced by corresponding property names (such as `Topic.View`) by dynamically routing direct requests through the property setters. Previously, however, this wasn't supported by `TopicReferenceDictionary`—i.e., `Topic.References`. This update expands that functionality to the `TopicReferenceDictionary` as well, thus allowing properties on `Topic` (or a derived type) to be marked as a `[ReferenceSetter]` and, thus, validate the value, modify any internal state, and otherwise enforce business logic specific to that value. This is enabled by using the new `TopicPropertyDispatcher` class (d7b63a8), which centralizes the logic from `AttributeValueCollection` into a reusable (and more intuitive) format. As part of this, I've applied the `[ReferenceSetter]` attribute to `Topic.DerivedTopic`, which correspond to a `Topic.References` object with the `referenceKey` of `DerivedTopic`. --- .../Collections/TopicReferenceDictionary.cs | 80 ++++++++++++++++++- OnTopic/ReferenceSetterAttribute.cs | 43 ++++++++++ OnTopic/Topic.cs | 1 + 3 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 OnTopic/ReferenceSetterAttribute.cs diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/Collections/TopicReferenceDictionary.cs index a9f91900..9420322f 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/Collections/TopicReferenceDictionary.cs @@ -6,7 +6,9 @@ using System; using System.Collections; using System.Collections.Generic; +using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; +using OnTopic.Internal.Reflection; namespace OnTopic.Collections { @@ -18,6 +20,11 @@ namespace OnTopic.Collections { /// public class TopicReferenceDictionary : IDictionary { + /*========================================================================================================================== + | DISPATCHER + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly TopicPropertyDispatcher _topicPropertyDispatcher; + /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ @@ -41,8 +48,9 @@ public TopicReferenceDictionary(Topic parent) { /*------------------------------------------------------------------------------------------------------------------------ | Initialize backing fields \-----------------------------------------------------------------------------------------------------------------------*/ - _parent = parent; - _storage = new Dictionary(); + _parent = parent; + _storage = new Dictionary(); + _topicPropertyDispatcher = new(parent); } @@ -65,14 +73,37 @@ public TopicReferenceDictionary(Topic parent) { public Topic this[string referenceKey] { get => _storage[referenceKey]; set { + + /*---------------------------------------------------------------------------------------------------------------------- + | Validate parameters + \---------------------------------------------------------------------------------------------------------------------*/ Contract.Requires( value != _parent, "A topic reference may not point to itself." ); + + /*---------------------------------------------------------------------------------------------------------------------- + | Enforce business logic + >----------------------------------------------------------------------------------------------------------------------- + | If the reference is eligible for business logic enforcement, but the business logic hasn't yet been enforce, skip + | further processing and instead route the request through the associated property setter. + \---------------------------------------------------------------------------------------------------------------------*/ + if (!_topicPropertyDispatcher.Enforce(referenceKey, value)) { + return; + } + + /*---------------------------------------------------------------------------------------------------------------------- + | Set dirty state + \---------------------------------------------------------------------------------------------------------------------*/ if (!_storage.TryGetValue(referenceKey, out var existing) || existing != value) { _isDirty = true; } + + /*---------------------------------------------------------------------------------------------------------------------- + | Set topic reference + \---------------------------------------------------------------------------------------------------------------------*/ _storage[referenceKey] = value; + } } @@ -106,6 +137,16 @@ void ICollection>.Add(KeyValuePair it "A topic reference may not point to itself." ); + /*------------------------------------------------------------------------------------------------------------------------ + | Enforce business logic + >------------------------------------------------------------------------------------------------------------------------- + | If the reference is eligible for business logic enforcement, but the business logic hasn't yet been enforce, skip + | further processing and instead route the request through the associated property setter. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!_topicPropertyDispatcher.Enforce(item.Key, item.Value)) { + return; + } + /*------------------------------------------------------------------------------------------------------------------------ | Mark dirty \-----------------------------------------------------------------------------------------------------------------------*/ @@ -149,8 +190,36 @@ public void Add(string key, Topic value) { /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is /// removed. /// - public void SetTopic(string key, Topic? value, bool? isDirty = null) { + public void SetTopic(string key, Topic? value, bool? isDirty = null) => SetTopic(key, value, isDirty, true); + + /// + /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is + /// removed. + /// + internal void SetTopic(string key, Topic? value, bool? isDirty, bool enforceBusinessLogic) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish state + \-----------------------------------------------------------------------------------------------------------------------*/ var wasDirty = _isDirty; + + /*------------------------------------------------------------------------------------------------------------------------ + | Register that business logic has already been enforced + >------------------------------------------------------------------------------------------------------------------------- + | We want to ensure that any attempt to set references that have corresponding (writable) properties use those properties, + | thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by IDictionary, and not + | just SetTopic, the underlying interceptors (e.g., Add, Item) call the Enforce() method. If it returns false, they assume + | the property set the value (e.g., by calling the internal SetTopic method with enforceBusinessLogic set to false). + | Otherwise, the corresponding property will be called. The Register() method thus avoids a redirect loop in this + | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!enforceBusinessLogic) { + _topicPropertyDispatcher.Register(key, value); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set value + \-----------------------------------------------------------------------------------------------------------------------*/ if (value is null) { if (ContainsKey(key)) { Remove(key); @@ -159,9 +228,14 @@ public void SetTopic(string key, Topic? value, bool? isDirty = null) { else { this[key] = value; } + + /*------------------------------------------------------------------------------------------------------------------------ + | Set dirty state + \-----------------------------------------------------------------------------------------------------------------------*/ if (wasDirty is false && isDirty is false) { _isDirty = false; } + } /*========================================================================================================================== diff --git a/OnTopic/ReferenceSetterAttribute.cs b/OnTopic/ReferenceSetterAttribute.cs new file mode 100644 index 00000000..69b5d377 --- /dev/null +++ b/OnTopic/ReferenceSetterAttribute.cs @@ -0,0 +1,43 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Collections; + +namespace OnTopic.Attributes { + + /*============================================================================================================================ + | CLASS: REFERENCE SETTER [ATTRIBUTE] + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Flags that a property should be used when setting a reference via . + /// + /// + /// + /// When a call is made to the code will check + /// to see if a property with the same name as the reference key exists, and whether that property is decorated with the + /// (i.e., [ReferenceSetter]). If it is, then the update will be + /// routed through that property. This ensures that business logic is enforced by local properties, instead of allowing + /// business logic to be potentially bypassed by writing directly to the collection. + /// + /// + /// As an example, the property is adorned with the . As a result, if a client calls topic.References.SetTopic("DerivedTopic", topic), then that update + /// will be routed through , thus enforcing any validation. + /// + /// + /// To ensure this logic, it is critical that implementers of ensure that the + /// property setters call the overload with the + /// final parameter set to false to disable the enforcement of business logic. Otherwise, an infinite loop will + /// occur. Calling that overload tells that the business logic has already been + /// enforced by the caller. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class ReferenceSetterAttribute : Attribute { + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 5cd251a8..c516fac3 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -638,6 +638,7 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// /// value != this /// + [ReferenceSetter] public Topic? DerivedTopic { get => References.GetTopic("DerivedTopic", false); set { From 1f81429af7bfbf0ac6d2bfcd6204ca18ded20c00 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 14:39:59 -0800 Subject: [PATCH 210/778] Established unit tests for the new `TopicReferenceDictionary` These ensure that a `Topic` passed to the `TopicReferenceDictionary` correctly calls the corresponding property on `Topic`, and throws an exception if the business logic is violated. --- OnTopic.Tests/Entities/CustomTopic.cs | 18 ++++++ OnTopic.Tests/TopicReferenceDictionaryTest.cs | 61 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 0b0482c4..6da7121e 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -86,5 +86,23 @@ public DateTime DateTimeAttribute { } } + /*========================================================================================================================== + | TOPIC REFERENCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a topic reference property which is intended to be mapped to a topic reference. + /// + [ReferenceSetter] + public Topic? TopicReference { + get => References.GetTopic("TopicReference"); + set { + Contract.Requires( + value.ContentType == ContentType, + $"{nameof(TopicReference)} expects a topic with the same content type as the parent: {ContentType}." + ); + References.SetTopic("TopicReference", value); + } + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index e8c97057..527e8683 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -4,8 +4,10 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Collections; +using OnTopic.Tests.Entities; namespace OnTopic.Tests { @@ -318,5 +320,64 @@ public void GetTopic_DerivedReferenceWithoutInherit_ReturnsNull() { } + /*========================================================================================================================== + | TEST: ADD: TOPIC REFERENCE WITH BUSINESS LOGIC: IS RETURNED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a topic reference on a topic instance; ensures it is routed through the corresponding property and correctly + /// retrieved. + /// + [TestMethod] + public void Add_TopicReferenceWithBusinessLogic_IsReturned() { + + var topic = new CustomTopic("Test", "Page"); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.Add("TopicReference", reference); + + Assert.AreEqual(reference, topic.TopicReference); + + } + + /*========================================================================================================================== + | TEST: ADD: TOPIC REFERENCE WITH BUSINESS LOGIC: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a topic reference on a topic instance with an invalid value; ensures an exception is thrown. + /// + [TestMethod] + [ExpectedException( + typeof(TargetInvocationException), + "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." + )] + public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { + + var topic = new CustomTopic("Test", "Page"); + var reference = TopicFactory.Create("Reference", "Container"); + + topic.References.Add("TopicReference", reference); + + } + + /*========================================================================================================================== + | TEST: SET: TOPIC REFERENCE WITH BUSINESS LOGIC: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Sets a topic reference on a topic instance with an invalid value; ensures an exception is thrown. + /// + [TestMethod] + [ExpectedException( + typeof(TargetInvocationException), + "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." + )] + public void Set_TopicReferenceWithBusinessLogic_ThrowsException() { + + var topic = new CustomTopic("Test", "Page"); + var reference = TopicFactory.Create("Reference", "Container"); + + topic.References["TopicReference"] = reference; + + } + } //Class } //Namespace \ No newline at end of file From 1b5dd5b95073dd263691790e3b33f900d89e1dec Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 15:29:44 -0800 Subject: [PATCH 211/778] Handle `TargetInvocationException` to deregister dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a `TargetInvocationException` is thrown—for any reason—while enforcing business logic, we want to ensure that registration of the current property is removed from the `TopicPropertyDispatch`, if it hasn't been already. Otherwise, if we don't, then a subsequent attempt to set the same attribute on the topic will bypass the business logic, even if it's a different value. To prevent this loophole, we explicitly catch the `TargetInvocationException`, check to see if the key is still in the `PropertyCache`, and, if it is, remove it. (We have to check because, hypothetically, a property could throw an exception _after_ it's successfully added the item to the collection, in which case `Enforce()` would have been already called a second time, and the request will no longer be in the `PropertyCache`.) Afterwards, we rethrow the exception so that callers are aware that the action (potentially) failed. --- .../Reflection/TopicPropertyDispatcher.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 471f50c7..f2a3896c 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -6,6 +6,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.ExceptionServices; using OnTopic.Attributes; using OnTopic.Collections; @@ -261,11 +263,19 @@ internal bool Enforce(string itemKey, TValueType? initialObject) { ); } var attribute = initialObject as AttributeValue; - if (attribute is not null) { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, attribute.Value); + try { + if (attribute is not null) { + _typeCache.SetPropertyValue(_associatedTopic, itemKey, attribute.Value); + } + else { + _typeCache.SetPropertyValue(_associatedTopic, itemKey, initialObject); + } } - else { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, initialObject); + catch (TargetInvocationException ex) { + if (PropertyCache.ContainsKey(itemKey)) { + PropertyCache.Remove(itemKey); + } + throw; } _setCounter = 0; return false; From bf7782c04ad17477838d89e8f964469743c38417 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 15:36:24 -0800 Subject: [PATCH 212/778] Rethrow _inner_ exception on `TopicPropertyDispatcher.Enforce()` When calling `SetPropertyValue()`, the most likely exception is a `TargetInvocationException`, which will occur if _any_ error occurs while invoking the corresponding property. That effectively buries the actual reason, and the `TargetInvocationException` really only makes sense if you're aware of the internal workings of OnTopic. To avoid that confusion, and provide a more meaningful exception, we're relying on `ExceptionDispatchInfo` to rethrow the `InnerException`, thus ensuring that the actual error bubbles up, instead of the `TargetInvocationException`. As part of this, the unit tests which look for the `TargetInvocationException` need to be updated to expect the actual underlying exception. (This also makes those unit tests more meaningful, as they're not tripping on any error, but the specific error that the target property is expected to throw.) --- OnTopic.Tests/AttributeValueCollectionTest.cs | 8 ++++---- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 4 ++-- OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index bdef42f8..b5a968a7 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -456,7 +456,7 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(InvalidKeyException), "The topic allowed a view to be set via a back door, without routing it through the View property." )] public void SetValue_InvalidValue_ThrowsException() { @@ -526,7 +526,7 @@ public void Add_BooleanValueWithBusinessLogic_IsReturned() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(ArgumentOutOfRangeException), "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." )] public void Add_NumericValueWithBusinessLogic_ThrowsException() { @@ -564,7 +564,7 @@ public void Add_DateTimeValueWithBusinessLogic_IsReturned() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(ArgumentOutOfRangeException), "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." )] public void Add_DateTimeValueWithBusinessLogic_ThrowsException() { @@ -583,7 +583,7 @@ public void Add_DateTimeValueWithBusinessLogic_ThrowsException() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(InvalidKeyException), "The topic allowed a key to be set via a back door, without routing it through the View property." )] public void Add_InvalidAttributeValue_ThrowsException() { diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index 527e8683..e6381280 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -347,7 +347,7 @@ public void Add_TopicReferenceWithBusinessLogic_IsReturned() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(ArgumentOutOfRangeException), "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." )] public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { @@ -367,7 +367,7 @@ public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { /// [TestMethod] [ExpectedException( - typeof(TargetInvocationException), + typeof(ArgumentOutOfRangeException), "The topic allowed a key to be set via a back door, without routing it through the NumericValue property." )] public void Set_TopicReferenceWithBusinessLogic_ThrowsException() { diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index f2a3896c..b3ae83f1 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -275,7 +275,7 @@ internal bool Enforce(string itemKey, TValueType? initialObject) { if (PropertyCache.ContainsKey(itemKey)) { PropertyCache.Remove(itemKey); } - throw; + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } _setCounter = 0; return false; From 7e99f4122a2b2429a22872af86f728d9f25e6cb7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 16:43:00 -0800 Subject: [PATCH 213/778] Move specific collections to more relevant namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As with the underlying .NET CLR, collections in the general `Collections` namespaces should be reserved for general-purpose collection. By contrast, collections that are specific to a particular purpose should be distributed among their relevant namespaces. So, for example, `AttributeValueCollection` should be in `OnTopic.Attributes`, alongside the `AttributeValue` type itself. There wasn't a relevant namespace for the relationship-related collections, which includes `RelatedTopicCollection`, `NamedTopicCollection`, and `TopicReferenceDictionary`. Placing them in `OnTopic.Relationships` introduces a conflict with the existing `Relationships` enum used by the mapping service. As such, I chose `OnTopic.References`. (It doesn't strictly matter, as this is being used for both relationships—i.e., 1:n mappings—and topic references—i.e., 1:1 mappings. Technically, they could each be described as either relationships or references.) This also covers the `OnTopic.Internal.Collections` namespace, with the `MappedTopicCache` being moved to `OnTopic.Internal.Mapping` (alongside the `MappedTopicCacheEntry`). As part of this, the namespaces needed to be updated in a number of other classes to ensure the new class locations could be resolved—even if that's only needed for the XML Docs. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 2 -- OnTopic.Tests/ITopicRepositoryTest.cs | 2 +- OnTopic.Tests/NamedTopicCollection.cs | 2 +- OnTopic.Tests/RelatedTopicCollectionTest.cs | 2 +- OnTopic.Tests/TopicCollectionTest.cs | 1 + OnTopic.Tests/TopicReferenceDictionaryTest.cs | 3 +-- .../AttributeValueCollection.cs | 3 +-- .../MappedTopicCache.cs | 3 +-- .../Internal/Mapping/PropertyConfiguration.cs | 2 +- .../Reflection/TopicPropertyDispatcher.cs | 21 ++++++++++--------- .../Reflection/TypeMemberInfoCollection.cs | 1 - .../Annotations/AttributeKeyAttribute.cs | 2 +- .../Mapping/Annotations/InheritAttribute.cs | 2 +- OnTopic/Mapping/TopicMappingService.cs | 3 +-- OnTopic/Metadata/ContentTypeDescriptor.cs | 1 + OnTopic/ReferenceSetterAttribute.cs | 2 +- .../NamedTopicCollection.cs | 3 ++- .../RelatedTopicCollection.cs | 2 +- .../TopicReferenceDictionary.cs | 2 +- OnTopic/Topic.cs | 2 +- 20 files changed, 29 insertions(+), 32 deletions(-) rename OnTopic/{Collections => Attributes}/AttributeValueCollection.cs (99%) rename OnTopic/Internal/{Collections => Mapping}/MappedTopicCache.cs (92%) rename OnTopic/{Collections => References}/NamedTopicCollection.cs (99%) rename OnTopic/{Collections => References}/RelatedTopicCollection.cs (99%) rename OnTopic/{Collections => References}/TopicReferenceDictionary.cs (99%) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index b5a968a7..e79d1519 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -5,10 +5,8 @@ \=============================================================================================================================*/ using System; using System.Globalization; -using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; -using OnTopic.Collections; using OnTopic.Tests.Entities; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index 4da294b1..be558f38 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -6,7 +6,7 @@ using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Collections; +using OnTopic.Attributes; using OnTopic.Data.Caching; using OnTopic.Repositories; using OnTopic.TestDoubles; diff --git a/OnTopic.Tests/NamedTopicCollection.cs b/OnTopic.Tests/NamedTopicCollection.cs index a7465755..fca2199c 100644 --- a/OnTopic.Tests/NamedTopicCollection.cs +++ b/OnTopic.Tests/NamedTopicCollection.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Collections; +using OnTopic.References; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/RelatedTopicCollectionTest.cs index 1e399f34..632ed443 100644 --- a/OnTopic.Tests/RelatedTopicCollectionTest.cs +++ b/OnTopic.Tests/RelatedTopicCollectionTest.cs @@ -6,7 +6,7 @@ using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Collections; +using OnTopic.References; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/TopicCollectionTest.cs b/OnTopic.Tests/TopicCollectionTest.cs index 9c7182d5..86c17b8a 100644 --- a/OnTopic.Tests/TopicCollectionTest.cs +++ b/OnTopic.Tests/TopicCollectionTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Attributes; using OnTopic.Collections; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index e6381280..58a3d2c7 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -4,9 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Collections; +using OnTopic.References; using OnTopic.Tests.Entities; namespace OnTopic.Tests { diff --git a/OnTopic/Collections/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs similarity index 99% rename from OnTopic/Collections/AttributeValueCollection.cs rename to OnTopic/Attributes/AttributeValueCollection.cs index 9fd6638b..da07088a 100644 --- a/OnTopic/Collections/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -8,12 +8,11 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; -using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Repositories; -namespace OnTopic.Collections { +namespace OnTopic.Attributes { /*============================================================================================================================ | CLASS: ATTRIBUTE VALUE COLLECTION diff --git a/OnTopic/Internal/Collections/MappedTopicCache.cs b/OnTopic/Internal/Mapping/MappedTopicCache.cs similarity index 92% rename from OnTopic/Internal/Collections/MappedTopicCache.cs rename to OnTopic/Internal/Mapping/MappedTopicCache.cs index 7628ec62..d5bd9c1f 100644 --- a/OnTopic/Internal/Collections/MappedTopicCache.cs +++ b/OnTopic/Internal/Mapping/MappedTopicCache.cs @@ -5,9 +5,8 @@ \=============================================================================================================================*/ using System.Collections.Concurrent; using OnTopic.Mapping; -using OnTopic.Internal.Mapping; -namespace OnTopic.Internal.Collections { +namespace OnTopic.Internal.Mapping { /*============================================================================================================================ | CLASS: MAPPED TOPIC CACHE diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Internal/Mapping/PropertyConfiguration.cs index 6de2d2eb..3d2fecf1 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Internal/Mapping/PropertyConfiguration.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using OnTopic.Collections; +using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Mapping; using OnTopic.Mapping.Annotations; diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index b3ae83f1..8c0aaae9 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -9,7 +9,7 @@ using System.Reflection; using System.Runtime.ExceptionServices; using OnTopic.Attributes; -using OnTopic.Collections; +using OnTopic.References; namespace OnTopic.Internal.Reflection { @@ -72,15 +72,16 @@ namespace OnTopic.Internal.Reflection { /// /// One caveat to this are cases where the caller attempts to set the value via the property directly, /// instead of adding the item directly to the corresponding collection—e.g., they call instead - /// of e.g. the method from . In that case, - /// the business logic will already have been enforced, but the method will - /// not have been called. To mitigate the property setter getting called twice, collection implementors are advised to - /// offer an internal overload that allows an item to be added to the collection while bypassing the business logic. For - /// instance, this can be done using or ; in each - /// case, the internally accessible enforceBusinessLogic parameter allows a property setter to disable business - /// logic. Internally, this is done by calling , thus assuring that the business logic has already occurred. + /// of e.g. the method + /// from . In that case, the business logic will already have been enforced, but the method will not have been called. To mitigate the property setter getting + /// called twice, collection implementors are advised to offer an internal overload that allows an item to be added to the + /// collection while bypassing the business logic. For instance, this can be done using or ; in each case, the internally accessible + /// enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this is done by + /// calling , thus assuring that the + /// business logic has already occurred. /// /// internal class TopicPropertyDispatcher diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index d218b401..28acc5b5 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; diff --git a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs index f6e579a2..3b57518e 100644 --- a/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs +++ b/OnTopic/Mapping/Annotations/AttributeKeyAttribute.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Collections; +using OnTopic.Attributes; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/Annotations/InheritAttribute.cs b/OnTopic/Mapping/Annotations/InheritAttribute.cs index 51b79fb4..96909951 100644 --- a/OnTopic/Mapping/Annotations/InheritAttribute.cs +++ b/OnTopic/Mapping/Annotations/InheritAttribute.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Collections; +using OnTopic.Attributes; namespace OnTopic.Mapping.Annotations { diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index d6fb608b..275e6926 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -11,7 +11,6 @@ using System.Reflection; using System.Threading.Tasks; using OnTopic.Attributes; -using OnTopic.Internal.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; @@ -351,7 +350,7 @@ await MapAsync( /// cref="SetScalarValue(Topic,Object, PropertyConfiguration)"/> method will attempt to set the property on the based on, in order, the 's Get{Property}() method, {Property} /// property, and, finally, its collection (using ). If the property is not of a settable type, + /// cref="Attributes.AttributeValueCollection.GetValue(String, Boolean)"/>). If the property is not of a settable type, /// or the source value cannot be identified on the , then the property is not set. /// /// The source from which to pull the value. diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index ba2166d8..69459576 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -8,6 +8,7 @@ using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; +using OnTopic.References; namespace OnTopic.Metadata { diff --git a/OnTopic/ReferenceSetterAttribute.cs b/OnTopic/ReferenceSetterAttribute.cs index 69b5d377..22c0b3d2 100644 --- a/OnTopic/ReferenceSetterAttribute.cs +++ b/OnTopic/ReferenceSetterAttribute.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Collections; +using OnTopic.References; namespace OnTopic.Attributes { diff --git a/OnTopic/Collections/NamedTopicCollection.cs b/OnTopic/References/NamedTopicCollection.cs similarity index 99% rename from OnTopic/Collections/NamedTopicCollection.cs rename to OnTopic/References/NamedTopicCollection.cs index 098dec11..0f20c334 100644 --- a/OnTopic/Collections/NamedTopicCollection.cs +++ b/OnTopic/References/NamedTopicCollection.cs @@ -6,9 +6,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using OnTopic.Collections; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Collections { +namespace OnTopic.References { /*============================================================================================================================ | CLASS: TOPIC diff --git a/OnTopic/Collections/RelatedTopicCollection.cs b/OnTopic/References/RelatedTopicCollection.cs similarity index 99% rename from OnTopic/Collections/RelatedTopicCollection.cs rename to OnTopic/References/RelatedTopicCollection.cs index 8cce0e15..8bb80b68 100644 --- a/OnTopic/Collections/RelatedTopicCollection.cs +++ b/OnTopic/References/RelatedTopicCollection.cs @@ -9,7 +9,7 @@ using System.Linq; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Collections { +namespace OnTopic.References { /*============================================================================================================================ | CLASS: RELATED TOPIC COLLECTION diff --git a/OnTopic/Collections/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs similarity index 99% rename from OnTopic/Collections/TopicReferenceDictionary.cs rename to OnTopic/References/TopicReferenceDictionary.cs index 9420322f..b035bf17 100644 --- a/OnTopic/Collections/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -10,7 +10,7 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; -namespace OnTopic.Collections { +namespace OnTopic.References { /*============================================================================================================================ | CLASS: TOPIC REFERENCE DICTIONARY diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index c516fac3..d3a3a7b6 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -12,6 +12,7 @@ using OnTopic.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; +using OnTopic.References; namespace OnTopic { @@ -680,7 +681,6 @@ public Topic? DerivedTopic { /// The current 's relationships. public RelatedTopicCollection Relationships { get; } - /*========================================================================================================================== | PROPERTY: REFERENCES \-------------------------------------------------------------------------------------------------------------------------*/ From f80e08475cfc9c212ea557689225c60282a1c6aa Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 17:02:37 -0800 Subject: [PATCH 214/778] Consolidated all `ITypeLookupService`s into `OnTopic.Lookup` namespace The root `OnTopic` namespace was getting cluttered with various `ITypeLookupService` implementations, including `CompositeTypeLookupService`, `DefaultTopicLookupService`, and `StaticTypeLookupService`. In addition, there was a somewhat arbitrary separation between the `OnTopic.Reflection` namespace, which housed a variety of `Dynamic*LookupService` implementations (e.g., `DynamicTopicViewModelLookupService`) due to the fact that they rely on reflection to lookup classes, instead of a static list; from a callers perspective, however, these are all lookup services, and are more alike than not. These have now all been consolidated into a new `OnTopic.Lookup` namespace. --- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 2 +- OnTopic.Tests/ITypeLookupServiceTest.cs | 2 +- OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs | 1 + OnTopic.Tests/TopicMappingServiceTest.cs | 1 + OnTopic.ViewModels/TopicViewModelLookupService.cs | 1 + OnTopic/{ => Lookup}/CompositeTypeLookupService.cs | 2 +- OnTopic/{ => Lookup}/DefaultTopicLookupService.cs | 2 +- .../DynamicTopicBindingModelLookupService.cs | 2 +- OnTopic/{Reflection => Lookup}/DynamicTopicLookupService.cs | 2 +- .../DynamicTopicViewModelLookupService.cs | 2 +- OnTopic/{Reflection => Lookup}/DynamicTypeLookupService.cs | 2 +- OnTopic/{ => Lookup}/ITypeLookupService.cs | 2 +- OnTopic/{ => Lookup}/StaticTypeLookupService.cs | 2 +- OnTopic/Mapping/TopicMappingService.cs | 1 + OnTopic/TopicFactory.cs | 1 + 15 files changed, 15 insertions(+), 10 deletions(-) rename OnTopic/{ => Lookup}/CompositeTypeLookupService.cs (99%) rename OnTopic/{ => Lookup}/DefaultTopicLookupService.cs (99%) rename OnTopic/{Reflection => Lookup}/DynamicTopicBindingModelLookupService.cs (98%) rename OnTopic/{Reflection => Lookup}/DynamicTopicLookupService.cs (97%) rename OnTopic/{Reflection => Lookup}/DynamicTopicViewModelLookupService.cs (98%) rename OnTopic/{Reflection => Lookup}/DynamicTypeLookupService.cs (98%) rename OnTopic/{ => Lookup}/ITypeLookupService.cs (98%) rename OnTopic/{ => Lookup}/StaticTypeLookupService.cs (99%) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 6a63c4fa..81eec5ee 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -11,9 +11,9 @@ using OnTopic.AspNetCore.Mvc.Host.Components; using OnTopic.Data.Caching; using OnTopic.Data.Sql; +using OnTopic.Lookup; using OnTopic.Mapping; using OnTopic.Mapping.Hierarchical; -using OnTopic.Reflection; using OnTopic.Repositories; using OnTopic.ViewModels; diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 86f0d9b0..8b9a36a0 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Reflection; +using OnTopic.Lookup; using OnTopic.Tests.TestDoubles; using OnTopic.Tests.ViewModels; using OnTopic.ViewModels; diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index 665bd718..6266632b 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Lookup; using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index da3fc689..fbe35590 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -12,6 +12,7 @@ using OnTopic.Attributes; using OnTopic.Data.Caching; using OnTopic.Internal.Mapping; +using OnTopic.Lookup; using OnTopic.Mapping; using OnTopic.Mapping.Annotations; using OnTopic.Metadata; diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index c4875e1e..d15b0704 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using OnTopic.Lookup; namespace OnTopic.ViewModels { diff --git a/OnTopic/CompositeTypeLookupService.cs b/OnTopic/Lookup/CompositeTypeLookupService.cs similarity index 99% rename from OnTopic/CompositeTypeLookupService.cs rename to OnTopic/Lookup/CompositeTypeLookupService.cs index 18e099c4..18ed8e8d 100644 --- a/OnTopic/CompositeTypeLookupService.cs +++ b/OnTopic/Lookup/CompositeTypeLookupService.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Linq; -namespace OnTopic { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: COMPOSITE TYPE LOOKUP SERVICE diff --git a/OnTopic/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs similarity index 99% rename from OnTopic/DefaultTopicLookupService.cs rename to OnTopic/Lookup/DefaultTopicLookupService.cs index 21ec04c8..41159e2f 100644 --- a/OnTopic/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -9,7 +9,7 @@ using OnTopic.Metadata; using OnTopic.Metadata.AttributeTypes; -namespace OnTopic { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: TYPE INDEX diff --git a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs similarity index 98% rename from OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs rename to OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs index fa46e1d4..548153d9 100644 --- a/OnTopic/Reflection/DynamicTopicBindingModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs @@ -6,7 +6,7 @@ using System; using OnTopic.Models; -namespace OnTopic.Reflection { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: DYNAMIC TOPIC BINDING MODEL LOOKUP SERVICE diff --git a/OnTopic/Reflection/DynamicTopicLookupService.cs b/OnTopic/Lookup/DynamicTopicLookupService.cs similarity index 97% rename from OnTopic/Reflection/DynamicTopicLookupService.cs rename to OnTopic/Lookup/DynamicTopicLookupService.cs index 1bfbecaa..b9995ff8 100644 --- a/OnTopic/Reflection/DynamicTopicLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicLookupService.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; -namespace OnTopic.Reflection { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: DYNAMIC TOPIC LOOKUP SERVICE diff --git a/OnTopic/Reflection/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs similarity index 98% rename from OnTopic/Reflection/DynamicTopicViewModelLookupService.cs rename to OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index 4fdd383d..d5090955 100644 --- a/OnTopic/Reflection/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; -namespace OnTopic.Reflection { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: DYNAMIC TOPIC VIEW MODEL LOOKUP SERVICE diff --git a/OnTopic/Reflection/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs similarity index 98% rename from OnTopic/Reflection/DynamicTypeLookupService.cs rename to OnTopic/Lookup/DynamicTypeLookupService.cs index 6a2eb34c..ce58aabe 100644 --- a/OnTopic/Reflection/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -6,7 +6,7 @@ using System; using System.Linq; -namespace OnTopic.Reflection { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: DYNAMIC TYPE LOOKUP SERVICE diff --git a/OnTopic/ITypeLookupService.cs b/OnTopic/Lookup/ITypeLookupService.cs similarity index 98% rename from OnTopic/ITypeLookupService.cs rename to OnTopic/Lookup/ITypeLookupService.cs index 656cd142..5f991612 100644 --- a/OnTopic/ITypeLookupService.cs +++ b/OnTopic/Lookup/ITypeLookupService.cs @@ -6,7 +6,7 @@ using System; using OnTopic.Mapping; -namespace OnTopic { +namespace OnTopic.Lookup { /*============================================================================================================================ | INTERFACE: TYPE LOOKUP SERVICE diff --git a/OnTopic/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs similarity index 99% rename from OnTopic/StaticTypeLookupService.cs rename to OnTopic/Lookup/StaticTypeLookupService.cs index 791757c1..a60b9fef 100644 --- a/OnTopic/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -9,7 +9,7 @@ using OnTopic.Internal.Collections; using OnTopic.Internal.Diagnostics; -namespace OnTopic { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: STATIC TYPE LOOKUP SERVICE diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 275e6926..8d5939ec 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -14,6 +14,7 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; +using OnTopic.Lookup; using OnTopic.Mapping.Annotations; using OnTopic.Models; using OnTopic.Repositories; diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 216274a1..75e403dd 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; +using OnTopic.Lookup; namespace OnTopic { From a38d619aefa4d276cc9ed2ee87185fe59f57f875 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 17:15:51 -0800 Subject: [PATCH 215/778] Marked `OnTopic.Internal.Reflection` classes as `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `MemberInfoCollection`, `MemberInfoCollection`, and `TypeMemberInfoCollection` were marked as `public`—despite being intended for internal use. These are not being used by any current projects that we plan on upgrading to OnTopic 5.0.0, however, and can _actually_ be marked as `internal`, instead of simply being placed within the `OnTopic.Internal` namespace. --- .../Reflection/MemberInfoCollection.cs | 6 +-- .../Reflection/MemberInfoCollection{T}.cs | 8 ++-- .../Reflection/TypeMemberInfoCollection.cs | 38 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection.cs b/OnTopic/Internal/Reflection/MemberInfoCollection.cs index 707b2dd7..1cabca63 100644 --- a/OnTopic/Internal/Reflection/MemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/MemberInfoCollection.cs @@ -15,7 +15,7 @@ namespace OnTopic.Internal.Reflection { /// /// Provides keyed access to a collection of instances. /// - public class MemberInfoCollection : MemberInfoCollection { + internal class MemberInfoCollection : MemberInfoCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -25,7 +25,7 @@ public class MemberInfoCollection : MemberInfoCollection { /// name. /// /// The associated with the collection. - public MemberInfoCollection(Type type) : base(type) { + internal MemberInfoCollection(Type type) : base(type) { } /// @@ -36,7 +36,7 @@ public MemberInfoCollection(Type type) : base(type) { /// /// An of instances to populate the collection. /// - public MemberInfoCollection(Type type, IEnumerable members) : base(type, members) { + internal MemberInfoCollection(Type type, IEnumerable members) : base(type, members) { } } //Class diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs index 9e890eaa..e97ddcfa 100644 --- a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs +++ b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs @@ -18,7 +18,7 @@ namespace OnTopic.Internal.Reflection { /// /// Provides keyed access to a collection of instances. /// - public class MemberInfoCollection : KeyedCollection where T : MemberInfo { + internal class MemberInfoCollection : KeyedCollection where T : MemberInfo { /*========================================================================================================================== | CONSTRUCTOR @@ -28,7 +28,7 @@ public class MemberInfoCollection : KeyedCollection where T : Memb /// name. /// /// The associated with the collection. - public MemberInfoCollection(Type type) : base(StringComparer.OrdinalIgnoreCase) { + internal MemberInfoCollection(Type type) : base(StringComparer.OrdinalIgnoreCase) { Contract.Requires(type); Type = type; foreach ( @@ -54,7 +54,7 @@ in type.GetMembers( /// /// An of instances to populate the collection. /// - public MemberInfoCollection(Type type, IEnumerable members) : base(StringComparer.OrdinalIgnoreCase) { + internal MemberInfoCollection(Type type, IEnumerable members) : base(StringComparer.OrdinalIgnoreCase) { Contract.Requires(type); Contract.Requires(members); Type = type; @@ -99,7 +99,7 @@ protected override void InsertItem(int index, T item) { /// /// Returns the type associated with this collection. /// - public Type Type { get; } + internal Type Type { get; } /*========================================================================================================================== | OVERRIDE: GET KEY FOR ITEM diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index 28acc5b5..bda0e192 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -17,7 +17,7 @@ namespace OnTopic.Internal.Reflection { /// /// A collection of instances, each associated with a specific . /// - public class TypeMemberInfoCollection : KeyedCollection { + internal class TypeMemberInfoCollection : KeyedCollection { /*========================================================================================================================== | PRIVATE VARIABLES @@ -54,7 +54,7 @@ static TypeMemberInfoCollection() { /// /// An optional which properties must have defined to be considered writable. /// - public TypeMemberInfoCollection(Type? attributeFlag = null) : base() { + internal TypeMemberInfoCollection(Type? attributeFlag = null) : base() { _attributeFlag = attributeFlag; } @@ -68,7 +68,7 @@ public TypeMemberInfoCollection(Type? attributeFlag = null) : base() { /// If the collection cannot be found locally, it will be created. /// /// The type for which the members should be retrieved. - public MemberInfoCollection GetMembers(Type type) { + internal MemberInfoCollection GetMembers(Type type) { if (!Contains(type)) { lock(Items) { if (!Contains(type)) { @@ -89,7 +89,7 @@ public MemberInfoCollection GetMembers(Type type) { /// If the collection cannot be found locally, it will be created. /// /// The type for which the members should be retrieved. - public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => + internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => new(type, GetMembers(type).Where(m => typeof(T).IsAssignableFrom(m.GetType())).Cast()); /*========================================================================================================================== @@ -99,7 +99,7 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// Used reflection to identify a local member by a given name, and returns the associated /// instance. /// - public MemberInfo? GetMember(Type type, string name) { + internal MemberInfo? GetMember(Type type, string name) { var members = GetMembers(type); if (members.Contains(name)) { return members[name]; @@ -114,7 +114,7 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// Used reflection to identify a local member by a given name, and returns the associated /// instance. /// - public T? GetMember(Type type, string name) where T : MemberInfo { + internal T? GetMember(Type type, string name) where T : MemberInfo { var members = GetMembers(type); if (members.Contains(name) && typeof(T).IsAssignableFrom(members[name].GetType())) { return members[name] as T; @@ -128,7 +128,7 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// /// Used reflection to identify if a local member is available. /// - public bool HasMember(Type type, string name) => GetMember(type, name) is not null; + internal bool HasMember(Type type, string name) => GetMember(type, name) is not null; /*========================================================================================================================== | METHOD: HAS MEMBER {T} @@ -136,7 +136,7 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// /// Used reflection to identify if a local member of type is available. /// - public bool HasMember(Type type, string name) where T: MemberInfo => GetMember(type, name) is not null; + internal bool HasMember(Type type, string name) where T: MemberInfo => GetMember(type, name) is not null; /*========================================================================================================================== | METHOD: HAS SETTABLE PROPERTY @@ -150,7 +150,7 @@ public MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// The on which the property is defined. /// The name of the property to assess. /// Optional, the expected. - public bool HasSettableProperty(Type type, string name, Type? targetType = null) { + internal bool HasSettableProperty(Type type, string name, Type? targetType = null) { var property = GetMember(type, name); return ( property is not null and { CanWrite: true } && @@ -169,7 +169,7 @@ public bool HasSettableProperty(Type type, string name, Type? targetType = null) /// The object on which the property is defined. /// The name of the property to assess. /// The value to set on the property. - public bool SetPropertyValue(object target, string name, string? value) { + internal bool SetPropertyValue(object target, string name, string? value) { Contract.Requires(target, nameof(target)); Contract.Requires(name, nameof(name)); @@ -200,7 +200,7 @@ public bool SetPropertyValue(object target, string name, string? value) { /// The object on which the property is defined. /// The name of the property to assess. /// The value to set on the property. - public bool SetPropertyValue(object target, string name, object? value) { + internal bool SetPropertyValue(object target, string name, object? value) { Contract.Requires(target, nameof(target)); Contract.Requires(name, nameof(name)); @@ -234,7 +234,7 @@ public bool SetPropertyValue(object target, string name, object? value) { /// The on which the property is defined. /// The name of the property to assess. /// Optional, the expected. - public bool HasGettableProperty(Type type, string name, Type? targetType = null) { + internal bool HasGettableProperty(Type type, string name, Type? targetType = null) { var property = GetMember(type, name); return ( property is not null and { CanRead: true } && @@ -253,7 +253,7 @@ public bool HasGettableProperty(Type type, string name, Type? targetType = null) /// The object instance on which the property is defined. /// The name of the property to assess. /// Optional, the expected. - public object? GetPropertyValue(object target, string name, Type? targetType = null) { + internal object? GetPropertyValue(object target, string name, Type? targetType = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -293,7 +293,7 @@ public bool HasGettableProperty(Type type, string name, Type? targetType = null) /// The on which the method is defined. /// The name of the method to assess. /// Optional, the expected. - public bool HasSettableMethod(Type type, string name, Type? targetType = null) { + internal bool HasSettableMethod(Type type, string name, Type? targetType = null) { var method = GetMember(type, name); return ( method is not null && @@ -317,7 +317,7 @@ method is not null && /// The object instance on which the method is defined. /// The name of the method to assess. /// The value to set the method to. - public bool SetMethodValue(object target, string name, string? value) { + internal bool SetMethodValue(object target, string name, string? value) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -362,7 +362,7 @@ public bool SetMethodValue(object target, string name, string? value) { /// The object instance on which the method is defined. /// The name of the method to assess. /// The value to set the method to. - public bool SetMethodValue(object target, string name, object? value) { + internal bool SetMethodValue(object target, string name, object? value) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -407,7 +407,7 @@ public bool SetMethodValue(object target, string name, object? value) { /// The on which the method is defined. /// The name of the method to assess. /// Optional, the expected. - public bool HasGettableMethod(Type type, string name, Type? targetType = null) { + internal bool HasGettableMethod(Type type, string name, Type? targetType = null) { var method = GetMember(type, name); return ( method is not null && @@ -426,7 +426,7 @@ method is not null && /// The object instance on which the method is defined. /// The name of the method to assess. /// Optional, the expected. - public object? GetMethodValue(object target, string name, Type? targetType = null) { + internal object? GetMethodValue(object target, string name, Type? targetType = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -527,7 +527,7 @@ private static bool IsSettableType(Type sourceType, Type? targetType = null) { /// /// A list of types that are allowed to be set using . /// - public static Collection SettableTypes { get; } + internal static Collection SettableTypes { get; } /*========================================================================================================================== | OVERRIDE: INSERT ITEM From 346104e88f807bd84480dca5957923f9557c593e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 6 Jan 2021 17:22:20 -0800 Subject: [PATCH 216/778] Updated `NetAnaylzers` package to latest version This includes some bug fixes, some of which will allow us to remove some exclusions. --- OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj | 2 +- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 2 +- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index 9ebfca4f..64c69ddc 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index f2c36a75..68fc1b07 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index c237431c..eeba6795 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -45,7 +45,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 78cb7861..4c8d9740 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -45,7 +45,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 8d18d689..8b1d7fe9 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -42,7 +42,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 458e68c8..e2975435 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -25,7 +25,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index c667655d..7e2ef426 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 9f4db5fa..a6c4a912 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -44,7 +44,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index b2443d2d..550355c7 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -45,7 +45,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 099ba4fd8ec40260166c502c1ce1450d57610849 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 00:49:53 -0800 Subject: [PATCH 217/778] Removed warning suppressions for injection vulnerabilities In `Microsoft.CodeAnalysis.NetAnalyzers` 5.0.0 and 5.0.1, there was a bug that caused multiple exceptions when assessing various injection vulnerabilities (CA3001, 3003, 3006, 3008, 3009, 3011, 3012) (see dotnet/roslyn-analyzers#4495). This was resolved in 5.0.3, so the warnings no longer need to be suppressed. Unfortunately, a couple of other suppressions that I had understood to be resolved still appear to be needed, so that will require additional evaluation to ensure there isn't some confounding variable. Note: Normally these would be suppressed in the `GlobalSuppressions` class or via an inline `pragma` directive. Perhaps because these analyzers were throwing exceptions, that didn't satisfy code analysis, and thus they had to be handled globally as the `` element in the `csproj`. --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index eeba6795..1a22f769 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -32,7 +32,7 @@ full false latest - 1701;1702;CA1303;CA3001;CA3003;CA3006;CA3008;CA3009;CA3011;CA3012 + 1701;1702;CA1303; pdbonly From bde98870591c0663a495805e22035d4e6bb8b5ff Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 13:17:36 -0800 Subject: [PATCH 218/778] Moved `TypeCollection` to `OnTopic.Lookup`, marked `internal` While the `TypeCollection` is conceivably useful for broader purposes, it is currently exclusively used by the `StaticTypeLookupService` as a private backing field. As such, there's no need for it to be marked as `public`, so I've marked it as `internal`. Further, as an `internal` dependency, there's no need to it to be explicitly organized under `OnTopic.Internal` (which is meant to distinguish `public` classes that aren't intended for public use). As such, it can be moved to `OnTopic.Lookup`, next to the `StaticTypeLookupService. If other internal classes want to use it, `OnTopic.Collections` will make more sense, since it's a fairly general collection. As an `internal` dependency, we can move it at a later date if, in needed. --- OnTopic/Lookup/StaticTypeLookupService.cs | 1 - OnTopic/{Internal/Collections => Lookup}/TypeCollection.cs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename OnTopic/{Internal/Collections => Lookup}/TypeCollection.cs (92%) diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index a60b9fef..a8d91fb2 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using OnTopic.Internal.Collections; using OnTopic.Internal.Diagnostics; namespace OnTopic.Lookup { diff --git a/OnTopic/Internal/Collections/TypeCollection.cs b/OnTopic/Lookup/TypeCollection.cs similarity index 92% rename from OnTopic/Internal/Collections/TypeCollection.cs rename to OnTopic/Lookup/TypeCollection.cs index 236b7ef2..1ca787ff 100644 --- a/OnTopic/Internal/Collections/TypeCollection.cs +++ b/OnTopic/Lookup/TypeCollection.cs @@ -10,7 +10,7 @@ using System.Reflection; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Internal.Collections { +namespace OnTopic.Lookup { /*============================================================================================================================ | CLASS: TYPE COLLECTION @@ -19,7 +19,7 @@ namespace OnTopic.Internal.Collections { /// Provides a of instances indexed by . /// - public class TypeCollection : KeyedCollection { + internal class TypeCollection : KeyedCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -28,7 +28,7 @@ public class TypeCollection : KeyedCollection { /// Instantiates a new . Optionally accepts an of instances to prepopulate the collection. /// - public TypeCollection(IEnumerable? types = null) : base(StringComparer.InvariantCultureIgnoreCase) { + internal TypeCollection(IEnumerable? types = null) : base(StringComparer.InvariantCultureIgnoreCase) { /*---------------------------------------------------------------------------------------------------------------------- | Populate collection From 3c4dc3f329210ba39f4ae0bc9598dfea933ecfc9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 13:29:41 -0800 Subject: [PATCH 219/778] Moved `OnTopic.Internal.Mapping` to `OnTopic.Mapping.Internal` As the `Mapping` namespace is fairly involved, and the `OnTopic.Internal.Mapping` services are exclusively used within the `Mapping` namespace, it makes sense to move them to `OnTopic.Mapping.Internal` so they're closer to the source. This will also be useful if we opt to migrate these to a separate project, or set of projects, at a future date, as we're evaluating. Technically, there's an argument for moving some of these out of `Internal`. For instance, both `PropertyConfiguration` and `MappedTopicCache` are utilized by `protected` methods on `TopicMappingService`. As such, it's available for use by derived instances of `TopicMappingService`. That's not a scenario we expect, however, and it remains true that most implementations will never need to be aware of or use these classes. --- OnTopic.Tests/TopicMappingServiceTest.cs | 2 +- .../{Internal/Mapping => Mapping/Internal}/MappedTopicCache.cs | 3 +-- .../Mapping => Mapping/Internal}/MappedTopicCacheEntry.cs | 3 +-- .../Mapping => Mapping/Internal}/PropertyConfiguration.cs | 3 +-- .../{Internal/Mapping => Mapping/Internal}/RelationshipMap.cs | 2 +- OnTopic/Mapping/Reverse/BindingModelValidator.cs | 2 +- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 2 +- OnTopic/Mapping/TopicMappingService.cs | 2 +- 8 files changed, 8 insertions(+), 11 deletions(-) rename OnTopic/{Internal/Mapping => Mapping/Internal}/MappedTopicCache.cs (94%) rename OnTopic/{Internal/Mapping => Mapping/Internal}/MappedTopicCacheEntry.cs (98%) rename OnTopic/{Internal/Mapping => Mapping/Internal}/PropertyConfiguration.cs (99%) rename OnTopic/{Internal/Mapping => Mapping/Internal}/RelationshipMap.cs (98%) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index fbe35590..02619a89 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -11,9 +11,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; using OnTopic.Data.Caching; -using OnTopic.Internal.Mapping; using OnTopic.Lookup; using OnTopic.Mapping; +using OnTopic.Mapping.Internal; using OnTopic.Mapping.Annotations; using OnTopic.Metadata; using OnTopic.Metadata.AttributeTypes; diff --git a/OnTopic/Internal/Mapping/MappedTopicCache.cs b/OnTopic/Mapping/Internal/MappedTopicCache.cs similarity index 94% rename from OnTopic/Internal/Mapping/MappedTopicCache.cs rename to OnTopic/Mapping/Internal/MappedTopicCache.cs index d5bd9c1f..1af9ab76 100644 --- a/OnTopic/Internal/Mapping/MappedTopicCache.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCache.cs @@ -4,9 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System.Collections.Concurrent; -using OnTopic.Mapping; -namespace OnTopic.Internal.Mapping { +namespace OnTopic.Mapping.Internal { /*============================================================================================================================ | CLASS: MAPPED TOPIC CACHE diff --git a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs similarity index 98% rename from OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs rename to OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs index 7fb339d7..adec97a9 100644 --- a/OnTopic/Internal/Mapping/MappedTopicCacheEntry.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs @@ -3,10 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping; using OnTopic.Mapping.Annotations; -namespace OnTopic.Internal.Mapping { +namespace OnTopic.Mapping.Internal { /*============================================================================================================================ | CLASS: MAPPED TOPIC CACHE ENTRY diff --git a/OnTopic/Internal/Mapping/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs similarity index 99% rename from OnTopic/Internal/Mapping/PropertyConfiguration.cs rename to OnTopic/Mapping/Internal/PropertyConfiguration.cs index 3d2fecf1..eb2b582c 100644 --- a/OnTopic/Internal/Mapping/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -12,10 +12,9 @@ using System.Reflection; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; -using OnTopic.Mapping; using OnTopic.Mapping.Annotations; -namespace OnTopic.Internal.Mapping { +namespace OnTopic.Mapping.Internal { /*============================================================================================================================ | CLASS: PROPERTY ATTRIBUTES diff --git a/OnTopic/Internal/Mapping/RelationshipMap.cs b/OnTopic/Mapping/Internal/RelationshipMap.cs similarity index 98% rename from OnTopic/Internal/Mapping/RelationshipMap.cs rename to OnTopic/Mapping/Internal/RelationshipMap.cs index 2f5d08ab..50bc8236 100644 --- a/OnTopic/Internal/Mapping/RelationshipMap.cs +++ b/OnTopic/Mapping/Internal/RelationshipMap.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using OnTopic.Mapping.Annotations; -namespace OnTopic.Internal.Mapping { +namespace OnTopic.Mapping.Internal { /*============================================================================================================================ | CLASS: RELATIONSHIP MAP diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index eb8d31dc..1f012f51 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -11,9 +11,9 @@ using System.Linq; using System.Reflection; using OnTopic.Internal.Diagnostics; -using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; using OnTopic.Mapping.Annotations; +using OnTopic.Mapping.Internal; using OnTopic.Metadata; using OnTopic.Models; using OnTopic.Repositories; diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 2394897f..a4172b39 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -13,8 +13,8 @@ using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; -using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; +using OnTopic.Mapping.Internal; using OnTopic.Metadata; using OnTopic.Models; using OnTopic.Repositories; diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 8d5939ec..02c229ea 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -12,10 +12,10 @@ using System.Threading.Tasks; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; -using OnTopic.Internal.Mapping; using OnTopic.Internal.Reflection; using OnTopic.Lookup; using OnTopic.Mapping.Annotations; +using OnTopic.Mapping.Internal; using OnTopic.Models; using OnTopic.Repositories; From 2f4a5a12e78734178c3c9c6b1cf729f3128775fe Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 13:35:22 -0800 Subject: [PATCH 220/778] Updated XML Docs to use framwork types For consistency. --- OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 8c0aaae9..d4819211 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -80,7 +80,7 @@ namespace OnTopic.Internal.Reflection { /// AttributeValueCollection.SetValue(String, String?, Boolean?, Boolean, DateTime?, Boolean?)"/> or ; in each case, the internally accessible /// enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this is done by - /// calling , thus assuring that the + /// calling , thus assuring that the /// business logic has already occurred. /// /// @@ -165,12 +165,12 @@ internal TopicPropertyDispatcher(Topic associatedTopic) { /// internal to prevent external actors from bypassing the business logic; the purpose is to confirm that the business /// logic has already been enforced, not to make the business logic optional. Two examples of this are the internal /// enforceBusinessLogic parameters on and and . /// /// /// It's worth noting that any calls to are invalidated the next time is called. As such, is not a way + /// cref="Enforce(String, TValueType?)"/> is called. As such, is not a way /// to permanently disable calling a property setter. (The correct way to do that is to remove the property setter, or /// at least its corresponding .) Instead, it only disables the next attempt to add /// an item corresponding to that key—which, if correctly implemented, will be when the current and classes. + /// Provides unit tests for the and classes. /// /// /// These are internal collections and not accessible publicly. /// [TestClass] - public class TypeMemberInfoCollectionTest { + public class MemberDispatcherTest { /*========================================================================================================================== | TEST: CONSTRUCTOR: VALID TYPE: IDENTIFIES PROPERTY @@ -78,13 +78,13 @@ public void Constructor_ValidType_IdentifiesMethod() { | TEST: GET MEMBERS: PROPERTY INFO: RETURNS PROPERTIES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that functions. + /// Establishes a and confirms that functions. /// [TestMethod] public void GetMembers_PropertyInfo_ReturnsProperties() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var properties = types.GetMembers(typeof(ContentTypeDescriptor)); @@ -99,13 +99,13 @@ public void GetMembers_PropertyInfo_ReturnsProperties() { | TEST: GET MEMBER: PROPERTY INFO BY KEY: RETURNS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that correctly returns the expected properties. + /// Establishes a and confirms that correctly returns the expected properties. /// [TestMethod] public void GetMember_PropertyInfoByKey_ReturnsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); Assert.IsNotNull(types.GetMember(typeof(ContentTypeDescriptor), "Key")); Assert.IsNotNull(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); @@ -117,13 +117,13 @@ public void GetMember_PropertyInfoByKey_ReturnsValue() { | TEST: GET MEMBER: METHOD INFO BY KEY: RETURNS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that correctly returns the expected methods. + /// Establishes a and confirms that correctly returns the expected methods. /// [TestMethod] public void GetMember_MethodInfoByKey_ReturnsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); Assert.IsNotNull(types.GetMember(typeof(ContentTypeDescriptor), "GetWebPath")); Assert.IsNull(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); @@ -134,13 +134,13 @@ public void GetMember_MethodInfoByKey_ReturnsValue() { | TEST: GET MEMBER: GENERIC TYPE MISMATCH: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that does not return values if the + /// Establishes a and confirms that does not return values if the /// [TestMethod] public void GetMember_GenericTypeMismatch_ReturnsNull() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); Assert.IsNull(types.GetMember(typeof(ContentTypeDescriptor), "IsTypeOf")); Assert.IsNull(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors")); @@ -151,13 +151,13 @@ public void GetMember_GenericTypeMismatch_ReturnsNull() { | TEST: SET PROPERTY VALUE: KEY: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a key value can be properly set using the - /// method. + /// Establishes a and confirms that a key value can be properly set using the + /// method. /// [TestMethod] public void SetPropertyValue_Key_SetsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var topic = TopicFactory.Create("Test", "ContentType"); var isKeySet = types.SetPropertyValue(topic, "Key", "NewKey"); @@ -175,13 +175,13 @@ public void SetPropertyValue_Key_SetsValue() { | TEST: SET PROPERTY VALUE: BOOLEAN: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a boolean value can be properly set using the - /// method. + /// Establishes a and confirms that a boolean value can be properly set using the + /// method. /// [TestMethod] public void SetPropertyValue_Boolean_SetsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var topic = TopicFactory.Create("Test", "ContentType"); types.SetPropertyValue(topic, "IsHidden", "1"); @@ -194,13 +194,13 @@ public void SetPropertyValue_Boolean_SetsValue() { | TEST: SET PROPERTY VALUE: DATE/TIME: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a date/time value can be properly set using the - /// method. + /// Establishes a and confirms that a date/time value can be properly set using the + /// method. /// [TestMethod] public void SetPropertyValue_DateTime_SetsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var topic = TopicFactory.Create("Test", "ContentType"); var isDateSet = types.SetPropertyValue(topic, "LastModified", "June 3, 2008"); @@ -223,13 +223,13 @@ public void SetPropertyValue_DateTime_SetsValue() { | TEST: SET PROPERTY VALUE: INVALID PROPERTY: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that an invalid property being set via the - /// method returns false. + /// Establishes a and confirms that an invalid property being set via the + /// method returns false. /// [TestMethod] public void SetPropertyValue_InvalidProperty_ReturnsFalse() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var topic = TopicFactory.Create("Test", "ContentType"); var isInvalidPropertySet = types.SetPropertyValue(topic, "InvalidProperty", "Invalid"); @@ -242,13 +242,13 @@ public void SetPropertyValue_InvalidProperty_ReturnsFalse() { | TEST: SET METHOD: VALID VALUE: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a value can be properly set using the - /// method. + /// Establishes a and confirms that a value can be properly set using the + /// method. /// [TestMethod] public void SetMethod_ValidValue_SetsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var source = new MethodBasedViewModel(); var isValueSet = types.SetMethodValue(source, "SetMethod", "123"); @@ -264,13 +264,13 @@ public void SetMethod_ValidValue_SetsValue() { | TEST: SET METHOD: INVALID VALUE: DOESN'T SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a value set with an invalid value using the - /// method returns an exception. + /// Establishes a and confirms that a value set with an invalid value using the + /// method returns an exception. /// [TestMethod] public void SetMethod_InvalidValue_DoesNotSetValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var source = new MethodBasedViewModel(); var isValueSet = types.SetMethodValue(source, "SetMethod", "ABC"); @@ -287,13 +287,13 @@ public void SetMethod_InvalidValue_DoesNotSetValue() { | TEST: SET METHOD: INVALID MEMBER: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that setting an invalid property name using the - /// method returns false. + /// Establishes a and confirms that setting an invalid property name using the + /// method returns false. /// [TestMethod] public void SetMethod_Integer_SetsValue() { - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var source = new MethodBasedViewModel(); var isInvalidSet = types.SetMethodValue(source, "BogusMethod", "123"); @@ -318,7 +318,7 @@ public void SetMethod_Integer_SetsValue() { public void SetPropertyValue_ReflectionPerformance() { var totalIterations = 1; - var types = new TypeMemberInfoCollection(); + var types = new MemberDispatcher(); var topic = TopicFactory.Create("Test", "ContentType"); int i; diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs similarity index 97% rename from OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs rename to OnTopic/Internal/Reflection/MemberDispatcher.cs index bda0e192..430b1726 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -12,12 +12,12 @@ namespace OnTopic.Internal.Reflection { /*============================================================================================================================ - | CLASS: TYPE COLLECTION + | CLASS: MEMBER DISPATCHER \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// A collection of instances, each associated with a specific . + /// A collection of instances, each associated with a specific . /// - internal class TypeMemberInfoCollection : KeyedCollection { + internal class MemberDispatcher : KeyedCollection { /*========================================================================================================================== | PRIVATE VARIABLES @@ -28,9 +28,9 @@ internal class TypeMemberInfoCollection : KeyedCollection - /// Initializes static properties on . + /// Initializes static properties on . /// - static TypeMemberInfoCollection() { + static MemberDispatcher() { SettableTypes = new() { typeof(bool), typeof(bool?), @@ -49,12 +49,12 @@ static TypeMemberInfoCollection() { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// An optional which properties must have defined to be considered writable. /// - internal TypeMemberInfoCollection(Type? attributeFlag = null) : base() { + internal MemberDispatcher(Type? attributeFlag = null) : base() { _attributeFlag = attributeFlag; } @@ -560,7 +560,7 @@ protected override void InsertItem(int index, MemberInfoCollection item) { } else { throw new ArgumentException( - $"The '{nameof(TypeMemberInfoCollection)}' already contains the {nameof(MemberInfoCollection)} of the Type " + + $"The '{nameof(MemberDispatcher)}' already contains the {nameof(MemberInfoCollection)} of the Type " + $"'{item.Type}'.", nameof(item) ); diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index d4819211..ae63cfcc 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -92,7 +92,7 @@ internal class TopicPropertyDispatcher /*========================================================================================================================== | STATIC VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly TypeMemberInfoCollection _typeCache = new(typeof(TAttributeType)); + static readonly MemberDispatcher _typeCache = new(typeof(TAttributeType)); /*========================================================================================================================== | PRIVATE VARIABLES diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index a4172b39..ff4a4833 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -30,7 +30,7 @@ public class ReverseTopicMappingService : IReverseTopicMappingService { /*========================================================================================================================== | STATIC VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly TypeMemberInfoCollection _typeCache = new(); + static readonly MemberDispatcher _typeCache = new(); /*========================================================================================================================== | PRIVATE VARIABLES diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 02c229ea..e72a5de8 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -33,7 +33,7 @@ public class TopicMappingService : ITopicMappingService { /*========================================================================================================================== | STATIC VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - static readonly TypeMemberInfoCollection _typeCache = new(); + static readonly MemberDispatcher _typeCache = new(); /*========================================================================================================================== | PRIVATE VARIABLES @@ -797,7 +797,7 @@ protected IList FlattenTopicGraph(Topic source, IList targetList, /// Sets a property on the target view model to a compatible value on the source object. /// /// - /// Even if the property values can't be set by the , properties should be settable + /// Even if the property values can't be set by the , properties should be settable /// assuming the source and target types are compatible. In this case, needn't know /// anything about the property type as it doesn't need to do a conversion; it can just do a one-to-one mapping. /// From 8f7d537ce6440df94442a8bea9e8b40b03e8cd0b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 16:06:07 -0800 Subject: [PATCH 222/778] Extracts `TypeMemberInfoCollection` from `MemberDispatcher` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initially, the `MemberDispatcher` was a keyed collection of `MemberInfoCollection` instanced, keyed by `Type`. This grew into a dynamic dispatch service, and thus the rename to `MemberDispatcher` (50019c7). As the base class renamed a `KeyedCollection<>`, however, it has a lot of members that are exclusively needed for internal access, and only act to clutter—and confuse—the class's interface. To mitigate this, the underlying collection has been extracted, and moved into a reestablished `TypeMemberInfoCollection` which exclusively contains the collection-related logic. This is then used as a private cache in the `MemberDispatch` instead of as a base class. This dramatically cleans up the interface of the `MemberDispatch`, and better distinguishes between responsibilities. --- .../Internal/Reflection/MemberDispatcher.cs | 66 ++------------- .../Reflection/TypeMemberInfoCollection.cs | 81 +++++++++++++++++++ 2 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 430b1726..6d53fb99 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -15,14 +15,15 @@ namespace OnTopic.Internal.Reflection { | CLASS: MEMBER DISPATCHER \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// A collection of instances, each associated with a specific . + /// The provides methods that simplify late-binding access to properties and methods. /// - internal class MemberDispatcher : KeyedCollection { + internal class MemberDispatcher { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private readonly Type? _attributeFlag; + private readonly TypeMemberInfoCollection _memberInfoCache = new(); /*========================================================================================================================== | CONSTRUCTOR (STATIC) @@ -69,14 +70,14 @@ internal MemberDispatcher(Type? attributeFlag = null) : base() { /// /// The type for which the members should be retrieved. internal MemberInfoCollection GetMembers(Type type) { - if (!Contains(type)) { - lock(Items) { - if (!Contains(type)) { - Add(new(type)); + if (!_memberInfoCache.Contains(type)) { + lock(_memberInfoCache) { + if (!_memberInfoCache.Contains(type)) { + _memberInfoCache.Add(new(type)); } } } - return this[type]; + return _memberInfoCache[type]; } /*========================================================================================================================== @@ -529,56 +530,5 @@ private static bool IsSettableType(Type sourceType, Type? targetType = null) { /// internal static Collection SettableTypes { get; } - /*========================================================================================================================== - | OVERRIDE: INSERT ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Fires any time an item is added to the collection. - /// - /// - /// Compared to the base implementation, will throw a specific error if a duplicate key - /// is inserted. This conveniently provides the , so it's clear what - /// is being duplicated. - /// - /// The zero-based index at which should be inserted. - /// The instance to insert. - /// - /// The TypeMemberInfoCollection already contains the MemberInfoCollection of the Type '{item.Type}'. - /// - protected override void InsertItem(int index, MemberInfoCollection item) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(item, nameof(item)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Insert item, if not already present - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!Contains(item.Type)) { - base.InsertItem(index, item); - } - else { - throw new ArgumentException( - $"The '{nameof(MemberDispatcher)}' already contains the {nameof(MemberInfoCollection)} of the Type " + - $"'{item.Type}'.", - nameof(item) - ); - } - } - - /*========================================================================================================================== - | OVERRIDE: GET KEY FOR ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Method must be overridden for the EntityCollection to extract the keys from the items. - /// - /// The object from which to extract the key. - /// The key for the specified collection item. - protected override Type GetKeyForItem(MemberInfoCollection item) { - Contract.Requires(item, "The item must be available in order to derive its key."); - return item.Type; - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs new file mode 100644 index 00000000..27c44141 --- /dev/null +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -0,0 +1,81 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.ObjectModel; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Internal.Reflection { + + /*============================================================================================================================ + | CLASS: KEYED MEMBER INFO COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides keyed access to a collection of instances. + /// + internal class TypeMemberInfoCollection: KeyedCollection { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class. + /// + internal TypeMemberInfoCollection() : base() { + } + + /*========================================================================================================================== + | OVERRIDE: INSERT ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Fires any time an item is added to the collection. + /// + /// + /// Compared to the base implementation, will throw a specific error if a duplicate key + /// is inserted. This conveniently provides the , so it's clear what + /// is being duplicated. + /// + /// The zero-based index at which should be inserted. + /// The instance to insert. + /// + /// The TypeMemberInfoCollection already contains the MemberInfoCollection of the Type '{item.Type}'. + /// + protected override void InsertItem(int index, MemberInfoCollection item) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(item, nameof(item)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Insert item, if not already present + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!Contains(item.Type)) { + base.InsertItem(index, item); + } + else { + throw new ArgumentException( + $"The '{nameof(TypeMemberInfoCollection)}' already contains the {nameof(MemberInfoCollection)} of the Type " + + $"'{item.Type}'.", + nameof(item) + ); + } + } + + /*========================================================================================================================== + | OVERRIDE: GET KEY FOR ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Method must be overridden for the EntityCollection to extract the keys from the items. + /// + /// The object from which to extract the key. + /// The key for the specified collection item. + protected override Type GetKeyForItem(MemberInfoCollection item) { + Contract.Requires(item, "The item must be available in order to derive its key."); + return item.Type; + } + + } //Class +} //Namespace \ No newline at end of file From a151d35c4a75a16fedc160bdb68e3da887fdabcf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 16:06:45 -0800 Subject: [PATCH 223/778] Improved documentation for the `MemberDispatcher` --- .../Internal/Reflection/MemberDispatcher.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 6d53fb99..9e96b008 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -17,6 +17,38 @@ namespace OnTopic.Internal.Reflection { /// /// The provides methods that simplify late-binding access to properties and methods. /// + /// + /// + /// The allows properties and members to be looked up and called based on string + /// representations of both the member names as well as, optionally, the values. String values can be deserialized into + /// various value formats supported by . + /// + /// + /// For retrieving values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. + /// + /// + /// For setting values, the typical workflow is for a caller to check either or , followed by or to retrieve the value. In these + /// scenarios, the will attempt to deserialize the value parameter from to the type expected by the corresponding property or method. Typically, this will be a , , , or . + /// + /// + /// Alternatively, setters can call or , in which case the final value parameter will be set the target property, or passed + /// as the parameter of the method without any attempt to convert it. Obviously, this requires that the target type be + /// assignable from the value object. + /// + /// + /// The is an internal service intended to meet the specific needs of OnTopic, and comes + /// with certain limitations. It only supports setting values of methods with a single parameter, which is assumed to + /// correspond to the value parameter. It will only operate against the first overload of a method, and/or the most + /// derived version of a member. + /// + /// internal class MemberDispatcher { /*========================================================================================================================== From 18edfca0a1efdcbf1e5e1e4083c774117233fa2f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 16:17:21 -0800 Subject: [PATCH 224/778] Moved non-dispatch related methods to `TypeMemberInfoCollection` Some methods included in `MemberDispatcher` aren't specific to the dynamic dispatch functionality, per se, and have broader relevance to the underlying `TypeMemberInfoCollection` used as a cache. As such, the implementations of these have been moved to `TypeMemberInfoCollection`. Unfortunately, these continue to be called from internal implementors. It's unclear if all of those are required. But while those are evaluated and confirmed, I am maintaining the original signatures as internal passthroughs to the `TypeMemberInfoCollection`. --- .../Internal/Reflection/MemberDispatcher.cs | 34 ++------ .../Reflection/TypeMemberInfoCollection.cs | 82 +++++++++++++++++++ 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 9e96b008..2f208a4f 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -101,16 +101,7 @@ internal MemberDispatcher(Type? attributeFlag = null) : base() { /// If the collection cannot be found locally, it will be created. /// /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) { - if (!_memberInfoCache.Contains(type)) { - lock(_memberInfoCache) { - if (!_memberInfoCache.Contains(type)) { - _memberInfoCache.Add(new(type)); - } - } - } - return _memberInfoCache[type]; - } + internal MemberInfoCollection GetMembers(Type type) => _memberInfoCache.GetMembers(type); /*========================================================================================================================== | METHOD: GET MEMBERS {T} @@ -122,8 +113,7 @@ internal MemberInfoCollection GetMembers(Type type) { /// If the collection cannot be found locally, it will be created. /// /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => - new(type, GetMembers(type).Where(m => typeof(T).IsAssignableFrom(m.GetType())).Cast()); + internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo => _memberInfoCache.GetMembers(type); /*========================================================================================================================== | METHOD: GET MEMBER @@ -132,13 +122,7 @@ internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// Used reflection to identify a local member by a given name, and returns the associated /// instance. /// - internal MemberInfo? GetMember(Type type, string name) { - var members = GetMembers(type); - if (members.Contains(name)) { - return members[name]; - } - return null; - } + internal MemberInfo? GetMember(Type type, string name) => _memberInfoCache.GetMember(type, name); /*========================================================================================================================== | METHOD: GET MEMBER {T} @@ -147,13 +131,7 @@ internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// Used reflection to identify a local member by a given name, and returns the associated /// instance. /// - internal T? GetMember(Type type, string name) where T : MemberInfo { - var members = GetMembers(type); - if (members.Contains(name) && typeof(T).IsAssignableFrom(members[name].GetType())) { - return members[name] as T; - } - return null; - } + internal T? GetMember(Type type, string name) where T : MemberInfo => _memberInfoCache.GetMember(type, name); /*========================================================================================================================== | METHOD: HAS MEMBER @@ -161,7 +139,7 @@ internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// /// Used reflection to identify if a local member is available. /// - internal bool HasMember(Type type, string name) => GetMember(type, name) is not null; + internal bool HasMember(Type type, string name) => _memberInfoCache.HasMember(type, name); /*========================================================================================================================== | METHOD: HAS MEMBER {T} @@ -169,7 +147,7 @@ internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo => /// /// Used reflection to identify if a local member of type is available. /// - internal bool HasMember(Type type, string name) where T: MemberInfo => GetMember(type, name) is not null; + internal bool HasMember(Type type, string name) where T : MemberInfo => _memberInfoCache.HasMember(type, name); /*========================================================================================================================== | METHOD: HAS SETTABLE PROPERTY diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index 27c44141..d8724e91 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -5,6 +5,8 @@ \=============================================================================================================================*/ using System; using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; using OnTopic.Internal.Diagnostics; namespace OnTopic.Internal.Reflection { @@ -26,6 +28,86 @@ internal class TypeMemberInfoCollection: KeyedCollection + /// Returns a collection of objects associated with a specific type. + /// + /// + /// If the collection cannot be found locally, it will be created. + /// + /// The type for which the members should be retrieved. + internal MemberInfoCollection GetMembers(Type type) { + if (!Contains(type)) { + lock (Items) { + if (!Contains(type)) { + Add(new(type)); + } + } + } + return this[type]; + } + + /*========================================================================================================================== + | METHOD: GET MEMBERS {T} + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Returns a collection of objects associated with a specific type. + /// + /// + /// If the collection cannot be found locally, it will be created. + /// + /// The type for which the members should be retrieved. + internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo => + new(type, GetMembers(type).Where(m => typeof(T).IsAssignableFrom(m.GetType())).Cast()); + + /*========================================================================================================================== + | METHOD: GET MEMBER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify a local member by a given name, and returns the associated + /// instance. + /// + internal MemberInfo? GetMember(Type type, string name) { + var members = GetMembers(type); + if (members.Contains(name)) { + return members[name]; + } + return null; + } + + /*========================================================================================================================== + | METHOD: GET MEMBER {T} + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify a local member by a given name, and returns the associated + /// instance. + /// + internal T? GetMember(Type type, string name) where T : MemberInfo { + var members = GetMembers(type); + if (members.Contains(name) && typeof(T).IsAssignableFrom(members[name].GetType())) { + return members[name] as T; + } + return null; + } + + /*========================================================================================================================== + | METHOD: HAS MEMBER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local member is available. + /// + internal bool HasMember(Type type, string name) => GetMember(type, name) is not null; + + /*========================================================================================================================== + | METHOD: HAS MEMBER {T} + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Used reflection to identify if a local member of type is available. + /// + internal bool HasMember(Type type, string name) where T : MemberInfo => GetMember(type, name) is not null; + /*========================================================================================================================== | OVERRIDE: INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ From 08c7615feb2beaf90972beb665288933e890a0c1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 16:22:07 -0800 Subject: [PATCH 225/778] Removed unused passthroughs from `MemberDispatcher` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the previous commit, a number of methods that are more relevant to `TypeMemberInfoCollection` were moved to there—but passthroughs were maintained on `MemberDispatch`. In this update, the passthroughs that were not used—notably, those which didn't accept generic type parameters—were removed, so as to avoid confusion. I'd stil like to reevaluate the ones that remain at some point, but those can be revisited later, if necessary. --- .../Internal/Reflection/MemberDispatcher.cs | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 2f208a4f..70639a74 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -91,18 +91,6 @@ internal MemberDispatcher(Type? attributeFlag = null) : base() { _attributeFlag = attributeFlag; } - /*========================================================================================================================== - | METHOD: GET MEMBERS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Returns a collection of objects associated with a specific type. - /// - /// - /// If the collection cannot be found locally, it will be created. - /// - /// The type for which the members should be retrieved. - internal MemberInfoCollection GetMembers(Type type) => _memberInfoCache.GetMembers(type); - /*========================================================================================================================== | METHOD: GET MEMBERS {T} \-------------------------------------------------------------------------------------------------------------------------*/ @@ -115,15 +103,6 @@ internal MemberDispatcher(Type? attributeFlag = null) : base() { /// The type for which the members should be retrieved. internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo => _memberInfoCache.GetMembers(type); - /*========================================================================================================================== - | METHOD: GET MEMBER - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify a local member by a given name, and returns the associated - /// instance. - /// - internal MemberInfo? GetMember(Type type, string name) => _memberInfoCache.GetMember(type, name); - /*========================================================================================================================== | METHOD: GET MEMBER {T} \-------------------------------------------------------------------------------------------------------------------------*/ @@ -133,22 +112,6 @@ internal MemberDispatcher(Type? attributeFlag = null) : base() { /// internal T? GetMember(Type type, string name) where T : MemberInfo => _memberInfoCache.GetMember(type, name); - /*========================================================================================================================== - | METHOD: HAS MEMBER - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local member is available. - /// - internal bool HasMember(Type type, string name) => _memberInfoCache.HasMember(type, name); - - /*========================================================================================================================== - | METHOD: HAS MEMBER {T} - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Used reflection to identify if a local member of type is available. - /// - internal bool HasMember(Type type, string name) where T : MemberInfo => _memberInfoCache.HasMember(type, name); - /*========================================================================================================================== | METHOD: HAS SETTABLE PROPERTY \-------------------------------------------------------------------------------------------------------------------------*/ From 30a7284ae5db0c1f420592bf678c023ae9fcfeb0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 18:13:20 -0800 Subject: [PATCH 226/778] Implemented `GetBoolean()` extension --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 4d1355b8..b6882f1a 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -10,6 +10,7 @@ using System.Xml; using System.Xml.Linq; using Microsoft.AspNetCore.Mvc; +using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; @@ -175,8 +176,8 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false | Validate topic \-----------------------------------------------------------------------------------------------------------------------*/ if (topic is null) return topics; - if (topic.Attributes.GetValue("NoIndex") is "1") return topics; - if (topic.Attributes.GetValue("IsDisabled") is "1") return topics; + if (topic.Attributes.GetBoolean("NoIndex", false)) return topics; + if (topic.Attributes.GetBoolean("IsDisabled", false)) return topics; if (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics; /*------------------------------------------------------------------------------------------------------------------------ From fde9c83117814dbc5135bc200887a0275b6fe326 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 18:15:41 -0800 Subject: [PATCH 227/778] Enable entry points to map disabled topics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It should be up to callers as to whether or not a topic that `IsDisabled` should be mapped. Generally, it is expected that it won't be—and, thus, we enforce this via e.g. the `[ValidateTopic]` action filter attribute. But, there are other scenarios where we wish this to be possible, such as the OnTopic Editor, as otherwise disabled topics can't be edited. --- OnTopic/Mapping/TopicMappingService.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index e72a5de8..d8a79405 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -97,11 +97,9 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - #pragma warning disable IDE0078 // Use pattern matching - if (topic is null || topic is { IsDisabled: true }) { + if (topic is null) { return null; } - #pragma warning restore IDE0078 // Use pattern matching /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects @@ -187,11 +185,9 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - #pragma warning disable IDE0078 // Use pattern matching - if (topic is null || topic is { IsDisabled: true }) { + if (topic is null) { return target; } - #pragma warning restore IDE0078 // Use pattern matching /*------------------------------------------------------------------------------------------------------------------------ | Handle topics From 83a09b2fb4c8b2880374a4c990191584151cf75b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 18:20:34 -0800 Subject: [PATCH 228/778] Prevent disabled topics from being mapped as relationships, references While callers may request that an `IsDisabled` topic be mapped (fde9c83), they have no way of validating referenced topics within the topic graph. We should therefore assume that child topics that are disabled should not be mapped, to prevent potential pollution of the topic graph. This will still allow disabled topics to be e.g. edited in OnTopic Editor, but will prevent them from showing up as children, relationships, or references off topics. In the future, we may wish to provide a way of configuring this per request, possibly via an options object or even a validator delegate. But that would break the interface and adds a bit of complexity. For now, we expect the idea of enabling disabled root topics to be mapped, but not disabled referenced topics, should satisfy most requirements. --- OnTopic/Mapping/TopicMappingService.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index d8a79405..b859dc68 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -664,13 +664,11 @@ configuration.ContentTypeFilter is not null && continue; } - //Ensure the source topic isn't disabled or hidden; disabled and hidden topics should never be returned to the - //presentation layer - /* - if (!childTopic.IsVisible()) { + //Ensure the source topic isn't disabled; disabled topics should never be returned to the presentation layer unless + //explicitly requested by a top-level request. + if (childTopic.IsDisabled) { continue; } - */ //Map child topic to target DTO var childDto = (object)childTopic; @@ -739,6 +737,15 @@ MappedTopicCache cache Contract.Requires(configuration, nameof(configuration)); Contract.Requires(cache, nameof(cache)); + /*------------------------------------------------------------------------------------------------------------------------ + | Bypass disabled topics + \-----------------------------------------------------------------------------------------------------------------------*/ + //Ensure the source topic isn't disabled; disabled topics should never be returned to the presentation layer unless + //explicitly requested by a top-level request. + if (source.IsDisabled) { + return; + } + /*------------------------------------------------------------------------------------------------------------------------ | Map referenced topic \-----------------------------------------------------------------------------------------------------------------------*/ From 2c0d6900005b5eb6647fec4568f69cee76fbcfa9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 7 Jan 2021 18:23:41 -0800 Subject: [PATCH 229/778] Validate new `IsDeleted` business logic Root topics that are disabled should be mapped by the `TopicMappingService` (fde9c83), while referenced objects should not be (83a09b2). The new tests confirm this is the case for `Topic.Children`, `Topic.Relationships`, and `Topic.References`. --- OnTopic.Tests/TopicMappingServiceTest.cs | 78 ++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 02619a89..833237c1 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -351,6 +351,33 @@ public async Task Map_Relationships_ReturnsMappedModel() { } + /*========================================================================================================================== + | TEST: MAP: RELATIONSHIPS: SKIPS DISABLED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests whether it successfully skips disabled. + /// + [TestMethod] + public async Task Map_Relationships_SkipsDisabled() { + + var relatedTopic1 = TopicFactory.Create("Cousin1", "Relation"); + var relatedTopic2 = TopicFactory.Create("Cousin2", "Relation"); + var topic = TopicFactory.Create("Test", "Relation"); + + topic.Relationships.SetTopic("Cousins", relatedTopic1); + topic.Relationships.SetTopic("Cousins", relatedTopic2); + + topic.IsDisabled = true; + relatedTopic2.IsDisabled = true; + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.AreEqual(1, target.Cousins.Count); + Assert.IsNotNull(GetChildTopic(target.Cousins, "Cousin1")); + Assert.IsNull(GetChildTopic(target.Cousins, "Cousin2")); + + } + /*========================================================================================================================== | TEST: MAP: ALTERNATE RELATIONSHIP: RETURNS CORRECT RELATIONSHIP \-------------------------------------------------------------------------------------------------------------------------*/ @@ -474,6 +501,33 @@ public async Task Map_Children_ReturnsMappedModel() { )); } + /*========================================================================================================================== + | TEST: MAP: WITH DISABLED: SKIPS DISABLED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a with children and tests whether it successfully skips disabled child + /// topics. + /// + [TestMethod] + public async Task Map_Children_SkipsDisabled() { + + var topic = TopicFactory.Create("Test", "Descendent"); + var childTopic1 = TopicFactory.Create("ChildTopic1", "Descendent", topic); + var childTopic2 = TopicFactory.Create("ChildTopic2", "Descendent", topic); + var childTopic3 = TopicFactory.Create("ChildTopic3", "Descendent", topic); + + topic.IsDisabled = true; + childTopic3.IsDisabled = true; + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.AreEqual(2, target.Children.Count); + Assert.IsNotNull(GetChildTopic(target.Children, "ChildTopic1")); + Assert.IsNotNull(GetChildTopic(target.Children, "ChildTopic2")); + Assert.IsNull(GetChildTopic(target.Children, "ChildTopic3")); + + } + /*========================================================================================================================== | TEST: MAP: MAP TO PARENT: RETURNS MAPPED MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -547,6 +601,30 @@ public async Task Map_TopicReferences_ReturnsMappedModel() { } + /*========================================================================================================================== + | TEST: MAP: TOPIC REFERENCES: SKIPS DISABLED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests whether it successfully skips disabled topics. + /// + [TestMethod] + public async Task Map_TopicReferences_SkipsDisabled() { + + var mappingService = new TopicMappingService(_topicRepository, _typeLookupService); + + var topic = TopicFactory.Create("Test", "TopicReference"); + var topicReference = TopicFactory.Create("Reference", "Page"); + + topicReference.IsDisabled = true; + + topic.References.SetTopic("TopicReference", topicReference); + + var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.IsNull(target.TopicReference); + + } + /*========================================================================================================================== | TEST: MAP: RECURSIVE RELATIONSHIPS: RETURNS GRAPH \-------------------------------------------------------------------------------------------------------------------------*/ From fdd67ce84751be836eab91f84c0ce71179181efd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 12:59:56 -0800 Subject: [PATCH 230/778] Established structure for testing `HierarchicalTopicMappingService` This includes establishing a shared `StubTopicRepository` wrapped in a `CachedTopicRepository`, as well as a `TopicMappingService` that can be used across the individual unit tests. --- .../HierarchicalTopicMappingServiceTest.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs new file mode 100644 index 00000000..4ae1e2e8 --- /dev/null +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -0,0 +1,76 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Data.Caching; +using OnTopic.Mapping; +using OnTopic.Mapping.Hierarchical; +using OnTopic.Repositories; +using OnTopic.TestDoubles; +using OnTopic.ViewModels; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: HIERARCHICAL TOPIC MAPPING SERVICE TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the . + /// + [TestClass] + public class HierarchicalTopicMappingServiceTest { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + readonly ITopicRepository _topicRepository; + readonly ITopicMappingService _topicMappingService; + readonly Topic _topic; + + /*========================================================================================================================== + | HIERARCHICAL TOPIC MAPPING SERVICE + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly IHierarchicalTopicMappingService _hierarchicalMappingService; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the with shared resources. + /// + /// + /// This uses the to provide data, and then to + /// manage the in-memory representation of the data. While this introduces some overhead to the tests, the latter is a + /// relatively lightweight façade to any , and prevents the need to duplicate logic for + /// crawling the object graph. In addition, it initializes a shared reference to use for the various + /// tests. + /// + public HierarchicalTopicMappingServiceTest() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + _topicRepository = new CachedTopicRepository(new StubTopicRepository()); + _topic = _topicRepository.Load("Root:Web:Web_3:Web_3_0")!; + _topicMappingService = new TopicMappingService(_topicRepository, new TopicViewModelLookupService()); + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish hierarchical topic mapping service + \-----------------------------------------------------------------------------------------------------------------------*/ + _hierarchicalMappingService = new CachedHierarchicalTopicMappingService( + new HierarchicalTopicMappingService( + _topicRepository, + _topicMappingService + ) + ); + + } + + + } //Class +} //Namespace \ No newline at end of file From 894da16ee2d6e2a091ceee9403fdb1359fbd91f3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:00:53 -0800 Subject: [PATCH 231/778] Introduced unit test: `GetHierarchicalRoot()` Added a test to validate that `GetHierarchicalRoot()` correctly returns the expected root `n` number of jumps away from the true root. --- .../HierarchicalTopicMappingServiceTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs index 4ae1e2e8..2cb5c955 100644 --- a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -71,6 +71,23 @@ public HierarchicalTopicMappingServiceTest() { } + /*========================================================================================================================== + | TEST: GET HIERARCHICAL ROOT: WITH DEEP TOPIC: RETURNS ROOT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls method with a deeply + /// nested topic and ensures that it returns the expected root. + /// + [TestMethod] + public void GetHierarchicalRoot_WithDeepTopic_ReturnsRoot() { + + var rootTopic = _hierarchicalMappingService.GetHierarchicalRoot(_topic, 2, "Configuration"); + + Assert.IsNotNull(rootTopic); + Assert.AreEqual("Web", rootTopic.Key); + + } + } //Class } //Namespace \ No newline at end of file From 29513dfcb3105d6cbb481e31797a89c3799eed68 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:02:31 -0800 Subject: [PATCH 232/778] Introduced unit test: `GetViewModel(tiers: 1)` Added a test to validate that `GetViewModel()` correctly returns one tier of navigation only, if `tiers` is set to `1`. --- .../HierarchicalTopicMappingServiceTest.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs index 2cb5c955..a4b3206b 100644 --- a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -88,6 +88,25 @@ public void GetHierarchicalRoot_WithDeepTopic_ReturnsRoot() { } + /*========================================================================================================================== + | TEST: GET VIEW MODEL: WITH TWO LEVELS: RETURNS GRAPH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls method + /// and ensures that the expected data is returned. + /// + [TestMethod] + public async Task GetViewModel_WithTwoLevels_ReturnsGraph() { + + var rootTopic = _topicRepository.Load("Root:Web"); + var viewModel = await _hierarchicalMappingService.GetViewModelAsync(rootTopic, 1).ConfigureAwait(false); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(3, viewModel.Children.Count); + Assert.AreEqual(0, viewModel.Children[0].Children.Count); + + } + } //Class } //Namespace \ No newline at end of file From 07a4612c4c14aa2d9950c0429feb51325e3fb9d7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:03:24 -0800 Subject: [PATCH 233/778] Introduced unit test: `GetViewModel(validationDelegate)` Added a test to validate that `GetViewModel()` correctly returns one only topics who satisfy the provided `validationDelegate`. --- .../HierarchicalTopicMappingServiceTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs index a4b3206b..6c552256 100644 --- a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -107,6 +107,27 @@ public async Task GetViewModel_WithTwoLevels_ReturnsGraph() { } + /*========================================================================================================================== + | TEST: GET VIEW MODEL: WITH VALIDATION DELEGATE: EXCLUDES TOPICS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls method + /// with a validationDelegate and ensures that it correctly trims the topic graph. + /// + [TestMethod] + public async Task GetViewModel_WithValidationDelegate_ExcludesTopics() { + + var rootTopic = _topicRepository.Load("Root:Web"); + var viewModel = await _hierarchicalMappingService + .GetViewModelAsync(rootTopic, 2, (t) => t.Key.EndsWith("1", StringComparison.Ordinal)) + .ConfigureAwait(false); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(1, viewModel.Children.Count); + Assert.AreEqual(1, viewModel.Children[0].Children.Count); + + } + } //Class } //Namespace \ No newline at end of file From 58fd8f857594b5f64dafe1146f5a5ce695f3f9b5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:04:06 -0800 Subject: [PATCH 234/778] Introduced unit test: `GetViewModel()` with `IsDisabled` Added a test to validate that `GetViewModel()` correctly excludes topics that are marked `IsDisabled`. --- .../HierarchicalTopicMappingServiceTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs index 6c552256..f90d45cc 100644 --- a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs +++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs @@ -128,6 +128,28 @@ public async Task GetViewModel_WithValidationDelegate_ExcludesTopics() { } + /*========================================================================================================================== + | TEST: GET VIEW MODEL: WITH DISABLED: EXCLUDES DISABLED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls method + /// with a topic in the graph, and ensures it is not returned. + /// + [TestMethod] + public async Task GetViewModel_WithDisabled_ExcludesDisabled() { + + var rootTopic = _topicRepository.Load("Root:Web:Web_3")!; + var disabledTopic = _topicRepository.Load("Root:Web:Web_3:Web_3_0"); + + rootTopic.IsDisabled = true; + disabledTopic.IsDisabled = true; + + var viewModel = await _hierarchicalMappingService.GetViewModelAsync(rootTopic, 1).ConfigureAwait(false); + + Assert.IsNotNull(viewModel); + Assert.AreEqual(1, viewModel.Children.Count); + + } } //Class } //Namespace \ No newline at end of file From de0c155831a0bef522b0f9bff3524693c7e5303f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:38:36 -0800 Subject: [PATCH 235/778] Ensures that `Rollback()` correctly sets `AttributeValue.IsDirty` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, when a new version is loaded, all attributes will be set to `!IsDirty`. The `Rollback()` method is intended to check if any attributes are new or have changed and, if so, set them to `IsDirty` to ensure that they are changed. Previously, it was setting `IsDirty` to `false`—which is what the value already would have been. --- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index e1d0e308..14ccb261 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -260,7 +260,7 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var attribute in originalVersion.Attributes) { if (!topic.Attributes.Contains(attribute.Key) || topic.Attributes.GetValue(attribute.Key) != attribute.Value) { - originalVersion.Attributes.SetValue(attribute.Key, attribute.Value, false); + originalVersion.Attributes.SetValue(attribute.Key, attribute.Value, true); } } From ddcf6870e30f58b382974630559eb779e87f5c04 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:43:17 -0800 Subject: [PATCH 236/778] Remove setting of core attributes on `Rollback()` Previously, core attributes such as `Key`, `ContentType`, and `ParentId` were stored as attributes, where they could, conceivably, be versioned. This introduced a lot of complexity, and potential for error. (For instance, if a rollback introduces a duplicate `Key`, or attempts to move a topic back to a previous `ParentId`.) As such, we moved these to the `Topics` table, where they are not versioned. As a result, we don't expect `Rollback()` to chenge these values. Furthermore, there's no need to manage their `IsDirty` state. --- OnTopic/Repositories/TopicRepositoryBase.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 14ccb261..b969d986 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -272,22 +272,6 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { topic.Attributes.Add(attribute); } - /*------------------------------------------------------------------------------------------------------------------------ - | Rename topic, if necessary - \-----------------------------------------------------------------------------------------------------------------------*/ - if (topic.Key == originalVersion.Key) { - topic.Attributes.SetValue("Key", topic.Key, false); - } - else { - topic.Key = originalVersion.Key; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Ensure Parent, ContentType are maintained - \-----------------------------------------------------------------------------------------------------------------------*/ - topic.Attributes.SetValue("ContentType", topic.ContentType, topic.ContentType != originalVersion.ContentType); - topic.Attributes.SetValue("ParentId", topic.Parent?.Id.ToString(CultureInfo.InvariantCulture)?? "-1", false); - /*------------------------------------------------------------------------------------------------------------------------ | Save as new version \-----------------------------------------------------------------------------------------------------------------------*/ From 3b6f02ef17eec4002c9ec7f7e2411fa28425c0ce Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:49:13 -0800 Subject: [PATCH 237/778] Ensured that `ClearItems()` correctly adds attributes as `DeletedAttributes` When `Remove()` is called, the `RemoveItem()` override correctly adds the removed `AttributeValue.Key` to the `DeletedAttributes` collection, which effectively marks the `AttributeValueCollection` as `IsDirty()`. When calling `Clear()`, however, this did not happen. That's because `Clear()` doesn't call `Remove()`. To mitigate that, I've added a `ClearItems()` override to `AttributeValueCollection`, and it will add all current `AttributeValue.Key` values to the `DeletedAttributes` collection, before calling `base.ClearItems()`. --- OnTopic/Attributes/AttributeValueCollection.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index da07088a..93d931cc 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -557,6 +557,22 @@ protected override void RemoveItem(int index) { base.RemoveItem(index); } + /*========================================================================================================================== + | OVERRIDE: CLEAR ITEMS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Intercepts all attempts to clear the , to ensure that it is appropriately marked + /// as . + /// + /// + /// When an is removed, will return true—even if no remaining + /// s are marked as . + /// + protected override void ClearItems() { + DeletedAttributes.AddRange(Items.Select(a => a.Key)); + base.ClearItems(); + } + /*========================================================================================================================== | OVERRIDE: GET KEY FOR ITEM \-------------------------------------------------------------------------------------------------------------------------*/ From b58cdeb3d9ce94c194f72b3a55c6a99c2794aa4d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 8 Jan 2021 13:50:20 -0800 Subject: [PATCH 238/778] Added unit test to validate `ClearItems()` bug fix This unit test validates that the previous bug fix (3b6f02e) correctly marks a collection as `IsDirty()` after it's been cleared, and that any cleared keys are in the `AttributeValueCollection.DeletedAttributes` cache. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index e79d1519..2a9f071b 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Collections; using System.Globalization; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; @@ -283,6 +284,26 @@ public void SetValue_ValueChanged_IsDirty() { } + /*========================================================================================================================== + | TEST: CLEAR: EXISTING VALUES: IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls and confirms that the collection is marked as dirty, due to deleted attrbutes. + /// + [TestMethod] + public void Clear_ExistingValues_IsDirty() { + + var topic = TopicFactory.Create("Test", "Container"); + + topic.Attributes.SetValue("Foo", "Bar", false); + + topic.Attributes.Clear(); + + Assert.IsTrue(topic.Attributes.IsDirty()); + Assert.IsTrue(topic.Attributes.DeletedAttributes.Contains("Foo")); + + } + /*========================================================================================================================== | TEST: SET VALUE: VALUE UNCHANGED: IS NOT DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ From d049ea90f3c7775587712050429c78ce0b7106c9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 16:24:49 -0800 Subject: [PATCH 239/778] Introduced a new `TopicMultiMap` class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicMultiMap` is a dictionary—or, technically, a `KeyedCollection`—which provides a 1:n relationship between a given collection `key` and a collection of `Topic` objects. In addition to the basic features of a `KeyedCollection`, it includes convenience overloads which include not only the `key`—as the base methods do—but also the `Topic` as well. As such, it includes e.g. `Contains(key, topic)`, `Add(key, topic)`, and `Remove(key, topic)`. It also has a `Clear()` overload which will clear one of the target collections based on the collection `key`. Unlike a `Dictionary`, which uses a `KeyValuePair` object, the `TopicMultiMap` introduces its own `KeyValuesPair` class. This therefore operates very similar to a dictionary, but with more intuitive semantics given that the `Values` property is a collection. These are intended to support an updated `Topic.Relationships` type, though they're not implemented with any relationship-specific semantics, and are thus stored in the `OnTopic.Collections` namespace to denote that they're intended to be multi-purpose. --- OnTopic/Collections/KeyValuesPair.cs | 77 +++++++++++++ OnTopic/Collections/TopicMultiMap.cs | 156 +++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 OnTopic/Collections/KeyValuesPair.cs create mode 100644 OnTopic/Collections/TopicMultiMap.cs diff --git a/OnTopic/Collections/KeyValuesPair.cs b/OnTopic/Collections/KeyValuesPair.cs new file mode 100644 index 00000000..9c60d9c7 --- /dev/null +++ b/OnTopic/Collections/KeyValuesPair.cs @@ -0,0 +1,77 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Generic; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: KEY VALUES PAIR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a 1:n relationship used for implementations of as a means of + /// supporting a multimap. + /// + /// + /// Out of the box, the .NET CLR includes a similar class, which serves an + /// identical purpose. The class, however, provides more intuitive semantics for + /// working with multimap—i.e., 1:n—scenarios. + /// + /// + /// As an example, the supports the following: + /// + /// foreach (var relationship in topic.Relationships) { + /// foreach (var topic in relationship.Values) { + /// Console.Log(topic.Key); + /// } + /// } + /// + /// + public class KeyValuesPair where TValue: class, ICollection { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a new instance of a class with a and, + /// optionally, a . + /// + /// The key for the given instance. + /// The optional set of values for the given instance. + public KeyValuesPair(TKey key, TValue values) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(key, nameof(key)); + Contract.Requires(values, nameof(values)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Key = key; + Values = values; + + } + + /*========================================================================================================================== + | PROPERTY: KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the for a given item. + /// + public TKey Key { get; init; } + + /*========================================================================================================================== + | PROPERTY: VALUES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the for the given item. + /// + public TValue Values { get; init; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Collections/TopicMultiMap.cs b/OnTopic/Collections/TopicMultiMap.cs new file mode 100644 index 00000000..b0de8564 --- /dev/null +++ b/OnTopic/Collections/TopicMultiMap.cs @@ -0,0 +1,156 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.ObjectModel; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: TOPIC MULTIMAP + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The offers support for a keyed collection where each key is mapped to a collection of instances, thus supporting a 1:n relationship with zero or more topics, organized by key. + /// + public class TopicMultiMap: KeyedCollection>> { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a new instance of a class. + /// + public TopicMultiMap() { + + } + + /*========================================================================================================================== + | METHOD: CONTAINS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if the exists in a collection with the supplied . + /// + /// + /// Returns true if the exists in the specified collection. Returns false if the + /// collection doesn't exist. + /// + public bool Contains(string key, Topic topic) => Contains(key) && this[key].Values.Contains(topic); + + /*========================================================================================================================== + | METHOD: GET TOPICS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of objects grouped by a specific . + /// + /// + /// Returns a reference to the underlying collection. + /// + /// The key of the collection to be returned. + public Collection GetTopics(string key) { + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + if (Contains(key)) { + return this[key].Values; + } + return new(); + } + + /*========================================================================================================================== + | METHOD: ADD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a to a collection with the supplied . If the collection with + /// doesn't exist, it will be established. + /// + public void Add(string key, Topic topic) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(key, nameof(key)); + Contract.Requires(topic, nameof(topic)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Ensure collection is established + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!Contains(key)) { + Add(new(key, new())); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Add topic, if it hasn't already been added + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!Contains(key, topic)) { + this[key].Values.Add(topic); + } + + } + + /*========================================================================================================================== + | METHOD: REMOVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes a from a collection with the supplied . + /// + /// The key of the collection. + /// The to be removed. + /// + /// Returns true if the is removed; returns false if either the + /// or the cannot be found. + /// + public bool Remove(string key, Topic topic) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate contracts + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + Contract.Requires(topic, nameof(topic)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate key + \-----------------------------------------------------------------------------------------------------------------------*/ + var topics = GetTopics(key); + + if (topics is null || !topics.Contains(topic)) { + return false; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Remove relationship + \-----------------------------------------------------------------------------------------------------------------------*/ + topics.Remove(topic); + + /*------------------------------------------------------------------------------------------------------------------------ + | Remove true + \-----------------------------------------------------------------------------------------------------------------------*/ + return true; + + } + + /*========================================================================================================================== + | METHOD: CLEAR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes all objects grouped by a specific . + /// + /// The key of the collection to be cleared. + public void Clear(string key) => GetTopics(key).Clear(); + + /*========================================================================================================================== + | OVERRIDE: GET KEY FOR ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Method must be overridden for the to extract the keys from the items. + /// + /// The object from which to extract the key. + /// The key for the specified collection item. + protected override string GetKeyForItem(KeyValuesPair> item) { + Contract.Requires(item, "The item must be available in order to derive its key."); + return item.Key; + } + + } //Class +} //Namespace \ No newline at end of file From eb5c1c34f8974cfd6dbe6464363c1d4bea64f9af Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 17:17:17 -0800 Subject: [PATCH 240/778] Introduced a new `ReadOnlyTopicMultiMap` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `ReadOnlyTopicMultiMap` operates similar to the new `TopicMultiMap`—and, indeed, requires a `TopicMultiMap` object as a source object—but only exposes a read only interface using an `IEnumerable>>`. This ensures it doesn't have any confusing or unnecessary interface elements associated with e.g. `IDictionary` or `ICollection`, while still allowing users to enumerate and query the collection similar to an actual `TopicMultiMap`. The main difference is that the methods return a `ReadOnlyCollection` or `IEnumerable>` instead of a `Collection` or `Collection>`, as the `TopicMultiMap` does. This is intended to act as the base class for the underlying type of `Topic.Relationships`, as we want that to offer the semantics of a collection, while forcing users to update relationships using a controller interface in order to ensure we can enforce business logic such as tracking state and handling recipricol relationships. Note: This doesn't implement `IDictionary>`, even though it implements several related interface members, as that would force it to return an `IEnumerator>`, which introduces confusing semantics for a multi-map (e.g., `collection.Value` vs. `collection.Values`). --- OnTopic/Collections/ReadOnlyTopicMultiMap.cs | 176 +++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 OnTopic/Collections/ReadOnlyTopicMultiMap.cs diff --git a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs new file mode 100644 index 00000000..a2cdd493 --- /dev/null +++ b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs @@ -0,0 +1,176 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using OnTopic.Internal.Diagnostics; + +#pragma warning disable CA1710 // Identifiers should have correct suffix + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: READ-ONLY TOPIC MULTIMAP + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The provides a read-only façade to a . + /// + public class ReadOnlyTopicMultiMap: IEnumerable>> { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a new instance of a class with a reference to an underlying instance. + /// + public ReadOnlyTopicMultiMap(TopicMultiMap source) { + Contract.Requires(source, nameof(source)); + Source = source; + } + + /// + /// Constructs a new instance of a class. + /// + /// + /// The requires an underlying to + /// derive values from. It's normally expected that callers will pass that via the public constructor. Derived classes, however, cannot pass instance parameters to a + /// base class. As such, the protected constructor allows the derived class to + /// intialize the without a —but expects that it will immediately + /// set one via its constructor. + /// + protected ReadOnlyTopicMultiMap() {} + + /*========================================================================================================================== + | PROPERTY: SOURCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides access to the underlying from which the will + /// derive values. + /// + /// + /// The must be passed in via either the public + /// constructor, or must be set manually from the constructor of a derived class when using the protected constructor. + /// + [NotNull, DisallowNull] + protected TopicMultiMap? Source { get; init; } + + /*========================================================================================================================== + | PROPERTY: KEYS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of keys available for the available collections. + /// + /// + /// Returns an enumerable list of keys. + /// + public IEnumerable Keys => Source.Select(m => m.Key).ToList(); + + /*========================================================================================================================== + | PROPERTY: VALUES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of values available for the available collections. + /// + /// + /// Returns an enumerable list of instances. + /// + public IEnumerable> Values => Source.Select(m => new ReadOnlyCollection(m.Values)); + + /*========================================================================================================================== + | PROPERTY: COUNT + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a count of items in the source collection. + /// + /// + /// The number of collections in the underlying source collection. + /// + public int Count => Source.Count; + + /*========================================================================================================================== + | INDEXER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a collection from the source collection based on the . + /// + /// + /// A collection. + /// + public ReadOnlyCollection this[string key] => new(Source[key].Values); + + /*========================================================================================================================== + | METHOD: CONTAINS? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool Contains(string key) => Source.Contains(key); + + /// + public bool Contains(string key, Topic topic) => Source.Contains(key, topic); + + /*========================================================================================================================== + | METHOD: GET TOPICS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of objects grouped by a specific . + /// + /// + /// Returns a reference to the underlying collection. + /// + /// The key of the collection to be returned. + public ReadOnlyCollection GetTopics(string key) { + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + if (Contains(key)) { + return new(Source[key].Values); + } + return new(new List()); + } + + /*========================================================================================================================== + | METHOD: GET ALL TOPICS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a list of all related objects, independent of collection key. + /// + /// + /// Returns an enumerable list of objects. + /// + public ReadOnlyCollection GetAllTopics() => + Source.SelectMany(list => list.Values).Distinct().ToList().AsReadOnly(); + + /// + /// Retrieves a list of all related objects, independent of relationship key, filtered by content + /// type. + /// + /// + /// Returns an enumerable list of objects. + /// + public ReadOnlyCollection GetAllTopics(string contentType) => + GetAllTopics().Where(t => t.ContentType == contentType).ToList().AsReadOnly(); + + /*========================================================================================================================== + | GET ENUMERATOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public IEnumerator>> GetEnumerator() { + foreach (var collection in Source) { + yield return new(collection.Key, new(collection.Values)); + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); + + } //Class +} //Namespace + +#pragma warning restore CA1710 // Identifiers should have correct suffix \ No newline at end of file From 9d8ae6410b1abe28b7eb35651e917bb5796f4a72 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 17:21:43 -0800 Subject: [PATCH 241/778] Allow the `ReadOnlyTopicCollection` to be initialized as empty Previously, the `ReadOnlyTopicCollection` necessitated that it be initialized with an `innerCollection`. That makes sense, as otherwise it won't have any values. By allowing this to be `null`, however, we make it easy to initialize empty collections as e.g. a default return type. --- OnTopic/Collections/ReadOnlyTopicCollection.cs | 2 +- OnTopic/Collections/ReadOnlyTopicCollection{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs index 8ba0855c..f9c13f21 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -23,7 +23,7 @@ public class ReadOnlyTopicCollection : ReadOnlyTopicCollection { /// Establishes a new based on an existing . /// /// The underlying . - public ReadOnlyTopicCollection(IList innerCollection) : base(innerCollection) { + public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { } /*========================================================================================================================== diff --git a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs index 26615350..73c72be3 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs @@ -30,7 +30,7 @@ public class ReadOnlyTopicCollection : ReadOnlyCollection where T : Topic /// Establishes a new based on an existing . /// /// The underlying . - public ReadOnlyTopicCollection(IList innerCollection) : base(innerCollection) { + public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { Contract.Requires(innerCollection, "innerCollection should not be null"); _innerCollection = innerCollection as TopicCollection?? new(innerCollection); } From 57ff3f99ae18c0be545ef8144ebc62b14dadd35a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 17:38:40 -0800 Subject: [PATCH 242/778] Established new `TopicRelationshipMultiMap` from `RelatedTopicCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The legacy `RelatedTopicCollection` has been renamed to `TopicRelationshipMultiMap` and reconfigured to a) derive from the new `ReadOnlyTopicMultiMap` class (eb5c1c3) in order to provide read-only access to relationships, while b) exposing a façade to a private `TopicMultiMap` object for write access. This ensures that the `TopicRelationshipMultiMap` maintains the semantics of a collection, while enforcing write access through a limited interface which is not only friendlier, but also enforces business logic such as handling reciprocal relationships and state tracking. Speaking of state tracking, the `IsDirty()` status is now tracked internally within the `TopicRelationshipMultiMap` instead of "polluting" the more general downstream objects, such as the `TopicMultiMap` or the `Collection` which it relies upon. This is more consistent with how state tracking is (now) handled elsewhere. This is a bit more involved, but helps centralized the tracking functionality. Overall, however, this class is now greatly simplified, since much of the functionality is now handled by the underlying `ReadOnlyTopicMultiMap` or the internal `TopicMultiMap`. --- ...ection.cs => TopicRelationshipMultiMap.cs} | 242 ++++-------------- 1 file changed, 51 insertions(+), 191 deletions(-) rename OnTopic/References/{RelatedTopicCollection.cs => TopicRelationshipMultiMap.cs} (50%) diff --git a/OnTopic/References/RelatedTopicCollection.cs b/OnTopic/References/TopicRelationshipMultiMap.cs similarity index 50% rename from OnTopic/References/RelatedTopicCollection.cs rename to OnTopic/References/TopicRelationshipMultiMap.cs index 8bb80b68..07163bbc 100644 --- a/OnTopic/References/RelatedTopicCollection.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -5,8 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using OnTopic.Collections; using OnTopic.Internal.Diagnostics; namespace OnTopic.References { @@ -17,17 +16,15 @@ namespace OnTopic.References { /// /// Provides a simple interface for accessing collections of topic collections. /// - public class RelatedTopicCollection : KeyedCollection { + public class RelatedTopicCollection : ReadOnlyTopicMultiMap { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ readonly Topic _parent; readonly bool _isIncoming; - - /*========================================================================================================================== - | DATA STORE - \-------------------------------------------------------------------------------------------------------------------------*/ + readonly List _isDirty = new(); + readonly TopicMultiMap _storage = new(); /*========================================================================================================================== | CONSTRUCTOR @@ -42,69 +39,10 @@ public class RelatedTopicCollection : KeyedCollection overload. /// - public RelatedTopicCollection(Topic parent, bool isIncoming = false) : base(StringComparer.OrdinalIgnoreCase) { - _parent = parent; - _isIncoming = isIncoming; - } - - /*========================================================================================================================== - | PROPERTY: KEYS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a list of relationship key available. - /// - /// - /// Returns an enumerable list of relationship keys. - /// - public ReadOnlyCollection Keys => new(Items.Select(t => t.Name).ToList()); - - /*========================================================================================================================== - | METHOD: GET ALL TOPICS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a list of all related objects, independent of relationship key. - /// - /// - /// Returns an enumerable list of objects. - /// - public IEnumerable GetAllTopics() { - var topics = new List(); - foreach (var topicCollection in this) { - foreach (var topic in topicCollection) { - if (!topics.Contains(topic)) { - topics.Add(topic); - } - } - } - return topics; - } - - /// - /// Retrieves a list of all related objects, independent of relationship key, filtered by content - /// type. - /// - /// - /// Returns an enumerable list of objects. - /// - public IEnumerable GetAllTopics(string contentType) => GetAllTopics().Where(t => t.ContentType == contentType); - - /*========================================================================================================================== - | METHOD: GET TOPICS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a list of objects grouped by a specific relationship key. - /// - /// - /// Returns a reference to the underlying ; modifications to this collection will modify - /// the 's . As such, this should be used with care. - /// - /// The key of the relationship to be returned. - public NamedTopicCollection GetTopics(string relationshipKey) { - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); - if (Contains(relationshipKey)) { - return this[relationshipKey]; - } - return new(); + public RelatedTopicCollection(Topic parent, bool isIncoming = false): base() { + _parent = parent; + _isIncoming = isIncoming; + base.Source = _storage; } /*========================================================================================================================== @@ -113,11 +51,19 @@ public NamedTopicCollection GetTopics(string relationshipKey) { /// /// Removes all objects grouped by a specific relationship key. /// + /// + /// If there are any objects in the specified , then the will be marked as . + /// /// The key of the relationship to be cleared. public void ClearTopics(string relationshipKey) { Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); - if (Contains(relationshipKey)) { - this[relationshipKey].Clear(); + if (_storage.Contains(relationshipKey)) { + var relationship = _storage.GetTopics(relationshipKey); + if (relationship.Count > 0) { + MarkDirty(relationshipKey); + } + _storage.Clear(relationshipKey); } } @@ -128,62 +74,13 @@ public void ClearTopics(string relationshipKey) { /// Removes a specific object associated with a specific relationship key. /// /// The key of the relationship. - /// The key of the topic to be removed. - /// - /// Returns true if the is removed; returns false if either the relationship key or the - /// cannot be found. - /// - public bool RemoveTopic(string relationshipKey, string topicKey) => RemoveTopic(relationshipKey, topicKey, false); - - /// - /// Removes a specific object associated with a specific relationship key. - /// - /// The key of the relationship. - /// The key of the topic to be removed. - /// - /// Notes that this is setting an internal relationship, and thus shouldn't set the reciprocal relationship. - /// - /// - /// Returns true if the is removed; returns false if either the relationship key or the - /// cannot be found. - /// - internal bool RemoveTopic(string relationshipKey, string topicKey, bool isIncoming) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate contracts - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); - Contract.Requires(!String.IsNullOrWhiteSpace(topicKey), nameof(topicKey)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate topic key - \-----------------------------------------------------------------------------------------------------------------------*/ - var topics = Contains(relationshipKey)? this[relationshipKey] : null; - var topic = topics?.Contains(topicKey)?? false? topics[topicKey] : null; - - if (topics is null || topic is null) { - return false; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Call overload - \-----------------------------------------------------------------------------------------------------------------------*/ - return RemoveTopic(relationshipKey, topic, isIncoming); - - } - - /// - /// Removes a specific object associated with a specific relationship key. - /// - /// The key of the relationship. - /// The topic to be removed. + /// The to be removed. /// /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// public bool RemoveTopic(string relationshipKey, Topic topic) => RemoveTopic(relationshipKey, topic, false); - /// /// Removes a specific object associated with a specific relationship key. /// @@ -220,16 +117,15 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) /*------------------------------------------------------------------------------------------------------------------------ | Validate relationshipKey \-----------------------------------------------------------------------------------------------------------------------*/ - var topics = Contains(relationshipKey)? this[relationshipKey] : null; - - if (topics is null || !topics.Contains(topic)) { + if (!_storage.Contains(relationshipKey, topic)) { return false; } /*------------------------------------------------------------------------------------------------------------------------ | Remove relationship \-----------------------------------------------------------------------------------------------------------------------*/ - topics.Remove(topic); + MarkDirty(relationshipKey); + _storage.Remove(relationshipKey, topic); /*------------------------------------------------------------------------------------------------------------------------ | Remove true @@ -250,7 +146,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) /// The key of the relationship. /// The topic to be added, if it doesn't already exist. /// - /// Optionally forces the collection to a state, assuming the topic was set. + /// Optionally forces the collection to an state, assuming the topic was set. /// public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) => SetTopic(relationshipKey, topic, isDirty, false); @@ -267,7 +163,7 @@ public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) /// Notes that this is setting an internal relationship, and thus shouldn't set the reciprocal relationship. /// /// - /// Optionally forces the collection to a state, assuming the topic was set. + /// Optionally forces the collection to an state, assuming the topic was set. /// internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) { @@ -281,14 +177,15 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool /*------------------------------------------------------------------------------------------------------------------------ | Add relationship \-----------------------------------------------------------------------------------------------------------------------*/ - if (!Contains(relationshipKey)) { - Add(new(relationshipKey)); - } - var topics = this[relationshipKey]; - if (!topics.Contains(topic.Key)) { - topics.Add(topic); - if (!(isDirty?? topics.IsDirty())) { - topics.MarkClean(); + var topics = _storage.GetTopics(relationshipKey); + var wasDirty = _isDirty.Contains(relationshipKey); + if (!topics.Contains(topic)) { + _storage.Add(relationshipKey, topic); + if (isDirty.HasValue && !isDirty.Value && !wasDirty) { + MarkClean(relationshipKey); + } + else { + MarkDirty(relationshipKey); } } @@ -311,75 +208,38 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool | METHOD: IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Evaluates each of the child s to determine if any of them are set to . If they are, returns true. + /// Determines if any of the relationships have been modified; if they have, returns true. /// - public bool IsDirty() => Items.Any(r => r.IsDirty()); + public bool IsDirty() => _isDirty.Count > 0; /*========================================================================================================================== - | METHOD: MARK CLEAN + | METHOD: MARK DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets the property of every in this to false. + /// Evaluates each of the relationships to determine if any of them are set to . If they are, + /// returns true. /// - public void MarkClean() { - foreach (var relationship in Items) { - relationship.MarkClean(); + private void MarkDirty(string relationshipKey) { + if (!_isDirty.Contains(relationshipKey)) { + _isDirty.Add(relationshipKey); } } /*========================================================================================================================== - | OVERRIDE: INSERT ITEM + | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ - /// Fires any time a is added to the collection. - /// - /// Compared to the base implementation, will throw a specific error if a duplicate key is - /// inserted. This conveniently provides the name of the , so it's clear what key is - /// being duplicated. - /// - /// The zero-based index at which should be inserted. - /// The instance to insert. - /// - /// A NamedTopicCollection with the Name '{item.Name}' already exists in this RelatedTopicCollection. The existing key is - /// {this[item.Name].Name}'; the new item's is '{item.Name}'. This collection is associated with the '{GetUniqueKey()}' - /// Topic. - /// - protected override void InsertItem(int index, NamedTopicCollection item) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(item, nameof(item)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Insert item, if it doesn't already exist - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!Contains(item.Name)) { - base.InsertItem(index, item); - } - else { - throw new ArgumentException( - $"A {nameof(NamedTopicCollection)} with the Name '{item.Name}' already exists in this " + - $"{nameof(RelatedTopicCollection)}. The existing key is '{this[item.Name].Name}'; the new item's is '{item.Name}'. " + - $"This collection is associated with the '{_parent.GetUniqueKey()}' Topic.", - nameof(item) - ); - } - } + /// + /// Marks the relationships collections as clean. + /// + public void MarkClean() => _isDirty.Clear(); - /*========================================================================================================================== - | OVERRIDE: GET KEY FOR ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a method for the to retrieve the key from the underlying - /// collection of objects, in this case s. + /// Removes the from the collection, if it exists. /// - /// The object from which to extract the key. - /// The key for the specified collection item. - protected override string GetKeyForItem(NamedTopicCollection item) { - Contract.Requires(item, "The item must be available in order to derive its key."); - return item.Name; + public void MarkClean(string relationshipKey) { + if (_isDirty.Contains(relationshipKey)) { + _isDirty.Remove(relationshipKey); + } } } //Class From 8d610e336285bd4db358508af0fb09b9d9fa495a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 18:02:17 -0800 Subject: [PATCH 243/778] Removed legacy artifacts of `IReadOnlyDictionary<>` Initially, the `ReadOnlyTopicMultiMap` was implemented as an `IReadOnlyDictionary<>`. This didn't work since `IReadOnlyDictionary<>` returns an `IEnumerator` of `KeyValuePair`, which introduces unintuitive semantics for a multimap. To mitigate that, I changed it to an `IEnumerable>`. Given this, the `Values` member is no longer needed, nor do we need to suppress CA1710 (which recommends that classes implementing `IReadOnlyDictionary<>` use the `Dictionary` suffix in their name). Further, we can now return actual `ReadOnlyCollection<>` instances instead of `IEnumerable`. This is useful as it is more consistent with the original interface of the legacy `RelatedTopicCollection`, and will thus prevent some breaking changes. --- OnTopic/Collections/ReadOnlyTopicMultiMap.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs index a2cdd493..e12008f9 100644 --- a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs @@ -11,8 +11,6 @@ using System.Linq; using OnTopic.Internal.Diagnostics; -#pragma warning disable CA1710 // Identifiers should have correct suffix - namespace OnTopic.Collections { /*============================================================================================================================ @@ -72,18 +70,7 @@ protected ReadOnlyTopicMultiMap() {} /// /// Returns an enumerable list of keys. /// - public IEnumerable Keys => Source.Select(m => m.Key).ToList(); - - /*========================================================================================================================== - | PROPERTY: VALUES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a list of values available for the available collections. - /// - /// - /// Returns an enumerable list of instances. - /// - public IEnumerable> Values => Source.Select(m => new ReadOnlyCollection(m.Values)); + public ReadOnlyCollection Keys => new(Source.Select(m => m.Key).ToList()); /*========================================================================================================================== | PROPERTY: COUNT @@ -171,6 +158,4 @@ public IEnumerator>> GetEnumerat IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); } //Class -} //Namespace - -#pragma warning restore CA1710 // Identifiers should have correct suffix \ No newline at end of file +} //Namespace \ No newline at end of file From 0d9dcd516a7d967fba2e03ae614eb27addce38cb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 18:04:01 -0800 Subject: [PATCH 244/778] Updated references to the `Topic.Relationships` class to new interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The change from `RelatedTopicCollection` (which was a `KeyedCollection`) to `TopicRelationshipMultiMap` (which is now an `IEnumerable>>`) is broadly consistent in terms of primary entry points, but does change the interface some when dealing with iterators. This update resolves those breaking changes by e.g., changing the relationship `Name` key to `Key`, accessing the values from the new `Values` property, and working with `Topic` instead of `Topic.Key` (to account for the fact that it now allows duplicate key names). These are breaking changes that may need to be applied in implementors as well—though, the hope, is that most implementors will be relying on the higher-level interfaces and, thus, not be affected by this. --- .../Controllers/SitemapController.cs | 4 ++-- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- OnTopic.Tests/RelatedTopicCollectionTest.cs | 17 +++++++++-------- OnTopic/Repositories/TopicRepositoryBase.cs | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index b6882f1a..62de5a3a 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -236,8 +236,8 @@ where topic.Attributes.GetValue(attribute.Key)?.Length < 256 IEnumerable getRelationships() => from relationship in topic.Relationships select new XElement(_pagemapNamespace + "DataObject", - new XAttribute("type", relationship.Name), - from relatedTopic in topic.Relationships[relationship.Name] + new XAttribute("type", relationship.Key), + from relatedTopic in relationship.Values select new XElement(_pagemapNamespace + "Attribute", new XAttribute("name", "TopicKey"), new XText(relatedTopic.GetUniqueKey().Replace("Root:", "", StringComparison.InvariantCultureIgnoreCase)) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 97a6c11e..5d064e8e 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -418,7 +418,7 @@ SqlDateTime version \-----------------------------------------------------------------------------------------------------------------------*/ if ( isRecursive && - ( topic.Relationships.Any(r => r.Any(t => t.Id < 0)) || + ( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || topic.References.Values.Any(t => t.Id < 0) ) ) { @@ -654,7 +654,7 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { //Reset isDirty, assuming there aren't any unresolved references if (savedTopics.Count() == relatedTopics.Count) { - relatedTopics.MarkClean(); + topic.Relationships.MarkClean(key); } } diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/RelatedTopicCollectionTest.cs index 632ed443..5b9d8295 100644 --- a/OnTopic.Tests/RelatedTopicCollectionTest.cs +++ b/OnTopic.Tests/RelatedTopicCollectionTest.cs @@ -6,6 +6,7 @@ using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Collections; using OnTopic.References; namespace OnTopic.Tests { @@ -68,7 +69,7 @@ public void RemoveTopic_RemovesRelationship() { var related = TopicFactory.Create("Related", "Page"); parent.Relationships.SetTopic("Friends", related); - parent.Relationships.RemoveTopic("Friends", related.Key); + parent.Relationships.RemoveTopic("Friends", related); Assert.IsNull(parent.Relationships.GetTopics("Friends").FirstOrDefault()); @@ -89,7 +90,7 @@ public void RemoveTopic_RemovesIncomingRelationship() { var relationships = new RelatedTopicCollection(parent); relationships.SetTopic("Friends", related); - relationships.RemoveTopic("Friends", related.Key); + relationships.RemoveTopic("Friends", related); Assert.IsNull(related.IncomingRelationships.GetTopics("Friends").FirstOrDefault()); @@ -153,19 +154,19 @@ public void GetAllTopics_ReturnsAllTopics() { Assert.AreEqual(5, relationships.Count); Assert.AreEqual("Related3", relationships.GetTopics("Relationship3").First().Key); - Assert.AreEqual(5, relationships.GetAllTopics().Count()); + Assert.AreEqual(5, relationships.GetAllTopics().Count); } /*========================================================================================================================== - | TEST: GET ALL CONTENT TYPES: RETURNS ALL CONTENT TYPES + | TEST: GET ALL TOPICS: CONTENT TYPES: RETURNS ALL CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets relationships in multiple namespaces, with different ContentTypes, then filters the results of - /// by content type. + /// by content type. /// [TestMethod] - public void GetAllContentTypes_ReturnsAllContentTypes() { + public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { var parent = TopicFactory.Create("Parent", "Page"); var relationships = new RelatedTopicCollection(parent); @@ -174,8 +175,8 @@ public void GetAllContentTypes_ReturnsAllContentTypes() { relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i)); } - Assert.AreEqual(5, relationships.Count); - Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count()); + Assert.AreEqual(5, relationships.Keys.Count); + Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count); } diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index b969d986..8d266847 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -481,9 +481,9 @@ public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { foreach (var descendantTopic in descendantTopics) { foreach (var relationship in descendantTopic.Relationships) { - foreach (var relatedTopic in relationship.ToArray()) { + foreach (var relatedTopic in relationship.Values.ToList()) { if (!descendantTopics.Contains(relatedTopic)) { - descendantTopic.Relationships.RemoveTopic(relationship.Name, relatedTopic); + descendantTopic.Relationships.RemoveTopic(relationship.Key, relatedTopic); } } } @@ -494,9 +494,9 @@ public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var descendantTopic in descendantTopics) { foreach (var relationship in descendantTopic.IncomingRelationships) { - foreach (var relatedTopic in relationship.ToArray()) { + foreach (var relatedTopic in relationship.Values.ToList()) { if (!descendantTopics.Contains(relatedTopic)) { - relatedTopic.Relationships.RemoveTopic(relationship.Name, descendantTopic.Key); + relatedTopic.Relationships.RemoveTopic(relationship.Key, descendantTopic); } } } From 69339d8b26b02ad6de213760aa9f4c9ddcffea6b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 18:06:55 -0800 Subject: [PATCH 245/778] Removed legacy `NamedTopicCollection`, merged unit tests The (clumsily named) `NamedTopicCollection` was used by the legacy `RelatedTopicCollection` (57ff3f9), and is no longer used with the new implementation of the `TopicMultiMap` (d049ea9) and `TopicRelationshipMultiMap` (57ff3f9). As such, the `NamedTopicCollection` and its related `NamedTopicCollectionTest` unit tests can be deleted. The core functionality of the unit tests, however, remains relevant, and has now been merged into the `RelatedTopicCollectionTest` unit tests, to operate off of the new `TopicRelationshipMultiMap` class. --- OnTopic.Tests/NamedTopicCollection.cs | 197 -------------------- OnTopic.Tests/RelatedTopicCollectionTest.cs | 192 +++++++++++++++++-- OnTopic/References/NamedTopicCollection.cs | 124 ------------ 3 files changed, 174 insertions(+), 339 deletions(-) delete mode 100644 OnTopic.Tests/NamedTopicCollection.cs delete mode 100644 OnTopic/References/NamedTopicCollection.cs diff --git a/OnTopic.Tests/NamedTopicCollection.cs b/OnTopic.Tests/NamedTopicCollection.cs deleted file mode 100644 index fca2199c..00000000 --- a/OnTopic.Tests/NamedTopicCollection.cs +++ /dev/null @@ -1,197 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.References; - -namespace OnTopic.Tests { - - /*============================================================================================================================ - | CLASS: NAMED TOPIC COLLECTION TEST - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides unit tests for the class. - /// - [TestClass] - public class NamedTopicCollectionTest { - - /*========================================================================================================================== - | TEST: ADD TOPIC: IS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Adds a topic to a and confirms that is - /// set. - /// - [TestMethod] - public void AddTopic_IsDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related = TopicFactory.Create("Topic", "Page"); - - relationships.Add(related); - - Assert.IsTrue(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: ADD TOPIC: IS DUPLICATE: IS NOT DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Adds a duplicate topic to a and confirms that value of is false. - /// - [TestMethod] - public void AddTopic_IsDuplicate_IsNotDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related1 = TopicFactory.Create("Topic", "Page"); - var related2 = TopicFactory.Create("Topic", "Page"); - - relationships.Add(related1); - relationships.MarkClean(); - - try { - relationships.Add(related2); - } - catch (ArgumentException) { - //Expected due to duplicate key - } - - Assert.IsFalse(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: ADD TOPIC: IS DUPLICATE: STAYS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Adds a duplicate topic to a and confirms that value of is false. - /// - [TestMethod] - public void AddTopic_IsDuplicate_StaysDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related1 = TopicFactory.Create("Topic", "Page"); - var related2 = TopicFactory.Create("Topic", "Page"); - - relationships.Add(related1); - - try { - relationships.Add(related2); - } - catch (ArgumentException) { - //Expected due to duplicate key - } - - Assert.IsTrue(relationships.IsDirty()); - - } - - - /*========================================================================================================================== - | TEST: REMOVE TOPIC: IS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Removes an existing from a and conirms that the value for returns true. - /// - [TestMethod] - public void RemoveTopic_IsDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related = TopicFactory.Create("Topic", "Page"); - - relationships.Add(related); - relationships.MarkClean(); - relationships.Remove(related); - - Assert.IsTrue(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: REMOVE TOPIC: MISSING TOPIC: IS NOT DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Removes a non-existent from a and conirms that the value for - /// returns false. - /// - [TestMethod] - public void RemoveTopic_MissingTopic_IsNotDirty() { - - var related = TopicFactory.Create("Topic", "Page"); - var relationships = new NamedTopicCollection("Test"); - - relationships.Remove(related); - - Assert.IsFalse(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: REMOVE TOPIC: MISSING TOPIC: STAYS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Removes a non-existent from a and conirms that the value for - /// stays true. - /// - [TestMethod] - public void RemoveTopic_MissingTopic_StaysDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related = TopicFactory.Create("Topic1", "Page"); - var missing = TopicFactory.Create("Topic2", "Page"); - - relationships.Add(related); - relationships.Remove(missing); - - Assert.IsTrue(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: CLEAR: EXISTING TOPICS: IS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Call and confirms that value of is true. - /// - [TestMethod] - public void Clear_ExistingTopics_IsDirty() { - - var relationships = new NamedTopicCollection("Test"); - var related = TopicFactory.Create("Topic", "Page"); - - relationships.Add(related); - relationships.MarkClean(); - relationships.Clear(); - - Assert.IsTrue(relationships.IsDirty()); - - } - - /*========================================================================================================================== - | TEST: CLEAR: NO TOPICS: IS NOT DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Call with no existing s and confirms that value of - /// is false. - /// - [TestMethod] - public void Clear_NoTopics_IsNotDirty() { - - var relationships = new NamedTopicCollection("Test"); - - relationships.Clear(); - - Assert.IsFalse(relationships.IsDirty()); - - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/RelatedTopicCollectionTest.cs index 5b9d8295..4376c52a 100644 --- a/OnTopic.Tests/RelatedTopicCollectionTest.cs +++ b/OnTopic.Tests/RelatedTopicCollectionTest.cs @@ -38,24 +38,6 @@ public void SetTopic_CreatesRelationship() { } - /*========================================================================================================================== - | TEST: SET TOPIC: IS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Sets a relationship and confirms that the returns true. - /// - [TestMethod] - public void SetTopic_IsDirty() { - - var parent = TopicFactory.Create("Parent", "Page"); - var related = TopicFactory.Create("Related", "Page"); - - parent.Relationships.SetTopic("Friends", related); - - Assert.IsTrue(parent.Relationships.IsDirty()); - - } - /*========================================================================================================================== | TEST: REMOVE TOPIC: REMOVES RELATIONSHIP \-------------------------------------------------------------------------------------------------------------------------*/ @@ -180,5 +162,179 @@ public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { } + /*========================================================================================================================== + | TEST: SET TOPIC: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a topic to a and confirms that is + /// set. + /// + [TestMethod] + public void SetTopic_IsDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related); + + Assert.IsTrue(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: SET TOPIC: IS DUPLICATE: IS NOT DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a duplicate topic to a and confirms that value of is false. + /// + [TestMethod] + public void SetTopic_IsDuplicate_IsNotDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related); + relationships.MarkClean(); + + relationships.SetTopic("Related", related); + + Assert.IsFalse(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: SET TOPIC: IS DUPLICATE: STAYS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a duplicate topic to a and confirms that value of is false. + /// + [TestMethod] + public void SetTopic_IsDuplicate_StaysDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related1 = TopicFactory.Create("Topic", "Page"); + var related2 = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related1); + relationships.SetTopic("Related", related2); + + Assert.IsTrue(relationships.IsDirty()); + + } + + + /*========================================================================================================================== + | TEST: REMOVE TOPIC: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes an existing from a and conirms that the value for returns true. + /// + [TestMethod] + public void RemoveTopic_IsDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related); + relationships.MarkClean(); + relationships.RemoveTopic("Related", related); + + Assert.IsTrue(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: REMOVE TOPIC: MISSING TOPIC: IS NOT DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes a non-existent from a and conirms that the value for + /// returns false. + /// + [TestMethod] + public void RemoveTopic_MissingTopic_IsNotDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic", "Page"); + + var isSuccessful = relationships.RemoveTopic("Related", related); + + Assert.IsFalse(isSuccessful); + Assert.IsFalse(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: REMOVE TOPIC: MISSING TOPIC: STAYS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Removes a non-existent from a and conirms that the value for + /// stays true. + /// + [TestMethod] + public void RemoveTopic_MissingTopic_StaysDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic1", "Page"); + var missing = TopicFactory.Create("Topic2", "Page"); + + relationships.SetTopic("Related", related); + + var isSuccessful = relationships.RemoveTopic("Related", missing); + + Assert.IsFalse(isSuccessful); + Assert.IsTrue(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: CLEAR TOPICS: EXISTING TOPICS: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Call and confirms that value of is true. + /// + [TestMethod] + public void ClearTopics_ExistingTopics_IsDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + var related = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related); + relationships.MarkClean(); + relationships.ClearTopics("Related"); + + Assert.IsTrue(relationships.IsDirty()); + + } + + /*========================================================================================================================== + | TEST: CLEAR TOPICS: NO TOPICS: IS NOT DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Call with no existing s and confirms that + /// the value of is set to false. + /// + [TestMethod] + public void ClearTopics_NoTopics_IsNotDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new RelatedTopicCollection(topic); + + relationships.ClearTopics("Related"); + + Assert.IsFalse(relationships.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/References/NamedTopicCollection.cs b/OnTopic/References/NamedTopicCollection.cs deleted file mode 100644 index 0f20c334..00000000 --- a/OnTopic/References/NamedTopicCollection.cs +++ /dev/null @@ -1,124 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using OnTopic.Collections; -using OnTopic.Internal.Diagnostics; - -namespace OnTopic.References { - - /*============================================================================================================================ - | CLASS: TOPIC - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a named version of the , suitable for use in - /// , or other derivatives of . - /// - public class NamedTopicCollection: TopicCollection { - - /*========================================================================================================================== - | PRIVATE VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - private bool _isDirty; - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class. - /// - /// Provides a name for the collection, used to identify different collections. - /// Optionally seeds the collection with an optional list of topic references. - public NamedTopicCollection(string name = "", IEnumerable? topics = null) : base() { - Name = name; - if (topics is not null) { - CopyTo(topics.ToArray(), 0); - } - } - - /*========================================================================================================================== - | IS DIRTY? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Determines if the collection has been modified. This value is set to true any time a new item is inserted or - /// removed from the collection. - /// - public bool IsDirty() => _isDirty; - - /*========================================================================================================================== - | MARK CLEAN - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Resets the status of the . - /// - public void MarkClean() => _isDirty = false; - - /*========================================================================================================================== - | INSERT ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// When inserting an item, determine if it will change the collection; if it will, mark the collection as . - /// - protected override void InsertItem(int index, Topic item) { - Contract.Requires(index, nameof(index)); - Contract.Requires(item, nameof(item)); - _isDirty = _isDirty || !Contains(item.Key); - base.InsertItem(index, item); - } - - /*========================================================================================================================== - | SET ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// When updating an existing item, determine if it will change the collection; if it will, mark the collection as . - /// - protected override void SetItem(int index, Topic item) { - Contract.Requires(index, nameof(index)); - Contract.Requires(item, nameof(item)); - _isDirty = _isDirty || !Contains(item.Key); - base.SetItem(index, item); - } - - /*========================================================================================================================== - | REMOVE ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// When removing an item from the collection, mark the collection as . - /// - protected override void RemoveItem(int index) { - Contract.Requires(index, nameof(index)); - _isDirty = _isDirty || index < Count; - base.RemoveItem(index); - } - - /*========================================================================================================================== - | CLEAR ITEMS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// When clearing the collection, mark the collection as if it had items in it. - /// - protected override void ClearItems() { - _isDirty = _isDirty || Count > 0; - base.ClearItems(); - } - - /*========================================================================================================================== - | PROPERTY: NAME - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides an optional name for the collection. - /// - /// - /// The Name property is optional, and primary intended to differentiate multiple - /// instances being referenced in a single collection, such as the . - /// - public string Name { get; } - - } //Class -} //Namespace \ No newline at end of file From 71292893e17cb03d026d0d8a8caf0e2709d58491 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 18:16:05 -0800 Subject: [PATCH 246/778] Renamed `RelatedTopicCollection` to `TopicRelationshipMultiMap` In a previous update, I renamed `RelatedTopicCollection.cs` to `TopicRelationshipMultiMap.cs` (57ff3f9), but neglected to update the actual class name and references. Whoops! This update corrects for that! --- ...st.cs => TopicRelationshipMultiMapTest.cs} | 62 +++++++++---------- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/README.md | 2 +- .../References/TopicRelationshipMultiMap.cs | 26 +++++--- OnTopic/Topic.cs | 4 +- 5 files changed, 51 insertions(+), 45 deletions(-) rename OnTopic.Tests/{RelatedTopicCollectionTest.cs => TopicRelationshipMultiMapTest.cs} (84%) diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs similarity index 84% rename from OnTopic.Tests/RelatedTopicCollectionTest.cs rename to OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 4376c52a..10078c7b 100644 --- a/OnTopic.Tests/RelatedTopicCollectionTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -12,13 +12,13 @@ namespace OnTopic.Tests { /*============================================================================================================================ - | CLASS: RELATED TOPIC COLLECTION TEST + | CLASS: TOPIC RELATIONSHIP MULTI-MAP TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the class. + /// Provides unit tests for the class. /// [TestClass] - public class RelatedTopicCollectionTest { + public class TopicRelationshipMultiMapTest { /*========================================================================================================================== | TEST: SET TOPIC: CREATES RELATIONSHIP @@ -69,7 +69,7 @@ public void RemoveTopic_RemovesIncomingRelationship() { var parent = TopicFactory.Create("Parent", "Page"); var related = TopicFactory.Create("Related", "Page"); - var relationships = new RelatedTopicCollection(parent); + var relationships = new TopicRelationshipMultiMap(parent); relationships.SetTopic("Friends", related); relationships.RemoveTopic("Friends", related); @@ -89,7 +89,7 @@ public void SetTopic_CreatesIncomingRelationship() { var parent = TopicFactory.Create("Parent", "Page"); var related = TopicFactory.Create("Related", "Page"); - var relationships = new RelatedTopicCollection(parent); + var relationships = new TopicRelationshipMultiMap(parent); relationships.SetTopic("Friends", related); @@ -107,7 +107,7 @@ public void SetTopic_CreatesIncomingRelationship() { public void SetTopic_UpdatesKeyCount() { var parent = TopicFactory.Create("Parent", "Page"); - var relationships = new RelatedTopicCollection(parent); + var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); @@ -128,7 +128,7 @@ public void SetTopic_UpdatesKeyCount() { public void GetAllTopics_ReturnsAllTopics() { var parent = TopicFactory.Create("Parent", "Page"); - var relationships = new RelatedTopicCollection(parent); + var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); @@ -151,7 +151,7 @@ public void GetAllTopics_ReturnsAllTopics() { public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { var parent = TopicFactory.Create("Parent", "Page"); - var relationships = new RelatedTopicCollection(parent); + var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i)); @@ -166,14 +166,14 @@ public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { | TEST: SET TOPIC: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a topic to a and confirms that is + /// Adds a topic to a and confirms that is /// set. /// [TestMethod] public void SetTopic_IsDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); relationships.SetTopic("Related", related); @@ -186,14 +186,14 @@ public void SetTopic_IsDirty() { | TEST: SET TOPIC: IS DUPLICATE: IS NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a duplicate topic to a and confirms that value of is false. + /// Adds a duplicate topic to a and confirms that value of is false. /// [TestMethod] public void SetTopic_IsDuplicate_IsNotDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); relationships.SetTopic("Related", related); @@ -209,14 +209,14 @@ public void SetTopic_IsDuplicate_IsNotDirty() { | TEST: SET TOPIC: IS DUPLICATE: STAYS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a duplicate topic to a and confirms that value of is false. + /// Adds a duplicate topic to a and confirms that value of is false. /// [TestMethod] public void SetTopic_IsDuplicate_StaysDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related1 = TopicFactory.Create("Topic", "Page"); var related2 = TopicFactory.Create("Topic", "Page"); @@ -232,14 +232,14 @@ public void SetTopic_IsDuplicate_StaysDirty() { | TEST: REMOVE TOPIC: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Removes an existing from a and conirms that the value for returns true. + /// Removes an existing from a and conirms that the value for returns true. /// [TestMethod] public void RemoveTopic_IsDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); relationships.SetTopic("Related", related); @@ -254,14 +254,14 @@ public void RemoveTopic_IsDirty() { | TEST: REMOVE TOPIC: MISSING TOPIC: IS NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Removes a non-existent from a and conirms that the value for - /// returns false. + /// Removes a non-existent from a and conirms that the value for + /// returns false. /// [TestMethod] public void RemoveTopic_MissingTopic_IsNotDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); var isSuccessful = relationships.RemoveTopic("Related", related); @@ -275,14 +275,14 @@ public void RemoveTopic_MissingTopic_IsNotDirty() { | TEST: REMOVE TOPIC: MISSING TOPIC: STAYS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Removes a non-existent from a and conirms that the value for - /// stays true. + /// Removes a non-existent from a and conirms that the value for + /// stays true. /// [TestMethod] public void RemoveTopic_MissingTopic_StaysDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic1", "Page"); var missing = TopicFactory.Create("Topic2", "Page"); @@ -299,14 +299,14 @@ public void RemoveTopic_MissingTopic_StaysDirty() { | TEST: CLEAR TOPICS: EXISTING TOPICS: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Call and confirms that value of is true. + /// Call and confirms that value of is true. /// [TestMethod] public void ClearTopics_ExistingTopics_IsDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); relationships.SetTopic("Related", related); @@ -321,14 +321,14 @@ public void ClearTopics_ExistingTopics_IsDirty() { | TEST: CLEAR TOPICS: NO TOPICS: IS NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Call with no existing s and confirms that - /// the value of is set to false. + /// Call with no existing s and confirms that + /// the value of is set to false. /// [TestMethod] public void ClearTopics_NoTopics_IsNotDirty() { var topic = TopicFactory.Create("Test", "Page"); - var relationships = new RelatedTopicCollection(topic); + var relationships = new TopicRelationshipMultiMap(topic); relationships.ClearTopics("Related"); diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 69459576..ed2acde7 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -126,7 +126,7 @@ public bool DisableChildTopics { /// /// /// To add content types to the collection, use . + /// cref="TopicRelationshipMultiMap.SetTopic(String, Topic, Boolean?)"/>. /// /// public ReadOnlyTopicCollection PermittedContentTypes { diff --git a/OnTopic/README.md b/OnTopic/README.md index cfa14bbd..f16af1dc 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -57,7 +57,7 @@ In addition to the above key classes, the `OnTopic` assembly contains a number o - **[`NamedTopicCollection`](Collections/NamedTopicCollection.cs)**: Provides a unique name to a `TopicCollection` so it can be keyed as part of a collection-of-collections. - **[`ReadOnlyTopicCollection{T}`](Collections/ReadOnlyTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - **[`ReadOnlyTopicCollection`](Collections/ReadOnlyTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`. -- **[`RelatedTopicCollection`](Collections/RelatedTopicCollection.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. +- **[`TopicRelationshipMultiMap`](Collections/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. - **[`AttributeValueCollection`](collections/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. ### Editor diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 07163bbc..ca6c6d3a 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -11,12 +11,18 @@ namespace OnTopic.References { /*============================================================================================================================ - | CLASS: RELATED TOPIC COLLECTION + | CLASS: TOPIC RELATIONSHIP MULTIMAP \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a simple interface for accessing collections of topic collections. /// - public class RelatedTopicCollection : ReadOnlyTopicMultiMap { + /// + /// The derives from to provide read-only access + /// to the underlying collection, then acts as a façade for the write operations, thus not only + /// simplifying access to the , but also ensuring that business logic is enforced, such as local + /// state tracking and handling of reciprocal relationships. + /// + public class TopicRelationshipMultiMap : ReadOnlyTopicMultiMap { /*========================================================================================================================== | PRIVATE VARIABLES @@ -30,16 +36,16 @@ public class RelatedTopicCollection : ReadOnlyTopicMultiMap { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the . + /// Initializes a new instance of the . /// /// /// The constructor requires a reference to a instance, which the related topics are to be associated - /// with. This will be used when setting incoming relationships. In addition, a may - /// be set as if it is specifically intended to track incoming relationships; if this is not - /// set, then it will not allow incoming relationships to be set via the internal + /// may be set as if it is specifically intended to track incoming relationships; if this is + /// not set, then it will not allow incoming relationships to be set via the internal overload. /// - public RelatedTopicCollection(Topic parent, bool isIncoming = false): base() { + public TopicRelationshipMultiMap(Topic parent, bool isIncoming = false): base() { _parent = parent; _isIncoming = isIncoming; base.Source = _storage; @@ -53,7 +59,7 @@ public RelatedTopicCollection(Topic parent, bool isIncoming = false): base() { /// /// /// If there are any objects in the specified , then the will be marked as . + /// TopicRelationshipMultiMap"/> will be marked as . /// /// The key of the relationship to be cleared. public void ClearTopics(string relationshipKey) { @@ -107,7 +113,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) if (!isIncoming) { if (_isIncoming) { throw new InvalidOperationException( - "You are attempting to remove an incoming relationship on a RelatedTopicCollection that is not flagged as " + + "You are attempting to remove an incoming relationship on a TopicRelationshipMultiMap that is not flagged as " + nameof(isIncoming) ); } @@ -195,7 +201,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool if (!isIncoming) { if (_isIncoming) { throw new InvalidOperationException( - "You are attempting to set an incoming relationship on a RelatedTopicCollection that is not flagged as " + + "You are attempting to set an incoming relationship on a TopicRelationshipMultiMap that is not flagged as " + nameof(isIncoming) ); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index d3a3a7b6..ea5e586a 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -679,7 +679,7 @@ public Topic? DerivedTopic { /// topic, thus allowing the topic hierarchy to be represented as a network graph. /// /// The current 's relationships. - public RelatedTopicCollection Relationships { get; } + public TopicRelationshipMultiMap Relationships { get; } /*========================================================================================================================== | PROPERTY: REFERENCES @@ -707,7 +707,7 @@ public Topic? DerivedTopic { /// all topics associated with that tag. /// /// The current 's incoming relationships. - public RelatedTopicCollection IncomingRelationships { get; } + public TopicRelationshipMultiMap IncomingRelationships { get; } /*========================================================================================================================== | PROPERTY: VERSION HISTORY From db53dd5fc3935760437a3d85bc533833e6cbbcd4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 11 Jan 2021 18:23:20 -0800 Subject: [PATCH 247/778] Updated documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paths to the documentation hadn't been updated after reorganizing the namespaces (fc23a61)—whoops! Nor had the new `TopicMultiMap` or `ReadOnlyTopicMultiMap` classes been accounted for. Both are now factored into the `README.md`. While I was at it, I now account for specialized collections under a new "Specialty Collections" subheading. --- OnTopic/README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index f16af1dc..26afd61c 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -13,6 +13,7 @@ The `OnTopic` assembly represents the core domain layer of the OnTopic library. - [Implementation](#implementation) - [Extension Methods](#extension-methods) - [Collections](#collections) + - [Specialty Collections](#specialty-collections) - [Editor](#editor-1) - [View Models](#view-models) @@ -32,7 +33,7 @@ Out of the box, the OnTopic library contains two specially derived topics for su - **[`ITopicRepository`](Repositories/ITopicRepository.cs)**: Defines the data access layer interface, with `Load()`, `Save()`, `Delete()`, `Move()`, and `Rollback()` methods. - **[`ITopicMappingService`](Mapping/README.md)**: Defines interface for a service that can convert a `Topic` class into any arbitrary data transfer object based on predetermined conventions—or vice versa (via the `IReverseTopicMappingService`. - **[`IHierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: Defines an interface for applying the `ITopicMappingService` to hierarchical data with constraints on depth. Used primarily for mapping navigation, such as in the [`NavigationTopicViewComponentBase`](../OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs). -- **[`ITypeLookupService`](ITypeLookupService.cs)**: Defines the interface that can identify `Type` objects based on a `GetType(typeName)` query. Used by e.g. `ITopicMappingService` to find corresponding `TopicViewModel` classes to map to. +- **[`ITypeLookupService`](lookup/ITypeLookupService.cs)**: Defines the interface that can identify `Type` objects based on a `GetType(typeName)` query. Used by e.g. `ITopicMappingService` to find corresponding `TopicViewModel` classes to map to. ## Implementations - **[`TopicMappingService`](Mapping/README.md)**: A default implementation of the `ITopicMappingService`, with built-in conventions that should address that majority of mapping requirements. This also includes a number of attributes for annotating view models with hints that the `TopicMappingService` can use in populating target objects. @@ -40,25 +41,29 @@ Out of the box, the OnTopic library contains two specially derived topics for su - **[`ReverseTopicMappingService`](Mapping/Reverse/README.md)**: A default implementation of the `IReverseTopicMappingService`, honoring similar conventions and attribute hints as the `TopicMappingService`. - **[`HierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: A default implementation of the `IHierarchicalTopicMappingService`, which accepts an `ITopicMappingService` for mapping each individual node in the hierarchy. - **[`CachedHierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: Provides an optional caching layer for the `HierarchicalTopicMappingService`—or any `IHierarchicalTopicMappingService` implementation. -- **[`StaticTypeLookupService`](StaticTypeLookupService.cs)**: A basic implementation of the `ITypeLookupService` interface that allows types to be explicitly registered; useful when a small number of types are expected. - - **[`DynamicTypeLookupService`](Reflection/DynamicTypeLookupService.cs)**: A reflection-based implementation of the `ITypeLookupService` interface that looks up types from all loaded assemblies based on a `Func` delegate. - - **[`DynamicTopicLookupService`](Reflection/DynamicTopicLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that derive from `Topic`; this is the default implementation for `TopicFactory`. - - **[`DynamicTopicViewModeLookupService`](Reflection/DynamicTopicViewModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that end with `TopicViewModel`; this is useful for the `TopicMappingService`. - - **[`DynamicTopicBindingModelLookupService`](Reflection/DynamicTopicBindingModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that implement `ITopicBindingModel`; this is useful for the `ReverseTopicMappingService`. +- **[`StaticTypeLookupService`](Lookup/StaticTypeLookupService.cs)**: A basic implementation of the `ITypeLookupService` interface that allows types to be explicitly registered; useful when a small number of types are expected. + - **[`DynamicTypeLookupService`](Lookup/DynamicTypeLookupService.cs)**: A reflection-based implementation of the `ITypeLookupService` interface that looks up types from all loaded assemblies based on a `Func` delegate. + - **[`DynamicTopicLookupService`](Lookup/DynamicTopicLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that derive from `Topic`; this is the default implementation for `TopicFactory`. + - **[`DynamicTopicViewModeLookupService`](Lookup/DynamicTopicViewModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that end with `TopicViewModel`; this is useful for the `TopicMappingService`. + - **[`DynamicTopicBindingModelLookupService`](Lookup/DynamicTopicBindingModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that implement `ITopicBindingModel`; this is useful for the `ReverseTopicMappingService`. ## Extension Methods - **[`Querying`](Querying/TopicExtensions.cs)**: The `TopicExtensions` class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate. -- **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeValueCollection`](Collections/AttributeValueCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. +- **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeValueCollection`](Attributes/AttributeValueCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. ## Collections In addition to the above key classes, the `OnTopic` assembly contains a number of specialized collections. These include: - **[`TopicCollection{T}`](Collections/TopicCollection{T}.cs)**: A `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - **[`TopicCollection`](Collections/TopicCollection.cs)**: A `KeyedCollection` of `Topic` keyed by `Id` and `Key`. - - **[`NamedTopicCollection`](Collections/NamedTopicCollection.cs)**: Provides a unique name to a `TopicCollection` so it can be keyed as part of a collection-of-collections. - **[`ReadOnlyTopicCollection{T}`](Collections/ReadOnlyTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - **[`ReadOnlyTopicCollection`](Collections/ReadOnlyTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`. -- **[`TopicRelationshipMultiMap`](Collections/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. -- **[`AttributeValueCollection`](collections/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. +- **[`TopicMultiMap`](Collections/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection name. + - **[`ReadOnlyTopicMultiMap`](Collections/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. + +### Specialty Collections +- **[`TopicRelationshipMultiMap`](References/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. +- **[`TopicRelationshipMultiMap`](References/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. +- **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. ### Editor The following are intended to provide support for the Editor domain objects, `ContentTypeDescriptor` and `AttributeDescriptor`. From 0cb427e3001b2c368ad477da63fd0df5e6f53b0c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 14:56:10 -0800 Subject: [PATCH 248/778] Set default values for `AttributeValueCollection` extensions Previously, when calling the non-nullable, strongly typed extension methods for `AttributeValueCollection`, such as `GetBoolean()` or `GetDateTime()`, a `defaultValue` parameter was explicitly required. This isn't necessarily intuitive since all of these data types have well-established default values. Given that I've made those parameters optional, with the default set to `default`. --- OnTopic/Attributes/AttributeValueCollectionExtensions.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 07a14206..bddae70d 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using System; using System.Globalization; -using OnTopic.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; @@ -41,7 +40,7 @@ public static class AttributeValueCollectionExtensions { public static bool GetBoolean( this AttributeValueCollection attributes, string name, - bool defaultValue, + bool defaultValue = default, bool inheritFromParent = false, bool inheritFromDerived = true ) { @@ -79,7 +78,7 @@ out var result public static int GetInteger( this AttributeValueCollection attributes, string name, - int defaultValue, + int defaultValue = default, bool inheritFromParent = false, bool inheritFromDerived = true ) { @@ -117,7 +116,7 @@ out var result public static double GetDouble( this AttributeValueCollection attributes, string name, - double defaultValue, + double defaultValue = default, bool inheritFromParent = false, bool inheritFromDerived = true ) { @@ -155,7 +154,7 @@ out var result public static DateTime GetDateTime( this AttributeValueCollection attributes, string name, - DateTime defaultValue, + DateTime defaultValue = default, bool inheritFromParent = false, bool inheritFromDerived = true ) { From cd440525641743311164e4400fb366991a059fec Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 14:57:26 -0800 Subject: [PATCH 249/778] Updated unit tests to evaluate implicit defaults, as well as explicit defaults Previously, the unit tests for `AttributeValueCollectionExtensions` all evaluated explicit defaults. Now that we also support implicit defaults, I've added support for those in as well. Since these aren't materially different from the existing tests, I simply added the assertions to the existing tests. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 2a9f071b..5fe524b6 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -77,6 +77,7 @@ public void GetInteger_IncorrectValue_ReturnsDefault() { topic.Attributes.SetValue("Number3", "Invalid"); Assert.AreEqual(5, topic.Attributes.GetInteger("Number3", 5)); + Assert.AreEqual(0, topic.Attributes.GetInteger("Number3")); } @@ -92,6 +93,7 @@ public void GetInteger_IncorrectKey_ReturnsDefault() { var topic = TopicFactory.Create("Test", "Container"); Assert.AreEqual(5, topic.Attributes.GetInteger("InvalidKey", 5)); + Assert.AreEqual(0, topic.Attributes.GetInteger("InvalidKey")); } @@ -126,6 +128,7 @@ public void GetDouble_IncorrectValue_ReturnsDefault() { topic.Attributes.SetValue("Number3", "Invalid"); Assert.AreEqual(5.0, topic.Attributes.GetDouble("Number3", 5.0)); + Assert.AreEqual(0, topic.Attributes.GetDouble("Number3")); } @@ -141,6 +144,7 @@ public void GetDouble_IncorrectKey_ReturnsDefault() { var topic = TopicFactory.Create("Test", "Container"); Assert.AreEqual(5.0, topic.Attributes.GetDouble("InvalidKey", 5.0)); + Assert.AreEqual(0, topic.Attributes.GetDouble("InvalidKey")); } @@ -178,6 +182,7 @@ public void GetDateTime_IncorrectValue_ReturnsDefault() { topic.Attributes.SetDateTime("DateTime2", dateTime2); Assert.AreEqual(dateTime1, topic.Attributes.GetDateTime("DateTime3", dateTime1)); + Assert.AreEqual(new DateTime(), topic.Attributes.GetDateTime("DateTime3")); } @@ -197,6 +202,7 @@ public void GetDateTime_IncorrectKey_ReturnsDefault() { topic.Attributes.SetDateTime("DateTime2", dateTime2); Assert.AreEqual(dateTime1, topic.Attributes.GetDateTime("DateTime3", dateTime1)); + Assert.AreEqual(new DateTime(), topic.Attributes.GetDateTime("DateTime3")); } @@ -234,6 +240,7 @@ public void GetBoolean_IncorrectValue_ReturnDefault() { Assert.IsTrue(topic.Attributes.GetBoolean("IsValue", true)); Assert.IsFalse(topic.Attributes.GetBoolean("IsValue", false)); + Assert.IsFalse(topic.Attributes.GetBoolean("IsValue")); } @@ -250,6 +257,7 @@ public void GetBoolean_IncorrectKey_ReturnDefault() { Assert.IsTrue(topic.Attributes.GetBoolean("InvalidKey", true)); Assert.IsFalse(topic.Attributes.GetBoolean("InvalidKey", false)); + Assert.IsFalse(topic.Attributes.GetBoolean("InvalidKey")); } From e0213034471285d2b7e70c78c18a742d25501cd9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 15:04:03 -0800 Subject: [PATCH 250/778] Updated calls to `AttributeValueCollection` extensions to use implicit defaults With the introduction of implicit defaults for the `AttributeValueCollection` extension methods (0cb427e), I've updated existing calls to rely on those defaults instead of explicitly defining the default value. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 4 ++-- OnTopic.Tests/Entities/CustomTopic.cs | 6 +++--- OnTopic/Metadata/AttributeDescriptor.cs | 4 ++-- .../Metadata/AttributeTypes/TokenizedTopicListAttribute.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/Topic.cs | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 62de5a3a..fb4b82c1 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -176,8 +176,8 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false | Validate topic \-----------------------------------------------------------------------------------------------------------------------*/ if (topic is null) return topics; - if (topic.Attributes.GetBoolean("NoIndex", false)) return topics; - if (topic.Attributes.GetBoolean("IsDisabled", false)) return topics; + if (topic.Attributes.GetBoolean("NoIndex")) return topics; + if (topic.Attributes.GetBoolean("IsDisabled")) return topics; if (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 6da7121e..ea5ecc35 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -46,7 +46,7 @@ public string TextAttribute { /// [AttributeSetter] public bool BooleanAttribute { - get => Attributes.GetBoolean("BooleanAttribute", false); + get => Attributes.GetBoolean("BooleanAttribute"); set => SetAttributeValue("BooleanAttribute", value? "1" : "0"); } @@ -58,7 +58,7 @@ public bool BooleanAttribute { /// [AttributeSetter] public int NumericAttribute { - get => Attributes.GetInteger("NumericAttribute", 0); + get => Attributes.GetInteger("NumericAttribute"); set { Contract.Requires( value >= 0, @@ -76,7 +76,7 @@ public int NumericAttribute { /// [AttributeSetter] public DateTime DateTimeAttribute { - get => Attributes.GetDateTime("DateTimeAttribute", DateTime.MinValue); + get => Attributes.GetDateTime("DateTimeAttribute"); set { Contract.Requires( value.Year > 2000, diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 8455ea06..e469eeae 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -147,7 +147,7 @@ public string? DisplayGroup { /// [AttributeSetter] public bool IsRequired { - get => Attributes.GetBoolean("IsRequired", false); + get => Attributes.GetBoolean("IsRequired"); set => SetAttributeValue("IsRequired", value ? "1" : "0"); } @@ -193,7 +193,7 @@ public string? DefaultValue { /// [AttributeSetter] public virtual bool IsExtendedAttribute { - get => Attributes.GetBoolean("IsExtendedAttribute", Attributes.GetBoolean("StoreInBlob", false)); + get => Attributes.GetBoolean("IsExtendedAttribute", Attributes.GetBoolean("StoreInBlob")); set => SetAttributeValue("IsExtendedAttribute", value ? "1" : "0"); } diff --git a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs index 274f0b50..48cd341d 100644 --- a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs @@ -42,7 +42,7 @@ public TokenizedTopicListAttribute( \-------------------------------------------------------------------------------------------------------------------------*/ /// public override ModelType ModelType => - Attributes.GetBoolean("SaveAsRelationship", false)? ModelType.Relationship : ModelType.ScalarValue; + Attributes.GetBoolean("SaveAsRelationship")? ModelType.Relationship : ModelType.ScalarValue; } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index ed2acde7..d1ec4b11 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -99,7 +99,7 @@ public ContentTypeDescriptor( /// [AttributeSetter] public bool DisableChildTopics { - get => Attributes.GetBoolean("DisableChildTopics", false); + get => Attributes.GetBoolean("DisableChildTopics"); set => SetAttributeValue("DisableChildTopics", value ? "1" : "0"); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index ea5e586a..6ca94852 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -326,7 +326,7 @@ public string? View { /// [AttributeSetter] public bool IsHidden { - get => Attributes.GetBoolean("IsHidden", false); + get => Attributes.GetBoolean("IsHidden"); set => SetAttributeValue("IsHidden", value ? "1" : "0"); } @@ -341,7 +341,7 @@ public bool IsHidden { /// [AttributeSetter] public bool IsDisabled { - get => Attributes.GetBoolean("IsDisabled", false); + get => Attributes.GetBoolean("IsDisabled"); set => SetAttributeValue("IsDisabled", value ? "1" : "0"); } From 99e84ca55bc55c91126d4c93d05f0cd3ad8b3bce Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 15:34:36 -0800 Subject: [PATCH 251/778] Renamed `TopicCollection` to `KeyedTopicCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This includes the generic versions (e.g., `KeyedTopicCollection`) and readonly versions (e.g., `ReadOnlyKeyedTopicCollection`). This helps clarify that these collections are keyed—and, therefore, won't accept topics with the same `Key`, even if they are otherwise distinct entities (e.g., they could be from different places in the hierarchy that just happen to have the same local `Key` name). --- ...ionTest.cs => KeyedTopicCollectionTest.cs} | 18 +++++++-------- .../Attributes/AttributeSetterAttribute.cs | 9 ++++---- ...cCollection.cs => KeyedTopicCollection.cs} | 8 +++---- ...ction{T}.cs => KeyedTopicCollection{T}.cs} | 12 +++++----- ...ion.cs => ReadOnlyKeyedTopicCollection.cs} | 18 +++++++-------- ....cs => ReadOnlyKeyedTopicCollection{T}.cs} | 22 +++++++++---------- .../Reverse/ReverseTopicMappingService.cs | 2 +- .../Metadata/AttributeDescriptorCollection.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 6 ++--- .../ContentTypeDescriptorCollection.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 2 +- OnTopic/README.md | 12 +++++----- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- OnTopic/Topic.cs | 2 +- 14 files changed, 57 insertions(+), 60 deletions(-) rename OnTopic.Tests/{TopicCollectionTest.cs => KeyedTopicCollectionTest.cs} (85%) rename OnTopic/Collections/{TopicCollection.cs => KeyedTopicCollection.cs} (81%) rename OnTopic/Collections/{TopicCollection{T}.cs => KeyedTopicCollection{T}.cs} (92%) rename OnTopic/Collections/{ReadOnlyTopicCollection.cs => ReadOnlyKeyedTopicCollection.cs} (72%) rename OnTopic/Collections/{ReadOnlyTopicCollection{T}.cs => ReadOnlyKeyedTopicCollection{T}.cs} (80%) diff --git a/OnTopic.Tests/TopicCollectionTest.cs b/OnTopic.Tests/KeyedTopicCollectionTest.cs similarity index 85% rename from OnTopic.Tests/TopicCollectionTest.cs rename to OnTopic.Tests/KeyedTopicCollectionTest.cs index 86c17b8a..6342c503 100644 --- a/OnTopic.Tests/TopicCollectionTest.cs +++ b/OnTopic.Tests/KeyedTopicCollectionTest.cs @@ -12,13 +12,13 @@ namespace OnTopic.Tests { /*============================================================================================================================ - | CLASS: TOPIC COLLECTION TESTS + | CLASS: KEYED TOPIC COLLECTION TESTS \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the class. + /// Provides unit tests for the class. /// [TestClass] - public class TopicCollectionTest { + public class KeyedTopicCollectionTest { /*========================================================================================================================== | TEST: SET TOPIC: INDEXER: RETURNS TOPIC @@ -29,7 +29,7 @@ public class TopicCollectionTest { [TestMethod] public void SetTopic_Indexer_ReturnsTopic() { - var topics = new TopicCollection(); + var topics = new KeyedTopicCollection(); for (var i = 0; i < 10; i++) { topics.Add(TopicFactory.Create("Topic" + i, "Page")); @@ -43,7 +43,7 @@ public void SetTopic_Indexer_ReturnsTopic() { | TEST: CONSTRUCTOR: IENUMERABLE: SEEDS TOPICS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a number of topics, then seeds a new with them. + /// Establishes a number of topics, then seeds a new with them. /// [TestMethod] public void Constructor_IEnumerable_SeedsTopics() { @@ -54,22 +54,22 @@ public void Constructor_IEnumerable_SeedsTopics() { topics.Add(TopicFactory.Create("Topic" + i, "Page")); } - var topicsCollection = new TopicCollection(topics); + var topicsCollection = new KeyedTopicCollection(topics); Assert.AreEqual(10, topicsCollection.Count); } /*========================================================================================================================== - | TEST: AS READ ONLY: RETURNS READ ONLY TOPIC COLLECTION + | TEST: AS READ ONLY: RETURNS READ ONLY KEYED TOPIC COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Establishes a number of topics, converts the collection to read only, and ensures they are still present. /// [TestMethod] - public void AsReadOnly_ReturnsReadOnlyTopicCollection() { + public void AsReadOnly_ReturnsReadOnlyKeyedTopicCollection() { - var topics = new TopicCollection(); + var topics = new KeyedTopicCollection(); for (var i = 0; i < 10; i++) { topics.Add(TopicFactory.Create("Topic" + i, "Page")); diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs index d2272a00..973a4337 100644 --- a/OnTopic/Attributes/AttributeSetterAttribute.cs +++ b/OnTopic/Attributes/AttributeSetterAttribute.cs @@ -26,11 +26,10 @@ namespace OnTopic.Attributes { /// /// /// As an example, the property is adorned with the . As a - /// result, if a client calls topic.Attributes.SetValue("Key", "NewKey") then that update will be routed - /// through , thus enforcing key validation, and calling - /// . Similarly, if topic.Attributes.SetValue("Key", ":/? ") - /// were called, a contract exception will be thrown since :/? violates - /// . + /// result, if a client calls topic.Attributes.SetValue("Key", "NewKey") then that update will be routed through + /// , thus enforcing key validation, and calling . Similarly, if topic.Attributes.SetValue("Key", ":/? ") were called, a contract exception will be + /// thrown since :/? violates . /// /// /// To ensure this logic, it is critical that implementers of ensure that the diff --git a/OnTopic/Collections/TopicCollection.cs b/OnTopic/Collections/KeyedTopicCollection.cs similarity index 81% rename from OnTopic/Collections/TopicCollection.cs rename to OnTopic/Collections/KeyedTopicCollection.cs index 6cca3f19..089f54c5 100644 --- a/OnTopic/Collections/TopicCollection.cs +++ b/OnTopic/Collections/KeyedTopicCollection.cs @@ -8,21 +8,21 @@ namespace OnTopic.Collections { /*============================================================================================================================ - | CLASS: TOPIC COLLECTION + | CLASS: KEYED TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents a collection of objects. /// - public class TopicCollection : TopicCollection { + public class KeyedTopicCollection : KeyedTopicCollection { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the . + /// Initializes a new instance of the . /// /// Seeds the collection with an optional list of topic references. - public TopicCollection(IEnumerable? topics = null) : base(topics) { + public KeyedTopicCollection(IEnumerable? topics = null) : base(topics) { } } //Class diff --git a/OnTopic/Collections/TopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs similarity index 92% rename from OnTopic/Collections/TopicCollection{T}.cs rename to OnTopic/Collections/KeyedTopicCollection{T}.cs index 6c268ec1..ee61a5f3 100644 --- a/OnTopic/Collections/TopicCollection{T}.cs +++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs @@ -11,21 +11,21 @@ namespace OnTopic.Collections { /*============================================================================================================================ - | CLASS: TOPIC COLLECTION + | CLASS: KEYED TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a strongly-typed collection of instances, or a derived type. /// - public class TopicCollection: KeyedCollection, IEnumerable where T : Topic { + public class KeyedTopicCollection: KeyedCollection, IEnumerable where T : Topic { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Seeds the collection with an optional list of topic references. - public TopicCollection(IEnumerable? topics = null) : base(StringComparer.OrdinalIgnoreCase) { + public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer.OrdinalIgnoreCase) { if (topics is not null) { foreach (var topic in topics) { Add(topic); @@ -51,9 +51,9 @@ public TopicCollection(IEnumerable? topics = null) : base(StringComparer.Ordi | METHOD: AS READ ONLY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a read-only version of this . + /// Retrieves a read-only version of this . /// - public ReadOnlyTopicCollection AsReadOnly() => new(this); + public ReadOnlyKeyedTopicCollection AsReadOnly() => new(this); /*========================================================================================================================== | OVERRIDE: INSERT ITEM diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs similarity index 72% rename from OnTopic/Collections/ReadOnlyTopicCollection.cs rename to OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs index f9c13f21..3e5fdc45 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs @@ -9,34 +9,34 @@ namespace OnTopic.Collections { /*============================================================================================================================ - | CLASS: READ ONLY TOPIC COLLECTION + | CLASS: READ-ONLY KEYED TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents a collection of objects. /// - public class ReadOnlyTopicCollection : ReadOnlyTopicCollection { + public class ReadOnlyKeyedTopicCollection : ReadOnlyKeyedTopicCollection { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new based on an existing . + /// Establishes a new based on an existing . /// - /// The underlying . - public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { + /// The underlying . + public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection) { } /*========================================================================================================================== | FACTORY METHOD: FROM LIST \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new based on an existing . + /// Establishes a new based on an existing . /// /// - /// The will be converted to a . + /// The will be converted to a . /// - /// The underlying . - public new static ReadOnlyTopicCollection FromList(IList innerCollection) { + /// The underlying . + public new static ReadOnlyKeyedTopicCollection FromList(IList innerCollection) { Contract.Requires(innerCollection, "innerCollection should not be null"); return new(innerCollection); } diff --git a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs similarity index 80% rename from OnTopic/Collections/ReadOnlyTopicCollection{T}.cs rename to OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index 73c72be3..60d72702 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -11,28 +11,28 @@ namespace OnTopic.Collections { /*============================================================================================================================ - | CLASS: READ ONLY TOPIC COLLECTION + | CLASS: READ-ONLY KEYED TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a read-only collection of topics. /// - public class ReadOnlyTopicCollection : ReadOnlyCollection where T : Topic { + public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : Topic { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly TopicCollection _innerCollection; + private readonly KeyedTopicCollection _innerCollection; /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new based on an existing . + /// Establishes a new based on an existing . /// - /// The underlying . - public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { + /// The underlying . + public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection) { Contract.Requires(innerCollection, "innerCollection should not be null"); - _innerCollection = innerCollection as TopicCollection?? new(innerCollection); + _innerCollection = innerCollection as KeyedTopicCollection?? new(innerCollection); } /*========================================================================================================================== @@ -53,14 +53,14 @@ public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCol | FACTORY METHOD: FROM LIST \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new based on an existing . + /// Establishes a new based on an existing . /// /// - /// The will be converted to a . + /// The will be converted to a . /// - /// The underlying . + /// The underlying . [Obsolete("This is effectively satisfied by the related overload, and will be removed in OnTopic 5.0.0.", true)] - public ReadOnlyTopicCollection FromList(IList innerCollection) { + public ReadOnlyKeyedTopicCollection FromList(IList innerCollection) { Contract.Requires(innerCollection, "innerCollection should not be null"); return new(innerCollection); } diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index ff4a4833..b8312022 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -570,7 +570,7 @@ PropertyConfiguration configuration /// The target to add the mapped objects to. protected async Task PopulateTargetCollectionAsync( IList sourceList, - TopicCollection targetList + KeyedTopicCollection targetList ) { /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Metadata/AttributeDescriptorCollection.cs b/OnTopic/Metadata/AttributeDescriptorCollection.cs index 3ec1cba3..d213cb23 100644 --- a/OnTopic/Metadata/AttributeDescriptorCollection.cs +++ b/OnTopic/Metadata/AttributeDescriptorCollection.cs @@ -13,7 +13,7 @@ namespace OnTopic.Metadata { /// /// Represents a collection of objects. /// - public class AttributeDescriptorCollection : TopicCollection { + public class AttributeDescriptorCollection : KeyedTopicCollection { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index d1ec4b11..f1c29fb6 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -39,7 +39,7 @@ public class ContentTypeDescriptor : Topic { | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private AttributeDescriptorCollection? _attributeDescriptors; - private ReadOnlyTopicCollection? _permittedContentTypes; + private ReadOnlyKeyedTopicCollection? _permittedContentTypes; /*========================================================================================================================== | CONSTRUCTOR @@ -129,14 +129,14 @@ public bool DisableChildTopics { /// cref="TopicRelationshipMultiMap.SetTopic(String, Topic, Boolean?)"/>. /// /// - public ReadOnlyTopicCollection PermittedContentTypes { + public ReadOnlyKeyedTopicCollection PermittedContentTypes { get { /*---------------------------------------------------------------------------------------------------------------------- | Populate values from relationships \---------------------------------------------------------------------------------------------------------------------*/ if (_permittedContentTypes is null) { - var permittedContentTypes = new TopicCollection(); + var permittedContentTypes = new KeyedTopicCollection(); var contentTypes = Relationships.GetTopics("ContentTypes"); foreach (ContentTypeDescriptor contentType in contentTypes) { permittedContentTypes.Add(contentType); diff --git a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs index 176a3210..4b4d2efc 100644 --- a/OnTopic/Metadata/ContentTypeDescriptorCollection.cs +++ b/OnTopic/Metadata/ContentTypeDescriptorCollection.cs @@ -25,7 +25,7 @@ namespace OnTopic.Metadata { /// constructor for handling a flattened list of s used for the method. /// - public class ContentTypeDescriptorCollection : TopicCollection { + public class ContentTypeDescriptorCollection : KeyedTopicCollection { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 9caa4fc1..27f36ae3 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -129,7 +129,7 @@ public static IEnumerable FindAll(this Topic topic, Func pre /*------------------------------------------------------------------------------------------------------------------------ | Search attributes \-----------------------------------------------------------------------------------------------------------------------*/ - var results = new TopicCollection(); + var results = new KeyedTopicCollection(); if (predicate(topic)) { results.Add(topic); diff --git a/OnTopic/README.md b/OnTopic/README.md index 26afd61c..ba369653 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -4,7 +4,7 @@ ![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) [![OnTopic package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/fb67677f-2b83-4318-9007-0c46b4da55c1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=fb67677f-2b83-4318-9007-0c46b4da55c1&preferRelease=true) -The `OnTopic` assembly represents the core domain layer of the OnTopic library. It includes the primary entity ([`Topic`](Topic.cs)), abstractions (e.g., [`ITopicRepository`](Repositories/ITopicRepository.cs)), and associated classes (e.g., [`TopicCollection<>`](Collections/TopicCollection{T}.cs)). +The `OnTopic` assembly represents the core domain layer of the OnTopic library. It includes the primary entity ([`Topic`](Topic.cs)), abstractions (e.g., [`ITopicRepository`](Repositories/ITopicRepository.cs)), and associated classes (e.g., [`KeyedTopicCollection<>`](Collections/KeyedTopicCollection{T}.cs)). ### Contents - [Entities](#entities) @@ -53,16 +53,14 @@ Out of the box, the OnTopic library contains two specially derived topics for su ## Collections In addition to the above key classes, the `OnTopic` assembly contains a number of specialized collections. These include: -- **[`TopicCollection{T}`](Collections/TopicCollection{T}.cs)**: A `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - - **[`TopicCollection`](Collections/TopicCollection.cs)**: A `KeyedCollection` of `Topic` keyed by `Id` and `Key`. -- **[`ReadOnlyTopicCollection{T}`](Collections/ReadOnlyTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - - **[`ReadOnlyTopicCollection`](Collections/ReadOnlyTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`. +- **[`KeyedTopicCollection{T}`](Collections/KeyedTopicCollection{T}.cs)**: A `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. + - **[`KeyedTopicCollection`](Collections/KeyedTopicCollection.cs)**: A `KeyedCollection` of `Topic` keyed by `Id` and `Key`. +- **[`ReadOnlyKeyedTopicCollection{T}`](Collections/ReadOnlyKeyedTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. + - **[`ReadOnlyKeyedTopicCollection`](Collections/ReadOnlyKeyedTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`. - **[`TopicMultiMap`](Collections/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection name. - **[`ReadOnlyTopicMultiMap`](Collections/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. ### Specialty Collections -- **[`TopicRelationshipMultiMap`](References/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. -- **[`TopicRelationshipMultiMap`](References/TopicRelationshipMultiMap.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections. - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. ### Editor diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 8d266847..dcd3fc7b 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -640,7 +640,7 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { /*------------------------------------------------------------------------------------------------------------------------ | Get unmatched attribute descriptors \-----------------------------------------------------------------------------------------------------------------------*/ - var attributes = new TopicCollection(); + var attributes = new AttributeDescriptorCollection(); foreach (var attribute in contentType.AttributeDescriptors) { diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 6ca94852..64a79c54 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -165,7 +165,7 @@ public Topic? Parent { /// /// The children of the current . /// - public TopicCollection Children { get; } + public KeyedTopicCollection Children { get; } /*========================================================================================================================== | PROPERTY: CONTENT TYPE From 75f6369d2aca6fba13cd265bea60276d17289d6d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 15:52:51 -0800 Subject: [PATCH 252/778] Fixed `Topic.FindAll()` extension method to use `Collection` Previously, the `Topic.FindAll()` method was using a `KeyedTopicCollection`, which prevented duplicate keys. We had migrated `Topic.FindAll()` to an `IEnumerable` in order to allow duplicates. This remedies that conflict. --- OnTopic/Querying/TopicExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 27f36ae3..6174a6a6 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; @@ -129,7 +130,7 @@ public static IEnumerable FindAll(this Topic topic, Func pre /*------------------------------------------------------------------------------------------------------------------------ | Search attributes \-----------------------------------------------------------------------------------------------------------------------*/ - var results = new KeyedTopicCollection(); + var results = new Collection(); if (predicate(topic)) { results.Add(topic); @@ -141,7 +142,7 @@ public static IEnumerable FindAll(this Topic topic, Func pre foreach (var child in topic.Children) { var nestedResults = child.FindAll(predicate); foreach (var matchedTopic in nestedResults) { - if (!results.Contains(matchedTopic.Key)) { + if (!results.Contains(matchedTopic)) { results.Add(matchedTopic); } } From 9b68bb2bb99f3e4b48b739cb49551b75787359bf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 15:57:34 -0800 Subject: [PATCH 253/778] Introduced new `TopicCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the legacy `TopicCollection` renamed to the more descriptive `KeyedTopicCollection`, we can now introduce a new `TopicCollection` class which is _not_ keyed, and is thus suitable for cases where we expect topics from across the topic graph to be represented. It's unclear what, if any, additional members we will want to add to this—though having it helps formalize the return types for the few cases where this is necessary, and works off of a more consistent naming convention. --- OnTopic/Collections/TopicCollection.cs | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 OnTopic/Collections/TopicCollection.cs diff --git a/OnTopic/Collections/TopicCollection.cs b/OnTopic/Collections/TopicCollection.cs new file mode 100644 index 00000000..f73c6a71 --- /dev/null +++ b/OnTopic/Collections/TopicCollection.cs @@ -0,0 +1,39 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: TOPIC COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects. + /// + public class TopicCollection : Collection { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the . + /// + /// Seeds the collection with an optional list of topic references. + public TopicCollection(IEnumerable? topics = null) : base(topics.ToList()) { + } + + /*========================================================================================================================== + | METHOD: AS READ ONLY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a read-only version of this . + /// + public ReadOnlyTopicCollection AsReadOnly() => new(this); + + } //Class +} //Namespace \ No newline at end of file From 77ca3c935bccc544624997474418dd1881f7b339 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:01:12 -0800 Subject: [PATCH 254/778] Introduced new `ReadOnlyTopicCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the legacy `ReadOnlyTopicCollection` renamed to the more descriptive `ReadOnlyKeyedTopicCollection`, and the introduction of a new `TopicCollection` class (9b68bb2), we can now introduce a new `ReadOnlyTopicCollection` class which is _not_ keyed, and is thus suitable for cases where we expect topics from across the topic graph to be represented. It's unclear what, if any, additional members we will want to add to this—though having it helps formalize the return types for the few cases where this is necessary, and works off of a more consistent naming convention. Note: This class is not generic as we don't anticipate much use of topic derivatives moving forward—and in the few cases that we do, we expect they'll want to be keyed. For instance, the `ContentTypeDescriptorCollection` and `AttributeDescriptorCollection` both operate off of `Topic` derivatives, and are intended to have unique keys in context, so using the generic `KeyedTopicCollection` is preferred. --- .../Collections/ReadOnlyTopicCollection.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 OnTopic/Collections/ReadOnlyTopicCollection.cs diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs new file mode 100644 index 00000000..7814d7c9 --- /dev/null +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -0,0 +1,31 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: READ-ONLY TOPIC COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects. + /// + public class ReadOnlyTopicCollection : ReadOnlyCollection { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new based on an existing . + /// + /// The underlying . + public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { + } + + } //Class +} //Namespace \ No newline at end of file From 8ef441b78b2da0254cfc4387cb5808ec66e3c21a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:13:57 -0800 Subject: [PATCH 255/778] Implemented `TopicCollection`, in favor of e.g. `Collection` Implemented `TopicCollection` to replace instances of where `Collection` or `IEnumerable` had been used. At this point, this doesn't add much functionality, but it's a bit shorter, and offers a more consistent naming convention. A couple of instances of `IEnumerable` were maintained, as required or expected by interfaces (e.g., `IDictionary`). --- OnTopic/Collections/ReadOnlyTopicCollection.cs | 1 - OnTopic/Collections/TopicMultiMap.cs | 6 +++--- OnTopic/Querying/TopicExtensions.cs | 10 ++++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs index 7814d7c9..a57638fb 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using System.Collections.Generic; using System.Collections.ObjectModel; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections { diff --git a/OnTopic/Collections/TopicMultiMap.cs b/OnTopic/Collections/TopicMultiMap.cs index b0de8564..056a23d9 100644 --- a/OnTopic/Collections/TopicMultiMap.cs +++ b/OnTopic/Collections/TopicMultiMap.cs @@ -16,7 +16,7 @@ namespace OnTopic.Collections { /// The offers support for a keyed collection where each key is mapped to a collection of instances, thus supporting a 1:n relationship with zero or more topics, organized by key. /// - public class TopicMultiMap: KeyedCollection>> { + public class TopicMultiMap: KeyedCollection> { /*========================================================================================================================== | CONSTRUCTOR @@ -50,7 +50,7 @@ public TopicMultiMap() { /// Returns a reference to the underlying collection. /// /// The key of the collection to be returned. - public Collection GetTopics(string key) { + public TopicCollection GetTopics(string key) { Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); if (Contains(key)) { return this[key].Values; @@ -147,7 +147,7 @@ public bool Remove(string key, Topic topic) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(KeyValuesPair> item) { + protected override string GetKeyForItem(KeyValuesPair item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key; } diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 6174a6a6..c5ca407d 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -4,8 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; @@ -111,7 +109,7 @@ public static class TopicExtensions { /// /// The instance of the to operate against; populated automatically by .NET. /// A collection of topics descending from the current topic. - public static IEnumerable FindAll(this Topic topic) => topic.FindAll(t => true); + public static TopicCollection FindAll(this Topic topic) => topic.FindAll(t => true); /// /// Retrieves a collection of topics based on a supplied function. @@ -119,7 +117,7 @@ public static class TopicExtensions { /// The instance of the to operate against; populated automatically by .NET. /// The function to validate whether a should be included in the output. /// A collection of topics matching the input parameters. - public static IEnumerable FindAll(this Topic topic, Func predicate) { + public static TopicCollection FindAll(this Topic topic, Func predicate) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -130,7 +128,7 @@ public static IEnumerable FindAll(this Topic topic, Func pre /*------------------------------------------------------------------------------------------------------------------------ | Search attributes \-----------------------------------------------------------------------------------------------------------------------*/ - var results = new Collection(); + var results = new TopicCollection(); if (predicate(topic)) { results.Add(topic); @@ -173,7 +171,7 @@ public static IEnumerable FindAll(this Topic topic, Func pre /// exception="T:System.ArgumentException"> /// !name.Contains(" ") /// - public static IEnumerable FindAllByAttribute(this Topic topic, string name, string value) { + public static TopicCollection FindAllByAttribute(this Topic topic, string name, string value) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts From a8cd08197a332af581b60d1b8a556f4be59f5b34 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:22:49 -0800 Subject: [PATCH 256/778] Ensured empty constructor is supported The `Collection` constructor accepts an `IList` as an optional parameter to prepopulate the collection. But, if that parameter is supplied, it cannot be nullable. As such, in order to keep the parameter nullable, we need to fall back to a new `List` instead of passing `null`. Whoops. --- OnTopic/Collections/TopicCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/TopicCollection.cs b/OnTopic/Collections/TopicCollection.cs index f73c6a71..a3d9dd60 100644 --- a/OnTopic/Collections/TopicCollection.cs +++ b/OnTopic/Collections/TopicCollection.cs @@ -24,7 +24,7 @@ public class TopicCollection : Collection { /// Initializes a new instance of the . /// /// Seeds the collection with an optional list of topic references. - public TopicCollection(IEnumerable? topics = null) : base(topics.ToList()) { + public TopicCollection(IEnumerable? topics = null) : base(topics?.ToList()?? new()) { } /*========================================================================================================================== From 3dc0de2066e4f2802a2187cbd5a12829a185f28a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:23:45 -0800 Subject: [PATCH 257/778] Prefer `ReadOnlyTopicCollection` over e.g. `ReadOnlyCollection` Implemented the new `ReadOnlyTopicCollection` to replace instances of where `ReadOnlyCollection` had been used. At this point, this doesn't add much functionality, but it's a bit shorter, and offers a more consistent naming convention. This includes the extension methods previously updated to use `TopicCollection`, since we expect these results to be read-only. --- OnTopic.Tests/TopicQueryingTest.cs | 2 +- OnTopic/Collections/ReadOnlyTopicMultiMap.cs | 16 ++++++++-------- OnTopic/Querying/TopicExtensions.cs | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/OnTopic.Tests/TopicQueryingTest.cs b/OnTopic.Tests/TopicQueryingTest.cs index ddaf6037..9a969109 100644 --- a/OnTopic.Tests/TopicQueryingTest.cs +++ b/OnTopic.Tests/TopicQueryingTest.cs @@ -64,7 +64,7 @@ public void FindAllByAttribute_ReturnsCorrectTopics() { grandNieceTopic.Attributes.SetValue("Foo", "Bar"); Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Bar").First(), grandNieceTopic); - Assert.AreEqual(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count()); + Assert.AreEqual(2, parentTopic.FindAllByAttribute("Foo", "Bar").Count); Assert.ReferenceEquals(parentTopic.FindAllByAttribute("Foo", "Baz").First(), grandChildTopic); } diff --git a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs index e12008f9..d0edbdef 100644 --- a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/ReadOnlyTopicMultiMap.cs @@ -19,7 +19,7 @@ namespace OnTopic.Collections { /// /// The provides a read-only façade to a . /// - public class ReadOnlyTopicMultiMap: IEnumerable>> { + public class ReadOnlyTopicMultiMap: IEnumerable> { /*========================================================================================================================== | CONSTRUCTOR @@ -93,7 +93,7 @@ protected ReadOnlyTopicMultiMap() {} /// /// A collection. /// - public ReadOnlyCollection this[string key] => new(Source[key].Values); + public ReadOnlyTopicCollection this[string key] => new(Source[key].Values); /*========================================================================================================================== | METHOD: CONTAINS? @@ -114,7 +114,7 @@ protected ReadOnlyTopicMultiMap() {} /// Returns a reference to the underlying collection. /// /// The key of the collection to be returned. - public ReadOnlyCollection GetTopics(string key) { + public ReadOnlyTopicCollection GetTopics(string key) { Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); if (Contains(key)) { return new(Source[key].Values); @@ -131,8 +131,8 @@ public ReadOnlyCollection GetTopics(string key) { /// /// Returns an enumerable list of objects. /// - public ReadOnlyCollection GetAllTopics() => - Source.SelectMany(list => list.Values).Distinct().ToList().AsReadOnly(); + public ReadOnlyTopicCollection GetAllTopics() => + new(Source.SelectMany(list => list.Values).Distinct().ToList()); /// /// Retrieves a list of all related objects, independent of relationship key, filtered by content @@ -141,14 +141,14 @@ public ReadOnlyCollection GetAllTopics() => /// /// Returns an enumerable list of objects. /// - public ReadOnlyCollection GetAllTopics(string contentType) => - GetAllTopics().Where(t => t.ContentType == contentType).ToList().AsReadOnly(); + public ReadOnlyTopicCollection GetAllTopics(string contentType) => + new(GetAllTopics().Where(t => t.ContentType == contentType).ToList()); /*========================================================================================================================== | GET ENUMERATOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public IEnumerator>> GetEnumerator() { + public IEnumerator> GetEnumerator() { foreach (var collection in Source) { yield return new(collection.Key, new(collection.Values)); } diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index c5ca407d..a1be5fc7 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -109,7 +109,7 @@ public static class TopicExtensions { /// /// The instance of the to operate against; populated automatically by .NET. /// A collection of topics descending from the current topic. - public static TopicCollection FindAll(this Topic topic) => topic.FindAll(t => true); + public static ReadOnlyTopicCollection FindAll(this Topic topic) => topic.FindAll(t => true); /// /// Retrieves a collection of topics based on a supplied function. @@ -117,7 +117,7 @@ public static class TopicExtensions { /// The instance of the to operate against; populated automatically by .NET. /// The function to validate whether a should be included in the output. /// A collection of topics matching the input parameters. - public static TopicCollection FindAll(this Topic topic, Func predicate) { + public static ReadOnlyTopicCollection FindAll(this Topic topic, Func predicate) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -149,7 +149,7 @@ public static TopicCollection FindAll(this Topic topic, Func predic /*------------------------------------------------------------------------------------------------------------------------ | Return results \-----------------------------------------------------------------------------------------------------------------------*/ - return results; + return new(results); } @@ -171,7 +171,7 @@ public static TopicCollection FindAll(this Topic topic, Func predic /// exception="T:System.ArgumentException"> /// !name.Contains(" ") /// - public static TopicCollection FindAllByAttribute(this Topic topic, string name, string value) { + public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, string name, string value) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts From 560aacab40febc82b88d8250e2cb775e674de461 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:53:21 -0800 Subject: [PATCH 258/778] Added mapping support for derived collections Previously, the collection support of the `TopicMappingService` expected that the collection would be generic, and that's where it would determine the compatible list type. That works fine if, in fact, the collection type _is_ generic. It fails, however, if the class derives from a generic type. In that case, instead, we need to loop through the available interfaces, identify the `IList` interface, and then get the generic type from _that_ interface, since we won't be able to get it off the actual non-generic type. This implements that support. --- OnTopic/Mapping/TopicMappingService.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index b859dc68..ae1e371f 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -635,9 +635,11 @@ MappedTopicCache cache | Determine the type of item in the list \-----------------------------------------------------------------------------------------------------------------------*/ var listType = typeof(ITopicViewModel); - if (configuration.Property.PropertyType.IsGenericType) { - //Uses last argument in case it's a KeyedCollection; in that case, we want the TItem type - listType = configuration.Property.PropertyType.GetGenericArguments().Last(); + foreach (var type in configuration.Property.PropertyType.GetInterfaces()) { + if (type.IsGenericType && typeof(IList<>) == type.GetGenericTypeDefinition()) { + //Uses last argument in case it's a KeyedCollection; in that case, we want the TItem type + listType = type.GetGenericArguments().Last(); + } } /*------------------------------------------------------------------------------------------------------------------------ From 240005782d1aec82683f5029713c716fbf5e7d51 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 12 Jan 2021 16:55:20 -0800 Subject: [PATCH 259/778] Implement `TopicCollection` in compatible-collection unit test Now that we support mapping to collections even if they don't directly implement `IList` (560aaca), we can convert the `RelatedEntityTopicViewModel` to use a return type of `TopicCollection` instead of `Collection`, and still ensure that the collection is properly mapped. --- OnTopic.Tests/ViewModels/RelatedEntityTopicViewModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/ViewModels/RelatedEntityTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelatedEntityTopicViewModel.cs index 73897096..7f49f2a1 100644 --- a/OnTopic.Tests/ViewModels/RelatedEntityTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelatedEntityTopicViewModel.cs @@ -3,7 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Collections.ObjectModel; +using OnTopic.Collections; namespace OnTopic.Tests.ViewModels { @@ -25,7 +25,7 @@ namespace OnTopic.Tests.ViewModels { /// public class RelatedEntityTopicViewModel: KeyOnlyTopicViewModel { - public Collection RelatedTopics { get; } = new(); + public TopicCollection RelatedTopics { get; } = new(); } //Class } //Namespace \ No newline at end of file From a6d18016faef1eba8b0da3af4e4da66d09d58a06 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 14:05:09 -0800 Subject: [PATCH 260/778] Renamed `isDirty` parameter to `markDirty` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `isDirty` parameter name is slightly confusing because it's not intended to be descriptive of the current state, but rather an optional instruction of the final state. Semantically, `markDirty` is a bit clearer—and especially when compared against local `isDirty` or `_isDirty` variables which are intended to be descriptive. --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 ++-- OnTopic/Attributes/AttributeValueCollection.cs | 16 ++++++++-------- OnTopic/References/TopicReferenceDictionary.cs | 6 +++--- OnTopic/References/TopicRelationshipMultiMap.cs | 14 +++++++------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 3fededf3..d645c36a 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -286,7 +286,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() var topic = TopicFactory.Create("Test", "ContentTypes"); - topic.Attributes.SetValue("Title", "Title", isDirty:false, isExtendedAttribute:false); + topic.Attributes.SetValue("Title", "Title", markDirty:false, isExtendedAttribute:false); var attributes = _topicRepository.GetAttributesProxy(topic, true, true); @@ -309,7 +309,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsNothing() { var topic = TopicFactory.Create("Test", "ContentTypes"); - topic.Attributes.SetValue("Title", "Title", isDirty: false, isExtendedAttribute: false); + topic.Attributes.SetValue("Title", "Title", markDirty: false, isExtendedAttribute: false); var attributes = _topicRepository.GetAttributesProxy(topic, false, true); diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 93d931cc..91c5758a 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -286,7 +286,7 @@ _associatedTopic.DerivedTopic is not null && /// /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. - /// + /// /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being @@ -316,11 +316,11 @@ _associatedTopic.DerivedTopic is not null && public void SetValue( string key, string? value, - bool? isDirty = null, + bool? markDirty = null, DateTime? version = null, bool? isExtendedAttribute = null ) - => SetValue(key, value, isDirty, true, version, isExtendedAttribute); + => SetValue(key, value, markDirty, true, version, isExtendedAttribute); /// /// Protected helper method that either adds a new object or updates the value of an existing @@ -333,7 +333,7 @@ public void SetValue( /// /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. - /// + /// /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being @@ -367,7 +367,7 @@ public void SetValue( internal void SetValue( string key, string? value, - bool? isDirty, + bool? markDirty, bool enforceBusinessLogic, DateTime? version = null, bool? isExtendedAttribute = null @@ -409,8 +409,8 @@ internal void SetValue( \-----------------------------------------------------------------------------------------------------------------------*/ else if (originalAttributeValue is not null) { var markAsDirty = originalAttributeValue.IsDirty; - if (isDirty.HasValue) { - markAsDirty = isDirty.Value; + if (markDirty.HasValue) { + markAsDirty = markDirty.Value; } else if (originalAttributeValue.Value != value) { markAsDirty = true; @@ -437,7 +437,7 @@ internal void SetValue( | Create new attribute value \-----------------------------------------------------------------------------------------------------------------------*/ else { - updatedAttributeValue = new AttributeValue(key, value, isDirty ?? true, version, isExtendedAttribute); + updatedAttributeValue = new AttributeValue(key, value, markDirty ?? true, version, isExtendedAttribute); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index b035bf17..71c7b0dc 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -190,13 +190,13 @@ public void Add(string key, Topic value) { /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is /// removed. /// - public void SetTopic(string key, Topic? value, bool? isDirty = null) => SetTopic(key, value, isDirty, true); + public void SetTopic(string key, Topic? value, bool? markDirty = null) => SetTopic(key, value, markDirty, true); /// /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is /// removed. /// - internal void SetTopic(string key, Topic? value, bool? isDirty, bool enforceBusinessLogic) { + internal void SetTopic(string key, Topic? value, bool? markDirty, bool enforceBusinessLogic) { /*------------------------------------------------------------------------------------------------------------------------ | Establish state @@ -232,7 +232,7 @@ internal void SetTopic(string key, Topic? value, bool? isDirty, bool enforceBusi /*------------------------------------------------------------------------------------------------------------------------ | Set dirty state \-----------------------------------------------------------------------------------------------------------------------*/ - if (wasDirty is false && isDirty is false) { + if (wasDirty is false && markDirty is false) { _isDirty = false; } diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index ca6c6d3a..50a50b4d 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -151,11 +151,11 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) /// /// The key of the relationship. /// The topic to be added, if it doesn't already exist. - /// + /// /// Optionally forces the collection to an state, assuming the topic was set. /// - public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) - => SetTopic(relationshipKey, topic, isDirty, false); + public void SetTopic(string relationshipKey, Topic topic, bool? markDirty = null) + => SetTopic(relationshipKey, topic, markDirty, false); /// /// Ensures that an incoming is associated with the specified relationship key. @@ -168,10 +168,10 @@ public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) /// /// Notes that this is setting an internal relationship, and thus shouldn't set the reciprocal relationship. /// - /// + /// /// Optionally forces the collection to an state, assuming the topic was set. /// - internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) { + internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -187,7 +187,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool var wasDirty = _isDirty.Contains(relationshipKey); if (!topics.Contains(topic)) { _storage.Add(relationshipKey, topic); - if (isDirty.HasValue && !isDirty.Value && !wasDirty) { + if (markDirty.HasValue && !markDirty.Value && !wasDirty) { MarkClean(relationshipKey); } else { @@ -205,7 +205,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool nameof(isIncoming) ); } - topic.IncomingRelationships.SetTopic(relationshipKey, _parent, isDirty, true); + topic.IncomingRelationships.SetTopic(relationshipKey, _parent, markDirty, true); } } From 8e3984a796dc0d9d56b3e9d9305645879fb955d1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 15:30:35 -0800 Subject: [PATCH 261/778] Introduced `referenceTopic` parameter into `ITopicRepository.Load()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to the `ITopicRepository.Load()` signature, a topic can be loaded from anywhere within the topic graph. And, indeed, that works just fine assuming one is querying e.g. the `CachedTopicRepository`, which has access to the entire tree. If one queries a data store—e.g., via the `SqlTopicRepository`—however, that can result in an incomplete topic being returned, since it won't be able to find topic references and relationships which occur outside the `Load()` scope. That can then cause problems if the topic is then saved, as those orphaned references may get overwritten. (Note: In practice, this usually only affects `Load(topicId, version)` since most applications use the `CachedTopicRepository`. And since relationships and references aren't currently versioned, this doesn't have an impact on `Rollback()` either. Nevertheless, it remains a hole in the design that could yield unexpected bugs, if not data loss.) As a first step to resolving this, the `ITopicRepository.Load()` signature is extended to accept an optional `referenceTopic`. When present, this prepopulates the list of topics with those already in memory. This ensures that a topic being loaded has access to the rest of the graph, at least insofar as the current application is aware of it. Further, this optionally allows `ITopicRepository` instances to update existing instances of topics, instead of loading new object references referring to the same entity, which can cause additional problems. While this commit introduces the extension of the signature, it doesn't implement the overload in the code (except as part of `ITopicRepository` passthroughs), nor does it implement it in any of the concrete implementations, such as `SqlTopicRepository`. Those steps will come in future commits. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 8 +++---- OnTopic.Data.Sql/SqlTopicRepository.cs | 10 ++++----- OnTopic.TestDoubles/DummyTopicRepository.cs | 6 +++--- OnTopic.TestDoubles/StubTopicRepository.cs | 8 +++---- OnTopic/Repositories/ITopicRepository.cs | 21 ++++++++++++++++--- OnTopic/Repositories/TopicRepositoryBase.cs | 12 ++++++++--- 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 7fcf94ec..cbef86b2 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -79,7 +79,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override Topic? Load(int topicId, bool isRecursive = true) { + public override Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Handle request for entire tree @@ -96,7 +96,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } /// - public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) { + public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Lookup by TopicKey @@ -113,7 +113,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } /// - public override Topic? Load(int topicId, DateTime version) { + public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -127,7 +127,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Return appropriate topic \-----------------------------------------------------------------------------------------------------------------------*/ - return _dataProvider.Load(topicId, version); + return _dataProvider.Load(topicId, version, referenceTopic?? _cache); } diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 5d064e8e..e95d5e0c 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -61,7 +61,7 @@ public SqlTopicRepository(string connectionString) : base() { | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override Topic Load(string? uniqueKey = null, bool isRecursive = true) { + public override Topic Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Handle empty topic @@ -70,7 +70,7 @@ public override Topic Load(string? uniqueKey = null, bool isRecursive = true) { | call Load() with the special integer value of -1, which will load all topics from the root. \-----------------------------------------------------------------------------------------------------------------------*/ if (String.IsNullOrEmpty(uniqueKey)) { - return Load(-1, isRecursive); + return Load(-1, referenceTopic, isRecursive); } /*------------------------------------------------------------------------------------------------------------------------ @@ -118,12 +118,12 @@ public override Topic Load(string? uniqueKey = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Return topic \-----------------------------------------------------------------------------------------------------------------------*/ - return Load(topicId, isRecursive); + return Load(topicId, referenceTopic, isRecursive); } /// - public override Topic Load(int topicId, bool isRecursive = true) { + public override Topic Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection @@ -188,7 +188,7 @@ public override Topic Load(int topicId, bool isRecursive = true) { } /// - public override Topic Load(int topicId, DateTime version) { + public override Topic Load(int topicId, DateTime version, Topic? referenceTopic = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index d40cd7b5..c55ef0b1 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -30,13 +30,13 @@ public DummyTopicRepository() : base() { } | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override Topic? Load(int topicId, bool isRecursive = true) => null; + public override Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true) => null; /// - public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) => null; + public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) => null; /// - public override Topic? Load(int topicId, DateTime version) => throw new NotImplementedException(); + public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: SAVE diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 21e7b9f4..b530ca8b 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -49,11 +49,11 @@ public StubTopicRepository() : base() { | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override Topic? Load(int topicId, bool isRecursive = true) => + public override Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true) => (topicId < 0)? _cache :_cache.FindFirst(t => t.Id.Equals(topicId)); /// - public override Topic? Load(string? uniqueKey = null, bool isRecursive = true) { + public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) { /*------------------------------------------------------------------------------------------------------------------------ | Lookup by TopicKey @@ -71,7 +71,7 @@ public StubTopicRepository() : base() { } /// - public override Topic? Load(int topicId, DateTime version) { + public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -85,7 +85,7 @@ public StubTopicRepository() : base() { /*------------------------------------------------------------------------------------------------------------------------ | Get topic \-----------------------------------------------------------------------------------------------------------------------*/ - var topic = Load(topicId); + var topic = Load(topicId, referenceTopic, false); /*------------------------------------------------------------------------------------------------------------------------ | Reset version diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 01887282..7ee01aff 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -53,17 +53,25 @@ public interface ITopicRepository { /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier. /// /// The topic identifier. + /// + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references + /// and relationships, including , are integrated with existing entities. + /// /// Determines whether or not to recurse through and load a topic's children. /// A topic object. - Topic? Load(int topicId, bool isRecursive = true); + Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true); /// /// Loads a topic (and, optionally, all of its descendants) based on the specified key name. /// /// The fully-qualified unique topic key. + /// + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references + /// and relationships, including , are integrated with existing entities. + /// /// Determines whether or not to recurse through and load a topic's children. /// A topic object. - Topic? Load(string? uniqueKey = null, bool isRecursive = true); + Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); /// /// Loads a specific version of a topic based on its version. @@ -74,8 +82,15 @@ public interface ITopicRepository { /// /// The topic identifier. /// The version. + /// + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references + /// and relationships, including , are integrated with existing entities. + /// /// A topic object. - Topic? Load(int topicId, DateTime version); + Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); + + /// + Topic? Load(Topic referenceTopic, DateTime version); /*========================================================================================================================== | METHOD: ROLLBACK diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index dcd3fc7b..6fa47a55 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -221,13 +221,19 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public abstract Topic? Load(int topicId, bool isRecursive = true); + public abstract Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true); /// - public abstract Topic? Load(string? uniqueKey = null, bool isRecursive = true); + public abstract Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); /// - public abstract Topic? Load(int topicId, DateTime version); + public Topic? Load(Topic referenceTopic, DateTime version) { + Contract.Requires(referenceTopic, nameof(referenceTopic)); + return Load(referenceTopic.Id, version, referenceTopic); + } + + /// + public abstract Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); /*========================================================================================================================== | METHOD: ROLLBACK From 1f6e8f1edaa16bb4e0d4ec885e09c480acccb498 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 15:50:25 -0800 Subject: [PATCH 262/778] Pass `referenceTopic` where available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While the `referenceTopic` isn't yet wired up in any of the concrete `ITopicRepository` implementations—namely `SqlTopicRepository`—the interface now exists in `ITopicRepository` and should be utilized where possible. In this setup, a current topic from the cached topic graph will be passed to `ITopicRepository.Load()` if one is available. Note: In most cases, we actually expect the implementation to be operating against a `CachedTopicRepository` decorator, in which case this doesn't provide any value. Nevertheless, the OnTopic Library is not aware of what `ITopicRepository` implementation will be used in any application, and should not make any assumptions. There remain some cases where one isn't available—such as when bootstrapping a call. --- OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs | 2 +- .../Hierarchical/HierarchicalTopicMappingService{T}.cs | 2 +- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 4 ++-- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index 2765003a..add98820 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -79,7 +79,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService var configuredRoot = CurrentTopic.Attributes.GetValue("NavigationRoot", true); if (!String.IsNullOrEmpty(configuredRoot)) { - navigationRootTopic = TopicRepository.Load("Root:" + configuredRoot); + navigationRootTopic = TopicRepository.Load("Root:" + configuredRoot, CurrentTopic); } if (navigationRootTopic is null) { navigationRootTopic = HierarchicalTopicMappingService.GetHierarchicalRoot(CurrentTopic, 2, "Web"); diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index c8868432..1ce8a8d1 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -90,7 +90,7 @@ ITopicMappingService topicMappingService $"The current route could not be resolved to a topic and the {nameof(defaultRoot)} was not set." ); } - navigationRootTopic = TopicRepository.Load(defaultRoot); + navigationRootTopic = TopicRepository.Load(defaultRoot, currentTopic); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index b8312022..26c5bc32 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -434,7 +434,7 @@ PropertyConfiguration configuration | Set relationships for each \-----------------------------------------------------------------------------------------------------------------------*/ foreach (IRelatedTopicBindingModel relationship in sourceList) { - var targetTopic = _topicRepository.Load(relationship.UniqueKey); + var targetTopic = _topicRepository.Load(relationship.UniqueKey, target); if (targetTopic is null) { throw new TopicMappingException( $"The relationship '{relationship.UniqueKey}' mapped in the '{configuration.Property.Name}' property could not " + @@ -536,7 +536,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Identify target value \-----------------------------------------------------------------------------------------------------------------------*/ - var topicReference = _topicRepository.Load(modelReference.UniqueKey); + var topicReference = _topicRepository.Load(modelReference.UniqueKey, target); /*------------------------------------------------------------------------------------------------------------------------ | Provide error handling diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index ae1e371f..19922658 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -313,7 +313,7 @@ protected async Task SetPropertyAsync( await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); } else if (topicReferenceId > 0 && relationships.HasFlag(Relationships.References)) { - topicReference = _topicRepository.Load(topicReferenceId); + topicReference = _topicRepository.Load(topicReferenceId, source); if (topicReference is not null) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); } @@ -571,7 +571,7 @@ protected IList GetSourceCollection(Topic source, Relationships relations \-----------------------------------------------------------------------------------------------------------------------*/ if (listSource.Count == 0 && !String.IsNullOrWhiteSpace(configuration.MetadataKey)) { var metadataKey = $"Root:Configuration:Metadata:{configuration.MetadataKey}:LookupList"; - var metadataParent = _topicRepository.Load(metadataKey); + var metadataParent = _topicRepository.Load(metadataKey, source); if (metadataParent is not null) { listSource = metadataParent.Children.ToList(); } From 3af4c789e53e1dda58ca89a06f16720530a0d377 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:01:17 -0800 Subject: [PATCH 263/778] Establish new `TopicIndex` collection A `TopicIndex` is simply a lookup table of `Topic` objects keyed by `Topic.Id`. By default, using e.g., `Topic.Load(topicId)` or even `Topic.FindFirst(t => t.Id == topicId)` is potentially expensive as it requires a recursive search of the topic graph. That's fine for one-time queries. If multiple queries need to be performed, a `TopicIndex` offers better performance by caching the results in an index. This is primarily intended to support backend infrastructure, though it may be of use for some specialized client implementations. --- OnTopic/Collections/TopicIndex.cs | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 OnTopic/Collections/TopicIndex.cs diff --git a/OnTopic/Collections/TopicIndex.cs b/OnTopic/Collections/TopicIndex.cs new file mode 100644 index 00000000..56cffc2d --- /dev/null +++ b/OnTopic/Collections/TopicIndex.cs @@ -0,0 +1,34 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Generic; + +namespace OnTopic.Collections { + + /*============================================================================================================================ + | CLASS: TOPIC INDEX + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects indexed by . + /// + public class TopicIndex : Dictionary { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the . + /// + /// Seeds the collection with an optional list of topic references. + public TopicIndex(IEnumerable? topics = null) : base() { + if (topics is not null) { + foreach(var topic in topics) { + Add(topic.Id, topic); + } + } + } + + } //Class +} //Namespace \ No newline at end of file From 3777b6b0854c21e2db651b3c2d40e30c1e5390a9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:04:03 -0800 Subject: [PATCH 264/778] Introduced `GetTopicIndex()` extensions method The `GetTopicIndex()` extension method allows a `TopicIndex` (3af4c78) to be generated for all descendents of the current in-memory topic graph. --- OnTopic/Querying/TopicExtensions.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index a1be5fc7..75706964 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Collections.Generic; using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; @@ -191,6 +192,16 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, strin } + /*========================================================================================================================== + | METHOD: GET TOPIC INDEX + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves all topics from the topic cache, and places them in an dictionary indexed by . + /// + /// The instance of the to operate against; populated automatically by .NET. + /// A dictionary of topics indexed by . + public static TopicIndex GetTopicIndex(this Topic topic) => new TopicIndex(topic.FindAll()); + /*========================================================================================================================== | METHOD: GET ROOT TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ From 72f0560318b5aa44e36f6e7c92f8f567381b75f0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:15:26 -0800 Subject: [PATCH 265/778] Introduce `referenceTopic` parameter to `LoadTopicGraph()` extension The `SqlDataReader.LoadTopicGraph()` extension method is the primary entry point for the `SqlTopicRepository.Load()` method. By introducing this parameter, we ensure that the `referenceTopic` is accounted for to any calls to `SqlTopicRepository.Load()`. Notably, this initializes the `referenceTopic` by calling `GetRootTopic()` followed by `GetTopicIndex()`, thus prepopulating the topic index used by subsequent methods. This effectively replaces the previous `Dictionary` used by those methods. If no `referenceTopic` is passed, or a topic can't be found in the `TopicIndex`, then the code continues to work exactly like it did before: a new `Topic` is created and added to the index. If the `Topic` already exists, however, it is used instead. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 33 ++++++++++++++------- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 +-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index dfc78852..c1809791 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -11,7 +11,9 @@ using System.Net; using Microsoft.Data.SqlClient; using OnTopic.Attributes; +using OnTopic.Collections; using OnTopic.Internal.Diagnostics; +using OnTopic.Querying; namespace OnTopic.Data.Sql { @@ -40,17 +42,25 @@ internal static class SqlDataReaderExtensions { /// topics and populate their attributes, relationships, and children. /// /// The with output from the GetTopics stored procedure. + /// + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references + /// and relationships, including , are integrated with existing entities. + /// /// /// Optionally disables populating external references such as and . This is useful for cases where it's known that a shallow copy is being retrieved, and /// thus external references aren't likely to be available. /// - internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExternalReferences = true) { + internal static Topic LoadTopicGraph( + this SqlDataReader reader, + Topic? referenceTopic = null, + bool includeExternalReferences = true + ) { /*---------------------------------------------------------------------------------------------------------------------- | Establish topic index \---------------------------------------------------------------------------------------------------------------------*/ - var topics = new Dictionary(); + var topics = referenceTopic is not null? referenceTopic.GetRootTopic().GetTopicIndex() : new(); /*---------------------------------------------------------------------------------------------------------------------- | Populate topics @@ -151,7 +161,7 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void AddTopic(this SqlDataReader reader, Dictionary topics) { + private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -164,9 +174,10 @@ private static void AddTopic(this SqlDataReader reader, Dictionary t /*------------------------------------------------------------------------------------------------------------------------ | Establish topic \-----------------------------------------------------------------------------------------------------------------------*/ - var current = TopicFactory.Create(key, contentType, topicId); - - topics.Add(current.Id, current); + if (!topics.TryGetValue(topicId, out var current)) { + current = TopicFactory.Create(key, contentType, topicId); + topics.Add(current.Id, current); + } /*------------------------------------------------------------------------------------------------------------------------ | Assign parent @@ -187,7 +198,7 @@ private static void AddTopic(this SqlDataReader reader, Dictionary t /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetIndexedAttributes(this SqlDataReader reader, Dictionary topics) { + private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -233,7 +244,7 @@ private static void SetIndexedAttributes(this SqlDataReader reader, Dictionary /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetExtendedAttributes(this SqlDataReader reader, Dictionary topics) { + private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -305,7 +316,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, Dictionary< /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetRelationships(this SqlDataReader reader, Dictionary topics) { + private static void SetRelationships(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -347,7 +358,7 @@ private static void SetRelationships(this SqlDataReader reader, Dictionary /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetReferences(this SqlDataReader reader, Dictionary topics) { + private static void SetReferences(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -390,7 +401,7 @@ private static void SetReferences(this SqlDataReader reader, Dictionary /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetVersionHistory(this SqlDataReader reader, Dictionary topics) { + private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index e95d5e0c..f476b153 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -148,7 +148,7 @@ public override Topic Load(int topicId, Topic? referenceTopic = null, bool isRec try { connection.Open(); using var reader = command.ExecuteReader(); - topic = reader.LoadTopicGraph(); + topic = reader.LoadTopicGraph(referenceTopic); } /*------------------------------------------------------------------------------------------------------------------------ @@ -224,7 +224,7 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic try { connection.Open(); using var reader = command.ExecuteReader(); - topic = reader.LoadTopicGraph(false); + topic = reader.LoadTopicGraph(referenceTopic, includeExternalReferences: referenceTopic is not null); } /*------------------------------------------------------------------------------------------------------------------------ From 96932b6417b0147f45aa86e242d24b579c5da750 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:20:00 -0800 Subject: [PATCH 266/778] Removed `SetDerivedTopics()` helper method The legacy `SetDerivedTopics()` helper method looped through the topic graph and populated any `DerivedTopic` references against the indexed list of topics. This was necessary prior to `Topic.References`. Now that we have topic references, however, this is now implicitly handled by the more general `SetReferences()` helper method, and is thus redundant. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 36 --------------------- 1 file changed, 36 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index c1809791..0d559c3d 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -136,15 +136,6 @@ internal static Topic LoadTopicGraph( reader.SetVersionHistory(topics); } - /*---------------------------------------------------------------------------------------------------------------------- - | Populate strongly typed references - \---------------------------------------------------------------------------------------------------------------------*/ - Debug.WriteLine("SqlTopicRepository.Load(): SetDerivedTopics() [" + DateTime.Now + "]"); - - if (includeExternalReferences) { - SetDerivedTopics(topics); - } - /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ @@ -418,33 +409,6 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi | Set history \-----------------------------------------------------------------------------------------------------------------------*/ current.VersionHistory.Add(dateTime); - - } - - /*========================================================================================================================== - | METHOD: SET DERIVED TOPICS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Sets references to . - /// - /// - /// Topics can be cross-referenced with each other via . Once the topics are - /// populated in memory, loop through the data to create these associations. By handling this in the repository, we avoid - /// needing to rely on lazy-loading, which would complicate dependency injection. - /// - /// A of topics to be loaded. - private static void SetDerivedTopics(Dictionary topics) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Loop through topics - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var topic in topics.Values) { - var derivedTopicId = topic.Attributes.GetInteger("TopicId", -1, false, false); - if (derivedTopicId < 0) continue; - if (topics.Keys.Contains(derivedTopicId)) { - topic.DerivedTopic = topics[derivedTopicId]; - } - } } From b4fec6b076fe5673246f3c945d048a5e1f194963 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:21:14 -0800 Subject: [PATCH 267/778] Marked helper functions as `private` A couple of the methods in the `SqlDataReaderExtensions` class are not actually `SqlDataReader` extensions, but are instead simply intended as private helper functions. Those were errantly marked as `internal`. This corrects that by marking them as `private`. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 0d559c3d..004cd1f2 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -421,7 +421,7 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi /// /// The object. /// The name of the column to retrieve the value from. - internal static int GetInteger(this SqlDataReader reader, string columnName) => + private static int GetInteger(this SqlDataReader reader, string columnName) => Int32.TryParse(reader.GetValue(reader.GetOrdinal(columnName)).ToString(), out var output)? output : -1; /*========================================================================================================================== @@ -432,7 +432,7 @@ internal static int GetInteger(this SqlDataReader reader, string columnName) => /// /// The object. /// The name of the column to retrieve the value from. - internal static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") => + private static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") => reader.GetInt32(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -443,7 +443,7 @@ internal static int GetTopicId(this SqlDataReader reader, string columnName = "T /// /// The object. /// The name of the column to retrieve the value from. - internal static string GetString(this SqlDataReader reader, string columnName) => + private static string GetString(this SqlDataReader reader, string columnName) => reader.GetString(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -453,7 +453,7 @@ internal static string GetString(this SqlDataReader reader, string columnName) = /// Retrieves the version column, with precisions appropriate for setting the . /// /// The object. - internal static DateTime GetVersion(this SqlDataReader reader) => + private static DateTime GetVersion(this SqlDataReader reader) => reader.GetDateTime(reader.GetOrdinal("Version")); } //Class From 76ab1f62eb1208c5d77d7f6081a47a8ed43b015f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:33:31 -0800 Subject: [PATCH 268/778] Added update logic to the `SqlDataReader` extensions Currently, `TopicRepositoryBase.Rollback()` loads a new topic via `ITopicRepository.Load()`, then compares the attributes to the existing attributes manually, and updates them accordingly. This is a bit clumsy, and won't will cause problems if we choose to version relationships and references in the future (since reciprocol relationships would point to the temporary topic). To mitigate that, the logic of the `SqlDataReader` extensions now supports update logic if a `topicReference` is passed in. This allows e.g. the new `Load(topicReference, version)` overload to pass in the current version, and have that instance be updated directly, instead of creating a temporary object and manually merging the values. This will also lay the groundwork for a proposed `ITopicRepository.Refresh()` method in the future for handling cache updates. (Though that will be a separate project independent of this branch.) In most cases, this was already supported implicitly since calls were made to e.g. `Topic.Attributes.SetValue()`, `Topic.References.SetTopic()`, or `Topic.Relationships.SetTopic()`. Those calls don't need to be updated just yet. Instead, we just need to make sure that a) the core attributes (such as `Key`, `ContentType`, and `ParentId`) are addressed, and check for duplicates for `VersionHistory` (which will be an issue when reloading an existing version). While I was at it, I removed a legacy line of code which set the `ParentID` attribute to `IsDirty = false`. As the `ParentID` is no longer stored as an attribute, this did nothing outside of create an arbitrary attribute reference. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 004cd1f2..b6321863 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -169,12 +169,15 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { current = TopicFactory.Create(key, contentType, topicId); topics.Add(current.Id, current); } + else { + current.Key = key; + current.ContentType = contentType; + } /*------------------------------------------------------------------------------------------------------------------------ | Assign parent \-----------------------------------------------------------------------------------------------------------------------*/ - if (parentId >= 0 && topics.Keys.Contains(parentId)) { - current.Attributes.SetValue("ParentID", parentId.ToString(CultureInfo.InvariantCulture), false); + if (parentId >= 0 && current.Parent?.Id != parentId && topics.Keys.Contains(parentId)) { current.Parent = topics[parentId]; } @@ -408,7 +411,8 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi /*------------------------------------------------------------------------------------------------------------------------ | Set history \-----------------------------------------------------------------------------------------------------------------------*/ - current.VersionHistory.Add(dateTime); + if (!current.VersionHistory.Contains(dateTime)) { + current.VersionHistory.Add(dateTime); } } From 6169065ac892e1920414be49202d0b2934dffdf0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 16:56:39 -0800 Subject: [PATCH 269/778] Introduced `markDirty` parameter to `SqlDataReader` extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Originally, the `SqlDataReader` extensions were exclusively intended to support loading the latest version of existing topics, in which case the topics (and their attributes, relationships, and topic references) should all be set to `markDirty=false`, since they are consistent with the values in the database. By introducing support for `Load(referenceTopic, version)`, we break that assumption by allowing older versions of the topic to be loaded from the database. As such, the `SqlDataReader` needs to be updated to conditionally allow collections or values to be marked `IsDirty`. This is acheived by adding in the nullable `markDirty` parameter. If ommitted, the default `IsDirty` handling will be applied (i.e., new, removed, or changed values will flag `IsDirty`). If it is set to `false`, then an update will not alter the `IsDirty` status of the `Topic` (i.e., it will return to whatever state it was at prior to the update—which could still be `IsDirty`). --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 76 ++++++++++++++++----- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index b6321863..6a84b290 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -6,11 +6,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Net; using Microsoft.Data.SqlClient; -using OnTopic.Attributes; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; @@ -46,6 +44,12 @@ internal static class SqlDataReaderExtensions { /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references /// and relationships, including , are integrated with existing entities. /// + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// /// /// Optionally disables populating external references such as and . This is useful for cases where it's known that a shallow copy is being retrieved, and @@ -54,6 +58,7 @@ internal static class SqlDataReaderExtensions { internal static Topic LoadTopicGraph( this SqlDataReader reader, Topic? referenceTopic = null, + bool? markDirty = null, bool includeExternalReferences = true ) { @@ -67,7 +72,7 @@ internal static Topic LoadTopicGraph( \---------------------------------------------------------------------------------------------------------------------*/ Debug.WriteLine("SqlTopicRepository.Load(): AddTopic() [" + DateTime.Now + "]"); while (reader.Read()) { - reader.AddTopic(topics); + reader.AddTopic(topics, markDirty); } /*---------------------------------------------------------------------------------------------------------------------- @@ -79,7 +84,7 @@ internal static Topic LoadTopicGraph( reader.NextResult(); while (reader.Read()) { - reader.SetIndexedAttributes(topics); + reader.SetIndexedAttributes(topics, markDirty); } /*---------------------------------------------------------------------------------------------------------------------- @@ -92,7 +97,7 @@ internal static Topic LoadTopicGraph( // Loop through each extended attribute record associated with a specific topic while (reader.Read()) { - reader.SetExtendedAttributes(topics); + reader.SetExtendedAttributes(topics, markDirty); } /*---------------------------------------------------------------------------------------------------------------------- @@ -106,7 +111,7 @@ internal static Topic LoadTopicGraph( // Loop through each relationship; multiple records may exist per topic if (includeExternalReferences) { while (reader.Read()) { - reader.SetRelationships(topics); + reader.SetRelationships(topics, markDirty); } } @@ -120,7 +125,7 @@ internal static Topic LoadTopicGraph( // Loop through each version; multiple records may exist per topic while (reader.Read()) { - reader.SetReferences(topics); + reader.SetReferences(topics, markDirty); } /*---------------------------------------------------------------------------------------------------------------------- @@ -152,7 +157,13 @@ internal static Topic LoadTopicGraph( /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// + private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -161,6 +172,7 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { var key = reader.GetString("TopicKey"); var contentType = reader.GetString("ContentType"); var parentId = reader.GetInteger("ParentID"); + var wasDirty = false; /*------------------------------------------------------------------------------------------------------------------------ | Establish topic @@ -170,6 +182,7 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { topics.Add(current.Id, current); } else { + wasDirty = current.IsDirty(); current.Key = key; current.ContentType = contentType; } @@ -181,6 +194,13 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { current.Parent = topics[parentId]; } + /*------------------------------------------------------------------------------------------------------------------------ + | Mark clean + \-----------------------------------------------------------------------------------------------------------------------*/ + if (wasDirty is false && markDirty is not null and false) { + current.MarkClean(); + } + } /*========================================================================================================================== @@ -192,7 +212,13 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics) { /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex topics) { + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// + private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -221,7 +247,7 @@ private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex t /*------------------------------------------------------------------------------------------------------------------------ | Set attribute value \-----------------------------------------------------------------------------------------------------------------------*/ - current.Attributes.SetValue(attributeKey, attributeValue, false, version, false); + current.Attributes.SetValue(attributeKey, attributeValue, markDirty, version, false); } @@ -238,7 +264,13 @@ private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex t /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex topics) { + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// + private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -292,7 +324,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex | Set attribute value \---------------------------------------------------------------------------------------------------------------------*/ if (String.IsNullOrEmpty(attributeValue)) continue; - current.Attributes.SetValue(attributeKey, attributeValue, false, version, true); + current.Attributes.SetValue(attributeKey, attributeValue, markDirty, version, true); } while (xmlReader.Name is "attribute"); @@ -310,7 +342,13 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetRelationships(this SqlDataReader reader, TopicIndex topics) { + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// + private static void SetRelationships(this SqlDataReader reader, TopicIndex topics, bool? isDirty = false) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -336,7 +374,7 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ - current.Relationships.SetTopic(relationshipKey, related, isDirty: false); + current.Relationships.SetTopic(relationshipKey, related, isDirty); } @@ -352,7 +390,13 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic /// /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetReferences(this SqlDataReader reader, TopicIndex topics) { + /// + /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it + /// will be marked dirty if the value is new or has changed from a previous value. By setting this parameter, that + /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update + /// from being persisted to the data store on . + /// + private static void SetReferences(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -378,7 +422,7 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics) /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ - current.References.SetTopic(relationshipKey, referenced, isDirty: false); + current.References.SetTopic(relationshipKey, referenced, markDirty); } diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index f476b153..d413ab11 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -148,7 +148,7 @@ public override Topic Load(int topicId, Topic? referenceTopic = null, bool isRec try { connection.Open(); using var reader = command.ExecuteReader(); - topic = reader.LoadTopicGraph(referenceTopic); + topic = reader.LoadTopicGraph(referenceTopic, false); } /*------------------------------------------------------------------------------------------------------------------------ From df54231ec7985d64dd1673fb885f1cb735944971 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 17:01:02 -0800 Subject: [PATCH 270/778] Updated `Rollback()` to use new `Load(referenceTopic, version)` logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With `Load(referenceTopic, version)` being introduced to support updating an existing topic, there's no longer a need to have `Rollback()` load a temporary `Topic` and then manually compare those values to the existing version from the topic graph; this can all be handled directly by the derived `ITopicRepository` implementation. This not only simplifies the `Rollback()` logic, but also opens the doors for versioning relationships and topic references in the future. (Previously, relationships and topic references weren't loaded because `Load(topic, version)` didn't have access to the topic graph to establish those references. But even if it did, the previous approach would have introduced problems of its own since any newly established reciprocal relationships—i.e., `IncomingRelationships`—would have pointed to the temporary topic, instead of the original version in the topic graph.) --- OnTopic/Repositories/TopicRepositoryBase.cs | 24 +-------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 6fa47a55..a8886def 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -254,29 +254,7 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { /*------------------------------------------------------------------------------------------------------------------------ | Retrieve topic from database \-----------------------------------------------------------------------------------------------------------------------*/ - var originalVersion = Load(topic.Id, version); - - Contract.Assume( - originalVersion, - "The version requested for rollback does not exist in the Topic repository or database." - ); - - /*------------------------------------------------------------------------------------------------------------------------ - | Mark each attribute as dirty - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var attribute in originalVersion.Attributes) { - if (!topic.Attributes.Contains(attribute.Key) || topic.Attributes.GetValue(attribute.Key) != attribute.Value) { - originalVersion.Attributes.SetValue(attribute.Key, attribute.Value, true); - } - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Construct new AttributeCollection - \-----------------------------------------------------------------------------------------------------------------------*/ - topic.Attributes.Clear(); - foreach (var attribute in originalVersion.Attributes) { - topic.Attributes.Add(attribute); - } + Load(topic, version); /*------------------------------------------------------------------------------------------------------------------------ | Save as new version From 5c1a1fd06d93cfdd53c40642453a46f29726c218 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 17:27:57 -0800 Subject: [PATCH 271/778] Removed legacy version check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Version` column was added to the `GetTopics` stored procedure's `Attributes` query after OnTopic 4.0.0 was released. To maintain backward compatibility, a check was put in place to check the field count of the results, and only set the version if it was available. With the upcoming release of 5.0.0, we can accept this as a breaking change—and, indeed, other aspects of the `SqlTopicRepository` won't work with the legacy 4.0.0 schema anyway, since it didn't support e.g. `TopicReferences`. As such, there's no further necessity or benefit for this. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 6a84b290..08d1b116 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -226,13 +226,7 @@ private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex t var topicId = reader.GetTopicId(); var attributeKey = reader.GetString("AttributeKey"); var attributeValue = reader.GetString("AttributeValue"); - var version = DateTime.Now; - - //Check field count to avoid breaking changes with the 4.0.0 release, which didn't include a "Version" column - //### TODO JJC20200221: This condition can be removed and accepted as a breaking change in v5.0. - if (reader.FieldCount > 3) { - version = reader.GetVersion(); - } + var version = reader.GetVersion(); /*------------------------------------------------------------------------------------------------------------------------ | Handle empty attributes (treat empty as null) @@ -276,13 +270,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex | Identify attributes \-----------------------------------------------------------------------------------------------------------------------*/ var topicId = reader.GetTopicId(); - var version = DateTime.Now; - - //Check field count to avoid breaking changes with the 4.0.0 release, which didn't include a "Version" column - //### TODO JJC20200221: This condition can be removed and accepted as a breaking change in v5.0. - if (reader.FieldCount > 2) { - version = reader.GetVersion(); - } + var version = reader.GetVersion(); /*------------------------------------------------------------------------------------------------------------------------ | Load SQL XML into XmlDocument From 622d5752d2e3cfd2f77567b8c4b946aff62788a6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 17:30:20 -0800 Subject: [PATCH 272/778] Ensured `Version` column is returned from `GetTopicVersion` Previously, `Version` was added to the `Attributes` and `ExtendedAttributes` queries of the `GetTopics` stored procedure, but was inadvertantly overlooked in `GetTopicVersion`. In practice, this didn't cause any issues. But it prevents us from safely removing the schema check for calls to `Load()` (5c1a1fd). This remedies that issue. --- .../Stored Procedures/GetTopicVersion.sql | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index 9ccb6a15..2cb878cb 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -58,6 +58,7 @@ AS ( SELECT TopicID, AttributeKey, AttributeValue, + Version, RowNumber = ROW_NUMBER() OVER ( PARTITION BY TopicID, AttributeKey @@ -69,7 +70,8 @@ AS ( ) SELECT TopicID, AttributeKey, - AttributeValue + AttributeValue, + Version FROM TopicAttributes WHERE RowNumber = 1 @@ -80,6 +82,7 @@ WHERE RowNumber = 1 AS ( SELECT TopicID, AttributesXml, + Version, RowNumber = ROW_NUMBER() OVER ( PARTITION BY TopicID ORDER BY Version DESC @@ -89,7 +92,8 @@ AS ( AND Version <= @Version ) SELECT TopicID, - AttributesXml + AttributesXml, + Version FROM TopicExtendedAttributes WHERE RowNumber = 1 From f9802c1eaadd44c32b9ba91c87a821165ca555b3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 18:27:52 -0800 Subject: [PATCH 273/778] Fixed bug in `GetTopicVersion` so it correctly returns core attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the core attributes were moved from `Attributes` to `Topics`, the `GetTopics` stored procedure was correctly updated to look for the core attributes in `Topics`, instead of relying on the complex logic for extracting the latest version of them from `Attributes`. During this, however, `GetTopicVersion` was missed, and thus it continued to rely on the legacy logic. Since the core attributes are no longer stored in the `Attributes` table, that means the core attributes were not returned—and, thus, `GetTopicVersion` returned invalid results. This update fixes that issue by implementing the (much simpler!) query against the `Topics` table. --- .../Stored Procedures/GetTopicVersion.sql | 38 +++---------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index 2cb878cb..45c905d4 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -16,39 +16,13 @@ AS -------------------------------------------------------------------------------------------------------------------------------- -- SELECT KEY ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- -;WITH KeyAttributes -AS ( - SELECT TopicID, - AttributeKey, - AttributeValue, - RowNumber = ROW_NUMBER() OVER ( - PARTITION BY TopicID, - AttributeKey - ORDER BY Version DESC - ) - FROM Attributes - WHERE TopicID = @TopicID - AND Version <= @Version - AND AttributeKey - IN ( 'Key', - 'ParentID', - 'ContentType' - ) -) SELECT TopicID, - ContentType, - ParentID, - [Key] AS 'TopicKey', - 1 AS 'SortOrder' -FROM KeyAttributes -PIVOT ( MIN(AttributeValue) - FOR AttributeKey - IN ( [ContentType], - [ParentID], - [Key] - ) -) AS Pvt -WHERE RowNumber = 1 + ContentType, + ParentID, + TopicKey, + 0 AS SortOrder +FROM Topics +WHERE TopicID = @TopicID -------------------------------------------------------------------------------------------------------------------------------- -- SELECT TOPIC ATTRIBUTES From 34d795eae06fac483afdde536bc9b26d2afd3af0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 18:34:57 -0800 Subject: [PATCH 274/778] Fixed bug in which topic is returned from `LoadTopicGraph()`` When the `referenceTopic` was incorporated into `LoadTopicGraph()` (72f0560) a bug was inadvertantly introduced. `LoadTopicGraph()` is expected to return the topic that's being loaded, which will be the first `topicId` returned in the first data set, which represents the core topic attributes. Previously, this would _also_ be the first topic in the `topics` cache. With `referenceTopic`, however, the first topic in the `topics` cache will always be the root topic, regardless of which topic is being loaded. For many scenarios, this is fine, as we usually load the entire topic cache at once. When loading an individual topic, however, such as the `Root:Configuration`, or a specific version via `Load(referenceTopic, version)`, this introduces a problem. This becomes a bug when the caller then operates against the result of `LoadTopicGraph()` or `Load()` expecting it to be the specific topic requested. To remedy this, a check is injected into the loop over the first data set in order to grab the first `TopicID` and store it as a local `rootTopicId`, and then return the `Topic` associated with that at the end of `LoadTopicGraph()`. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 08d1b116..6e9eea69 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -66,12 +66,16 @@ internal static Topic LoadTopicGraph( | Establish topic index \---------------------------------------------------------------------------------------------------------------------*/ var topics = referenceTopic is not null? referenceTopic.GetRootTopic().GetTopicIndex() : new(); + var rootTopicId = -1; /*---------------------------------------------------------------------------------------------------------------------- | Populate topics \---------------------------------------------------------------------------------------------------------------------*/ Debug.WriteLine("SqlTopicRepository.Load(): AddTopic() [" + DateTime.Now + "]"); while (reader.Read()) { + if (rootTopicId < 0) { + rootTopicId = reader.GetTopicId(); + } reader.AddTopic(topics, markDirty); } @@ -144,6 +148,9 @@ internal static Topic LoadTopicGraph( /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ + if (topics.ContainsKey(rootTopicId)) { + return topics[rootTopicId]; + } return topics.Values.FirstOrDefault(); } From 1c2b25b1fa412319c5963347e81ff924788a507d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 13 Jan 2021 18:40:18 -0800 Subject: [PATCH 275/778] Delete orphaned attributes caused by `Load(referenceTopic, version)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When loading a specific version of a topic with an existing `referenceTopic`, all attribute values on the `referenceTopic` are overwritten with the values from the version. If, however, new attributes were introduced after that version, they won't be overwritten. That's because the data for the original version won't have a corresponding attribute key or value that's being set. In these cases, we just need to determine if there are any newer attributes—i.e., attributes with a `LastModified` date that's newer than the `version`—and, if there are, remove them. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index d413ab11..7df37332 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -234,6 +234,19 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic throw new TopicRepositoryException($"Topics failed to load: '{exception.Message}'", exception); } + /*------------------------------------------------------------------------------------------------------------------------ + | Delete orphaned attributes + >------------------------------------------------------------------------------------------------------------------------- + | If a referenceTopic is passed, and it contains the `topicId`, then that instance will be updated with the previous + | version. In that case, however, any attributes which were first introduced after that version won't be overwritten. + | That's because there isn't a previous value associated with that key to overwrite the current value. In those cases, + | those attributes must be manually removed. + \-----------------------------------------------------------------------------------------------------------------------*/ + var orphanedAttributes = topic.Attributes.Where(a => a.LastModified > version).ToList(); + foreach (var attribute in orphanedAttributes) { + topic.Attributes.Remove(attribute.Key); + } + /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ From 0f2d60dcc816036fcb020440bb1e9f9a7d02acd1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 14:51:39 -0800 Subject: [PATCH 276/778] Introduced versioning support to the `TopicReferences` table This requires adding a new `Version` column with a default of `GetUTCDate()`, and adding that column to the primary key definition. --- OnTopic.Data.Sql.Database/Tables/TopicReferences.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql index 963a1360..e3603041 100644 --- a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql +++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql @@ -8,9 +8,11 @@ TABLE [dbo].[TopicReferences] ( [Source_TopicID] INT NOT NULL, [ReferenceKey] VARCHAR(128) NOT NULL, [Target_TopicID] INT NOT NULL, + [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_TopicReferences] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, - [ReferenceKey] ASC + [ReferenceKey] ASC, + [Version] DESC ), CONSTRAINT [FK_TopicReferences_Source] FOREIGN KEY ( [Source_TopicID] From 0997a08ea91bb20cd83ecb361afe0c5a2204f58a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 14:56:26 -0800 Subject: [PATCH 277/778] Introduced versioning support to the `Relationships` table In addition to the `Version` column being added to both the table definition as well as the primary key constraint, the `Relationships` table also necessitates the introduction of an `IsDeleted` column. This is because, unlike `Attributes` and `TopicReferences`, there can be more than one value (`Target_TopicID`, in this case) per key. As such, we can't just overwrite the value of a key by marking it as `null`. Instead, we must introduce an additional column to track the state of the record. --- OnTopic.Data.Sql.Database/Tables/Relationships.sql | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Relationships.sql b/OnTopic.Data.Sql.Database/Tables/Relationships.sql index 69b4ae3d..c20da308 100644 --- a/OnTopic.Data.Sql.Database/Tables/Relationships.sql +++ b/OnTopic.Data.Sql.Database/Tables/Relationships.sql @@ -8,10 +8,13 @@ TABLE [dbo].[Relationships] ( [Target_TopicID] INT NOT NULL, [Source_TopicID] INT NOT NULL, [RelationshipKey] VARCHAR(255) NOT NULL, + [IsDeleted] BIT NOT NULL DEFAULT 0, + [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Relationships] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, [RelationshipKey] ASC, - [Target_TopicID] ASC + [Target_TopicID] ASC, + [Version] DESC ), CONSTRAINT [FK_Relationships_Source] FOREIGN KEY ( [Source_TopicID] From 5da983bee2d98471767eb745af119a56f6a3d027 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 14:59:38 -0800 Subject: [PATCH 278/778] Updated tables to prefer inline default constraints While I was at it, I updated the default values to use `GetUTCDate()` where appropriate, as opposed to `GetDate()`. --- OnTopic.Data.Sql.Database/Tables/Attributes.sql | 3 +-- .../Tables/ExtendedAttributes.sql | 10 ++-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql index 674940e5..d9228666 100644 --- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql @@ -11,8 +11,7 @@ TABLE [dbo].[Attributes] ( [TopicID] INT NOT NULL, [AttributeKey] VARCHAR (128) NOT NULL, [AttributeValue] NVARCHAR (255) NOT NULL, - [Version] DATETIME - CONSTRAINT [DF_Attributes_Version] DEFAULT (GETUTCDATE()) NOT NULL, + [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Attributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [AttributeKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql index d68a9844..025e9668 100644 --- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql @@ -9,14 +9,8 @@ CREATE TABLE [dbo].[ExtendedAttributes] ( [TopicID] INT NOT NULL, [AttributesXml] XML NOT NULL, - [DateModified] DATETIME - CONSTRAINT [DF_ExtendedAttributes_DateModified] DEFAULT ( - GetDate() - ) NOT NULL, - [Version] DATETIME - CONSTRAINT [DF_ExtendedAttributes_Version] DEFAULT ( - GetDate() - ) NOT NULL, + [DateModified] DATETIME NOT NULL DEFAULT GETUTCDATE(), + [Version] DATETIME NOT NULL DEFAULT GETUTCDATE(), CONSTRAINT [PK_ExtendedAttributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [Version] DESC From 242593c48b891de26e7a54b25a83c4e56032ce10 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:01:17 -0800 Subject: [PATCH 279/778] Removed unused and redundant `ExtendedAttributes.DateModified` column This is effectively replaced by `Version`, and is no longer utilized. --- OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql | 1 - .../Stored Procedures/DeleteConsecutiveExtendedAttributes.sql | 2 -- 2 files changed, 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql index 025e9668..5ee52570 100644 --- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql @@ -9,7 +9,6 @@ CREATE TABLE [dbo].[ExtendedAttributes] ( [TopicID] INT NOT NULL, [AttributesXml] XML NOT NULL, - [DateModified] DATETIME NOT NULL DEFAULT GETUTCDATE(), [Version] DATETIME NOT NULL DEFAULT GETUTCDATE(), CONSTRAINT [PK_ExtendedAttributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql index 35c7ed40..d553f8a6 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveExtendedAttributes.sql @@ -33,7 +33,6 @@ Print('Initial Count: ' + CAST(@Count AS VARCHAR) + ' Extended Attributes in the WITH GroupedValues AS ( SELECT TopicID, AttributesXml, - DateModified, Version, ValueGroup = ROW_NUMBER() OVER(PARTITION BY TopicID ORDER BY TopicID, Version) - ROW_NUMBER() OVER(PARTITION BY TopicID, CAST(AttributesXml AS NVARCHAR(MAX)) ORDER BY TopicID, Version) @@ -46,7 +45,6 @@ WITH GroupedValues AS ( RankedValues AS ( SELECT TopicID, AttributesXml, - DateModified, Version, ValueGroup, ValueRank = ROW_NUMBER() OVER(PARTITION BY ValueGroup, TopicID, CAST(AttributesXml AS NVARCHAR(MAX)) ORDER BY TopicID, Version) From a028243fc32cdc7eee3ae0b7d922f82f045d1d58 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:06:27 -0800 Subject: [PATCH 280/778] Introduced versioning support to the `UpdateReferences` stored procedure The `UpdateReferences` stored procedure now accounts for versions. Instead of comparing new values against existing values, it compares new values against the latest version of the existing values. Further, instead of e.g. deleting unmatched records, it instead created new records with a `NULL` `Target_TopicID`, thus overriding the previous version. The procedure also now accepts a `@Version` parameter, allowing the `Version` value to be set by the caller; if it isn't, it will default to `GETUTCDATE()` . --- .../Stored Procedures/UpdateReferences.sql | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql index bf0c01a1..35c392bd 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -6,10 +6,17 @@ CREATE PROCEDURE [dbo].[UpdateReferences] @TopicID INT, - @ReferencedTopics TopicReferences READONLY, - @DeleteUnmatched BIT = 0 + @ReferencedTopics TopicReferences READONLY , + @Version DATETIME = NULL , + @DeleteUnmatched BIT = 0 AS +-------------------------------------------------------------------------------------------------------------------------------- +-- SET DEFAULT VERSION DATETIME +-------------------------------------------------------------------------------------------------------------------------------- +IF @Version IS NULL +SET @Version = GETUTCDATE() + -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NOVEL VALUES -------------------------------------------------------------------------------------------------------------------------------- @@ -17,37 +24,36 @@ INSERT INTO TopicReferences ( Source_TopicID, ReferenceKey, - Target_TopicID + Target_TopicID, + Version ) SELECT @TopicID, - Target.ReferenceKey, - Target.TopicID -FROM @ReferencedTopics Target -LEFT JOIN TopicReferences Existing - ON Source_TopicID = @TopicID - AND Existing.ReferenceKey = Target.ReferenceKey -WHERE ISNULL(Source_TopicID, '') = '' - AND Target.TopicID > 0 - --------------------------------------------------------------------------------------------------------------------------------- --- UPDATE EXISTING VALUES --------------------------------------------------------------------------------------------------------------------------------- -UPDATE Existing -SET Target_TopicID = TopicID -FROM @ReferencedTopics Target -LEFT JOIN TopicReferences Existing - ON Source_TopicID = @TopicID - AND Existing.ReferenceKey = Target.ReferenceKey -WHERE Source_TopicID IS NOT NULL - AND Target.TopicID != Target_TopicID - AND Target.TopicID > 0 + New.ReferenceKey, + New.TopicID, + @Version +FROM @ReferencedTopics New +OUTER APPLY ( + SELECT TOP 1 + Target_TopicID AS ExistingValue + FROM TopicReferences + WHERE Source_TopicID = @TopicID + AND ReferenceKey = New.ReferenceKey + ORDER BY Version DESC +) Existing +WHERE ISNULL(ExistingValue, '') != New.TopicID + AND New.TopicID > 0 -------------------------------------------------------------------------------------------------------------------------------- -- DELETE UNMATCHED VALUES -------------------------------------------------------------------------------------------------------------------------------- IF @DeleteUnmatched = 1 BEGIN - DELETE Existing + INSERT + INTO TopicReferences + SELECT @TopicID, + Existing.ReferenceKey, + Existing.Target_TopicID, + @Version FROM @ReferencedTopics New RIGHT JOIN TopicReferences Existing ON Source_TopicID = @TopicID From 183ac279f4371b21b6ad87ffb5e2cfdcfe5fad0a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:07:28 -0800 Subject: [PATCH 281/778] Introduced versioning support to the `UpdateRelationships` stored procedure The `UpdateRelationships` stored procedure now accounts for versions. Instead of comparing new values against existing values, it compares new values against the latest version of the existing values. Further, instead of e.g. deleting unmatched records, it instead created new records with an `IsDeleted` bit set, thus overriding the previous version. The procedure also now accepts a `@Version` parameter, allowing the `Version` value to be set by the caller; if it isn't, it will default to `GETUTCDATE()` . --- .../Stored Procedures/UpdateRelationships.sql | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index 09257028..6d1b9090 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -7,10 +7,17 @@ CREATE PROCEDURE [dbo].[UpdateRelationships] @TopicID INT, @RelationshipKey VARCHAR(255), - @RelatedTopics TopicList READONLY, - @DeleteUnmatched BIT = 0 + @RelatedTopics TopicList READONLY , + @Version DATETIME = NULL , + @DeleteUnmatched BIT = 0 AS +-------------------------------------------------------------------------------------------------------------------------------- +-- SET DEFAULT VERSION DATETIME +-------------------------------------------------------------------------------------------------------------------------------- +IF @Version IS NULL +SET @Version = GETUTCDATE() + -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NOVEL VALUES -------------------------------------------------------------------------------------------------------------------------------- @@ -18,30 +25,52 @@ INSERT INTO Relationships ( Source_TopicID, RelationshipKey, - Target_TopicID + Target_TopicID, + IsDeleted, + Version ) SELECT @TopicID, @RelationshipKey, - TopicID -FROM @RelatedTopics Target -LEFT JOIN Relationships Existing - ON Target_TopicID = TopicID - AND Source_TopicID = @TopicID - AND RelationshipKey = @RelationshipKey -WHERE Target_TopicID IS NULL + TopicID, + 0, + @Version +FROM @RelatedTopics New +OUTER APPLY ( + SELECT TOP 1 + IsDeleted AS ExistingValue + FROM Relationships + WHERE Source_TopicID = @TopicID + AND RelationshipKey = @RelationshipKey + AND Target_TopicID = New.TopicID + ORDER BY Version DESC +) Existing +WHERE ISNULL(ExistingValue, 1) = 1 -------------------------------------------------------------------------------------------------------------------------------- -- DELETE UNMATCHED VALUES -------------------------------------------------------------------------------------------------------------------------------- IF @DeleteUnmatched = 1 BEGIN - DELETE Existing + INSERT + INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID, + IsDeleted, + Version + ) + SELECT @TopicID, + Existing.RelationshipKey, + Existing.Target_TopicID, + 1, + @Version FROM @RelatedTopics Relationships RIGHT JOIN Relationships Existing ON Target_TopicID = TopicID WHERE Source_TopicID = @TopicID AND ISNULL(TopicID, '') = '' AND RelationshipKey = @RelationshipKey + AND IsDeleted = 0 END -------------------------------------------------------------------------------------------------------------------------------- From 22c0fa4d5bd0d4e0a76256b25c1f700711d524e6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:12:37 -0800 Subject: [PATCH 282/778] Introduced `Relationships`, `TopicReferences` versioning to `GetTopicVersion` Previously, the `GetTopicVersion` stored procedure had no version support for topic references or relationships, and simply returned the `TopicReferences` and `Relationships` data for the given `@TopicID`. Now, it returns the latest version for the `TopicReferences` up to and including the provided `@Version`. This is a more complex query, but allows topic references and relationships to be restored to a previous state. --- .../Stored Procedures/GetTopicVersion.sql | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index 45c905d4..0398c7e1 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -74,20 +74,52 @@ WHERE RowNumber = 1 -------------------------------------------------------------------------------------------------------------------------------- -- SELECT RELATIONSHIPS -------------------------------------------------------------------------------------------------------------------------------- -;SELECT Source_TopicID, +;WITH Relationships AS ( + SELECT Source_TopicID, RelationshipKey, - Target_TopicID + Target_TopicID, + IsDeleted, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + RelationshipKey + ORDER BY Version DESC + ) + FROM [dbo].[Relationships] + WHERE Source_TopicID = @TopicID + AND Version <= @Version +) +SELECT Relationships.Source_TopicID, + Relationships.RelationshipKey, + Relationships.Target_TopicID, + Relationships.IsDeleted, + Relationships.Version FROM Relationships -WHERE Source_TopicID = @TopicID +WHERE RowNumber = 1 -------------------------------------------------------------------------------------------------------------------------------- -- SELECT REFERENCES -------------------------------------------------------------------------------------------------------------------------------- -SELECT ReferenceKey, - Source_TopicID, - Target_TopicID -FROM TopicReferences TopicReferences -WHERE Source_TopicID = @TopicID +;WITH TopicReferences AS ( + SELECT Source_TopicID, + ReferenceKey, + Target_TopicID, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + ReferenceKey + ORDER BY Version DESC + ) + FROM [dbo].[TopicReferences] + WHERE Source_TopicID = @TopicID + AND Version <= @Version +) +SELECT TopicReferences.Source_TopicID, + TopicReferences.ReferenceKey, + TopicReferences.Target_TopicID, + TopicReferences.Version +FROM TopicReferences +WHERE RowNumber = 1 -------------------------------------------------------------------------------------------------------------------------------- -- SELECT HISTORY From 826e5aad43bd88b59a1d1d5b4f467b384de4670a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:14:24 -0800 Subject: [PATCH 283/778] Introduced `Relationships`, `TopicReferences` versioning to `GetTopics` Previously, the `GetTopics` stored procedure had no version support for topic references or relationships, and simply returned the `TopicReferences` and `Relationships` data for the given `@TopicID`. Now, it returns the latest version for the `TopicReferences`. This is a more complex query, but allows topic references and relationships to be versioned, similar to attributes. While I was at it, I applied more consistent syntax for table aliases (always using the `AS` keyword, whereas previously this was inconsistently applied). --- .../Stored Procedures/GetTopics.sql | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql index 43f93be2..0d6d9a28 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql @@ -104,7 +104,7 @@ SELECT Attributes.TopicID, AttributeKey, AttributeValue, Version -FROM AttributeIndex Attributes +FROM AttributeIndex AS Attributes JOIN #Topics AS Storage ON Storage.TopicID = Attributes.TopicID @@ -123,8 +123,9 @@ JOIN #Topics AS Storage -------------------------------------------------------------------------------------------------------------------------------- SELECT Source_TopicID, RelationshipKey, - Target_TopicID -FROM Relationships Relationships + Target_TopicID, + IsDeleted +FROM RelationshipIndex AS Relationships JOIN #Topics AS Storage ON Storage.TopicID = Relationships.Source_TopicID @@ -134,7 +135,7 @@ JOIN #Topics AS Storage SELECT Source_TopicID, ReferenceKey, Target_TopicID -FROM TopicReferences TopicReferences +FROM ReferenceIndex AS TopicReferences JOIN #Topics AS Storage ON Storage.TopicID = TopicReferences.Source_TopicID @@ -143,6 +144,6 @@ JOIN #Topics AS Storage -------------------------------------------------------------------------------------------------------------------------------- SELECT History.TopicID, Version -FROM VersionHistoryIndex History +FROM VersionHistoryIndex AS History JOIN #Topics AS Storage ON Storage.TopicID = History.TopicID; \ No newline at end of file From 433088f290cda09174b066877c813e058bd79060 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:15:54 -0800 Subject: [PATCH 284/778] Set `@Version` default to UTC in the `UpdateTopic` stored procedure In a previous update, we set the default value of `@Version` to UTC time in `SqlTopicRepository`. This was not consistently applied in the database, however. We don't usually rely on this default, but it should be consistent just in case it is used. --- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index b35f7e03..46e65049 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -18,7 +18,7 @@ AS -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = GetDate() +SET @Version = GETUTCDATE() -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE KEY ATTRIBUTES From dbf0ba835778806fac7d464e12b16e1a63beb0be Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:30:31 -0800 Subject: [PATCH 285/778] Consolidated `UpdateAttributes` logic Previously, there was a separate query for adding new or updated values, and deleting values. Those queries were largely redundant, since a versioned deletion is just the insertion of an empty value, and could be handled via a single `WHERE` clause. --- .../Stored Procedures/UpdateAttributes.sql | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql index 602c05d9..a40604ba 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql @@ -35,34 +35,7 @@ OUTER APPLY ( AND AttributeKey = New.AttributeKey ORDER BY Version DESC ) Existing -WHERE ISNULL(AttributeValue, '') != '' - AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') - --------------------------------------------------------------------------------------------------------------------------------- --- INSERT NULL ATTRIBUTES --------------------------------------------------------------------------------------------------------------------------------- -INSERT -INTO Attributes ( - TopicID , - AttributeKey , - AttributeValue , - Version - ) -SELECT @TopicID, - AttributeKey, - '', - @Version -FROM @Attributes New -CROSS APPLY ( - SELECT TOP 1 - AttributeValue AS ExistingValue - FROM Attributes - WHERE TopicID = @TopicID - AND AttributeKey = New.AttributeKey - ORDER BY Version DESC -) Existing -WHERE ISNULL(AttributeValue, '') = '' - AND ExistingValue != '' +WHERE ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '') -------------------------------------------------------------------------------------------------------------------------------- -- DELETE UNMATCHED ATTRIBUTES From cff8eae622ff77af17b20298b68d677e968b9013 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:32:08 -0800 Subject: [PATCH 286/778] Introduced new `ReferenceIndex` view As with other views, the `ReferenceIndex` view provides the latest version of `TopicReferences`, thus simplifying queries used by e.g. the `GetTopics` stored procedure. (Note: This should have been committed previously; I apologize for the out-of-order commit history.) --- .../Views/ReferenceIndex.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Views/ReferenceIndex.sql diff --git a/OnTopic.Data.Sql.Database/Views/ReferenceIndex.sql b/OnTopic.Data.Sql.Database/Views/ReferenceIndex.sql new file mode 100644 index 00000000..b8eecc10 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Views/ReferenceIndex.sql @@ -0,0 +1,29 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- REFERENCES (INDEX) +-------------------------------------------------------------------------------------------------------------------------------- +-- Filters the TopicReferences table by the latest version for each topic and reference key. For most use cases, this should be +-- the primary sources for retrieving topic references, since it excludes historical versions. +-------------------------------------------------------------------------------------------------------------------------------- +CREATE +VIEW [dbo].[ReferenceIndex] +WITH SCHEMABINDING +AS + +WITH TopicReferences AS ( + SELECT Source_TopicID, + ReferenceKey, + Target_TopicID, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + ReferenceKey + ORDER BY Version DESC + ) + FROM [dbo].[TopicReferences] +) +SELECT TopicReferences.Source_TopicID, + TopicReferences.ReferenceKey, + TopicReferences.Target_TopicID, + TopicReferences.Version +FROM TopicReferences +WHERE RowNumber = 1 \ No newline at end of file From 700e02705b002854bd127ad75c10b3ba4aa7a1d8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:32:39 -0800 Subject: [PATCH 287/778] Introduced new `RelationshipIndex` view As with other views, the `RelationshipIndex` view provides the latest version of `Relationships`, thus simplifying queries used by e.g. the `GetTopics` stored procedure. (Note: This should have been committed previously; I apologize for the out-of-order commit history.) --- .../Views/RelationshipIndex.sql | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Views/RelationshipIndex.sql diff --git a/OnTopic.Data.Sql.Database/Views/RelationshipIndex.sql b/OnTopic.Data.Sql.Database/Views/RelationshipIndex.sql new file mode 100644 index 00000000..4f983732 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Views/RelationshipIndex.sql @@ -0,0 +1,32 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- RELATIONSHIPS (INDEX) +-------------------------------------------------------------------------------------------------------------------------------- +-- Filters the Relationships table by the latest version for each topic and relationship key. For most use cases, this should be +-- the primary sources for retrieving topic relationships, since it excludes historical versions. +-------------------------------------------------------------------------------------------------------------------------------- +CREATE +VIEW [dbo].[RelationshipIndex] +WITH SCHEMABINDING +AS + +WITH Relationships AS ( + SELECT Source_TopicID, + RelationshipKey, + Target_TopicID, + IsDeleted, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + RelationshipKey, + Target_TopicID + ORDER BY Version DESC + ) + FROM [dbo].[Relationships] +) +SELECT Relationships.Source_TopicID, + Relationships.RelationshipKey, + Relationships.Target_TopicID, + Relationships.IsDeleted, + Relationships.Version +FROM Relationships +WHERE RowNumber = 1 \ No newline at end of file From 0c2399ed4f5a0ed346175450bd70320c7639d6d9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:38:04 -0800 Subject: [PATCH 288/778] Added `IsDeleted` support to `SetRelationships()` extension With the introduction of relationship versioning, the `SqlDataReader.SetRelationships()` extension method must account for the `IsDeleted` column; if it is set to true (`1`), then the relationship should be _deleted_ (if it exists), not added. This ensures support for updating relationships on existing topics, which will be needed for e.g. the proposed `ITopicRepository.Refresh()` method, which will handle cache refreshes. As part of this, I added a new `GetBoolean(columnName)` private helper method, to complement existing helpers such as `GetString(columnName)` and `GetInteger(columnName)`. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 6e9eea69..30ad1f69 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -351,6 +351,7 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic var sourceTopicId = reader.GetTopicId("Source_TopicID"); var targetTopicId = reader.GetTopicId("Target_TopicID"); var relationshipKey = reader.GetString("RelationshipKey"); + var isDeleted = reader.GetBoolean("IsDeleted"); /*------------------------------------------------------------------------------------------------------------------------ | Identify affected topics @@ -369,7 +370,12 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ - current.Relationships.SetTopic(relationshipKey, related, isDirty); + if (!isDeleted) { + current.Relationships.SetTopic(relationshipKey, related, isDirty); + } + else if (current.Relationships.Contains(relationshipKey, related)) { + current.Relationships.RemoveTopic(relationshipKey, related); + } } @@ -456,6 +462,17 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi } + /*========================================================================================================================== + | METHOD: GET BOOLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a boolean value by column name. + /// + /// The object. + /// The name of the column to retrieve the value from. + private static bool GetBoolean(this SqlDataReader reader, string columnName) => + reader.GetBoolean(reader.GetOrdinal(columnName)); + /*========================================================================================================================== | METHOD: GET INTEGER \-------------------------------------------------------------------------------------------------------------------------*/ From 4f761617e8b64036e2cfbffe1691869294abbfeb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:38:46 -0800 Subject: [PATCH 289/778] Moved `GetString()` extension method to top of `private` helpers Minor ordering preference, since this is the most commonly accessed of the helpers. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 30ad1f69..1fdddb12 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -462,6 +462,17 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi } + /*========================================================================================================================== + | METHOD: GET STRING + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a string value by column name. + /// + /// The object. + /// The name of the column to retrieve the value from. + private static string GetString(this SqlDataReader reader, string columnName) => + reader.GetString(reader.GetOrdinal(columnName)); + /*========================================================================================================================== | METHOD: GET BOOLEAN \-------------------------------------------------------------------------------------------------------------------------*/ @@ -495,17 +506,6 @@ private static int GetInteger(this SqlDataReader reader, string columnName) => private static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") => reader.GetInt32(reader.GetOrdinal(columnName)); - /*========================================================================================================================== - | METHOD: GET STRING - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a string value by column name. - /// - /// The object. - /// The name of the column to retrieve the value from. - private static string GetString(this SqlDataReader reader, string columnName) => - reader.GetString(reader.GetOrdinal(columnName)); - /*========================================================================================================================== | METHOD: GET VERSION \-------------------------------------------------------------------------------------------------------------------------*/ From 51b7bee4c8fda132a875de642d2d95b73973a9ce Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 15:41:38 -0800 Subject: [PATCH 290/778] Introduced `@Version` for `UpdateReferences`, `UpdateRelationships` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Versioning support has been added to the `UpdateReferences` (a028243) and `UpdateRelationships` (183ac27) stored procedures. To ensure that these procedures use the same `version` value as other collections—such as `Attributes` and `ExtendedAttributes`—the `@Version` parameter is being passed, via a new `SqlDateTime version` parameter in C#. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 7df37332..99e97a34 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -476,11 +476,11 @@ SqlDateTime version ); if (areReferencesResolved && areRelationshipsDirty) { - PersistRelations(topic, connection); + PersistRelations(topic, version, connection); } if (areReferencesResolved && areReferencesDirty) { - PersistReferences(topic, connection); + PersistReferences(topic, version, connection); } if (!topic.VersionHistory.Contains(version.Value)) { @@ -627,7 +627,7 @@ public override void Delete(Topic topic, bool isRecursive = false) { /// /// The topic object whose relationships should be persisted. /// The SQL connection. - private static void PersistRelations(Topic topic, SqlConnection connection) { + private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnection connection) { /*------------------------------------------------------------------------------------------------------------------------ | Return blank if the topic has no relations. @@ -661,6 +661,7 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { command.AddParameter("TopicID", topicId); command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); + command.AddParameter("Version", version.Value); command.AddParameter("DeleteUnmatched", true); command.ExecuteNonQuery(); @@ -699,7 +700,7 @@ private static void PersistRelations(Topic topic, SqlConnection connection) { /// /// The topic object whose references should be persisted. /// The SQL connection. - private static void PersistReferences(Topic topic, SqlConnection connection) { + private static void PersistReferences(Topic topic, SqlDateTime version, SqlConnection connection) { /*------------------------------------------------------------------------------------------------------------------------ | Persist relations to database @@ -719,6 +720,7 @@ private static void PersistReferences(Topic topic, SqlConnection connection) { // Add Parameters command.AddParameter("TopicID", topicId); command.AddParameter("ReferencedTopics", references); + command.AddParameter("Version", version.Value); command.AddParameter("DeleteUnmatched", true); command.ExecuteNonQuery(); From 14b9c149cd20a351bc7686879a3a18a22b28d778 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 17:04:32 -0800 Subject: [PATCH 291/778] Updated migration script to drop `ExtendedAttributes.DateModified` Doing a standard schema comparison won't drop columns if a table contains data. As such, these need to be handled via the pre-deployment migration script. This is necessary since we recently dropped the `DateModified` column from `ExtendedAttributes` (242593c). --- .../Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 8a7def3f..1d4a638b 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -28,6 +28,16 @@ TABLE Attributes DROP COLUMN DateModified; +ALTER +TABLE ExtendedAttributes +DROP +CONSTRAINT [DF_ExtendedAttributes_DateModified] + +ALTER +TABLE ExtendedAttributes +DROP +COLUMN DateModified; + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE CORE ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- From 76e33753ca54e6bd9cbddab1ba8efb6d46fd741e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 17:24:21 -0800 Subject: [PATCH 292/778] Updated `TopicReferences` to handle `NULL` columns If a `TopicReference` is deleted, a new version should be created with a `NULL` value. To support this, the `TopicReferences.Target_TopicID` column must be nullable, the `UpdateReferences` stored procedure must insert a `NULL` when `@DeleteUnmatched`, and, finally, `SqlDataReader.SetReferences()` must account for the potentially null return type. This includes the addition of a new `GetNullableTopicId()` helper method, which returns a null `int?` if the response is `DBNULL`. --- .../Stored Procedures/UpdateReferences.sql | 2 +- .../Tables/TopicReferences.sql | 2 +- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql index 35c392bd..01515e96 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -52,7 +52,7 @@ IF @DeleteUnmatched = 1 INTO TopicReferences SELECT @TopicID, Existing.ReferenceKey, - Existing.Target_TopicID, + NULL, @Version FROM @ReferencedTopics New RIGHT JOIN TopicReferences Existing diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql index e3603041..ff061fb5 100644 --- a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql +++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql @@ -7,7 +7,7 @@ CREATE TABLE [dbo].[TopicReferences] ( [Source_TopicID] INT NOT NULL, [ReferenceKey] VARCHAR(128) NOT NULL, - [Target_TopicID] INT NOT NULL, + [Target_TopicID] INT NULL, [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_TopicReferences] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 1fdddb12..d5f2fcc3 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -404,7 +404,7 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics, \-----------------------------------------------------------------------------------------------------------------------*/ var sourceTopicId = reader.GetTopicId("Source_TopicID"); var relationshipKey = reader.GetString("ReferenceKey"); - var targetTopicId = reader.GetTopicId("Target_TopicID"); + var targetTopicId = reader.GetNullableTopicId("Target_TopicID"); /*------------------------------------------------------------------------------------------------------------------------ | Identify affected topics @@ -413,8 +413,8 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics, var referenced = (Topic?)null; // Fetch the related topic - if (topics.Keys.Contains(targetTopicId)) { - referenced = topics[targetTopicId]; + if (targetTopicId is not null && topics.Keys.Contains(targetTopicId.Value)) { + referenced = topics[targetTopicId.Value]; } // Bypass if either of the objects are missing @@ -506,6 +506,17 @@ private static int GetInteger(this SqlDataReader reader, string columnName) => private static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") => reader.GetInt32(reader.GetOrdinal(columnName)); + /*========================================================================================================================== + | METHOD: GET NULLABLE TOPIC ID + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Retrieves a value by column name, while accepting null values. + /// + /// The object. + /// The name of the column to retrieve the value from. + private static int? GetNullableTopicId(this SqlDataReader reader, string columnName = "TopicID") => + reader.IsDBNull(reader.GetOrdinal(columnName))? null : reader.GetInt32(reader.GetOrdinal(columnName)); + /*========================================================================================================================== | METHOD: GET VERSION \-------------------------------------------------------------------------------------------------------------------------*/ From d4cfc9a080d7cb3c5221030bedc7b2906c65061c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 17:34:03 -0800 Subject: [PATCH 293/778] Fixed bug in `@DeleteUnmatched` handling Previously, the `@DeleteUnmatched` handling was joining against the full `Relationships` or `TopicReferences` tables, which potentially resulted in multiple duplicate rows being inserted if a) a topic was not matched, but b) there was more than one previous version. Easy fix by joining against `RelationshipIndex` or `ReferenceIndex`, just as we do in `UpdateAttributes`. --- .../Stored Procedures/UpdateReferences.sql | 2 +- .../Stored Procedures/UpdateRelationships.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql index 01515e96..df36bbc3 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -55,7 +55,7 @@ IF @DeleteUnmatched = 1 NULL, @Version FROM @ReferencedTopics New - RIGHT JOIN TopicReferences Existing + RIGHT JOIN ReferenceIndex Existing ON Source_TopicID = @TopicID AND Existing.ReferenceKey = New.ReferenceKey WHERE Source_TopicID = @TopicID diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index 6d1b9090..5e43ab40 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -65,7 +65,7 @@ IF @DeleteUnmatched = 1 1, @Version FROM @RelatedTopics Relationships - RIGHT JOIN Relationships Existing + RIGHT JOIN RelationshipIndex Existing ON Target_TopicID = TopicID WHERE Source_TopicID = @TopicID AND ISNULL(TopicID, '') = '' From 05d5c637943f37a69d03c7ec338bdbd90b43289f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 17:43:37 -0800 Subject: [PATCH 294/778] Preemptively clear `Relationships`, `References` during `Rollback()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When doing a `Rollback()`—or, more specifically, a `Load(referenceTopic, version)` where the `topicId` is in the `referenceTopic` graph—we should preemptively clear the `Relationships` and `References` so that they can be reloaded with the target `version`. Otherwise, since we don't store the `version` metadata for these relationships, there isn't a(n easy) way to detect if any relationships or referenced topics should be deleted. Specifically, this occurred when those topics had been added for the first time after the loaded version—otherwise, we'd be able to deleted them due to an `IsDeleted` relationship, or `NULL` `Target_Topic` reference. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 99e97a34..08dbef8d 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -14,6 +14,7 @@ using Microsoft.Data.SqlClient; using OnTopic.Data.Sql.Models; using OnTopic.Internal.Diagnostics; +using OnTopic.Querying; using OnTopic.Repositories; namespace OnTopic.Data.Sql { @@ -200,10 +201,31 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic ); /*------------------------------------------------------------------------------------------------------------------------ - | Establish database connection + | Clear relationships, topic references + >------------------------------------------------------------------------------------------------------------------------- + | Because we don't (currently) track version as part of the .NET data model for relationships or topic references, there's + | no easy way to determine if a relationship should be deleted when doing a rollback. As such, existing relationships + | should be deleted, assuming a `referenceTopic` is passed, and it contains the `topicId`. \-----------------------------------------------------------------------------------------------------------------------*/ var topic = (Topic?)null; + if (referenceTopic?.Id == topicId) { + topic = referenceTopic; + } + else if (referenceTopic is not null) { + topic = referenceTopic.GetRootTopic().FindFirst(t => t.Id == topicId); + } + + if (topic is not null) { + foreach (var relationship in topic.Relationships) { + topic.Relationships.ClearTopics(relationship.Key); + } + topic.References.Clear(); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish database connection + \-----------------------------------------------------------------------------------------------------------------------*/ using var connection = new SqlConnection(_connectionString); using var command = new SqlCommand("GetTopicVersion", connection) { CommandType = CommandType.StoredProcedure, From e10ab8270fb9743493afa5349d923c56d93a7699 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 17:50:27 -0800 Subject: [PATCH 295/778] Renamed `referenceTopic` parameter to `topic` in `Load()` overload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Load(Topic, DateTime)` overload (re)loads a specific version of an existing in-memory topic reference. In this case, the original parameter name—`referenceTopic`—doesn't clearly communicate that it is the topic that will be operated against. Renaming it to `topic` better communicates this, in that it's consistent with e.g. `Save()`, `Move()`, and `Delete()`. --- OnTopic/Repositories/ITopicRepository.cs | 2 +- OnTopic/Repositories/TopicRepositoryBase.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 7ee01aff..3d89daea 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -90,7 +90,7 @@ public interface ITopicRepository { Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); /// - Topic? Load(Topic referenceTopic, DateTime version); + Topic? Load(Topic topic, DateTime version); /*========================================================================================================================== | METHOD: ROLLBACK diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index a8886def..efb0cca5 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -5,11 +5,9 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Microsoft; using OnTopic.Attributes; -using OnTopic.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Metadata.AttributeTypes; @@ -227,7 +225,7 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont public abstract Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); /// - public Topic? Load(Topic referenceTopic, DateTime version) { + public Topic? Load(Topic topic, DateTime version) { Contract.Requires(referenceTopic, nameof(referenceTopic)); return Load(referenceTopic.Id, version, referenceTopic); } From 22eea2e0a3e08685fad3dd5e72c63023d608a5c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 14 Jan 2021 18:04:30 -0800 Subject: [PATCH 296/778] Updated database documentation to include new views --- OnTopic.Data.Sql.Database/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 7034f754..2aafd66e 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -50,8 +50,10 @@ The following is a summary of the most relevant stored procedures. ## Views The majority of the views provide records corresponding to the latest version of records for each topic. These include: -- **[`AttributeIndex`](Views/AttributeIndex.sql)**: Includes the `TopicId`, `AttributeKey` and `AttributeValue`. -- **[`ExtendedAttributesIndex`](Views/ExtendedAttributeIndex.sql)**: Includes the `TopicId` and `AttributeXml`. +- **[`AttributeIndex`](Views/AttributeIndex.sql)**: Includes `TopicId`, `AttributeKey` and nullable `AttributeValue`. +- **[`ExtendedAttributesIndex`](Views/ExtendedAttributeIndex.sql)**: Includes `TopicId` and `AttributeXml`. +- **[`RelationshipIndex`](Views/RelationshipIndex.sql)**: Includes the `Source_TopicID`, `RelationshipKey`, `Target_TopicID, and `IsDeleted`. +- **[`ReferenceIndex`](Views/ReferenceIndex.sql)**: Includes `Source_TopicID`, `ReferenceKey`, and nullable `Target_TopicID`. - **[`VersionHistoryIndex`](Views/VersionHistoryIndex.sql)**: Includes up to the last five `Version` records for every `TopicId`. ## Types From 5ca0b21813b7634d9da90fdf5dd4b5f6167f8658 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 13:39:45 -0800 Subject: [PATCH 297/778] Established boilerplate unit tests for SSDT project This establishes a generated template for SQL Server unit tests associated with the `OnTopic.Data.Sql.Database` project. There are two main unit test classes: one for stored procedures (`StoredProcedures.cs`, `StoredProcedures.resx`) and another for functions (`Functions.cs`, `Functions.resx`). I'll be filling these in with actual test code over the following commits. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 355 +++++++++++++ .../Functions.resx | 213 ++++++++ .../OnTopic.Data.Sql.Database.Tests.csproj | 127 +++++ .../Properties/AssemblyInfo.cs | 36 ++ .../SqlDatabaseSetup.cs | 22 + .../StoredProcedures.cs | 484 ++++++++++++++++++ .../StoredProcedures.resx | 275 ++++++++++ OnTopic.Data.Sql.Database.Tests/app.config | 15 + OnTopic.sln | 4 + 9 files changed, 1531 insertions(+) create mode 100644 OnTopic.Data.Sql.Database.Tests/Functions.cs create mode 100644 OnTopic.Data.Sql.Database.Tests/Functions.resx create mode 100644 OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj create mode 100644 OnTopic.Data.Sql.Database.Tests/Properties/AssemblyInfo.cs create mode 100644 OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs create mode 100644 OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs create mode 100644 OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx create mode 100644 OnTopic.Data.Sql.Database.Tests/app.config diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs new file mode 100644 index 00000000..f742635b --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Text; +using Microsoft.Data.Tools.Schema.Sql.UnitTesting; +using Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace OnTopic.Data.Sql.Database.Tests { + [TestClass()] + public class Functions: SqlDatabaseTestClass { + + public Functions() { + InitializeComponent(); + } + + [TestInitialize()] + public void TestInitialize() { + base.InitializeTest(); + } + [TestCleanup()] + public void TestCleanup() { + base.CleanupTest(); + } + + #region Designer support code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_TestAction; + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Functions)); + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition2; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition6; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetChildTopicIDsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; + this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetUniqueKeyTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_FindTopicIDsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetAttributesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetChildTopicIDsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + dbo_GetExtendedAttributeTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition2 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition6 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetChildTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + // + // dbo_GetExtendedAttributeTestData + // + this.dbo_GetExtendedAttributeTestData.PosttestAction = null; + this.dbo_GetExtendedAttributeTestData.PretestAction = null; + this.dbo_GetExtendedAttributeTestData.TestAction = dbo_GetExtendedAttributeTest_TestAction; + // + // dbo_GetExtendedAttributeTest_TestAction + // + dbo_GetExtendedAttributeTest_TestAction.Conditions.Add(inconclusiveCondition1); + resources.ApplyResources(dbo_GetExtendedAttributeTest_TestAction, "dbo_GetExtendedAttributeTest_TestAction"); + // + // inconclusiveCondition1 + // + inconclusiveCondition1.Enabled = true; + inconclusiveCondition1.Name = "inconclusiveCondition1"; + // + // dbo_GetParentIDTestData + // + this.dbo_GetParentIDTestData.PosttestAction = null; + this.dbo_GetParentIDTestData.PretestAction = null; + this.dbo_GetParentIDTestData.TestAction = dbo_GetParentIDTest_TestAction; + // + // dbo_GetParentIDTest_TestAction + // + dbo_GetParentIDTest_TestAction.Conditions.Add(inconclusiveCondition2); + resources.ApplyResources(dbo_GetParentIDTest_TestAction, "dbo_GetParentIDTest_TestAction"); + // + // inconclusiveCondition2 + // + inconclusiveCondition2.Enabled = true; + inconclusiveCondition2.Name = "inconclusiveCondition2"; + // + // dbo_GetTopicIDTestData + // + this.dbo_GetTopicIDTestData.PosttestAction = null; + this.dbo_GetTopicIDTestData.PretestAction = null; + this.dbo_GetTopicIDTestData.TestAction = dbo_GetTopicIDTest_TestAction; + // + // dbo_GetTopicIDTest_TestAction + // + dbo_GetTopicIDTest_TestAction.Conditions.Add(inconclusiveCondition3); + resources.ApplyResources(dbo_GetTopicIDTest_TestAction, "dbo_GetTopicIDTest_TestAction"); + // + // inconclusiveCondition3 + // + inconclusiveCondition3.Enabled = true; + inconclusiveCondition3.Name = "inconclusiveCondition3"; + // + // dbo_GetUniqueKeyTestData + // + this.dbo_GetUniqueKeyTestData.PosttestAction = null; + this.dbo_GetUniqueKeyTestData.PretestAction = null; + this.dbo_GetUniqueKeyTestData.TestAction = dbo_GetUniqueKeyTest_TestAction; + // + // dbo_GetUniqueKeyTest_TestAction + // + dbo_GetUniqueKeyTest_TestAction.Conditions.Add(inconclusiveCondition4); + resources.ApplyResources(dbo_GetUniqueKeyTest_TestAction, "dbo_GetUniqueKeyTest_TestAction"); + // + // inconclusiveCondition4 + // + inconclusiveCondition4.Enabled = true; + inconclusiveCondition4.Name = "inconclusiveCondition4"; + // + // dbo_FindTopicIDsTestData + // + this.dbo_FindTopicIDsTestData.PosttestAction = null; + this.dbo_FindTopicIDsTestData.PretestAction = null; + this.dbo_FindTopicIDsTestData.TestAction = dbo_FindTopicIDsTest_TestAction; + // + // dbo_FindTopicIDsTest_TestAction + // + dbo_FindTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition5); + resources.ApplyResources(dbo_FindTopicIDsTest_TestAction, "dbo_FindTopicIDsTest_TestAction"); + // + // inconclusiveCondition5 + // + inconclusiveCondition5.Enabled = true; + inconclusiveCondition5.Name = "inconclusiveCondition5"; + // + // dbo_GetAttributesTestData + // + this.dbo_GetAttributesTestData.PosttestAction = null; + this.dbo_GetAttributesTestData.PretestAction = null; + this.dbo_GetAttributesTestData.TestAction = dbo_GetAttributesTest_TestAction; + // + // dbo_GetAttributesTest_TestAction + // + dbo_GetAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); + resources.ApplyResources(dbo_GetAttributesTest_TestAction, "dbo_GetAttributesTest_TestAction"); + // + // inconclusiveCondition6 + // + inconclusiveCondition6.Enabled = true; + inconclusiveCondition6.Name = "inconclusiveCondition6"; + // + // dbo_GetChildTopicIDsTestData + // + this.dbo_GetChildTopicIDsTestData.PosttestAction = null; + this.dbo_GetChildTopicIDsTestData.PretestAction = null; + this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; + // + // dbo_GetChildTopicIDsTest_TestAction + // + dbo_GetChildTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition7); + resources.ApplyResources(dbo_GetChildTopicIDsTest_TestAction, "dbo_GetChildTopicIDsTest_TestAction"); + // + // inconclusiveCondition7 + // + inconclusiveCondition7.Enabled = true; + inconclusiveCondition7.Name = "inconclusiveCondition7"; + } + + #endregion + + + #region Additional test attributes + // + // You can use the following additional attributes as you write your tests: + // + // Use ClassInitialize to run code before running the first test in the class + // [ClassInitialize()] + // public static void MyClassInitialize(TestContext testContext) { } + // + // Use ClassCleanup to run code after all tests in a class have run + // [ClassCleanup()] + // public static void MyClassCleanup() { } + // + #endregion + + [TestMethod()] + public void dbo_GetExtendedAttributeTest() { + SqlDatabaseTestActions testActions = this.dbo_GetExtendedAttributeTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetParentIDTest() { + SqlDatabaseTestActions testActions = this.dbo_GetParentIDTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetTopicIDTest() { + SqlDatabaseTestActions testActions = this.dbo_GetTopicIDTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetUniqueKeyTest() { + SqlDatabaseTestActions testActions = this.dbo_GetUniqueKeyTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_FindTopicIDsTest() { + SqlDatabaseTestActions testActions = this.dbo_FindTopicIDsTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetAttributesTest() { + SqlDatabaseTestActions testActions = this.dbo_GetAttributesTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetChildTopicIDsTest() { + SqlDatabaseTestActions testActions = this.dbo_GetChildTopicIDsTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + private SqlDatabaseTestActions dbo_GetExtendedAttributeTestData; + private SqlDatabaseTestActions dbo_GetParentIDTestData; + private SqlDatabaseTestActions dbo_GetTopicIDTestData; + private SqlDatabaseTestActions dbo_GetUniqueKeyTestData; + private SqlDatabaseTestActions dbo_FindTopicIDsTestData; + private SqlDatabaseTestActions dbo_GetAttributesTestData; + private SqlDatabaseTestActions dbo_GetChildTopicIDsTestData; + } +} diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx new file mode 100644 index 00000000..6d145efe --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + -- database unit test for dbo.GetExtendedAttribute +DECLARE @RC AS NVARCHAR (MAX), @TopicID AS INT, @AttributeKey AS NVARCHAR (255); + +SELECT @RC = NULL, + @TopicID = 0, + @AttributeKey = NULL; + +SELECT @RC = [dbo].[GetExtendedAttribute](@TopicID, @AttributeKey); + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.GetParentID +DECLARE @RC AS INT, @TopicID AS INT; + +SELECT @RC = NULL, + @TopicID = 0; + +SELECT @RC = [dbo].[GetParentID](@TopicID); + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.GetTopicID +DECLARE @RC AS INT, @UniqueKey AS NVARCHAR (2500); + +SELECT @RC = NULL, + @UniqueKey = NULL; + +SELECT @RC = [dbo].[GetTopicID](@UniqueKey); + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.GetUniqueKey +DECLARE @RC AS VARCHAR (MAX), @TopicID AS INT; + +SELECT @RC = NULL, + @TopicID = 0; + +SELECT @RC = [dbo].[GetUniqueKey](@TopicID); + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.FindTopicIDs +DECLARE @TopicID AS INT, @AttributeKey AS VARCHAR (255), @AttributeValue AS NVARCHAR (255), @IsExtendedAttribute AS BIT, @UsePartialMatch AS BIT; + +SELECT @TopicID = 0, + @AttributeKey = NULL, + @AttributeValue = NULL, + @IsExtendedAttribute = 0, + @UsePartialMatch = 0; + +SELECT * +FROM [dbo].[FindTopicIDs](@TopicID, @AttributeKey, @AttributeValue, @IsExtendedAttribute, @UsePartialMatch); + + + + + -- database unit test for dbo.GetAttributes +DECLARE @TopicID AS INT; + +SELECT @TopicID = 0; + +SELECT * +FROM [dbo].[GetAttributes](@TopicID); + + + + + -- database unit test for dbo.GetChildTopicIDs +DECLARE @TopicID AS INT; + +SELECT @TopicID = 0; + +SELECT * +FROM [dbo].[GetChildTopicIDs](@TopicID); + + + + + True + + \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj b/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj new file mode 100644 index 00000000..67fa2254 --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj @@ -0,0 +1,127 @@ + + + + $(VsInstallRoot)\Common7\IDE\Extensions\Microsoft\SQLDB + + + $(VsInstallRoot)\Common7\IDE\Extensions\Microsoft\SQLDB\DAC\130 + + + Debug + AnyCPU + {D7FE876D-A75F-4493-8283-B316271FD5AE} + Library + Properties + OnTopic.Data.Sql.Database.Tests + OnTopic.Data.Sql.Database.Tests + v4.7.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + Designer + + + + Designer + + + + + + + + + Functions.cs + + + StoredProcedures.cs + + + + + + $(SSDTPath)\Microsoft.Data.Tools.Schema.Sql.dll + True + + + $(SSDTUnitTestPath)\Microsoft.Data.Tools.Schema.Sql.UnitTesting.dll + True + + + $(SSDTUnitTestPath)\Microsoft.Data.Tools.Schema.Sql.UnitTestingAdapter.dll + True + + + + + + + False + + + False + + + False + + + False + + + + + + + + 3.1 + + + + + \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database.Tests/Properties/AssemblyInfo.cs b/OnTopic.Data.Sql.Database.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..79e6f71a --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("OnTopic.Data.Sql.Database.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("OnTopic.Data.Sql.Database.Tests")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d7fe876d-a75f-4493-8283-b316271fd5ae")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs b/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs new file mode 100644 index 00000000..b29d80bf --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Data.Common; +using Microsoft.Data.Tools.Schema.Sql.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace OnTopic.Data.Sql.Database.Tests { + [TestClass()] + public class SqlDatabaseSetup { + + [AssemblyInitialize()] + public static void InitializeAssembly(TestContext ctx) { + // Setup the test database based on setting in the + // configuration file + SqlDatabaseTestClass.TestService.DeployDatabaseProject(); + SqlDatabaseTestClass.TestService.GenerateData(); + } + + } +} diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs new file mode 100644 index 00000000..a966a9bf --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -0,0 +1,484 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Text; +using Microsoft.Data.Tools.Schema.Sql.UnitTesting; +using Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace OnTopic.Data.Sql.Database.Tests { + [TestClass()] + public class StoredProcedures: SqlDatabaseTestClass { + + public StoredProcedures() { + InitializeComponent(); + } + + [TestInitialize()] + public void TestInitialize() { + base.InitializeTest(); + } + [TestCleanup()] + public void TestCleanup() { + base.CleanupTest(); + } + + #region Designer support code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_TestAction; + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(StoredProcedures)); + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition2; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition6; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition8; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition9; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition10; + this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetTopicsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_MoveTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_UpdateAttributesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_UpdateExtendedAttributesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_UpdateReferencesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_UpdateRelationshipsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_UpdateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + dbo_CreateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_DeleteTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition2 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition6 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition8 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_UpdateRelationshipsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition9 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + // + // dbo_CreateTopicTestData + // + this.dbo_CreateTopicTestData.PosttestAction = null; + this.dbo_CreateTopicTestData.PretestAction = null; + this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; + // + // dbo_CreateTopicTest_TestAction + // + dbo_CreateTopicTest_TestAction.Conditions.Add(inconclusiveCondition1); + resources.ApplyResources(dbo_CreateTopicTest_TestAction, "dbo_CreateTopicTest_TestAction"); + // + // inconclusiveCondition1 + // + inconclusiveCondition1.Enabled = true; + inconclusiveCondition1.Name = "inconclusiveCondition1"; + // + // dbo_DeleteTopicTestData + // + this.dbo_DeleteTopicTestData.PosttestAction = null; + this.dbo_DeleteTopicTestData.PretestAction = null; + this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; + // + // dbo_DeleteTopicTest_TestAction + // + dbo_DeleteTopicTest_TestAction.Conditions.Add(inconclusiveCondition2); + resources.ApplyResources(dbo_DeleteTopicTest_TestAction, "dbo_DeleteTopicTest_TestAction"); + // + // inconclusiveCondition2 + // + inconclusiveCondition2.Enabled = true; + inconclusiveCondition2.Name = "inconclusiveCondition2"; + // + // dbo_GetTopicVersionTestData + // + this.dbo_GetTopicVersionTestData.PosttestAction = null; + this.dbo_GetTopicVersionTestData.PretestAction = null; + this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; + // + // dbo_GetTopicVersionTest_TestAction + // + dbo_GetTopicVersionTest_TestAction.Conditions.Add(inconclusiveCondition3); + resources.ApplyResources(dbo_GetTopicVersionTest_TestAction, "dbo_GetTopicVersionTest_TestAction"); + // + // inconclusiveCondition3 + // + inconclusiveCondition3.Enabled = true; + inconclusiveCondition3.Name = "inconclusiveCondition3"; + // + // dbo_GetTopicsTestData + // + this.dbo_GetTopicsTestData.PosttestAction = null; + this.dbo_GetTopicsTestData.PretestAction = null; + this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; + // + // dbo_GetTopicsTest_TestAction + // + dbo_GetTopicsTest_TestAction.Conditions.Add(inconclusiveCondition4); + resources.ApplyResources(dbo_GetTopicsTest_TestAction, "dbo_GetTopicsTest_TestAction"); + // + // inconclusiveCondition4 + // + inconclusiveCondition4.Enabled = true; + inconclusiveCondition4.Name = "inconclusiveCondition4"; + // + // dbo_MoveTopicTestData + // + this.dbo_MoveTopicTestData.PosttestAction = null; + this.dbo_MoveTopicTestData.PretestAction = null; + this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; + // + // dbo_MoveTopicTest_TestAction + // + dbo_MoveTopicTest_TestAction.Conditions.Add(inconclusiveCondition5); + resources.ApplyResources(dbo_MoveTopicTest_TestAction, "dbo_MoveTopicTest_TestAction"); + // + // inconclusiveCondition5 + // + inconclusiveCondition5.Enabled = true; + inconclusiveCondition5.Name = "inconclusiveCondition5"; + // + // dbo_UpdateAttributesTestData + // + this.dbo_UpdateAttributesTestData.PosttestAction = null; + this.dbo_UpdateAttributesTestData.PretestAction = null; + this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; + // + // dbo_UpdateAttributesTest_TestAction + // + dbo_UpdateAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); + resources.ApplyResources(dbo_UpdateAttributesTest_TestAction, "dbo_UpdateAttributesTest_TestAction"); + // + // inconclusiveCondition6 + // + inconclusiveCondition6.Enabled = true; + inconclusiveCondition6.Name = "inconclusiveCondition6"; + // + // dbo_UpdateExtendedAttributesTestData + // + this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; + this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; + this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; + // + // dbo_UpdateExtendedAttributesTest_TestAction + // + dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(inconclusiveCondition7); + resources.ApplyResources(dbo_UpdateExtendedAttributesTest_TestAction, "dbo_UpdateExtendedAttributesTest_TestAction"); + // + // inconclusiveCondition7 + // + inconclusiveCondition7.Enabled = true; + inconclusiveCondition7.Name = "inconclusiveCondition7"; + // + // dbo_UpdateReferencesTestData + // + this.dbo_UpdateReferencesTestData.PosttestAction = null; + this.dbo_UpdateReferencesTestData.PretestAction = null; + this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; + // + // dbo_UpdateReferencesTest_TestAction + // + dbo_UpdateReferencesTest_TestAction.Conditions.Add(inconclusiveCondition8); + resources.ApplyResources(dbo_UpdateReferencesTest_TestAction, "dbo_UpdateReferencesTest_TestAction"); + // + // inconclusiveCondition8 + // + inconclusiveCondition8.Enabled = true; + inconclusiveCondition8.Name = "inconclusiveCondition8"; + // + // dbo_UpdateRelationshipsTestData + // + this.dbo_UpdateRelationshipsTestData.PosttestAction = null; + this.dbo_UpdateRelationshipsTestData.PretestAction = null; + this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; + // + // dbo_UpdateRelationshipsTest_TestAction + // + dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(inconclusiveCondition9); + resources.ApplyResources(dbo_UpdateRelationshipsTest_TestAction, "dbo_UpdateRelationshipsTest_TestAction"); + // + // inconclusiveCondition9 + // + inconclusiveCondition9.Enabled = true; + inconclusiveCondition9.Name = "inconclusiveCondition9"; + // + // dbo_UpdateTopicTestData + // + this.dbo_UpdateTopicTestData.PosttestAction = null; + this.dbo_UpdateTopicTestData.PretestAction = null; + this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // dbo_UpdateTopicTest_TestAction + // + dbo_UpdateTopicTest_TestAction.Conditions.Add(inconclusiveCondition10); + resources.ApplyResources(dbo_UpdateTopicTest_TestAction, "dbo_UpdateTopicTest_TestAction"); + // + // inconclusiveCondition10 + // + inconclusiveCondition10.Enabled = true; + inconclusiveCondition10.Name = "inconclusiveCondition10"; + } + + #endregion + + + #region Additional test attributes + // + // You can use the following additional attributes as you write your tests: + // + // Use ClassInitialize to run code before running the first test in the class + // [ClassInitialize()] + // public static void MyClassInitialize(TestContext testContext) { } + // + // Use ClassCleanup to run code after all tests in a class have run + // [ClassCleanup()] + // public static void MyClassCleanup() { } + // + #endregion + + [TestMethod()] + public void dbo_CreateTopicTest() { + SqlDatabaseTestActions testActions = this.dbo_CreateTopicTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_DeleteTopicTest() { + SqlDatabaseTestActions testActions = this.dbo_DeleteTopicTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetTopicVersionTest() { + SqlDatabaseTestActions testActions = this.dbo_GetTopicVersionTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_GetTopicsTest() { + SqlDatabaseTestActions testActions = this.dbo_GetTopicsTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_MoveTopicTest() { + SqlDatabaseTestActions testActions = this.dbo_MoveTopicTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_UpdateAttributesTest() { + SqlDatabaseTestActions testActions = this.dbo_UpdateAttributesTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_UpdateExtendedAttributesTest() { + SqlDatabaseTestActions testActions = this.dbo_UpdateExtendedAttributesTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_UpdateReferencesTest() { + SqlDatabaseTestActions testActions = this.dbo_UpdateReferencesTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_UpdateRelationshipsTest() { + SqlDatabaseTestActions testActions = this.dbo_UpdateRelationshipsTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + + [TestMethod()] + public void dbo_UpdateTopicTest() { + SqlDatabaseTestActions testActions = this.dbo_UpdateTopicTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + private SqlDatabaseTestActions dbo_CreateTopicTestData; + private SqlDatabaseTestActions dbo_DeleteTopicTestData; + private SqlDatabaseTestActions dbo_GetTopicVersionTestData; + private SqlDatabaseTestActions dbo_GetTopicsTestData; + private SqlDatabaseTestActions dbo_MoveTopicTestData; + private SqlDatabaseTestActions dbo_UpdateAttributesTestData; + private SqlDatabaseTestActions dbo_UpdateExtendedAttributesTestData; + private SqlDatabaseTestActions dbo_UpdateReferencesTestData; + private SqlDatabaseTestActions dbo_UpdateRelationshipsTestData; + private SqlDatabaseTestActions dbo_UpdateTopicTestData; + } +} diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx new file mode 100644 index 00000000..a66bea72 --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + -- database unit test for dbo.CreateTopic +DECLARE @RC AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @ParentID AS INT, @Attributes AS [dbo].[AttributeValues], @ExtendedAttributes AS XML, @References AS [dbo].[TopicReferences], @Version AS DATETIME; + +SELECT @RC = 0, + @Key = NULL, + @ContentType = NULL, + @ParentID = 0, + @ExtendedAttributes = NULL, + @Version = getdate(); + +EXECUTE @RC = [dbo].[CreateTopic] @Key, @ContentType, @ParentID, @Attributes, @ExtendedAttributes, @References, @Version; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.DeleteTopic +DECLARE @RC AS INT, @TopicID AS INT; + +SELECT @RC = 0, + @TopicID = 0; + +EXECUTE @RC = [dbo].[DeleteTopic] @TopicID; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.GetTopicVersion +DECLARE @RC AS INT, @TopicID AS INT, @Version AS DATETIME; + +SELECT @RC = 0, + @TopicID = 0, + @Version = getdate(); + +EXECUTE @RC = [dbo].[GetTopicVersion] @TopicID, @Version; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.GetTopics +DECLARE @RC AS INT, @TopicID AS INT, @DeepLoad AS BIT, @UniqueKey AS NVARCHAR (255); + +SELECT @RC = 0, + @TopicID = 0, + @DeepLoad = 0, + @UniqueKey = NULL; + +EXECUTE @RC = [dbo].[GetTopics] @TopicID, @DeepLoad, @UniqueKey; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.MoveTopic +DECLARE @RC AS INT, @TopicID AS INT, @ParentID AS INT, @SiblingID AS INT; + +SELECT @RC = 0, + @TopicID = 0, + @ParentID = 0, + @SiblingID = 0; + +EXECUTE @RC = [dbo].[MoveTopic] @TopicID, @ParentID, @SiblingID; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.UpdateAttributes +DECLARE @RC AS INT, @TopicID AS INT, @Attributes AS [dbo].[AttributeValues], @Version AS DATETIME, @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateAttributes] @TopicID, @Attributes, @Version, @DeleteUnmatched; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.UpdateExtendedAttributes +DECLARE @RC AS INT, @TopicID AS INT, @ExtendedAttributes AS XML, @Version AS DATETIME, @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @ExtendedAttributes = NULL, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateExtendedAttributes] @TopicID, @ExtendedAttributes, @Version, @DeleteUnmatched; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.UpdateReferences +DECLARE @RC AS INT, @TopicID AS INT, @ReferencedTopics AS [dbo].[TopicReferences], @Version AS DATETIME, @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateReferences] @TopicID, @ReferencedTopics, @Version, @DeleteUnmatched; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.UpdateRelationships +DECLARE @RC AS INT, @TopicID AS INT, @RelationshipKey AS VARCHAR (255), @RelatedTopics AS [dbo].[TopicList], @Version AS DATETIME, @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @RelationshipKey = NULL, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateRelationships] @TopicID, @RelationshipKey, @RelatedTopics, @Version, @DeleteUnmatched; + +SELECT @RC AS RC; + + + + + -- database unit test for dbo.UpdateTopic +DECLARE @RC AS INT, @TopicID AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @Attributes AS [dbo].[AttributeValues], @ExtendedAttributes AS XML, @Version AS DATETIME, @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Key = NULL, + @ContentType = NULL, + @ExtendedAttributes = NULL, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateTopic] @TopicID, @Key, @ContentType, @Attributes, @ExtendedAttributes, @Version, @DeleteUnmatched; + +SELECT @RC AS RC; + + + True + + \ No newline at end of file diff --git a/OnTopic.Data.Sql.Database.Tests/app.config b/OnTopic.Data.Sql.Database.Tests/app.config new file mode 100644 index 00000000..07b4db00 --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/app.config @@ -0,0 +1,15 @@ + + + +
+ + + + + + + + \ No newline at end of file diff --git a/OnTopic.sln b/OnTopic.sln index b0c4fa43..e6c0a9df 100644 --- a/OnTopic.sln +++ b/OnTopic.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.AspNetCore.Mvc.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.TestDoubles", "OnTopic.TestDoubles\OnTopic.TestDoubles.csproj", "{FE175884-59C1-4C4D-A663-4CC570432ECC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnTopic.Data.Sql.Database.Tests", "OnTopic.Data.Sql.Database.Tests\OnTopic.Data.Sql.Database.Tests.csproj", "{D7FE876D-A75F-4493-8283-B316271FD5AE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -76,6 +78,8 @@ Global {FE175884-59C1-4C4D-A663-4CC570432ECC}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE175884-59C1-4C4D-A663-4CC570432ECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE175884-59C1-4C4D-A663-4CC570432ECC}.Release|Any CPU.Build.0 = Release|Any CPU + {D7FE876D-A75F-4493-8283-B316271FD5AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7FE876D-A75F-4493-8283-B316271FD5AE}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a883e6d2b9f57ad8b5ae749d9692fd3f1f796859 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 13:54:54 -0800 Subject: [PATCH 298/778] Added missing references to new views These views were previously added, but somehow never saved to the `sqlproj` file. --- OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index cbcf4a2a..2367e5f0 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -125,6 +125,8 @@ + + From 732b1e094afa0fc53ddab983e8abc57aa7f8e5f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 13:55:42 -0800 Subject: [PATCH 299/778] Fixed parameter references in `Load(Topic, DateTime)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a last minute change to the versioning support, I renamed a parameter—but apparently neglected to fix the default implementation in `TopicRepositoryBase`, which prevents compilation. Whoops. --- OnTopic/Repositories/TopicRepositoryBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index efb0cca5..c09538d2 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -226,8 +226,8 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont /// public Topic? Load(Topic topic, DateTime version) { - Contract.Requires(referenceTopic, nameof(referenceTopic)); - return Load(referenceTopic.Id, version, referenceTopic); + Contract.Requires(topic, nameof(topic)); + return Load(topic.Id, version, topic); } /// From 0c285d0d518e6ecc986dbda65f314fea8a125124 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 14:39:40 -0800 Subject: [PATCH 300/778] Reordered variables in generated C# classes Upon the first build, the order of the C# classes was rearranged by Visual Studio. Hopefully this is a one time issue, so we're not cluttering our git history with meaningless changes! --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 84 ++++++------ .../StoredProcedures.cs | 120 +++++++++--------- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index f742635b..74e53a78 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -68,12 +68,6 @@ private void InitializeComponent() { dbo_GetChildTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); // - // dbo_GetExtendedAttributeTestData - // - this.dbo_GetExtendedAttributeTestData.PosttestAction = null; - this.dbo_GetExtendedAttributeTestData.PretestAction = null; - this.dbo_GetExtendedAttributeTestData.TestAction = dbo_GetExtendedAttributeTest_TestAction; - // // dbo_GetExtendedAttributeTest_TestAction // dbo_GetExtendedAttributeTest_TestAction.Conditions.Add(inconclusiveCondition1); @@ -84,12 +78,6 @@ private void InitializeComponent() { inconclusiveCondition1.Enabled = true; inconclusiveCondition1.Name = "inconclusiveCondition1"; // - // dbo_GetParentIDTestData - // - this.dbo_GetParentIDTestData.PosttestAction = null; - this.dbo_GetParentIDTestData.PretestAction = null; - this.dbo_GetParentIDTestData.TestAction = dbo_GetParentIDTest_TestAction; - // // dbo_GetParentIDTest_TestAction // dbo_GetParentIDTest_TestAction.Conditions.Add(inconclusiveCondition2); @@ -100,12 +88,6 @@ private void InitializeComponent() { inconclusiveCondition2.Enabled = true; inconclusiveCondition2.Name = "inconclusiveCondition2"; // - // dbo_GetTopicIDTestData - // - this.dbo_GetTopicIDTestData.PosttestAction = null; - this.dbo_GetTopicIDTestData.PretestAction = null; - this.dbo_GetTopicIDTestData.TestAction = dbo_GetTopicIDTest_TestAction; - // // dbo_GetTopicIDTest_TestAction // dbo_GetTopicIDTest_TestAction.Conditions.Add(inconclusiveCondition3); @@ -116,12 +98,6 @@ private void InitializeComponent() { inconclusiveCondition3.Enabled = true; inconclusiveCondition3.Name = "inconclusiveCondition3"; // - // dbo_GetUniqueKeyTestData - // - this.dbo_GetUniqueKeyTestData.PosttestAction = null; - this.dbo_GetUniqueKeyTestData.PretestAction = null; - this.dbo_GetUniqueKeyTestData.TestAction = dbo_GetUniqueKeyTest_TestAction; - // // dbo_GetUniqueKeyTest_TestAction // dbo_GetUniqueKeyTest_TestAction.Conditions.Add(inconclusiveCondition4); @@ -132,12 +108,6 @@ private void InitializeComponent() { inconclusiveCondition4.Enabled = true; inconclusiveCondition4.Name = "inconclusiveCondition4"; // - // dbo_FindTopicIDsTestData - // - this.dbo_FindTopicIDsTestData.PosttestAction = null; - this.dbo_FindTopicIDsTestData.PretestAction = null; - this.dbo_FindTopicIDsTestData.TestAction = dbo_FindTopicIDsTest_TestAction; - // // dbo_FindTopicIDsTest_TestAction // dbo_FindTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition5); @@ -148,12 +118,6 @@ private void InitializeComponent() { inconclusiveCondition5.Enabled = true; inconclusiveCondition5.Name = "inconclusiveCondition5"; // - // dbo_GetAttributesTestData - // - this.dbo_GetAttributesTestData.PosttestAction = null; - this.dbo_GetAttributesTestData.PretestAction = null; - this.dbo_GetAttributesTestData.TestAction = dbo_GetAttributesTest_TestAction; - // // dbo_GetAttributesTest_TestAction // dbo_GetAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); @@ -164,12 +128,6 @@ private void InitializeComponent() { inconclusiveCondition6.Enabled = true; inconclusiveCondition6.Name = "inconclusiveCondition6"; // - // dbo_GetChildTopicIDsTestData - // - this.dbo_GetChildTopicIDsTestData.PosttestAction = null; - this.dbo_GetChildTopicIDsTestData.PretestAction = null; - this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; - // // dbo_GetChildTopicIDsTest_TestAction // dbo_GetChildTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition7); @@ -179,6 +137,48 @@ private void InitializeComponent() { // inconclusiveCondition7.Enabled = true; inconclusiveCondition7.Name = "inconclusiveCondition7"; + // + // dbo_GetExtendedAttributeTestData + // + this.dbo_GetExtendedAttributeTestData.PosttestAction = null; + this.dbo_GetExtendedAttributeTestData.PretestAction = null; + this.dbo_GetExtendedAttributeTestData.TestAction = dbo_GetExtendedAttributeTest_TestAction; + // + // dbo_GetParentIDTestData + // + this.dbo_GetParentIDTestData.PosttestAction = null; + this.dbo_GetParentIDTestData.PretestAction = null; + this.dbo_GetParentIDTestData.TestAction = dbo_GetParentIDTest_TestAction; + // + // dbo_GetTopicIDTestData + // + this.dbo_GetTopicIDTestData.PosttestAction = null; + this.dbo_GetTopicIDTestData.PretestAction = null; + this.dbo_GetTopicIDTestData.TestAction = dbo_GetTopicIDTest_TestAction; + // + // dbo_GetUniqueKeyTestData + // + this.dbo_GetUniqueKeyTestData.PosttestAction = null; + this.dbo_GetUniqueKeyTestData.PretestAction = null; + this.dbo_GetUniqueKeyTestData.TestAction = dbo_GetUniqueKeyTest_TestAction; + // + // dbo_FindTopicIDsTestData + // + this.dbo_FindTopicIDsTestData.PosttestAction = null; + this.dbo_FindTopicIDsTestData.PretestAction = null; + this.dbo_FindTopicIDsTestData.TestAction = dbo_FindTopicIDsTest_TestAction; + // + // dbo_GetAttributesTestData + // + this.dbo_GetAttributesTestData.PosttestAction = null; + this.dbo_GetAttributesTestData.PretestAction = null; + this.dbo_GetAttributesTestData.TestAction = dbo_GetAttributesTest_TestAction; + // + // dbo_GetChildTopicIDsTestData + // + this.dbo_GetChildTopicIDsTestData.PosttestAction = null; + this.dbo_GetChildTopicIDsTestData.PretestAction = null; + this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index a966a9bf..2e010f36 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -83,12 +83,6 @@ private void InitializeComponent() { dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); // - // dbo_CreateTopicTestData - // - this.dbo_CreateTopicTestData.PosttestAction = null; - this.dbo_CreateTopicTestData.PretestAction = null; - this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; - // // dbo_CreateTopicTest_TestAction // dbo_CreateTopicTest_TestAction.Conditions.Add(inconclusiveCondition1); @@ -99,12 +93,6 @@ private void InitializeComponent() { inconclusiveCondition1.Enabled = true; inconclusiveCondition1.Name = "inconclusiveCondition1"; // - // dbo_DeleteTopicTestData - // - this.dbo_DeleteTopicTestData.PosttestAction = null; - this.dbo_DeleteTopicTestData.PretestAction = null; - this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; - // // dbo_DeleteTopicTest_TestAction // dbo_DeleteTopicTest_TestAction.Conditions.Add(inconclusiveCondition2); @@ -115,12 +103,6 @@ private void InitializeComponent() { inconclusiveCondition2.Enabled = true; inconclusiveCondition2.Name = "inconclusiveCondition2"; // - // dbo_GetTopicVersionTestData - // - this.dbo_GetTopicVersionTestData.PosttestAction = null; - this.dbo_GetTopicVersionTestData.PretestAction = null; - this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; - // // dbo_GetTopicVersionTest_TestAction // dbo_GetTopicVersionTest_TestAction.Conditions.Add(inconclusiveCondition3); @@ -131,12 +113,6 @@ private void InitializeComponent() { inconclusiveCondition3.Enabled = true; inconclusiveCondition3.Name = "inconclusiveCondition3"; // - // dbo_GetTopicsTestData - // - this.dbo_GetTopicsTestData.PosttestAction = null; - this.dbo_GetTopicsTestData.PretestAction = null; - this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; - // // dbo_GetTopicsTest_TestAction // dbo_GetTopicsTest_TestAction.Conditions.Add(inconclusiveCondition4); @@ -147,12 +123,6 @@ private void InitializeComponent() { inconclusiveCondition4.Enabled = true; inconclusiveCondition4.Name = "inconclusiveCondition4"; // - // dbo_MoveTopicTestData - // - this.dbo_MoveTopicTestData.PosttestAction = null; - this.dbo_MoveTopicTestData.PretestAction = null; - this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; - // // dbo_MoveTopicTest_TestAction // dbo_MoveTopicTest_TestAction.Conditions.Add(inconclusiveCondition5); @@ -163,12 +133,6 @@ private void InitializeComponent() { inconclusiveCondition5.Enabled = true; inconclusiveCondition5.Name = "inconclusiveCondition5"; // - // dbo_UpdateAttributesTestData - // - this.dbo_UpdateAttributesTestData.PosttestAction = null; - this.dbo_UpdateAttributesTestData.PretestAction = null; - this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; - // // dbo_UpdateAttributesTest_TestAction // dbo_UpdateAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); @@ -179,12 +143,6 @@ private void InitializeComponent() { inconclusiveCondition6.Enabled = true; inconclusiveCondition6.Name = "inconclusiveCondition6"; // - // dbo_UpdateExtendedAttributesTestData - // - this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; - this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; - this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; - // // dbo_UpdateExtendedAttributesTest_TestAction // dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(inconclusiveCondition7); @@ -195,12 +153,6 @@ private void InitializeComponent() { inconclusiveCondition7.Enabled = true; inconclusiveCondition7.Name = "inconclusiveCondition7"; // - // dbo_UpdateReferencesTestData - // - this.dbo_UpdateReferencesTestData.PosttestAction = null; - this.dbo_UpdateReferencesTestData.PretestAction = null; - this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; - // // dbo_UpdateReferencesTest_TestAction // dbo_UpdateReferencesTest_TestAction.Conditions.Add(inconclusiveCondition8); @@ -211,12 +163,6 @@ private void InitializeComponent() { inconclusiveCondition8.Enabled = true; inconclusiveCondition8.Name = "inconclusiveCondition8"; // - // dbo_UpdateRelationshipsTestData - // - this.dbo_UpdateRelationshipsTestData.PosttestAction = null; - this.dbo_UpdateRelationshipsTestData.PretestAction = null; - this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; - // // dbo_UpdateRelationshipsTest_TestAction // dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(inconclusiveCondition9); @@ -227,12 +173,6 @@ private void InitializeComponent() { inconclusiveCondition9.Enabled = true; inconclusiveCondition9.Name = "inconclusiveCondition9"; // - // dbo_UpdateTopicTestData - // - this.dbo_UpdateTopicTestData.PosttestAction = null; - this.dbo_UpdateTopicTestData.PretestAction = null; - this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; - // // dbo_UpdateTopicTest_TestAction // dbo_UpdateTopicTest_TestAction.Conditions.Add(inconclusiveCondition10); @@ -242,6 +182,66 @@ private void InitializeComponent() { // inconclusiveCondition10.Enabled = true; inconclusiveCondition10.Name = "inconclusiveCondition10"; + // + // dbo_CreateTopicTestData + // + this.dbo_CreateTopicTestData.PosttestAction = null; + this.dbo_CreateTopicTestData.PretestAction = null; + this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; + // + // dbo_DeleteTopicTestData + // + this.dbo_DeleteTopicTestData.PosttestAction = null; + this.dbo_DeleteTopicTestData.PretestAction = null; + this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; + // + // dbo_GetTopicVersionTestData + // + this.dbo_GetTopicVersionTestData.PosttestAction = null; + this.dbo_GetTopicVersionTestData.PretestAction = null; + this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; + // + // dbo_GetTopicsTestData + // + this.dbo_GetTopicsTestData.PosttestAction = null; + this.dbo_GetTopicsTestData.PretestAction = null; + this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; + // + // dbo_MoveTopicTestData + // + this.dbo_MoveTopicTestData.PosttestAction = null; + this.dbo_MoveTopicTestData.PretestAction = null; + this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; + // + // dbo_UpdateAttributesTestData + // + this.dbo_UpdateAttributesTestData.PosttestAction = null; + this.dbo_UpdateAttributesTestData.PretestAction = null; + this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; + // + // dbo_UpdateExtendedAttributesTestData + // + this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; + this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; + this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; + // + // dbo_UpdateReferencesTestData + // + this.dbo_UpdateReferencesTestData.PosttestAction = null; + this.dbo_UpdateReferencesTestData.PretestAction = null; + this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; + // + // dbo_UpdateRelationshipsTestData + // + this.dbo_UpdateRelationshipsTestData.PosttestAction = null; + this.dbo_UpdateRelationshipsTestData.PretestAction = null; + this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; + // + // dbo_UpdateTopicTestData + // + this.dbo_UpdateTopicTestData.PosttestAction = null; + this.dbo_UpdateTopicTestData.PretestAction = null; + this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; } #endregion From 59eb91a177c8b791558e41d3b2e817072ace0b42 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 14:41:01 -0800 Subject: [PATCH 301/778] Introduced global suppressions for auto-generated code The C# code generated by Visual Studio for the SSDT SQL unit tests doesn't comply with the warnings and errors we have setup to help enforce standard and consistent code. Since this is generated code, I'm opting to globally suppress these warnings across the namespace. --- .../GlobalSuppressions.cs | 12 ++++++++++++ .../OnTopic.Data.Sql.Database.Tests.csproj | 1 + 2 files changed, 13 insertions(+) create mode 100644 OnTopic.Data.Sql.Database.Tests/GlobalSuppressions.cs diff --git a/OnTopic.Data.Sql.Database.Tests/GlobalSuppressions.cs b/OnTopic.Data.Sql.Database.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..37a6979f --- /dev/null +++ b/OnTopic.Data.Sql.Database.Tests/GlobalSuppressions.cs @@ -0,0 +1,12 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Incompatible with generated code", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Data.Sql.Database.Tests")] +[assembly: SuppressMessage("Style", "IDE0003:Remove qualification", Justification = "Incompatible with generated code", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Data.Sql.Database.Tests")] +[assembly: SuppressMessage("Style", "IDE0059:Unnecessary assignment of a value", Justification = "Incompatible with generated code", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Data.Sql.Database.Tests")] +[assembly: SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Incompatible with generated code", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Data.Sql.Database.Tests")] +[assembly: SuppressMessage("Style", "IDE0022:Use expression body for methods", Justification = "Incompatible with generated code", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Data.Sql.Database.Tests")] diff --git a/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj b/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj index 67fa2254..d9076326 100644 --- a/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj +++ b/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj @@ -60,6 +60,7 @@ Designer + Designer From c8ad40b938b8b93435221ebf5eda5efd1f962c59 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 14:42:07 -0800 Subject: [PATCH 302/778] Provided initial formatting of generated SQL unit tests The initial formatting of the SQL unit tests doesn't align the variables or values. To help make these more consistent with Ignia's standards, I've aligned them as an initial update. Later, I'll be adding comment boxes as well. --- .../Functions.resx | 120 +++---- .../StoredProcedures.resx | 294 +++++++++++------- 2 files changed, 245 insertions(+), 169 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 6d145efe..0695d55a 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -119,93 +119,107 @@ -- database unit test for dbo.GetExtendedAttribute -DECLARE @RC AS NVARCHAR (MAX), @TopicID AS INT, @AttributeKey AS NVARCHAR (255); +DECLARE @RC AS NVARCHAR (MAX), + @TopicID AS INT, + @AttributeKey AS NVARCHAR (255); -SELECT @RC = NULL, - @TopicID = 0, - @AttributeKey = NULL; +SELECT @RC = NULL, + @TopicID = 0, + @AttributeKey = NULL; -SELECT @RC = [dbo].[GetExtendedAttribute](@TopicID, @AttributeKey); +SELECT @RC = [dbo].[GetExtendedAttribute]( + @TopicID, + @AttributeKey + ); -SELECT @RC AS RC; - - +SELECT @RC AS RC; -- database unit test for dbo.GetParentID -DECLARE @RC AS INT, @TopicID AS INT; - -SELECT @RC = NULL, - @TopicID = 0; +DECLARE @RC AS INT, + @TopicID AS INT; -SELECT @RC = [dbo].[GetParentID](@TopicID); +SELECT @RC = NULL, + @TopicID = 0; -SELECT @RC AS RC; +SELECT @RC = [dbo].[GetParentID]( + @TopicID + ); - +SELECT @RC AS RC; -- database unit test for dbo.GetTopicID -DECLARE @RC AS INT, @UniqueKey AS NVARCHAR (2500); - -SELECT @RC = NULL, - @UniqueKey = NULL; +DECLARE @RC AS INT, + @UniqueKey AS NVARCHAR (2500); -SELECT @RC = [dbo].[GetTopicID](@UniqueKey); +SELECT @RC = NULL, + @UniqueKey = NULL; -SELECT @RC AS RC; +SELECT @RC = [dbo].[GetTopicID]( + @UniqueKey + ); - +SELECT @RC AS RC; -- database unit test for dbo.GetUniqueKey -DECLARE @RC AS VARCHAR (MAX), @TopicID AS INT; +DECLARE @RC AS VARCHAR (MAX), + @TopicID AS INT; -SELECT @RC = NULL, - @TopicID = 0; +SELECT @RC = NULL, + @TopicID = 0; -SELECT @RC = [dbo].[GetUniqueKey](@TopicID); +SELECT @RC = [dbo].[GetUniqueKey]( + @TopicID + ); -SELECT @RC AS RC; - - +SELECT @RC AS RC; -- database unit test for dbo.FindTopicIDs -DECLARE @TopicID AS INT, @AttributeKey AS VARCHAR (255), @AttributeValue AS NVARCHAR (255), @IsExtendedAttribute AS BIT, @UsePartialMatch AS BIT; - -SELECT @TopicID = 0, - @AttributeKey = NULL, - @AttributeValue = NULL, - @IsExtendedAttribute = 0, - @UsePartialMatch = 0; - -SELECT * -FROM [dbo].[FindTopicIDs](@TopicID, @AttributeKey, @AttributeValue, @IsExtendedAttribute, @UsePartialMatch); - - +DECLARE @TopicID AS INT, + @AttributeKey AS VARCHAR (255), + @AttributeValue AS NVARCHAR (255), + @IsExtendedAttribute AS BIT, + @UsePartialMatch AS BIT; + +SELECT @TopicID = 0, + @AttributeKey = NULL, + @AttributeValue = NULL, + @IsExtendedAttribute = 0, + @UsePartialMatch = 0; + +SELECT * +FROM [dbo].[FindTopicIDs]( + @TopicID, + @AttributeKey, + @AttributeValue, + @IsExtendedAttribute, + @UsePartialMatch + ); -- database unit test for dbo.GetAttributes -DECLARE @TopicID AS INT; +DECLARE @TopicID AS INT; -SELECT @TopicID = 0; +SELECT @TopicID = 0; -SELECT * -FROM [dbo].[GetAttributes](@TopicID); - - +SELECT * +FROM [dbo].[GetAttributes]( + @TopicID + ); -- database unit test for dbo.GetChildTopicIDs -DECLARE @TopicID AS INT; - -SELECT @TopicID = 0; +DECLARE @TopicID AS INT; -SELECT * -FROM [dbo].[GetChildTopicIDs](@TopicID); +SELECT @TopicID = 0; - +SELECT * +FROM [dbo].[GetChildTopicIDs]( + @TopicID + ); True diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index a66bea72..e5a05df5 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -119,155 +119,217 @@ -- database unit test for dbo.CreateTopic -DECLARE @RC AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @ParentID AS INT, @Attributes AS [dbo].[AttributeValues], @ExtendedAttributes AS XML, @References AS [dbo].[TopicReferences], @Version AS DATETIME; - -SELECT @RC = 0, - @Key = NULL, - @ContentType = NULL, - @ParentID = 0, - @ExtendedAttributes = NULL, - @Version = getdate(); - -EXECUTE @RC = [dbo].[CreateTopic] @Key, @ContentType, @ParentID, @Attributes, @ExtendedAttributes, @References, @Version; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME; + +SELECT @RC = 0, + @Key = 'CreateTopicTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = NULL, + @Version = GETUTCDATE(); + +EXECUTE @RC = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @RC AS RC; -- database unit test for dbo.DeleteTopic -DECLARE @RC AS INT, @TopicID AS INT; - -SELECT @RC = 0, - @TopicID = 0; +DECLARE @RC AS INT, + @TopicID AS INT; -EXECUTE @RC = [dbo].[DeleteTopic] @TopicID; +SELECT @RC = 0, + @TopicID = 0; -SELECT @RC AS RC; +EXECUTE @RC = [dbo].[DeleteTopic] + @TopicID; - +SELECT @RC AS RC; -- database unit test for dbo.GetTopicVersion -DECLARE @RC AS INT, @TopicID AS INT, @Version AS DATETIME; +DECLARE @RC AS INT, + @TopicID AS INT, + @Version AS DATETIME; -SELECT @RC = 0, - @TopicID = 0, - @Version = getdate(); +SELECT @RC = 0, + @TopicID = 0, + @Version = GETUTCDATE(); -EXECUTE @RC = [dbo].[GetTopicVersion] @TopicID, @Version; +EXECUTE @RC = [dbo].[GetTopicVersion] + @TopicID, + @Version; -SELECT @RC AS RC; - - +SELECT @RC AS RC; -- database unit test for dbo.GetTopics -DECLARE @RC AS INT, @TopicID AS INT, @DeepLoad AS BIT, @UniqueKey AS NVARCHAR (255); - -SELECT @RC = 0, - @TopicID = 0, - @DeepLoad = 0, - @UniqueKey = NULL; - -EXECUTE @RC = [dbo].[GetTopics] @TopicID, @DeepLoad, @UniqueKey; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @DeepLoad AS BIT, + @UniqueKey AS NVARCHAR (255); + +SELECT @RC = 0, + @TopicID = 0, + @DeepLoad = 0, + @UniqueKey = NULL; + +EXECUTE @RC = [dbo].[GetTopics] + @TopicID, + @DeepLoad, + @UniqueKey; + +SELECT @RC AS RC; -- database unit test for dbo.MoveTopic -DECLARE @RC AS INT, @TopicID AS INT, @ParentID AS INT, @SiblingID AS INT; - -SELECT @RC = 0, - @TopicID = 0, - @ParentID = 0, - @SiblingID = 0; - -EXECUTE @RC = [dbo].[MoveTopic] @TopicID, @ParentID, @SiblingID; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @ParentID AS INT, + @SiblingID AS INT; + +SELECT @RC = 0, + @TopicID = 0, + @ParentID = 0, + @SiblingID = 0; + +EXECUTE @RC = [dbo].[MoveTopic] + @TopicID, + @ParentID, + @SiblingID; + +SELECT @RC AS RC; -- database unit test for dbo.UpdateAttributes -DECLARE @RC AS INT, @TopicID AS INT, @Attributes AS [dbo].[AttributeValues], @Version AS DATETIME, @DeleteUnmatched AS BIT; - -SELECT @RC = 0, - @TopicID = 0, - @Version = getdate(), - @DeleteUnmatched = 0; - -EXECUTE @RC = [dbo].[UpdateAttributes] @TopicID, @Attributes, @Version, @DeleteUnmatched; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @Attributes AS [dbo].[AttributeValues], + @Version AS DATETIME, + @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Version = getdate(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateAttributes] + @TopicID, + @Attributes, + @Version, + @DeleteUnmatched; + +SELECT @RC AS RC; -- database unit test for dbo.UpdateExtendedAttributes -DECLARE @RC AS INT, @TopicID AS INT, @ExtendedAttributes AS XML, @Version AS DATETIME, @DeleteUnmatched AS BIT; - -SELECT @RC = 0, - @TopicID = 0, - @ExtendedAttributes = NULL, - @Version = getdate(), - @DeleteUnmatched = 0; - -EXECUTE @RC = [dbo].[UpdateExtendedAttributes] @TopicID, @ExtendedAttributes, @Version, @DeleteUnmatched; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @ExtendedAttributes AS XML, + @Version AS DATETIME, + @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @ExtendedAttributes = NULL, + @Version = GETUTCDATE(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateExtendedAttributes] + @TopicID, + @ExtendedAttributes, + @Version, + @DeleteUnmatched; + +SELECT @RC AS RC; -- database unit test for dbo.UpdateReferences -DECLARE @RC AS INT, @TopicID AS INT, @ReferencedTopics AS [dbo].[TopicReferences], @Version AS DATETIME, @DeleteUnmatched AS BIT; - -SELECT @RC = 0, - @TopicID = 0, - @Version = getdate(), - @DeleteUnmatched = 0; - -EXECUTE @RC = [dbo].[UpdateReferences] @TopicID, @ReferencedTopics, @Version, @DeleteUnmatched; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @ReferencedTopics AS [dbo].[TopicReferences], + @Version AS DATETIME, + @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Version = GETUTCDATE(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateReferences] + @TopicID, + @ReferencedTopics, + @Version, + @DeleteUnmatched; + +SELECT @RC AS RC; -- database unit test for dbo.UpdateRelationships -DECLARE @RC AS INT, @TopicID AS INT, @RelationshipKey AS VARCHAR (255), @RelatedTopics AS [dbo].[TopicList], @Version AS DATETIME, @DeleteUnmatched AS BIT; - -SELECT @RC = 0, - @TopicID = 0, - @RelationshipKey = NULL, - @Version = getdate(), - @DeleteUnmatched = 0; - -EXECUTE @RC = [dbo].[UpdateRelationships] @TopicID, @RelationshipKey, @RelatedTopics, @Version, @DeleteUnmatched; - -SELECT @RC AS RC; - - +DECLARE @RC AS INT, + @TopicID AS INT, + @RelationshipKey AS VARCHAR (255), + @RelatedTopics AS [dbo].[TopicList], + @Version AS DATETIME, + @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @RelationshipKey = NULL, + @Version = GETUTCDATE(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateRelationships] + @TopicID, + @RelationshipKey, + @RelatedTopics, + @Version, + @DeleteUnmatched; + +SELECT @RC AS RC; -- database unit test for dbo.UpdateTopic -DECLARE @RC AS INT, @TopicID AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @Attributes AS [dbo].[AttributeValues], @ExtendedAttributes AS XML, @Version AS DATETIME, @DeleteUnmatched AS BIT; - -SELECT @RC = 0, - @TopicID = 0, - @Key = NULL, - @ContentType = NULL, - @ExtendedAttributes = NULL, - @Version = getdate(), - @DeleteUnmatched = 0; - -EXECUTE @RC = [dbo].[UpdateTopic] @TopicID, @Key, @ContentType, @Attributes, @ExtendedAttributes, @Version, @DeleteUnmatched; - -SELECT @RC AS RC; +DECLARE @RC AS INT, + @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @Version AS DATETIME, + @DeleteUnmatched AS BIT; + +SELECT @RC = 0, + @TopicID = 0, + @Key = NULL, + @ContentType = NULL, + @ExtendedAttributes = NULL, + @Version = GETUTCDATE(), + @DeleteUnmatched = 0; + +EXECUTE @RC = [dbo].[UpdateTopic] + @TopicID, + @Key, + @ContentType, + @Attributes, + @ExtendedAttributes, + @Version, + @DeleteUnmatched; + +SELECT @RC AS RC; True From e77013b4f43ac9bd55851968db9823fca1bc8236 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 15:46:35 -0800 Subject: [PATCH 303/778] Implement basic unit test for the `CreateTopic` stored procedure The `CreateTopicTest` creates a new topic using the `CreateTopic` stored procedure, verifies that one record is created, and then deleted the record. It does not attempt to create any attributes, extended attributes, relationships, or referenced topics. --- .../StoredProcedures.cs | 32 ++++++++++--- .../StoredProcedures.resx | 45 ++++++++++++------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 2e010f36..e52e2071 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -33,7 +33,7 @@ public void TestCleanup() { private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_TestAction; System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(StoredProcedures)); - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition topicTotal; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition2; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; @@ -52,6 +52,8 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition9; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition10; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -63,7 +65,7 @@ private void InitializeComponent() { this.dbo_UpdateRelationshipsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_UpdateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); dbo_CreateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + topicTotal = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition2 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -82,16 +84,20 @@ private void InitializeComponent() { inconclusiveCondition9 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_CreateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + deleteCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // - dbo_CreateTopicTest_TestAction.Conditions.Add(inconclusiveCondition1); + dbo_CreateTopicTest_TestAction.Conditions.Add(topicTotal); resources.ApplyResources(dbo_CreateTopicTest_TestAction, "dbo_CreateTopicTest_TestAction"); // - // inconclusiveCondition1 + // topicTotal // - inconclusiveCondition1.Enabled = true; - inconclusiveCondition1.Name = "inconclusiveCondition1"; + topicTotal.Enabled = true; + topicTotal.Name = "topicTotal"; + topicTotal.ResultSet = 1; + topicTotal.RowCount = 1; // // dbo_DeleteTopicTest_TestAction // @@ -183,9 +189,21 @@ private void InitializeComponent() { inconclusiveCondition10.Enabled = true; inconclusiveCondition10.Name = "inconclusiveCondition10"; // + // dbo_CreateTopicTest_PosttestAction + // + dbo_CreateTopicTest_PosttestAction.Conditions.Add(deleteCount); + resources.ApplyResources(dbo_CreateTopicTest_PosttestAction, "dbo_CreateTopicTest_PosttestAction"); + // + // deleteCount + // + deleteCount.Enabled = true; + deleteCount.Name = "deleteCount"; + deleteCount.ResultSet = 1; + deleteCount.RowCount = 0; + // // dbo_CreateTopicTestData // - this.dbo_CreateTopicTestData.PosttestAction = null; + this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; this.dbo_CreateTopicTestData.PretestAction = null; this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; // diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index e5a05df5..90fffa58 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -119,13 +119,13 @@ -- database unit test for dbo.CreateTopic -DECLARE @RC AS INT, - @Key AS VARCHAR (128), - @ContentType AS VARCHAR (128), - @ParentID AS INT, - @Attributes AS [dbo].[AttributeValues], - @ExtendedAttributes AS XML, - @References AS [dbo].[TopicReferences], +DECLARE @RC AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], @Version AS DATETIME; SELECT @RC = 0, @@ -135,16 +135,18 @@ SELECT @RC = 0, @ExtendedAttributes = NULL, @Version = GETUTCDATE(); -EXECUTE @RC = [dbo].[CreateTopic] - @Key, - @ContentType, - @ParentID, - @Attributes, - @ExtendedAttributes, - @References, +EXECUTE @RC = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, @Version; -SELECT @RC AS RC; +SELECT TopicID +FROM Topics +WHERE TopicKey = @Key -- database unit test for dbo.DeleteTopic @@ -330,6 +332,19 @@ EXECUTE @RC = [dbo].[UpdateTopic] @DeleteUnmatched; SELECT @RC AS RC; + + + DELETE +FROM Topics +WHERE TopicKey = 'CreateTopicTest' + AND ContentType = 'Test' + AND ParentID is NULL + +SELECT * +FROM Topics +WHERE TopicKey = 'CreateTopicTest' + AND ContentType = 'Test' + AND ParentID is NULL True From b1367265f2b9465560250d4249998bcbcda2031a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 16:27:42 -0800 Subject: [PATCH 304/778] Implement basic unit test for the `DeleteTopic` stored procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `DeleteTopicTest` creates two new topics using the `CreateTopic` stored procedure, along with attributes, extended attributes, relationships, and topic references. It then verifies that those records are created—and then subsequently deleted using the `DeleteTopic` stored procedure. --- .../StoredProcedures.cs | 96 +++++++++++-- .../StoredProcedures.resx | 126 +++++++++++++++++- 2 files changed, 208 insertions(+), 14 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index e52e2071..3f1b0a0c 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -35,7 +35,6 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(StoredProcedures)); Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition topicTotal; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition2; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; @@ -54,6 +53,15 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition10; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteReferenceCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -67,7 +75,6 @@ private void InitializeComponent() { dbo_CreateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); topicTotal = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition2 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -86,6 +93,15 @@ private void InitializeComponent() { inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_CreateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); deleteCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_DeleteTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + initializeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postDeleteTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + initializeAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + initializeRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + initializeReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postDeleteAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postDeleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postDeleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -101,14 +117,12 @@ private void InitializeComponent() { // // dbo_DeleteTopicTest_TestAction // - dbo_DeleteTopicTest_TestAction.Conditions.Add(inconclusiveCondition2); + dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteTopicCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteAttributeCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteRelationshipCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteReferenceCount); resources.ApplyResources(dbo_DeleteTopicTest_TestAction, "dbo_DeleteTopicTest_TestAction"); // - // inconclusiveCondition2 - // - inconclusiveCondition2.Enabled = true; - inconclusiveCondition2.Name = "inconclusiveCondition2"; - // // dbo_GetTopicVersionTest_TestAction // dbo_GetTopicVersionTest_TestAction.Conditions.Add(inconclusiveCondition3); @@ -210,7 +224,7 @@ private void InitializeComponent() { // dbo_DeleteTopicTestData // this.dbo_DeleteTopicTestData.PosttestAction = null; - this.dbo_DeleteTopicTestData.PretestAction = null; + this.dbo_DeleteTopicTestData.PretestAction = dbo_DeleteTopicTest_PretestAction; this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; // // dbo_GetTopicVersionTestData @@ -260,6 +274,70 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = null; this.dbo_UpdateTopicTestData.PretestAction = null; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // dbo_DeleteTopicTest_PretestAction + // + dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeTopicCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeAttributeCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeRelationshipCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeReferenceCount); + resources.ApplyResources(dbo_DeleteTopicTest_PretestAction, "dbo_DeleteTopicTest_PretestAction"); + // + // initializeTopicCount + // + initializeTopicCount.Enabled = true; + initializeTopicCount.Name = "initializeTopicCount"; + initializeTopicCount.ResultSet = 1; + initializeTopicCount.RowCount = 2; + // + // postDeleteTopicCount + // + postDeleteTopicCount.Enabled = true; + postDeleteTopicCount.Name = "postDeleteTopicCount"; + postDeleteTopicCount.ResultSet = 1; + postDeleteTopicCount.RowCount = 0; + // + // initializeAttributeCount + // + initializeAttributeCount.Enabled = true; + initializeAttributeCount.Name = "initializeAttributeCount"; + initializeAttributeCount.ResultSet = 2; + initializeAttributeCount.RowCount = 4; + // + // initializeRelationshipCount + // + initializeRelationshipCount.Enabled = true; + initializeRelationshipCount.Name = "initializeRelationshipCount"; + initializeRelationshipCount.ResultSet = 3; + initializeRelationshipCount.RowCount = 1; + // + // initializeReferenceCount + // + initializeReferenceCount.Enabled = true; + initializeReferenceCount.Name = "initializeReferenceCount"; + initializeReferenceCount.ResultSet = 4; + initializeReferenceCount.RowCount = 1; + // + // postDeleteAttributeCount + // + postDeleteAttributeCount.Enabled = true; + postDeleteAttributeCount.Name = "postDeleteAttributeCount"; + postDeleteAttributeCount.ResultSet = 2; + postDeleteAttributeCount.RowCount = 0; + // + // postDeleteRelationshipCount + // + postDeleteRelationshipCount.Enabled = true; + postDeleteRelationshipCount.Name = "postDeleteRelationshipCount"; + postDeleteRelationshipCount.ResultSet = 3; + postDeleteRelationshipCount.RowCount = 0; + // + // postDeleteReferenceCount + // + postDeleteReferenceCount.Enabled = true; + postDeleteReferenceCount.Name = "postDeleteReferenceCount"; + postDeleteReferenceCount.ResultSet = 4; + postDeleteReferenceCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 90fffa58..7366515c 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -150,16 +150,35 @@ WHERE TopicKey = @Key -- database unit test for dbo.DeleteTopic -DECLARE @RC AS INT, - @TopicID AS INT; +DECLARE @RC AS INT, + @TopicID AS INT, + @TopicKey AS VARCHAR(128); SELECT @RC = 0, - @TopicID = 0; + @TopicKey = 'DeleteTopicTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @TopicKey -EXECUTE @RC = [dbo].[DeleteTopic] +EXECUTE @RC = [dbo].[DeleteTopic] @TopicID; -SELECT @RC AS RC; +SELECT * +FROM Topics +WHERE TopicKey LIKE 'DeleteTopic%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'DeleteTopic%' + +SELECT * +FROM Relationships +WHERE RelationshipKey LIKE 'DeleteTopic%' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey LIKE 'DeleteTopic%' -- database unit test for dbo.GetTopicVersion @@ -345,6 +364,103 @@ FROM Topics WHERE TopicKey = 'CreateTopicTest' AND ContentType = 'Test' AND ParentID is NULL + + + DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME; + +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'DeleteTopicTest%' + +DELETE +FROM Relationships +WHERE RelationshipKey = 'DeleteTopicTest' + +DELETE +FROM TopicReferences +WHERE ReferenceKey = 'DeleteTopicTest' + +DELETE +FROM Topics +WHERE TopicKey LIKE 'DeleteTopic%' + + +SELECT @Key = 'DeleteTopicTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @Version = GETUTCDATE(); + +INSERT +INTO @Attributes +VALUES ( 'DeleteTopicTest1', 'Value' ), + ( 'DeleteTopicTest2', 'Value' ) + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @Key = 'DeleteTopicChildTest', + @ParentID = @TopicID; + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'DeleteTopicTest', + @ParentID + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'DeleteTopicTest', + @ParentID + ) + +SELECT * +FROM Topics +WHERE TopicKey LIKE 'DeleteTopic%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'DeleteTopic%' + +SELECT * +FROM Relationships +WHERE RelationshipKey LIKE 'DeleteTopic%' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey LIKE 'DeleteTopic%' True From d73eba8ede7be08a26b33f1b4a68f174768de99e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 16:42:34 -0800 Subject: [PATCH 305/778] Renamed test condition names Test condition names must be globally unique. Hmm. Given this, I renamed the test condition names to standardize the conventions while avoiding naming conflicts. --- .../StoredProcedures.cs | 200 +++++++++--------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 3f1b0a0c..cd6fbcf2 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -33,8 +33,12 @@ public void TestCleanup() { private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_TestAction; System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(StoredProcedures)); - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition topicTotal; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition createTopicTotal; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; @@ -52,16 +56,12 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition10; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_PosttestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postCreateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_PretestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeRelationshipCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition initializeReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteRelationshipCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postDeleteReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteReferenceCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -73,8 +73,12 @@ private void InitializeComponent() { this.dbo_UpdateRelationshipsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_UpdateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); dbo_CreateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - topicTotal = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + createTopicTotal = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + deleteTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + deleteAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + deleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + deleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -92,37 +96,61 @@ private void InitializeComponent() { dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_CreateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - deleteCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postCreateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - initializeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - postDeleteTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - initializeAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - initializeRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - initializeReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - postDeleteAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - postDeleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - postDeleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preDeleteTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preDeleteAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preDeleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preDeleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // - dbo_CreateTopicTest_TestAction.Conditions.Add(topicTotal); + dbo_CreateTopicTest_TestAction.Conditions.Add(createTopicTotal); resources.ApplyResources(dbo_CreateTopicTest_TestAction, "dbo_CreateTopicTest_TestAction"); // - // topicTotal + // createTopicTotal // - topicTotal.Enabled = true; - topicTotal.Name = "topicTotal"; - topicTotal.ResultSet = 1; - topicTotal.RowCount = 1; + createTopicTotal.Enabled = true; + createTopicTotal.Name = "createTopicTotal"; + createTopicTotal.ResultSet = 1; + createTopicTotal.RowCount = 1; // // dbo_DeleteTopicTest_TestAction // - dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteTopicCount); - dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteAttributeCount); - dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteRelationshipCount); - dbo_DeleteTopicTest_TestAction.Conditions.Add(postDeleteReferenceCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(deleteTopicCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(deleteAttributeCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(deleteRelationshipCount); + dbo_DeleteTopicTest_TestAction.Conditions.Add(deleteReferenceCount); resources.ApplyResources(dbo_DeleteTopicTest_TestAction, "dbo_DeleteTopicTest_TestAction"); // + // deleteTopicCount + // + deleteTopicCount.Enabled = true; + deleteTopicCount.Name = "deleteTopicCount"; + deleteTopicCount.ResultSet = 1; + deleteTopicCount.RowCount = 0; + // + // deleteAttributeCount + // + deleteAttributeCount.Enabled = true; + deleteAttributeCount.Name = "deleteAttributeCount"; + deleteAttributeCount.ResultSet = 2; + deleteAttributeCount.RowCount = 0; + // + // deleteRelationshipCount + // + deleteRelationshipCount.Enabled = true; + deleteRelationshipCount.Name = "deleteRelationshipCount"; + deleteRelationshipCount.ResultSet = 3; + deleteRelationshipCount.RowCount = 0; + // + // deleteReferenceCount + // + deleteReferenceCount.Enabled = true; + deleteReferenceCount.Name = "deleteReferenceCount"; + deleteReferenceCount.ResultSet = 4; + deleteReferenceCount.RowCount = 0; + // // dbo_GetTopicVersionTest_TestAction // dbo_GetTopicVersionTest_TestAction.Conditions.Add(inconclusiveCondition3); @@ -205,15 +233,51 @@ private void InitializeComponent() { // // dbo_CreateTopicTest_PosttestAction // - dbo_CreateTopicTest_PosttestAction.Conditions.Add(deleteCount); + dbo_CreateTopicTest_PosttestAction.Conditions.Add(postCreateTopicCount); resources.ApplyResources(dbo_CreateTopicTest_PosttestAction, "dbo_CreateTopicTest_PosttestAction"); // - // deleteCount + // postCreateTopicCount + // + postCreateTopicCount.Enabled = true; + postCreateTopicCount.Name = "postCreateTopicCount"; + postCreateTopicCount.ResultSet = 1; + postCreateTopicCount.RowCount = 0; + // + // dbo_DeleteTopicTest_PretestAction + // + dbo_DeleteTopicTest_PretestAction.Conditions.Add(preDeleteTopicCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(preDeleteAttributeCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(preDeleteRelationshipCount); + dbo_DeleteTopicTest_PretestAction.Conditions.Add(preDeleteReferenceCount); + resources.ApplyResources(dbo_DeleteTopicTest_PretestAction, "dbo_DeleteTopicTest_PretestAction"); + // + // preDeleteTopicCount + // + preDeleteTopicCount.Enabled = true; + preDeleteTopicCount.Name = "preDeleteTopicCount"; + preDeleteTopicCount.ResultSet = 1; + preDeleteTopicCount.RowCount = 2; + // + // preDeleteAttributeCount + // + preDeleteAttributeCount.Enabled = true; + preDeleteAttributeCount.Name = "preDeleteAttributeCount"; + preDeleteAttributeCount.ResultSet = 2; + preDeleteAttributeCount.RowCount = 4; + // + // preDeleteRelationshipCount + // + preDeleteRelationshipCount.Enabled = true; + preDeleteRelationshipCount.Name = "preDeleteRelationshipCount"; + preDeleteRelationshipCount.ResultSet = 3; + preDeleteRelationshipCount.RowCount = 1; + // + // preDeleteReferenceCount // - deleteCount.Enabled = true; - deleteCount.Name = "deleteCount"; - deleteCount.ResultSet = 1; - deleteCount.RowCount = 0; + preDeleteReferenceCount.Enabled = true; + preDeleteReferenceCount.Name = "preDeleteReferenceCount"; + preDeleteReferenceCount.ResultSet = 4; + preDeleteReferenceCount.RowCount = 1; // // dbo_CreateTopicTestData // @@ -274,70 +338,6 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = null; this.dbo_UpdateTopicTestData.PretestAction = null; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; - // - // dbo_DeleteTopicTest_PretestAction - // - dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeTopicCount); - dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeAttributeCount); - dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeRelationshipCount); - dbo_DeleteTopicTest_PretestAction.Conditions.Add(initializeReferenceCount); - resources.ApplyResources(dbo_DeleteTopicTest_PretestAction, "dbo_DeleteTopicTest_PretestAction"); - // - // initializeTopicCount - // - initializeTopicCount.Enabled = true; - initializeTopicCount.Name = "initializeTopicCount"; - initializeTopicCount.ResultSet = 1; - initializeTopicCount.RowCount = 2; - // - // postDeleteTopicCount - // - postDeleteTopicCount.Enabled = true; - postDeleteTopicCount.Name = "postDeleteTopicCount"; - postDeleteTopicCount.ResultSet = 1; - postDeleteTopicCount.RowCount = 0; - // - // initializeAttributeCount - // - initializeAttributeCount.Enabled = true; - initializeAttributeCount.Name = "initializeAttributeCount"; - initializeAttributeCount.ResultSet = 2; - initializeAttributeCount.RowCount = 4; - // - // initializeRelationshipCount - // - initializeRelationshipCount.Enabled = true; - initializeRelationshipCount.Name = "initializeRelationshipCount"; - initializeRelationshipCount.ResultSet = 3; - initializeRelationshipCount.RowCount = 1; - // - // initializeReferenceCount - // - initializeReferenceCount.Enabled = true; - initializeReferenceCount.Name = "initializeReferenceCount"; - initializeReferenceCount.ResultSet = 4; - initializeReferenceCount.RowCount = 1; - // - // postDeleteAttributeCount - // - postDeleteAttributeCount.Enabled = true; - postDeleteAttributeCount.Name = "postDeleteAttributeCount"; - postDeleteAttributeCount.ResultSet = 2; - postDeleteAttributeCount.RowCount = 0; - // - // postDeleteRelationshipCount - // - postDeleteRelationshipCount.Enabled = true; - postDeleteRelationshipCount.Name = "postDeleteRelationshipCount"; - postDeleteRelationshipCount.ResultSet = 3; - postDeleteRelationshipCount.RowCount = 0; - // - // postDeleteReferenceCount - // - postDeleteReferenceCount.Enabled = true; - postDeleteReferenceCount.Name = "postDeleteReferenceCount"; - postDeleteReferenceCount.ResultSet = 4; - postDeleteReferenceCount.RowCount = 0; } #endregion From 5c388953e305518540444a36447cbac69d6e45db Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 16:49:08 -0800 Subject: [PATCH 306/778] Added comment headers to organize unit tests --- .../StoredProcedures.resx | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 7366515c..62d49b28 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -118,7 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - -- database unit test for dbo.CreateTopic + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- DECLARE @RC AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @@ -128,6 +130,9 @@ DECLARE @RC AS INT, @References AS [dbo].[TopicReferences], @Version AS DATETIME; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- SELECT @RC = 0, @Key = 'CreateTopicTest', @ContentType = 'Test', @@ -135,6 +140,9 @@ SELECT @RC = 0, @ExtendedAttributes = NULL, @Version = GETUTCDATE(); +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- EXECUTE @RC = [dbo].[CreateTopic] @Key, @ContentType, @@ -144,16 +152,24 @@ EXECUTE @RC = [dbo].[CreateTopic] @References, @Version; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- SELECT TopicID FROM Topics WHERE TopicKey = @Key - -- database unit test for dbo.DeleteTopic + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- DECLARE @RC AS INT, @TopicID AS INT, @TopicKey AS VARCHAR(128); +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- SELECT @RC = 0, @TopicKey = 'DeleteTopicTest'; @@ -161,9 +177,15 @@ SELECT @TopicID = TopicID FROM Topics WHERE TopicKey = @TopicKey +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- EXECUTE @RC = [dbo].[DeleteTopic] @TopicID; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics WHERE TopicKey LIKE 'DeleteTopic%' @@ -353,12 +375,18 @@ EXECUTE @RC = [dbo].[UpdateTopic] SELECT @RC AS RC; - DELETE + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE FROM Topics WHERE TopicKey = 'CreateTopicTest' AND ContentType = 'Test' AND ParentID is NULL +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics WHERE TopicKey = 'CreateTopicTest' @@ -366,7 +394,10 @@ WHERE TopicKey = 'CreateTopicTest' AND ParentID is NULL - DECLARE @TopicID AS INT, + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @ParentID AS INT, @@ -375,6 +406,9 @@ WHERE TopicKey = 'CreateTopicTest' @References AS [dbo].[TopicReferences], @Version AS DATETIME; +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- DELETE FROM Attributes WHERE AttributeKey LIKE 'DeleteTopicTest%' @@ -391,7 +425,9 @@ DELETE FROM Topics WHERE TopicKey LIKE 'DeleteTopic%' - +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'DeleteTopicTest', @ContentType = 'Test', @ParentID = NULL, @@ -403,6 +439,9 @@ INTO @Attributes VALUES ( 'DeleteTopicTest1', 'Value' ), ( 'DeleteTopicTest2', 'Value' ) +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- EXECUTE @TopicID = [dbo].[CreateTopic] @Key, @ContentType, @@ -446,6 +485,9 @@ VALUES ( @TopicID, @ParentID ) +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics WHERE TopicKey LIKE 'DeleteTopic%' From ac0d9b9eff5fe9edde54e4861d38e6732a1601f3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 17:20:39 -0800 Subject: [PATCH 307/778] Implement basic unit test for the `GetTopics` stored procedure The `GetTopicsTest` creates two new topics using the `CreateTopic` stored procedure, along with attributes, extended attributes, relationships, and topic references. It then verifies that those records are all selected via the `GetTopics` stored procedure. --- .../StoredProcedures.cs | 124 +++++++++++-- .../StoredProcedures.resx | 166 ++++++++++++++++-- 2 files changed, 267 insertions(+), 23 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index cd6fbcf2..984a4470 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -42,7 +42,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction; @@ -62,6 +61,18 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preDeleteReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -82,7 +93,6 @@ private void InitializeComponent() { dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -102,6 +112,18 @@ private void InitializeComponent() { preDeleteAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preDeleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preDeleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetTopicsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preGetTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetTopicsTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -163,14 +185,14 @@ private void InitializeComponent() { // // dbo_GetTopicsTest_TestAction // - dbo_GetTopicsTest_TestAction.Conditions.Add(inconclusiveCondition4); + dbo_GetTopicsTest_TestAction.Conditions.Add(getTopicCount); + dbo_GetTopicsTest_TestAction.Conditions.Add(getAttributeCount); + dbo_GetTopicsTest_TestAction.Conditions.Add(getExtendedAttributeCount); + dbo_GetTopicsTest_TestAction.Conditions.Add(getRelationshipCount); + dbo_GetTopicsTest_TestAction.Conditions.Add(getReferenceCount); + dbo_GetTopicsTest_TestAction.Conditions.Add(getVersionHistoryCount); resources.ApplyResources(dbo_GetTopicsTest_TestAction, "dbo_GetTopicsTest_TestAction"); // - // inconclusiveCondition4 - // - inconclusiveCondition4.Enabled = true; - inconclusiveCondition4.Name = "inconclusiveCondition4"; - // // dbo_MoveTopicTest_TestAction // dbo_MoveTopicTest_TestAction.Conditions.Add(inconclusiveCondition5); @@ -299,8 +321,8 @@ private void InitializeComponent() { // // dbo_GetTopicsTestData // - this.dbo_GetTopicsTestData.PosttestAction = null; - this.dbo_GetTopicsTestData.PretestAction = null; + this.dbo_GetTopicsTestData.PosttestAction = dbo_GetTopicsTest_PosttestAction; + this.dbo_GetTopicsTestData.PretestAction = dbo_GetTopicsTest_PretestAction; this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; // // dbo_MoveTopicTestData @@ -338,6 +360,88 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = null; this.dbo_UpdateTopicTestData.PretestAction = null; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // dbo_GetTopicsTest_PretestAction + // + dbo_GetTopicsTest_PretestAction.Conditions.Add(preGetTopicCount); + dbo_GetTopicsTest_PretestAction.Conditions.Add(preGetAttributeCount); + dbo_GetTopicsTest_PretestAction.Conditions.Add(preGetRelationshipCount); + dbo_GetTopicsTest_PretestAction.Conditions.Add(preGetReferenceCount); + resources.ApplyResources(dbo_GetTopicsTest_PretestAction, "dbo_GetTopicsTest_PretestAction"); + // + // preGetTopicCount + // + preGetTopicCount.Enabled = true; + preGetTopicCount.Name = "preGetTopicCount"; + preGetTopicCount.ResultSet = 1; + preGetTopicCount.RowCount = 2; + // + // preGetAttributeCount + // + preGetAttributeCount.Enabled = true; + preGetAttributeCount.Name = "preGetAttributeCount"; + preGetAttributeCount.ResultSet = 2; + preGetAttributeCount.RowCount = 4; + // + // preGetRelationshipCount + // + preGetRelationshipCount.Enabled = true; + preGetRelationshipCount.Name = "preGetRelationshipCount"; + preGetRelationshipCount.ResultSet = 3; + preGetRelationshipCount.RowCount = 1; + // + // preGetReferenceCount + // + preGetReferenceCount.Enabled = true; + preGetReferenceCount.Name = "preGetReferenceCount"; + preGetReferenceCount.ResultSet = 4; + preGetReferenceCount.RowCount = 1; + // + // dbo_GetTopicsTest_PosttestAction + // + resources.ApplyResources(dbo_GetTopicsTest_PosttestAction, "dbo_GetTopicsTest_PosttestAction"); + // + // getTopicCount + // + getTopicCount.Enabled = true; + getTopicCount.Name = "getTopicCount"; + getTopicCount.ResultSet = 1; + getTopicCount.RowCount = 2; + // + // getAttributeCount + // + getAttributeCount.Enabled = true; + getAttributeCount.Name = "getAttributeCount"; + getAttributeCount.ResultSet = 2; + getAttributeCount.RowCount = 4; + // + // getExtendedAttributeCount + // + getExtendedAttributeCount.Enabled = true; + getExtendedAttributeCount.Name = "getExtendedAttributeCount"; + getExtendedAttributeCount.ResultSet = 3; + getExtendedAttributeCount.RowCount = 2; + // + // getRelationshipCount + // + getRelationshipCount.Enabled = true; + getRelationshipCount.Name = "getRelationshipCount"; + getRelationshipCount.ResultSet = 4; + getRelationshipCount.RowCount = 1; + // + // getReferenceCount + // + getReferenceCount.Enabled = true; + getReferenceCount.Name = "getReferenceCount"; + getReferenceCount.ResultSet = 5; + getReferenceCount.RowCount = 1; + // + // getVersionHistoryCount + // + getVersionHistoryCount.Enabled = true; + getVersionHistoryCount.Name = "getVersionHistoryCount"; + getVersionHistoryCount.ResultSet = 6; + getVersionHistoryCount.RowCount = 2; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 62d49b28..18a4e354 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -219,23 +219,30 @@ EXECUTE @RC = [dbo].[GetTopicVersion] SELECT @RC AS RC; - -- database unit test for dbo.GetTopics -DECLARE @RC AS INT, - @TopicID AS INT, - @DeepLoad AS BIT, + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @DeepLoad AS BIT, @UniqueKey AS NVARCHAR (255); -SELECT @RC = 0, - @TopicID = 0, - @DeepLoad = 0, - @UniqueKey = NULL; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @DeepLoad = 1, + @UniqueKey = 'GetTopicsTest'; -EXECUTE @RC = [dbo].[GetTopics] - @TopicID, - @DeepLoad, - @UniqueKey; +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @UniqueKey -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[GetTopics] + @TopicID, + @DeepLoad, + NULL; -- database unit test for dbo.MoveTopic @@ -503,6 +510,139 @@ WHERE RelationshipKey LIKE 'DeleteTopic%' SELECT * FROM TopicReferences WHERE ReferenceKey LIKE 'DeleteTopic%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @UniqueKey AS NVARCHAR (255); + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = 'GetTopicsTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @UniqueKey + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[DeleteTopic] + @TopicID; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicsTest%' + +DELETE +FROM Relationships +WHERE RelationshipKey = 'GetTopicsTest' + +DELETE +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicsTest' + +DELETE +FROM Topics +WHERE TopicKey LIKE 'GetTopics%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetTopicsTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @Version = GETUTCDATE(); + +INSERT +INTO @Attributes +VALUES ( 'GetTopicsTest1', 'Value' ), + ( 'GetTopicsTest2', 'Value' ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @Key = 'GetTopicsChildTest', + @ParentID = @TopicID; + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'GetTopicsTest', + @ParentID + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'GetTopicsTest', + @ParentID + ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'GetTopics%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicsTest%' + +SELECT * +FROM Relationships +WHERE RelationshipKey = 'GetTopicsTest' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicsTest' True From d6250898bd6bc540e822d7786673845b022969d1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 15 Jan 2021 18:09:24 -0800 Subject: [PATCH 308/778] Implement basic unit test for the `GetTopicVersion` stored procedure The `GetTopicVersionTest` creates two new topics using the `CreateTopic` and `UpdateTopic` stored procedures, along with attributes, extended attributes, relationships, and topic references. It then creates a new version of those. In the actual test, those records are all selected via the `GetTopicVersion` stored procedure. --- .../StoredProcedures.cs | 124 +++++++++- .../StoredProcedures.resx | 217 +++++++++++++++++- 2 files changed, 320 insertions(+), 21 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 984a4470..ad1eb75a 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -40,7 +40,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; @@ -73,6 +72,18 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionVersionHistoryCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -91,7 +102,6 @@ private void InitializeComponent() { deleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); deleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); @@ -124,6 +134,18 @@ private void InitializeComponent() { getRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetTopicVersionTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preGetVersionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetVersionAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetTopicVersionTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getVersionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -175,14 +197,14 @@ private void InitializeComponent() { // // dbo_GetTopicVersionTest_TestAction // - dbo_GetTopicVersionTest_TestAction.Conditions.Add(inconclusiveCondition3); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionTopicCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionAttributeCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionExtendedAttributeCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionRelationshipCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionReferenceCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionVersionHistoryCount); resources.ApplyResources(dbo_GetTopicVersionTest_TestAction, "dbo_GetTopicVersionTest_TestAction"); // - // inconclusiveCondition3 - // - inconclusiveCondition3.Enabled = true; - inconclusiveCondition3.Name = "inconclusiveCondition3"; - // // dbo_GetTopicsTest_TestAction // dbo_GetTopicsTest_TestAction.Conditions.Add(getTopicCount); @@ -315,8 +337,8 @@ private void InitializeComponent() { // // dbo_GetTopicVersionTestData // - this.dbo_GetTopicVersionTestData.PosttestAction = null; - this.dbo_GetTopicVersionTestData.PretestAction = null; + this.dbo_GetTopicVersionTestData.PosttestAction = dbo_GetTopicVersionTest_PosttestAction; + this.dbo_GetTopicVersionTestData.PretestAction = dbo_GetTopicVersionTest_PretestAction; this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; // // dbo_GetTopicsTestData @@ -442,6 +464,88 @@ private void InitializeComponent() { getVersionHistoryCount.Name = "getVersionHistoryCount"; getVersionHistoryCount.ResultSet = 6; getVersionHistoryCount.RowCount = 2; + // + // dbo_GetTopicVersionTest_PretestAction + // + dbo_GetTopicVersionTest_PretestAction.Conditions.Add(preGetVersionTopicCount); + dbo_GetTopicVersionTest_PretestAction.Conditions.Add(preGetVersionAttributeCount); + dbo_GetTopicVersionTest_PretestAction.Conditions.Add(preGetVersionRelationshipCount); + dbo_GetTopicVersionTest_PretestAction.Conditions.Add(preGetVersionReferenceCount); + resources.ApplyResources(dbo_GetTopicVersionTest_PretestAction, "dbo_GetTopicVersionTest_PretestAction"); + // + // preGetVersionTopicCount + // + preGetVersionTopicCount.Enabled = true; + preGetVersionTopicCount.Name = "preGetVersionTopicCount"; + preGetVersionTopicCount.ResultSet = 1; + preGetVersionTopicCount.RowCount = 2; + // + // preGetVersionAttributeCount + // + preGetVersionAttributeCount.Enabled = true; + preGetVersionAttributeCount.Name = "preGetVersionAttributeCount"; + preGetVersionAttributeCount.ResultSet = 2; + preGetVersionAttributeCount.RowCount = 6; + // + // preGetVersionRelationshipCount + // + preGetVersionRelationshipCount.Enabled = true; + preGetVersionRelationshipCount.Name = "preGetVersionRelationshipCount"; + preGetVersionRelationshipCount.ResultSet = 3; + preGetVersionRelationshipCount.RowCount = 2; + // + // preGetVersionReferenceCount + // + preGetVersionReferenceCount.Enabled = true; + preGetVersionReferenceCount.Name = "preGetVersionReferenceCount"; + preGetVersionReferenceCount.ResultSet = 4; + preGetVersionReferenceCount.RowCount = 2; + // + // dbo_GetTopicVersionTest_PosttestAction + // + resources.ApplyResources(dbo_GetTopicVersionTest_PosttestAction, "dbo_GetTopicVersionTest_PosttestAction"); + // + // getVersionTopicCount + // + getVersionTopicCount.Enabled = true; + getVersionTopicCount.Name = "getVersionTopicCount"; + getVersionTopicCount.ResultSet = 1; + getVersionTopicCount.RowCount = 1; + // + // getVersionAttributeCount + // + getVersionAttributeCount.Enabled = true; + getVersionAttributeCount.Name = "getVersionAttributeCount"; + getVersionAttributeCount.ResultSet = 2; + getVersionAttributeCount.RowCount = 2; + // + // getVersionExtendedAttributeCount + // + getVersionExtendedAttributeCount.Enabled = true; + getVersionExtendedAttributeCount.Name = "getVersionExtendedAttributeCount"; + getVersionExtendedAttributeCount.ResultSet = 3; + getVersionExtendedAttributeCount.RowCount = 1; + // + // getVersionRelationshipCount + // + getVersionRelationshipCount.Enabled = true; + getVersionRelationshipCount.Name = "getVersionRelationshipCount"; + getVersionRelationshipCount.ResultSet = 4; + getVersionRelationshipCount.RowCount = 1; + // + // getVersionReferenceCount + // + getVersionReferenceCount.Enabled = true; + getVersionReferenceCount.Name = "getVersionReferenceCount"; + getVersionReferenceCount.ResultSet = 5; + getVersionReferenceCount.RowCount = 1; + // + // getVersionVersionHistoryCount + // + getVersionVersionHistoryCount.Enabled = true; + getVersionVersionHistoryCount.Name = "getVersionVersionHistoryCount"; + getVersionVersionHistoryCount.ResultSet = 6; + getVersionVersionHistoryCount.RowCount = 1; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 18a4e354..e360b585 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -203,20 +203,31 @@ FROM TopicReferences WHERE ReferenceKey LIKE 'DeleteTopic%' - -- database unit test for dbo.GetTopicVersion -DECLARE @RC AS INT, - @TopicID AS INT, - @Version AS DATETIME; + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @UniqueKey AS VARCHAR(128), + @TopicID AS INT, + @Version AS DATETIME, + @NewVersion AS DATETIME; -SELECT @RC = 0, - @TopicID = 0, - @Version = GETUTCDATE(); +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = 'GetTopicVersionTest', + @Version = '2020-01-01 12:00:00:000', + @NewVersion = '2021-01-01 12:00:00:000'; -EXECUTE @RC = [dbo].[GetTopicVersion] - @TopicID, - @Version; +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @UniqueKey -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[GetTopicVersion] + @TopicID, + @Version; -------------------------------------------------------------------------------------------------------------------------------- @@ -510,6 +521,190 @@ WHERE RelationshipKey LIKE 'DeleteTopic%' SELECT * FROM TopicReferences WHERE ReferenceKey LIKE 'DeleteTopic%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @UniqueKey AS NVARCHAR (255); + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = 'GetTopicVersionTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @UniqueKey + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[DeleteTopic] + @TopicID; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME, + @NewVersion AS DATETIME; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicVersionTest%' + +DELETE +FROM Relationships +WHERE RelationshipKey = 'GetTopicVersionTest' + +DELETE +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicVersionTest' + +DELETE +FROM Topics +WHERE TopicKey LIKE 'GetTopicVersion%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetTopicVersionTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @Version = '2020-01-01 12:00:00:000', + @NewVersion = '2021-01-01 12:00:00:000'; + +INSERT +INTO @Attributes +VALUES ( 'GetTopicVersionTest1', 'Value' ), + ( 'GetTopicVersionTest2', 'Value' ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @Key = 'GetTopicVersionChildTest', + @ParentID = @TopicID; + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicVersionTest', + @TopicID, + @Version + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicVersionTest', + @TopicID, + @Version + ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ExtendedAttributes = '<Attributes><Attribute key=''Body''>New Test</Attribute></Attributes>'; + +UPDATE @Attributes +SET AttributeValue = 'NewValue' + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH NEW VERSION +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[UpdateTopic] + @TopicID = @TopicID, + @Attributes = @Attributes, + @ExtendedAttributes = @ExtendedAttributes, + @Version = @NewVersion; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID, + IsDeleted, + Version + ) +VALUES ( @ParentID, + 'GetTopicVersionTest', + @TopicID, + 1, + @NewVersion + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicVersionTest', + NULL, + @NewVersion + ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'GetTopicVersion%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicVersionTest%' + +SELECT * +FROM Relationships +WHERE RelationshipKey = 'GetTopicVersionTest' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicVersionTest' -------------------------------------------------------------------------------------------------------------------------------- From 3b4474573f4eebbd634e0d3c9bd4ebe93db62fab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 14:26:48 -0800 Subject: [PATCH 309/778] Improve post-conditions for `GetTopicVersion` unit tests The original unit tests validated the number of rows, but that doesn't necessarily validate that the correct data is being returned in those rows. This update extends those conditions to better validate the data from the existing tests. --- .../StoredProcedures.cs | 351 ++++++++++-------- .../StoredProcedures.resx | 230 ++++++------ 2 files changed, 310 insertions(+), 271 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index ad1eb75a..f04f5c6f 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -40,7 +40,19 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition deleteReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction; @@ -66,24 +78,15 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_PosttestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getRelationshipCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_PosttestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionRelationshipCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionVersionHistoryCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionAttributeValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionRelationshipValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionReferenceValue; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -102,7 +105,19 @@ private void InitializeComponent() { deleteRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); deleteReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getVersionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -128,24 +143,15 @@ private void InitializeComponent() { preGetRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicsTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - getTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preGetVersionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetVersionAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - getVersionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getVersionVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getVersionRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getVersionReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_CreateTopicTest_TestAction // @@ -203,8 +209,53 @@ private void InitializeComponent() { dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionRelationshipCount); dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionReferenceCount); dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionVersionHistoryCount); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionAttributeValue); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionRelationshipValue); + dbo_GetTopicVersionTest_TestAction.Conditions.Add(getVersionReferenceValue); resources.ApplyResources(dbo_GetTopicVersionTest_TestAction, "dbo_GetTopicVersionTest_TestAction"); // + // getVersionTopicCount + // + getVersionTopicCount.Enabled = true; + getVersionTopicCount.Name = "getVersionTopicCount"; + getVersionTopicCount.ResultSet = 1; + getVersionTopicCount.RowCount = 1; + // + // getVersionAttributeCount + // + getVersionAttributeCount.Enabled = true; + getVersionAttributeCount.Name = "getVersionAttributeCount"; + getVersionAttributeCount.ResultSet = 2; + getVersionAttributeCount.RowCount = 2; + // + // getVersionExtendedAttributeCount + // + getVersionExtendedAttributeCount.Enabled = true; + getVersionExtendedAttributeCount.Name = "getVersionExtendedAttributeCount"; + getVersionExtendedAttributeCount.ResultSet = 3; + getVersionExtendedAttributeCount.RowCount = 1; + // + // getVersionRelationshipCount + // + getVersionRelationshipCount.Enabled = true; + getVersionRelationshipCount.Name = "getVersionRelationshipCount"; + getVersionRelationshipCount.ResultSet = 4; + getVersionRelationshipCount.RowCount = 1; + // + // getVersionReferenceCount + // + getVersionReferenceCount.Enabled = true; + getVersionReferenceCount.Name = "getVersionReferenceCount"; + getVersionReferenceCount.ResultSet = 5; + getVersionReferenceCount.RowCount = 1; + // + // getVersionVersionHistoryCount + // + getVersionVersionHistoryCount.Enabled = true; + getVersionVersionHistoryCount.Name = "getVersionVersionHistoryCount"; + getVersionVersionHistoryCount.ResultSet = 6; + getVersionVersionHistoryCount.RowCount = 1; + // // dbo_GetTopicsTest_TestAction // dbo_GetTopicsTest_TestAction.Conditions.Add(getTopicCount); @@ -215,6 +266,48 @@ private void InitializeComponent() { dbo_GetTopicsTest_TestAction.Conditions.Add(getVersionHistoryCount); resources.ApplyResources(dbo_GetTopicsTest_TestAction, "dbo_GetTopicsTest_TestAction"); // + // getTopicCount + // + getTopicCount.Enabled = true; + getTopicCount.Name = "getTopicCount"; + getTopicCount.ResultSet = 1; + getTopicCount.RowCount = 2; + // + // getAttributeCount + // + getAttributeCount.Enabled = true; + getAttributeCount.Name = "getAttributeCount"; + getAttributeCount.ResultSet = 2; + getAttributeCount.RowCount = 4; + // + // getExtendedAttributeCount + // + getExtendedAttributeCount.Enabled = true; + getExtendedAttributeCount.Name = "getExtendedAttributeCount"; + getExtendedAttributeCount.ResultSet = 3; + getExtendedAttributeCount.RowCount = 2; + // + // getRelationshipCount + // + getRelationshipCount.Enabled = true; + getRelationshipCount.Name = "getRelationshipCount"; + getRelationshipCount.ResultSet = 4; + getRelationshipCount.RowCount = 1; + // + // getReferenceCount + // + getReferenceCount.Enabled = true; + getReferenceCount.Name = "getReferenceCount"; + getReferenceCount.ResultSet = 5; + getReferenceCount.RowCount = 1; + // + // getVersionHistoryCount + // + getVersionHistoryCount.Enabled = true; + getVersionHistoryCount.Name = "getVersionHistoryCount"; + getVersionHistoryCount.ResultSet = 6; + getVersionHistoryCount.RowCount = 2; + // // dbo_MoveTopicTest_TestAction // dbo_MoveTopicTest_TestAction.Conditions.Add(inconclusiveCondition5); @@ -323,66 +416,6 @@ private void InitializeComponent() { preDeleteReferenceCount.ResultSet = 4; preDeleteReferenceCount.RowCount = 1; // - // dbo_CreateTopicTestData - // - this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; - this.dbo_CreateTopicTestData.PretestAction = null; - this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; - // - // dbo_DeleteTopicTestData - // - this.dbo_DeleteTopicTestData.PosttestAction = null; - this.dbo_DeleteTopicTestData.PretestAction = dbo_DeleteTopicTest_PretestAction; - this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; - // - // dbo_GetTopicVersionTestData - // - this.dbo_GetTopicVersionTestData.PosttestAction = dbo_GetTopicVersionTest_PosttestAction; - this.dbo_GetTopicVersionTestData.PretestAction = dbo_GetTopicVersionTest_PretestAction; - this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; - // - // dbo_GetTopicsTestData - // - this.dbo_GetTopicsTestData.PosttestAction = dbo_GetTopicsTest_PosttestAction; - this.dbo_GetTopicsTestData.PretestAction = dbo_GetTopicsTest_PretestAction; - this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; - // - // dbo_MoveTopicTestData - // - this.dbo_MoveTopicTestData.PosttestAction = null; - this.dbo_MoveTopicTestData.PretestAction = null; - this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; - // - // dbo_UpdateAttributesTestData - // - this.dbo_UpdateAttributesTestData.PosttestAction = null; - this.dbo_UpdateAttributesTestData.PretestAction = null; - this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; - // - // dbo_UpdateExtendedAttributesTestData - // - this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; - this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; - this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; - // - // dbo_UpdateReferencesTestData - // - this.dbo_UpdateReferencesTestData.PosttestAction = null; - this.dbo_UpdateReferencesTestData.PretestAction = null; - this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; - // - // dbo_UpdateRelationshipsTestData - // - this.dbo_UpdateRelationshipsTestData.PosttestAction = null; - this.dbo_UpdateRelationshipsTestData.PretestAction = null; - this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; - // - // dbo_UpdateTopicTestData - // - this.dbo_UpdateTopicTestData.PosttestAction = null; - this.dbo_UpdateTopicTestData.PretestAction = null; - this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; - // // dbo_GetTopicsTest_PretestAction // dbo_GetTopicsTest_PretestAction.Conditions.Add(preGetTopicCount); @@ -423,48 +456,6 @@ private void InitializeComponent() { // resources.ApplyResources(dbo_GetTopicsTest_PosttestAction, "dbo_GetTopicsTest_PosttestAction"); // - // getTopicCount - // - getTopicCount.Enabled = true; - getTopicCount.Name = "getTopicCount"; - getTopicCount.ResultSet = 1; - getTopicCount.RowCount = 2; - // - // getAttributeCount - // - getAttributeCount.Enabled = true; - getAttributeCount.Name = "getAttributeCount"; - getAttributeCount.ResultSet = 2; - getAttributeCount.RowCount = 4; - // - // getExtendedAttributeCount - // - getExtendedAttributeCount.Enabled = true; - getExtendedAttributeCount.Name = "getExtendedAttributeCount"; - getExtendedAttributeCount.ResultSet = 3; - getExtendedAttributeCount.RowCount = 2; - // - // getRelationshipCount - // - getRelationshipCount.Enabled = true; - getRelationshipCount.Name = "getRelationshipCount"; - getRelationshipCount.ResultSet = 4; - getRelationshipCount.RowCount = 1; - // - // getReferenceCount - // - getReferenceCount.Enabled = true; - getReferenceCount.Name = "getReferenceCount"; - getReferenceCount.ResultSet = 5; - getReferenceCount.RowCount = 1; - // - // getVersionHistoryCount - // - getVersionHistoryCount.Enabled = true; - getVersionHistoryCount.Name = "getVersionHistoryCount"; - getVersionHistoryCount.ResultSet = 6; - getVersionHistoryCount.RowCount = 2; - // // dbo_GetTopicVersionTest_PretestAction // dbo_GetTopicVersionTest_PretestAction.Conditions.Add(preGetVersionTopicCount); @@ -505,47 +496,95 @@ private void InitializeComponent() { // resources.ApplyResources(dbo_GetTopicVersionTest_PosttestAction, "dbo_GetTopicVersionTest_PosttestAction"); // - // getVersionTopicCount + // dbo_CreateTopicTestData // - getVersionTopicCount.Enabled = true; - getVersionTopicCount.Name = "getVersionTopicCount"; - getVersionTopicCount.ResultSet = 1; - getVersionTopicCount.RowCount = 1; + this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; + this.dbo_CreateTopicTestData.PretestAction = null; + this.dbo_CreateTopicTestData.TestAction = dbo_CreateTopicTest_TestAction; // - // getVersionAttributeCount + // dbo_DeleteTopicTestData // - getVersionAttributeCount.Enabled = true; - getVersionAttributeCount.Name = "getVersionAttributeCount"; - getVersionAttributeCount.ResultSet = 2; - getVersionAttributeCount.RowCount = 2; + this.dbo_DeleteTopicTestData.PosttestAction = null; + this.dbo_DeleteTopicTestData.PretestAction = dbo_DeleteTopicTest_PretestAction; + this.dbo_DeleteTopicTestData.TestAction = dbo_DeleteTopicTest_TestAction; // - // getVersionExtendedAttributeCount + // dbo_GetTopicVersionTestData // - getVersionExtendedAttributeCount.Enabled = true; - getVersionExtendedAttributeCount.Name = "getVersionExtendedAttributeCount"; - getVersionExtendedAttributeCount.ResultSet = 3; - getVersionExtendedAttributeCount.RowCount = 1; + this.dbo_GetTopicVersionTestData.PosttestAction = dbo_GetTopicVersionTest_PosttestAction; + this.dbo_GetTopicVersionTestData.PretestAction = dbo_GetTopicVersionTest_PretestAction; + this.dbo_GetTopicVersionTestData.TestAction = dbo_GetTopicVersionTest_TestAction; // - // getVersionRelationshipCount + // dbo_GetTopicsTestData // - getVersionRelationshipCount.Enabled = true; - getVersionRelationshipCount.Name = "getVersionRelationshipCount"; - getVersionRelationshipCount.ResultSet = 4; - getVersionRelationshipCount.RowCount = 1; + this.dbo_GetTopicsTestData.PosttestAction = dbo_GetTopicsTest_PosttestAction; + this.dbo_GetTopicsTestData.PretestAction = dbo_GetTopicsTest_PretestAction; + this.dbo_GetTopicsTestData.TestAction = dbo_GetTopicsTest_TestAction; // - // getVersionReferenceCount + // dbo_MoveTopicTestData // - getVersionReferenceCount.Enabled = true; - getVersionReferenceCount.Name = "getVersionReferenceCount"; - getVersionReferenceCount.ResultSet = 5; - getVersionReferenceCount.RowCount = 1; + this.dbo_MoveTopicTestData.PosttestAction = null; + this.dbo_MoveTopicTestData.PretestAction = null; + this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; // - // getVersionVersionHistoryCount + // dbo_UpdateAttributesTestData // - getVersionVersionHistoryCount.Enabled = true; - getVersionVersionHistoryCount.Name = "getVersionVersionHistoryCount"; - getVersionVersionHistoryCount.ResultSet = 6; - getVersionVersionHistoryCount.RowCount = 1; + this.dbo_UpdateAttributesTestData.PosttestAction = null; + this.dbo_UpdateAttributesTestData.PretestAction = null; + this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; + // + // dbo_UpdateExtendedAttributesTestData + // + this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; + this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; + this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; + // + // dbo_UpdateReferencesTestData + // + this.dbo_UpdateReferencesTestData.PosttestAction = null; + this.dbo_UpdateReferencesTestData.PretestAction = null; + this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; + // + // dbo_UpdateRelationshipsTestData + // + this.dbo_UpdateRelationshipsTestData.PosttestAction = null; + this.dbo_UpdateRelationshipsTestData.PretestAction = null; + this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; + // + // dbo_UpdateTopicTestData + // + this.dbo_UpdateTopicTestData.PosttestAction = null; + this.dbo_UpdateTopicTestData.PretestAction = null; + this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // getVersionAttributeValue + // + getVersionAttributeValue.ColumnNumber = 3; + getVersionAttributeValue.Enabled = true; + getVersionAttributeValue.ExpectedValue = "Value"; + getVersionAttributeValue.Name = "getVersionAttributeValue"; + getVersionAttributeValue.NullExpected = false; + getVersionAttributeValue.ResultSet = 2; + getVersionAttributeValue.RowNumber = 1; + // + // getVersionRelationshipValue + // + getVersionRelationshipValue.ColumnNumber = 5; + getVersionRelationshipValue.Enabled = true; + getVersionRelationshipValue.ExpectedValue = "2020-01-01 12:00:00"; + getVersionRelationshipValue.Name = "getVersionRelationshipValue"; + getVersionRelationshipValue.NullExpected = false; + getVersionRelationshipValue.ResultSet = 4; + getVersionRelationshipValue.RowNumber = 1; + // + // getVersionReferenceValue + // + getVersionReferenceValue.ColumnNumber = 4; + getVersionReferenceValue.Enabled = true; + getVersionReferenceValue.ExpectedValue = "2020-01-01 12:00:00"; + getVersionReferenceValue.Name = "getVersionReferenceValue"; + getVersionReferenceValue.NullExpected = false; + getVersionReferenceValue.ResultSet = 5; + getVersionReferenceValue.RowNumber = 1; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index e360b585..2c42f274 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -522,7 +522,118 @@ SELECT * FROM TopicReferences WHERE ReferenceKey LIKE 'DeleteTopic%' - + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicsTest%' + +DELETE +FROM Relationships +WHERE RelationshipKey = 'GetTopicsTest' + +DELETE +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicsTest' + +DELETE +FROM Topics +WHERE TopicKey LIKE 'GetTopics%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetTopicsTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @Version = GETUTCDATE(); + +INSERT +INTO @Attributes +VALUES ( 'GetTopicsTest1', 'Value' ), + ( 'GetTopicsTest2', 'Value' ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @Key = 'GetTopicsChildTest', + @ParentID = @TopicID; + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'GetTopicsTest', + @ParentID + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'GetTopicsTest', + @ParentID + ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'GetTopics%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicsTest%' + +SELECT * +FROM Relationships +WHERE RelationshipKey = 'GetTopicsTest' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicsTest' + + -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH VARIABLES -------------------------------------------------------------------------------------------------------------------------------- @@ -532,7 +643,7 @@ DECLARE @TopicID AS INT, -------------------------------------------------------------------------------------------------------------------------------- -- SET VARIABLES -------------------------------------------------------------------------------------------------------------------------------- -SELECT @UniqueKey = 'GetTopicVersionTest'; +SELECT @UniqueKey = 'GetTopicsTest'; SELECT @TopicID = TopicID FROM Topics @@ -706,7 +817,7 @@ SELECT * FROM TopicReferences WHERE ReferenceKey = 'GetTopicVersionTest' - + -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH VARIABLES -------------------------------------------------------------------------------------------------------------------------------- @@ -716,7 +827,7 @@ DECLARE @TopicID AS INT, -------------------------------------------------------------------------------------------------------------------------------- -- SET VARIABLES -------------------------------------------------------------------------------------------------------------------------------- -SELECT @UniqueKey = 'GetTopicsTest'; +SELECT @UniqueKey = 'GetTopicVersionTest'; SELECT @TopicID = TopicID FROM Topics @@ -728,117 +839,6 @@ WHERE TopicKey = @UniqueKey EXECUTE [dbo].[DeleteTopic] @TopicID; - - -------------------------------------------------------------------------------------------------------------------------------- --- DECLARE VARIABLES --------------------------------------------------------------------------------------------------------------------------------- -DECLARE @TopicID AS INT, - @Key AS VARCHAR (128), - @ContentType AS VARCHAR (128), - @ParentID AS INT, - @Attributes AS [dbo].[AttributeValues], - @ExtendedAttributes AS XML, - @References AS [dbo].[TopicReferences], - @Version AS DATETIME; - --------------------------------------------------------------------------------------------------------------------------------- --- DELETE TEST DATA --------------------------------------------------------------------------------------------------------------------------------- -DELETE -FROM Attributes -WHERE AttributeKey LIKE 'GetTopicsTest%' - -DELETE -FROM Relationships -WHERE RelationshipKey = 'GetTopicsTest' - -DELETE -FROM TopicReferences -WHERE ReferenceKey = 'GetTopicsTest' - -DELETE -FROM Topics -WHERE TopicKey LIKE 'GetTopics%' - --------------------------------------------------------------------------------------------------------------------------------- --- SET VARIABLES --------------------------------------------------------------------------------------------------------------------------------- -SELECT @Key = 'GetTopicsTest', - @ContentType = 'Test', - @ParentID = NULL, - @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', - @Version = GETUTCDATE(); - -INSERT -INTO @Attributes -VALUES ( 'GetTopicsTest1', 'Value' ), - ( 'GetTopicsTest2', 'Value' ) - --------------------------------------------------------------------------------------------------------------------------------- --- ESTABLISH TEST DATA --------------------------------------------------------------------------------------------------------------------------------- -EXECUTE @TopicID = [dbo].[CreateTopic] - @Key, - @ContentType, - @ParentID, - @Attributes, - @ExtendedAttributes, - @References, - @Version; - -SELECT @Key = 'GetTopicsChildTest', - @ParentID = @TopicID; - -EXECUTE @TopicID = [dbo].[CreateTopic] - @Key, - @ContentType, - @ParentID, - @Attributes, - @ExtendedAttributes, - @References, - @Version; - -INSERT -INTO Relationships ( - Source_TopicID, - RelationshipKey, - Target_TopicID - ) -VALUES ( @TopicID, - 'GetTopicsTest', - @ParentID - ) - -INSERT -INTO TopicReferences ( - Source_TopicID, - ReferenceKey, - Target_TopicID - ) -VALUES ( @TopicID, - 'GetTopicsTest', - @ParentID - ) - --------------------------------------------------------------------------------------------------------------------------------- --- VERIFY RESULTS --------------------------------------------------------------------------------------------------------------------------------- -SELECT * -FROM Topics -WHERE TopicKey LIKE 'GetTopics%' - -SELECT * -FROM Attributes -WHERE AttributeKey LIKE 'GetTopicsTest%' - -SELECT * -FROM Relationships -WHERE RelationshipKey = 'GetTopicsTest' - -SELECT * -FROM TopicReferences -WHERE ReferenceKey = 'GetTopicsTest' - True From 63069ba4a24f55a5f111792e581f0f2a294ad461 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 14:58:41 -0800 Subject: [PATCH 310/778] Implement basic unit test for the `MoveTopic` stored procedure The `MoveTopicTest` creates two parent topics each with two child topics using the `CreateTopic` stored procedure. It then moves the first topic of the second branch after the first topic of the first branch and confirms that the move was performed correctly. --- .../StoredProcedures.cs | 57 ++++++-- .../StoredProcedures.resx | 125 ++++++++++++++++-- 2 files changed, 160 insertions(+), 22 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index f04f5c6f..855b8be2 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -54,7 +54,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition6; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; @@ -87,6 +86,11 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionRelationshipValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionReferenceValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preMoveTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition moveTopicValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postMoveTopicCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -119,7 +123,6 @@ private void InitializeComponent() { getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition6 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -152,6 +155,11 @@ private void InitializeComponent() { getVersionAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); getVersionRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); getVersionReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + dbo_MoveTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preMoveTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + moveTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + dbo_MoveTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postMoveTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -310,14 +318,9 @@ private void InitializeComponent() { // // dbo_MoveTopicTest_TestAction // - dbo_MoveTopicTest_TestAction.Conditions.Add(inconclusiveCondition5); + dbo_MoveTopicTest_TestAction.Conditions.Add(moveTopicValue); resources.ApplyResources(dbo_MoveTopicTest_TestAction, "dbo_MoveTopicTest_TestAction"); // - // inconclusiveCondition5 - // - inconclusiveCondition5.Enabled = true; - inconclusiveCondition5.Name = "inconclusiveCondition5"; - // // dbo_UpdateAttributesTest_TestAction // dbo_UpdateAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); @@ -522,8 +525,8 @@ private void InitializeComponent() { // // dbo_MoveTopicTestData // - this.dbo_MoveTopicTestData.PosttestAction = null; - this.dbo_MoveTopicTestData.PretestAction = null; + this.dbo_MoveTopicTestData.PosttestAction = dbo_MoveTopicTest_PosttestAction; + this.dbo_MoveTopicTestData.PretestAction = dbo_MoveTopicTest_PretestAction; this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction; // // dbo_UpdateAttributesTestData @@ -585,6 +588,40 @@ private void InitializeComponent() { getVersionReferenceValue.NullExpected = false; getVersionReferenceValue.ResultSet = 5; getVersionReferenceValue.RowNumber = 1; + // + // dbo_MoveTopicTest_PretestAction + // + dbo_MoveTopicTest_PretestAction.Conditions.Add(preMoveTopicCount); + resources.ApplyResources(dbo_MoveTopicTest_PretestAction, "dbo_MoveTopicTest_PretestAction"); + // + // preMoveTopicCount + // + preMoveTopicCount.Enabled = true; + preMoveTopicCount.Name = "preMoveTopicCount"; + preMoveTopicCount.ResultSet = 1; + preMoveTopicCount.RowCount = 7; + // + // moveTopicValue + // + moveTopicValue.ColumnNumber = 1; + moveTopicValue.Enabled = true; + moveTopicValue.ExpectedValue = "MoveTopicChildTest3"; + moveTopicValue.Name = "moveTopicValue"; + moveTopicValue.NullExpected = false; + moveTopicValue.ResultSet = 2; + moveTopicValue.RowNumber = 4; + // + // dbo_MoveTopicTest_PosttestAction + // + dbo_MoveTopicTest_PosttestAction.Conditions.Add(postMoveTopicCount); + resources.ApplyResources(dbo_MoveTopicTest_PosttestAction, "dbo_MoveTopicTest_PosttestAction"); + // + // postMoveTopicCount + // + postMoveTopicCount.Enabled = true; + postMoveTopicCount.Name = "postMoveTopicCount"; + postMoveTopicCount.ResultSet = 1; + postMoveTopicCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 2c42f274..9f3d91ab 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -256,23 +256,43 @@ EXECUTE [dbo].[GetTopics] NULL; - -- database unit test for dbo.MoveTopic -DECLARE @RC AS INT, - @TopicID AS INT, - @ParentID AS INT, + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @ParentID AS INT, @SiblingID AS INT; -SELECT @RC = 0, - @TopicID = 0, - @ParentID = 0, - @SiblingID = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'MoveTopicChildTest3' + +SELECT @ParentID = TopicID +FROM Topics +WHERE TopicKey = 'MoveTopicTest1' + +SELECT @SiblingID = TopicID +FROM Topics +WHERE TopicKey = 'MoveTopicChildTest1' -EXECUTE @RC = [dbo].[MoveTopic] - @TopicID, - @ParentID, +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[MoveTopic] + @TopicID, + @ParentID, @SiblingID; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +SELECT TopicKey +FROM Topics +WHERE TopicKey LIKE 'MoveTopic%' +ORDER BY RangeLeft ASC -- database unit test for dbo.UpdateAttributes @@ -839,6 +859,87 @@ WHERE TopicKey = @UniqueKey EXECUTE [dbo].[DeleteTopic] @TopicID; + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Topics +WHERE TopicKey = 'MoveTopic%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey = 'MoveTopic%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @ContentType AS VARCHAR (128), + @RootTopicID AS INT, + @ParentID1 AS INT, + @ParentID2 AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Topics +WHERE TopicKey LIKE 'MoveTopic%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ContentType = 'Test'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @RootTopicID = [dbo].[CreateTopic] + 'MoveTopicTest', + @ContentType, + NULL; + +EXECUTE @ParentID1 = [dbo].[CreateTopic] + 'MoveTopicTest1', + @ContentType, + @RootTopicID; + +EXECUTE [dbo].[CreateTopic] + 'MoveTopicChildTest1', + @ContentType, + @ParentID1; + +EXECUTE [dbo].[CreateTopic] + 'MoveTopicChildTest2', + @ContentType, + @ParentID1; + +EXECUTE @ParentID2 = [dbo].[CreateTopic] + 'MoveTopicTest2', + @ContentType, + @RootTopicID; + +EXECUTE [dbo].[CreateTopic] + 'MoveTopicChildTest3', + @ContentType, + @ParentID2; + +EXECUTE [dbo].[CreateTopic] + 'MoveTopicChildTest4', + @ContentType, + @ParentID2; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'MoveTopic%' + True From d697fb0924463ba4f902999eebcaa6218dff6db9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 15:43:55 -0800 Subject: [PATCH 311/778] Implement basic unit test for the `UpdateAttributes` stored procedure The `UpdateAttributesTest` creates three attributes for a topic, then updates the attributes using the `UpdateAttributes` stored procedure. It confirms that the one unmatched attribute is deleted, and that the one new attribute is created. --- .../StoredProcedures.cs | 207 +++++++++++------- .../StoredProcedures.resx | 150 ++++++++++--- 2 files changed, 249 insertions(+), 108 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 855b8be2..b703e480 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -46,6 +46,9 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionVersionHistoryCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionAttributeValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionRelationshipValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionReferenceValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; @@ -54,8 +57,10 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition moveTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition6; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; @@ -83,14 +88,14 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetVersionReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicVersionTest_PosttestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionAttributeValue; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionRelationshipValue; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getVersionReferenceValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preMoveTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition moveTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_MoveTopicTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postMoveTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateAttributeCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -115,6 +120,9 @@ private void InitializeComponent() { getVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getVersionVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getVersionAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getVersionRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getVersionReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetTopicsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); getTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); @@ -123,8 +131,10 @@ private void InitializeComponent() { getReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_MoveTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + moveTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition6 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + updateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -152,14 +162,14 @@ private void InitializeComponent() { preGetVersionRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetVersionReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicVersionTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - getVersionAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); - getVersionRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); - getVersionReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_MoveTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preMoveTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - moveTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_MoveTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postMoveTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_UpdateAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preUpdateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_UpdateAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postUpdateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -264,6 +274,36 @@ private void InitializeComponent() { getVersionVersionHistoryCount.ResultSet = 6; getVersionVersionHistoryCount.RowCount = 1; // + // getVersionAttributeValue + // + getVersionAttributeValue.ColumnNumber = 3; + getVersionAttributeValue.Enabled = true; + getVersionAttributeValue.ExpectedValue = "Value"; + getVersionAttributeValue.Name = "getVersionAttributeValue"; + getVersionAttributeValue.NullExpected = false; + getVersionAttributeValue.ResultSet = 2; + getVersionAttributeValue.RowNumber = 1; + // + // getVersionRelationshipValue + // + getVersionRelationshipValue.ColumnNumber = 5; + getVersionRelationshipValue.Enabled = true; + getVersionRelationshipValue.ExpectedValue = "2020-01-01 12:00:00"; + getVersionRelationshipValue.Name = "getVersionRelationshipValue"; + getVersionRelationshipValue.NullExpected = false; + getVersionRelationshipValue.ResultSet = 4; + getVersionRelationshipValue.RowNumber = 1; + // + // getVersionReferenceValue + // + getVersionReferenceValue.ColumnNumber = 4; + getVersionReferenceValue.Enabled = true; + getVersionReferenceValue.ExpectedValue = "2020-01-01 12:00:00"; + getVersionReferenceValue.Name = "getVersionReferenceValue"; + getVersionReferenceValue.NullExpected = false; + getVersionReferenceValue.ResultSet = 5; + getVersionReferenceValue.RowNumber = 1; + // // dbo_GetTopicsTest_TestAction // dbo_GetTopicsTest_TestAction.Conditions.Add(getTopicCount); @@ -321,15 +361,38 @@ private void InitializeComponent() { dbo_MoveTopicTest_TestAction.Conditions.Add(moveTopicValue); resources.ApplyResources(dbo_MoveTopicTest_TestAction, "dbo_MoveTopicTest_TestAction"); // + // moveTopicValue + // + moveTopicValue.ColumnNumber = 1; + moveTopicValue.Enabled = true; + moveTopicValue.ExpectedValue = "MoveTopicChildTest3"; + moveTopicValue.Name = "moveTopicValue"; + moveTopicValue.NullExpected = false; + moveTopicValue.ResultSet = 2; + moveTopicValue.RowNumber = 4; + // // dbo_UpdateAttributesTest_TestAction // - dbo_UpdateAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); + dbo_UpdateAttributesTest_TestAction.Conditions.Add(updateAttributeCount); + dbo_UpdateAttributesTest_TestAction.Conditions.Add(updateAttributeValue); resources.ApplyResources(dbo_UpdateAttributesTest_TestAction, "dbo_UpdateAttributesTest_TestAction"); // - // inconclusiveCondition6 + // updateAttributeCount + // + updateAttributeCount.Enabled = true; + updateAttributeCount.Name = "updateAttributeCount"; + updateAttributeCount.ResultSet = 1; + updateAttributeCount.RowCount = 3; + // + // updateAttributeValue // - inconclusiveCondition6.Enabled = true; - inconclusiveCondition6.Name = "inconclusiveCondition6"; + updateAttributeValue.ColumnNumber = 1; + updateAttributeValue.Enabled = true; + updateAttributeValue.ExpectedValue = "UpdateAttributesTest4"; + updateAttributeValue.Name = "updateAttributeValue"; + updateAttributeValue.NullExpected = false; + updateAttributeValue.ResultSet = 1; + updateAttributeValue.RowNumber = 3; // // dbo_UpdateExtendedAttributesTest_TestAction // @@ -499,6 +562,54 @@ private void InitializeComponent() { // resources.ApplyResources(dbo_GetTopicVersionTest_PosttestAction, "dbo_GetTopicVersionTest_PosttestAction"); // + // dbo_MoveTopicTest_PretestAction + // + dbo_MoveTopicTest_PretestAction.Conditions.Add(preMoveTopicCount); + resources.ApplyResources(dbo_MoveTopicTest_PretestAction, "dbo_MoveTopicTest_PretestAction"); + // + // preMoveTopicCount + // + preMoveTopicCount.Enabled = true; + preMoveTopicCount.Name = "preMoveTopicCount"; + preMoveTopicCount.ResultSet = 1; + preMoveTopicCount.RowCount = 7; + // + // dbo_MoveTopicTest_PosttestAction + // + dbo_MoveTopicTest_PosttestAction.Conditions.Add(postMoveTopicCount); + resources.ApplyResources(dbo_MoveTopicTest_PosttestAction, "dbo_MoveTopicTest_PosttestAction"); + // + // postMoveTopicCount + // + postMoveTopicCount.Enabled = true; + postMoveTopicCount.Name = "postMoveTopicCount"; + postMoveTopicCount.ResultSet = 1; + postMoveTopicCount.RowCount = 0; + // + // dbo_UpdateAttributesTest_PretestAction + // + dbo_UpdateAttributesTest_PretestAction.Conditions.Add(preUpdateAttributeCount); + resources.ApplyResources(dbo_UpdateAttributesTest_PretestAction, "dbo_UpdateAttributesTest_PretestAction"); + // + // preUpdateAttributeCount + // + preUpdateAttributeCount.Enabled = true; + preUpdateAttributeCount.Name = "preUpdateAttributeCount"; + preUpdateAttributeCount.ResultSet = 1; + preUpdateAttributeCount.RowCount = 3; + // + // dbo_UpdateAttributesTest_PosttestAction + // + dbo_UpdateAttributesTest_PosttestAction.Conditions.Add(postUpdateAttributeCount); + resources.ApplyResources(dbo_UpdateAttributesTest_PosttestAction, "dbo_UpdateAttributesTest_PosttestAction"); + // + // postUpdateAttributeCount + // + postUpdateAttributeCount.Enabled = true; + postUpdateAttributeCount.Name = "postUpdateAttributeCount"; + postUpdateAttributeCount.ResultSet = 1; + postUpdateAttributeCount.RowCount = 0; + // // dbo_CreateTopicTestData // this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; @@ -531,8 +642,8 @@ private void InitializeComponent() { // // dbo_UpdateAttributesTestData // - this.dbo_UpdateAttributesTestData.PosttestAction = null; - this.dbo_UpdateAttributesTestData.PretestAction = null; + this.dbo_UpdateAttributesTestData.PosttestAction = dbo_UpdateAttributesTest_PosttestAction; + this.dbo_UpdateAttributesTestData.PretestAction = dbo_UpdateAttributesTest_PretestAction; this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction; // // dbo_UpdateExtendedAttributesTestData @@ -558,70 +669,6 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = null; this.dbo_UpdateTopicTestData.PretestAction = null; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; - // - // getVersionAttributeValue - // - getVersionAttributeValue.ColumnNumber = 3; - getVersionAttributeValue.Enabled = true; - getVersionAttributeValue.ExpectedValue = "Value"; - getVersionAttributeValue.Name = "getVersionAttributeValue"; - getVersionAttributeValue.NullExpected = false; - getVersionAttributeValue.ResultSet = 2; - getVersionAttributeValue.RowNumber = 1; - // - // getVersionRelationshipValue - // - getVersionRelationshipValue.ColumnNumber = 5; - getVersionRelationshipValue.Enabled = true; - getVersionRelationshipValue.ExpectedValue = "2020-01-01 12:00:00"; - getVersionRelationshipValue.Name = "getVersionRelationshipValue"; - getVersionRelationshipValue.NullExpected = false; - getVersionRelationshipValue.ResultSet = 4; - getVersionRelationshipValue.RowNumber = 1; - // - // getVersionReferenceValue - // - getVersionReferenceValue.ColumnNumber = 4; - getVersionReferenceValue.Enabled = true; - getVersionReferenceValue.ExpectedValue = "2020-01-01 12:00:00"; - getVersionReferenceValue.Name = "getVersionReferenceValue"; - getVersionReferenceValue.NullExpected = false; - getVersionReferenceValue.ResultSet = 5; - getVersionReferenceValue.RowNumber = 1; - // - // dbo_MoveTopicTest_PretestAction - // - dbo_MoveTopicTest_PretestAction.Conditions.Add(preMoveTopicCount); - resources.ApplyResources(dbo_MoveTopicTest_PretestAction, "dbo_MoveTopicTest_PretestAction"); - // - // preMoveTopicCount - // - preMoveTopicCount.Enabled = true; - preMoveTopicCount.Name = "preMoveTopicCount"; - preMoveTopicCount.ResultSet = 1; - preMoveTopicCount.RowCount = 7; - // - // moveTopicValue - // - moveTopicValue.ColumnNumber = 1; - moveTopicValue.Enabled = true; - moveTopicValue.ExpectedValue = "MoveTopicChildTest3"; - moveTopicValue.Name = "moveTopicValue"; - moveTopicValue.NullExpected = false; - moveTopicValue.ResultSet = 2; - moveTopicValue.RowNumber = 4; - // - // dbo_MoveTopicTest_PosttestAction - // - dbo_MoveTopicTest_PosttestAction.Conditions.Add(postMoveTopicCount); - resources.ApplyResources(dbo_MoveTopicTest_PosttestAction, "dbo_MoveTopicTest_PosttestAction"); - // - // postMoveTopicCount - // - postMoveTopicCount.Enabled = true; - postMoveTopicCount.Name = "postMoveTopicCount"; - postMoveTopicCount.ResultSet = 1; - postMoveTopicCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 9f3d91ab..aa63c1a5 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -295,25 +295,46 @@ WHERE TopicKey LIKE 'MoveTopic%' ORDER BY RangeLeft ASC - -- database unit test for dbo.UpdateAttributes -DECLARE @RC AS INT, - @TopicID AS INT, - @Attributes AS [dbo].[AttributeValues], - @Version AS DATETIME, - @DeleteUnmatched AS BIT; + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Attributes AS AttributeValues, + @Version AS DATETIME; -SELECT @RC = 0, - @TopicID = 0, - @Version = getdate(), - @DeleteUnmatched = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'UpdateAttributesTest'; + +SELECT @Version = GETUTCDATE(); -EXECUTE @RC = [dbo].[UpdateAttributes] +INSERT +INTO @Attributes +VALUES ( 'UpdateAttributesTest1', 'New' ), + ( 'UpdateAttributesTest2', 'New' ), + ( 'UpdateAttributesTest4', 'New' ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[UpdateAttributes] @TopicID, @Attributes, @Version, - @DeleteUnmatched; + 1; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT AttributeKey +FROM AttributeIndex +WHERE TopicID = @TopicID + AND AttributeKey LIKE 'UpdateAttributesTest%' + AND AttributeValue != '' +ORDER BY AttributeKey ASC; -- database unit test for dbo.UpdateExtendedAttributes @@ -859,21 +880,6 @@ WHERE TopicKey = @UniqueKey EXECUTE [dbo].[DeleteTopic] @TopicID; - - -------------------------------------------------------------------------------------------------------------------------------- --- DELETE TEST DATA --------------------------------------------------------------------------------------------------------------------------------- -DELETE -FROM Topics -WHERE TopicKey = 'MoveTopic%' - --------------------------------------------------------------------------------------------------------------------------------- --- VERIFY RESULTS --------------------------------------------------------------------------------------------------------------------------------- -SELECT * -FROM Topics -WHERE TopicKey = 'MoveTopic%' - -------------------------------------------------------------------------------------------------------------------------------- -- DECLARE VARIABLES @@ -939,6 +945,94 @@ EXECUTE [dbo].[CreateTopic] SELECT * FROM Topics WHERE TopicKey LIKE 'MoveTopic%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Topics +WHERE TopicKey = 'MoveTopic%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey = 'MoveTopic%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'UpdateAttributesTest%'; + +DELETE +FROM Topics +WHERE TopicKey = 'UpdateAttributesTest'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + 'UpdateAttributesTest', + 'Test', + NULL; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO Attributes ( + TopicID, + AttributeKey, + AttributeValue + ) +VALUES ( @TopicID, + 'UpdateAttributesTest1', + 'Value' + ), + ( @TopicID, + 'UpdateAttributesTest2', + 'Value' + ), + ( @TopicID, + 'UpdateAttributesTest3', + 'Value' + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'UpdateAttributesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'UpdateAttributesTest%'; + +DELETE +FROM Topics +WHERE TopicKey = 'UpdateAttributesTest'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'UpdateAttributesTest%'; True From 8214f928e12fbd6615ce344f7bc0a351c30d97c4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 16:02:40 -0800 Subject: [PATCH 312/778] Implement basic unit test for the `UpdateReferences` stored procedure The `UpdateReferencesTest` creates three topic references for a topic, then updates the references using the `UpdateReferences` stored procedure. It confirms that the one unmatched reference is deleted, and that the one new reference is created. --- .../StoredProcedures.cs | 57 ++++++-- .../StoredProcedures.resx | 131 ++++++++++++++++-- 2 files changed, 168 insertions(+), 20 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index b703e480..abbb17ae 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -64,7 +64,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition8; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition9; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; @@ -96,6 +95,11 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateReferenceValue; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -138,7 +142,6 @@ private void InitializeComponent() { dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition8 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateRelationshipsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition9 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -170,6 +173,11 @@ private void InitializeComponent() { preUpdateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_UpdateAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postUpdateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_UpdateReferencesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_UpdateReferencesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preUpdateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_CreateTopicTest_TestAction // @@ -406,14 +414,10 @@ private void InitializeComponent() { // // dbo_UpdateReferencesTest_TestAction // - dbo_UpdateReferencesTest_TestAction.Conditions.Add(inconclusiveCondition8); + dbo_UpdateReferencesTest_TestAction.Conditions.Add(updateReferenceCount); + dbo_UpdateReferencesTest_TestAction.Conditions.Add(updateReferenceValue); resources.ApplyResources(dbo_UpdateReferencesTest_TestAction, "dbo_UpdateReferencesTest_TestAction"); // - // inconclusiveCondition8 - // - inconclusiveCondition8.Enabled = true; - inconclusiveCondition8.Name = "inconclusiveCondition8"; - // // dbo_UpdateRelationshipsTest_TestAction // dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(inconclusiveCondition9); @@ -654,8 +658,8 @@ private void InitializeComponent() { // // dbo_UpdateReferencesTestData // - this.dbo_UpdateReferencesTestData.PosttestAction = null; - this.dbo_UpdateReferencesTestData.PretestAction = null; + this.dbo_UpdateReferencesTestData.PosttestAction = dbo_UpdateReferencesTest_PosttestAction; + this.dbo_UpdateReferencesTestData.PretestAction = dbo_UpdateReferencesTest_PretestAction; this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction; // // dbo_UpdateRelationshipsTestData @@ -669,6 +673,39 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = null; this.dbo_UpdateTopicTestData.PretestAction = null; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // dbo_UpdateReferencesTest_PretestAction + // + dbo_UpdateReferencesTest_PretestAction.Conditions.Add(preUpdateReferenceCount); + resources.ApplyResources(dbo_UpdateReferencesTest_PretestAction, "dbo_UpdateReferencesTest_PretestAction"); + // + // dbo_UpdateReferencesTest_PosttestAction + // + resources.ApplyResources(dbo_UpdateReferencesTest_PosttestAction, "dbo_UpdateReferencesTest_PosttestAction"); + // + // preUpdateReferenceCount + // + preUpdateReferenceCount.Enabled = true; + preUpdateReferenceCount.Name = "preUpdateReferenceCount"; + preUpdateReferenceCount.ResultSet = 1; + preUpdateReferenceCount.RowCount = 3; + // + // updateReferenceCount + // + updateReferenceCount.Enabled = true; + updateReferenceCount.Name = "updateReferenceCount"; + updateReferenceCount.ResultSet = 1; + updateReferenceCount.RowCount = 4; + // + // updateReferenceValue + // + updateReferenceValue.ColumnNumber = 1; + updateReferenceValue.Enabled = true; + updateReferenceValue.ExpectedValue = "-1"; + updateReferenceValue.Name = "updateReferenceValue"; + updateReferenceValue.NullExpected = false; + updateReferenceValue.ResultSet = 1; + updateReferenceValue.RowNumber = 3; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index aa63c1a5..d4ee72ef 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -359,25 +359,52 @@ EXECUTE @RC = [dbo].[UpdateExtendedAttributes] SELECT @RC AS RC; - -- database unit test for dbo.UpdateReferences -DECLARE @RC AS INT, - @TopicID AS INT, - @ReferencedTopics AS [dbo].[TopicReferences], + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @TargetID AS INT, + @ReferencedTopics AS TopicReferences, @Version AS DATETIME, @DeleteUnmatched AS BIT; -SELECT @RC = 0, - @TopicID = 0, - @Version = GETUTCDATE(), - @DeleteUnmatched = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'UpdateReferencesTest'; + +SELECT @TargetID = TopicID +FROM Topics +WHERE TopicKey = 'UpdateReferencesTestTarget2'; + +SELECT @Version = GETUTCDATE(), + @DeleteUnmatched = 1; -EXECUTE @RC = [dbo].[UpdateReferences] +INSERT +INTO @ReferencedTopics +VALUES ( 'UpdateReferencesTest1', @TargetID ), + ( 'UpdateReferencesTest2', @TargetID ), + ( 'UpdateReferencesTest4', @TargetID ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[UpdateReferences] @TopicID, @ReferencedTopics, @Version, @DeleteUnmatched; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT ISNULL(Target_TopicID, -1) +FROM ReferenceIndex +WHERE Source_TopicID = @TopicID + AND ReferenceKey LIKE 'UpdateReferencesTest%' +ORDER BY ReferenceKey ASC; -- database unit test for dbo.UpdateRelationships @@ -1033,6 +1060,90 @@ WHERE TopicKey = 'UpdateAttributesTest'; SELECT * FROM Attributes WHERE AttributeKey LIKE 'UpdateAttributesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateReferencesTest%'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @TargetID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateReferencesTest%'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + 'UpdateReferencesTest', + 'Test', + NULL; + +EXECUTE @TargetID = [dbo].[CreateTopic] + 'UpdateReferencesTestTarget1', + 'Test', + NULL; + +EXECUTE [dbo].[CreateTopic] + 'UpdateReferencesTestTarget2', + 'Test', + NULL; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID + ) +VALUES ( @TopicID, + 'UpdateReferencesTest1', + @TargetID + ), + ( @TopicID, + 'UpdateReferencesTest2', + @TargetID + ), + ( @TopicID, + 'UpdateReferencesTest3', + @TargetID + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; True From 04eb1bcf6e7dd417b40becad26e8598b36ae78fd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 16:40:28 -0800 Subject: [PATCH 313/778] Implement basic unit test for the `UpdateRelationships` stored procedure The `UpdateRelationshipsTest` creates three relationships for a topic, then updates the relationships using the `UpdateRelationships` stored procedure. It confirms that the one unmatched relationship is deleted, and that the one new relationship is created. --- .../StoredProcedures.cs | 186 ++++++++--- .../StoredProcedures.resx | 308 +++++++++++++++--- 2 files changed, 391 insertions(+), 103 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index abbb17ae..aafa3602 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -64,10 +64,13 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateReferenceValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition9; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateRelationshipValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition10; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postCreateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_PretestAction; @@ -96,10 +99,16 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PretestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateReferenceValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateTopicValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateTopicCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -142,10 +151,13 @@ private void InitializeComponent() { dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + updateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateRelationshipsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition9 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + updateRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition10 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + updateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_CreateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postCreateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -174,10 +186,16 @@ private void InitializeComponent() { dbo_UpdateAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postUpdateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_UpdateReferencesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - dbo_UpdateReferencesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preUpdateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - updateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - updateReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + dbo_UpdateReferencesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_UpdateRelationshipsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preUpdateRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_UpdateRelationshipsTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_UpdateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_UpdateTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + postUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -418,25 +436,58 @@ private void InitializeComponent() { dbo_UpdateReferencesTest_TestAction.Conditions.Add(updateReferenceValue); resources.ApplyResources(dbo_UpdateReferencesTest_TestAction, "dbo_UpdateReferencesTest_TestAction"); // + // updateReferenceCount + // + updateReferenceCount.Enabled = true; + updateReferenceCount.Name = "updateReferenceCount"; + updateReferenceCount.ResultSet = 1; + updateReferenceCount.RowCount = 4; + // + // updateReferenceValue + // + updateReferenceValue.ColumnNumber = 1; + updateReferenceValue.Enabled = true; + updateReferenceValue.ExpectedValue = "-1"; + updateReferenceValue.Name = "updateReferenceValue"; + updateReferenceValue.NullExpected = false; + updateReferenceValue.ResultSet = 1; + updateReferenceValue.RowNumber = 3; + // // dbo_UpdateRelationshipsTest_TestAction // - dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(inconclusiveCondition9); + dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(updateRelationshipCount); + dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(updateRelationshipValue); resources.ApplyResources(dbo_UpdateRelationshipsTest_TestAction, "dbo_UpdateRelationshipsTest_TestAction"); // - // inconclusiveCondition9 + // updateRelationshipCount + // + updateRelationshipCount.Enabled = true; + updateRelationshipCount.Name = "updateRelationshipCount"; + updateRelationshipCount.ResultSet = 1; + updateRelationshipCount.RowCount = 4; + // + // updateRelationshipValue // - inconclusiveCondition9.Enabled = true; - inconclusiveCondition9.Name = "inconclusiveCondition9"; + updateRelationshipValue.ColumnNumber = 1; + updateRelationshipValue.Enabled = true; + updateRelationshipValue.ExpectedValue = "True"; + updateRelationshipValue.Name = "updateRelationshipValue"; + updateRelationshipValue.NullExpected = false; + updateRelationshipValue.ResultSet = 1; + updateRelationshipValue.RowNumber = 3; // // dbo_UpdateTopicTest_TestAction // - dbo_UpdateTopicTest_TestAction.Conditions.Add(inconclusiveCondition10); + dbo_UpdateTopicTest_TestAction.Conditions.Add(updateTopicCount); + dbo_UpdateTopicTest_TestAction.Conditions.Add(updateTopicValue); resources.ApplyResources(dbo_UpdateTopicTest_TestAction, "dbo_UpdateTopicTest_TestAction"); // - // inconclusiveCondition10 + // updateTopicCount // - inconclusiveCondition10.Enabled = true; - inconclusiveCondition10.Name = "inconclusiveCondition10"; + updateTopicCount.Enabled = true; + updateTopicCount.Name = "updateTopicCount"; + updateTopicCount.ResultSet = 1; + updateTopicCount.RowCount = 1; // // dbo_CreateTopicTest_PosttestAction // @@ -614,6 +665,55 @@ private void InitializeComponent() { postUpdateAttributeCount.ResultSet = 1; postUpdateAttributeCount.RowCount = 0; // + // dbo_UpdateReferencesTest_PretestAction + // + dbo_UpdateReferencesTest_PretestAction.Conditions.Add(preUpdateReferenceCount); + resources.ApplyResources(dbo_UpdateReferencesTest_PretestAction, "dbo_UpdateReferencesTest_PretestAction"); + // + // preUpdateReferenceCount + // + preUpdateReferenceCount.Enabled = true; + preUpdateReferenceCount.Name = "preUpdateReferenceCount"; + preUpdateReferenceCount.ResultSet = 1; + preUpdateReferenceCount.RowCount = 3; + // + // dbo_UpdateReferencesTest_PosttestAction + // + resources.ApplyResources(dbo_UpdateReferencesTest_PosttestAction, "dbo_UpdateReferencesTest_PosttestAction"); + // + // dbo_UpdateRelationshipsTest_PretestAction + // + dbo_UpdateRelationshipsTest_PretestAction.Conditions.Add(preUpdateRelationshipCount); + resources.ApplyResources(dbo_UpdateRelationshipsTest_PretestAction, "dbo_UpdateRelationshipsTest_PretestAction"); + // + // preUpdateRelationshipCount + // + preUpdateRelationshipCount.Enabled = true; + preUpdateRelationshipCount.Name = "preUpdateRelationshipCount"; + preUpdateRelationshipCount.ResultSet = 1; + preUpdateRelationshipCount.RowCount = 3; + // + // dbo_UpdateRelationshipsTest_PosttestAction + // + resources.ApplyResources(dbo_UpdateRelationshipsTest_PosttestAction, "dbo_UpdateRelationshipsTest_PosttestAction"); + // + // dbo_UpdateTopicTest_PosttestAction + // + dbo_UpdateTopicTest_PosttestAction.Conditions.Add(postUpdateTopicCount); + resources.ApplyResources(dbo_UpdateTopicTest_PosttestAction, "dbo_UpdateTopicTest_PosttestAction"); + // + // dbo_UpdateTopicTest_PretestAction + // + dbo_UpdateTopicTest_PretestAction.Conditions.Add(preUpdateTopicCount); + resources.ApplyResources(dbo_UpdateTopicTest_PretestAction, "dbo_UpdateTopicTest_PretestAction"); + // + // preUpdateTopicCount + // + preUpdateTopicCount.Enabled = true; + preUpdateTopicCount.Name = "preUpdateTopicCount"; + preUpdateTopicCount.ResultSet = 1; + preUpdateTopicCount.RowCount = 1; + // // dbo_CreateTopicTestData // this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; @@ -664,48 +764,32 @@ private void InitializeComponent() { // // dbo_UpdateRelationshipsTestData // - this.dbo_UpdateRelationshipsTestData.PosttestAction = null; - this.dbo_UpdateRelationshipsTestData.PretestAction = null; + this.dbo_UpdateRelationshipsTestData.PosttestAction = dbo_UpdateRelationshipsTest_PosttestAction; + this.dbo_UpdateRelationshipsTestData.PretestAction = dbo_UpdateRelationshipsTest_PretestAction; this.dbo_UpdateRelationshipsTestData.TestAction = dbo_UpdateRelationshipsTest_TestAction; // // dbo_UpdateTopicTestData // - this.dbo_UpdateTopicTestData.PosttestAction = null; - this.dbo_UpdateTopicTestData.PretestAction = null; + this.dbo_UpdateTopicTestData.PosttestAction = dbo_UpdateTopicTest_PosttestAction; + this.dbo_UpdateTopicTestData.PretestAction = dbo_UpdateTopicTest_PretestAction; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; // - // dbo_UpdateReferencesTest_PretestAction - // - dbo_UpdateReferencesTest_PretestAction.Conditions.Add(preUpdateReferenceCount); - resources.ApplyResources(dbo_UpdateReferencesTest_PretestAction, "dbo_UpdateReferencesTest_PretestAction"); - // - // dbo_UpdateReferencesTest_PosttestAction - // - resources.ApplyResources(dbo_UpdateReferencesTest_PosttestAction, "dbo_UpdateReferencesTest_PosttestAction"); - // - // preUpdateReferenceCount + // updateTopicValue // - preUpdateReferenceCount.Enabled = true; - preUpdateReferenceCount.Name = "preUpdateReferenceCount"; - preUpdateReferenceCount.ResultSet = 1; - preUpdateReferenceCount.RowCount = 3; - // - // updateReferenceCount - // - updateReferenceCount.Enabled = true; - updateReferenceCount.Name = "updateReferenceCount"; - updateReferenceCount.ResultSet = 1; - updateReferenceCount.RowCount = 4; + updateTopicValue.ColumnNumber = 1; + updateTopicValue.Enabled = true; + updateTopicValue.ExpectedValue = "TestNew"; + updateTopicValue.Name = "updateTopicValue"; + updateTopicValue.NullExpected = false; + updateTopicValue.ResultSet = 1; + updateTopicValue.RowNumber = 1; // - // updateReferenceValue + // postUpdateTopicCount // - updateReferenceValue.ColumnNumber = 1; - updateReferenceValue.Enabled = true; - updateReferenceValue.ExpectedValue = "-1"; - updateReferenceValue.Name = "updateReferenceValue"; - updateReferenceValue.NullExpected = false; - updateReferenceValue.ResultSet = 1; - updateReferenceValue.RowNumber = 3; + postUpdateTopicCount.Enabled = true; + postUpdateTopicCount.Name = "postUpdateTopicCount"; + postUpdateTopicCount.ResultSet = 1; + postUpdateTopicCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index d4ee72ef..27cc84f5 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -407,58 +407,105 @@ WHERE Source_TopicID = @TopicID ORDER BY ReferenceKey ASC; - -- database unit test for dbo.UpdateRelationships -DECLARE @RC AS INT, - @TopicID AS INT, - @RelationshipKey AS VARCHAR (255), - @RelatedTopics AS [dbo].[TopicList], + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @TargetID1 AS INT, + @TargetID2 AS INT, + @TargetID4 AS INT, + @RelationshipKey AS VARCHAR(128), + @RelatedTopics AS TopicList, @Version AS DATETIME, @DeleteUnmatched AS BIT; -SELECT @RC = 0, - @TopicID = 0, - @RelationshipKey = NULL, +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @RelationshipKey = 'UpdateRelationshipsTest', @Version = GETUTCDATE(), - @DeleteUnmatched = 0; + @DeleteUnmatched = 1; -EXECUTE @RC = [dbo].[UpdateRelationships] +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @RelationshipKey; + +SELECT @TargetID1 = TopicID +FROM Topics +WHERE TopicKey = 'UpdateRelationshipsTestTarget1'; + +SELECT @TargetID2 = TopicID +FROM Topics +WHERE TopicKey = 'UpdateRelationshipsTestTarget2'; + +SELECT @TargetID4 = TopicID +FROM Topics +WHERE TopicKey = 'UpdateRelationshipsTestTarget4'; + + +INSERT +INTO @RelatedTopics +VALUES ( @TargetID1 ), + ( @TargetID2 ), + ( @TargetID4 ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[UpdateRelationships] @TopicID, @RelationshipKey, @RelatedTopics, @Version, @DeleteUnmatched; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT IsDeleted +FROM RelationshipIndex +WHERE Source_TopicID = @TopicID + AND RelationshipKey = @RelationshipKey +ORDER BY Target_TopicID ASC; - -- database unit test for dbo.UpdateTopic -DECLARE @RC AS INT, - @TopicID AS INT, + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), - @Attributes AS [dbo].[AttributeValues], - @ExtendedAttributes AS XML, - @Version AS DATETIME, - @DeleteUnmatched AS BIT; + @NewKey AS VARCHAR (128), + @NewContentType AS VARCHAR (128), + @ParentID AS INT; -SELECT @RC = 0, - @TopicID = 0, - @Key = NULL, - @ContentType = NULL, - @ExtendedAttributes = NULL, - @Version = GETUTCDATE(), - @DeleteUnmatched = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateTopicTest', + @ContentType = 'Test', + @Key = 'UpdateTopicTestNew', + @ContentType = 'TestNew', + @ParentID = NULL; -EXECUTE @RC = [dbo].[UpdateTopic] +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[UpdateTopic] @TopicID, - @Key, - @ContentType, - @Attributes, - @ExtendedAttributes, - @Version, - @DeleteUnmatched; + @NewKey, + @NewContentType; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT ContentType +FROM Topics +WHERE TopicKey = @NewKey -------------------------------------------------------------------------------------------------------------------------------- @@ -1060,25 +1107,6 @@ WHERE TopicKey = 'UpdateAttributesTest'; SELECT * FROM Attributes WHERE AttributeKey LIKE 'UpdateAttributesTest%'; - - - -------------------------------------------------------------------------------------------------------------------------------- --- DELETE TEST DATA --------------------------------------------------------------------------------------------------------------------------------- -DELETE -FROM TopicReferences -WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; - -DELETE -FROM Topics -WHERE TopicKey LIKE 'UpdateReferencesTest%'; - --------------------------------------------------------------------------------------------------------------------------------- --- VERIFY RESULTS --------------------------------------------------------------------------------------------------------------------------------- -SELECT * -FROM TopicReferences -WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; -------------------------------------------------------------------------------------------------------------------------------- @@ -1144,6 +1172,182 @@ VALUES ( @TopicID, SELECT * FROM TopicReferences WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateReferencesTest%'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM TopicReferences +WHERE ReferenceKey LIKE 'UpdateReferencesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @RelationshipKey AS VARCHAR(128), + @TargetID1 AS INT, + @TargetID2 AS INT, + @TargetID3 AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Relationships +WHERE RelationshipKey = 'UpdateRelationshipsTest'; + +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateRelationshipsTest%'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @RelationshipKey = 'UpdateRelationshipsTest' + + +EXECUTE @TopicID = [dbo].[CreateTopic] + @RelationshipKey, + 'Test', + NULL; + +EXECUTE @TargetID1 = [dbo].[CreateTopic] + 'UpdateRelationshipsTestTarget1', + 'Test', + NULL; + +EXECUTE @TargetID2 = [dbo].[CreateTopic] + 'UpdateRelationshipsTestTarget2', + 'Test', + NULL; + +EXECUTE @TargetID3 = [dbo].[CreateTopic] + 'UpdateRelationshipsTestTarget3', + 'Test', + NULL; + +EXECUTE [dbo].[CreateTopic] + 'UpdateRelationshipsTestTarget4', + 'Test', + NULL; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID + ) +VALUES ( @TopicID, + @RelationshipKey, + @TargetID1 + ), + ( @TopicID, + @RelationshipKey, + @TargetID2 + ), + ( @TopicID, + @RelationshipKey, + @TargetID3 + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Relationships +WHERE RelationshipKey = @RelationshipKey; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Relationships +WHERE RelationshipKey = 'UpdateRelationshipsTest'; + +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateRelationshipsTest%'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Relationships +WHERE RelationshipKey = 'UpdateRelationshipsTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Topics +WHERE TopicKey LIKE 'CreateTopicTest%' + AND ContentType = 'Test' + AND ParentID is NULL + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'CreateTopicTest%' + AND ContentType = 'Test' + AND ParentID is NULL + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Topics +WHERE TopicKey LIKE 'UpdateTopicTest%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateTopicTest', + @ContentType = 'Test', + @ParentID = NULL; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey 'UpdateTopicTest' True From f184d191d602e07f7a16e2f85f6347a6adc2a81b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 16:53:42 -0800 Subject: [PATCH 314/778] Implement basic unit test for the `UpdateTopic` stored procedure The `UpdateTopicTest` creates a new topic, then updates the `Key` and `ContentType` using the `UpdateTopic` stored procedure. It confirms that the `Key` and `ContentType` are updated. Note: Some of this was inadvertantly committed as part of the previous commit, which was supposed to be exclusive to the `UpdateRelationshipsTest` (04eb1bc). Apologies to future readers for the sloppy history. --- .../StoredProcedures.resx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 27cc84f5..22559846 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -484,13 +484,13 @@ DECLARE @TopicID AS INT, -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'UpdateTopicTest', @ContentType = 'Test', - @Key = 'UpdateTopicTestNew', - @ContentType = 'TestNew', + @NewKey = 'UpdateTopicTestNew', + @NewContentType = 'TestNew', @ParentID = NULL; SELECT @TopicID = TopicID FROM Topics -WHERE TopicKey = @Key +WHERE TopicKey = @Key; -------------------------------------------------------------------------------------------------------------------------------- -- EXECUTE PROCEDURE @@ -1298,18 +1298,14 @@ WHERE RelationshipKey = 'UpdateRelationshipsTest%'; -------------------------------------------------------------------------------------------------------------------------------- DELETE FROM Topics -WHERE TopicKey LIKE 'CreateTopicTest%' - AND ContentType = 'Test' - AND ParentID is NULL +WHERE TopicKey LIKE 'UpdateTopicTest%' -------------------------------------------------------------------------------------------------------------------------------- -- VERIFY RESULTS -------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics -WHERE TopicKey LIKE 'CreateTopicTest%' - AND ContentType = 'Test' - AND ParentID is NULL +WHERE TopicKey LIKE 'UpdateTopicTest%' -------------------------------------------------------------------------------------------------------------------------------- @@ -1325,7 +1321,7 @@ DECLARE @TopicID AS INT, -------------------------------------------------------------------------------------------------------------------------------- DELETE FROM Topics -WHERE TopicKey LIKE 'UpdateTopicTest%' +WHERE TopicKey LIKE 'UpdateTopicTest%'; -------------------------------------------------------------------------------------------------------------------------------- -- SET VARIABLES @@ -1347,7 +1343,7 @@ EXECUTE @TopicID = [dbo].[CreateTopic] -------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics -WHERE TopicKey 'UpdateTopicTest' +WHERE TopicKey = @Key; True From 1fb3016183023e0d036325fb599dbcb03575d465 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 16:55:53 -0800 Subject: [PATCH 315/778] Fixed bug in `MoveTopicTest` post-test cleanup script Was inadvertantly using an `=` operator where I needed to use a `LIKE` operator due to the presence of a `%` wildcard. --- OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 22559846..6ae5fb69 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -1026,14 +1026,14 @@ WHERE TopicKey LIKE 'MoveTopic%' -------------------------------------------------------------------------------------------------------------------------------- DELETE FROM Topics -WHERE TopicKey = 'MoveTopic%' +WHERE TopicKey LIKE 'MoveTopic%' -------------------------------------------------------------------------------------------------------------------------------- -- VERIFY RESULTS -------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics -WHERE TopicKey = 'MoveTopic%' +WHERE TopicKey LIKE 'MoveTopic%' -------------------------------------------------------------------------------------------------------------------------------- From 69c0500a61897405effcf535c3dd878f93661521 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 17:19:48 -0800 Subject: [PATCH 316/778] Implement basic unit test for the `UpdateExtendedAttributes` stored procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `UpdateExtendedAttributesTest` creates a new topic with extended attributes, and then updates the extended attributes using the `UpdateExtendedAttributes` stored procedure. It confirms that the extended attributes value is update—and that calling the update twice doesn't result in a duplicate record. --- .../StoredProcedures.cs | 77 ++++++++++-- .../StoredProcedures.resx | 113 ++++++++++++++++-- 2 files changed, 173 insertions(+), 17 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index aafa3602..12ac3a38 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -62,7 +62,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateReferenceValue; @@ -109,6 +108,13 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateExtendedAttributeValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -149,7 +155,6 @@ private void InitializeComponent() { updateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); updateAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); updateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); updateReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); @@ -196,6 +201,13 @@ private void InitializeComponent() { preUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); updateTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); postUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_UpdateExtendedAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + dbo_UpdateExtendedAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postUpdateExtendedAttributeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + postUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -422,14 +434,10 @@ private void InitializeComponent() { // // dbo_UpdateExtendedAttributesTest_TestAction // - dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(inconclusiveCondition7); + dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(updateExtendedAttributeCount); + dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(updateExtendedAttributeValue); resources.ApplyResources(dbo_UpdateExtendedAttributesTest_TestAction, "dbo_UpdateExtendedAttributesTest_TestAction"); // - // inconclusiveCondition7 - // - inconclusiveCondition7.Enabled = true; - inconclusiveCondition7.Name = "inconclusiveCondition7"; - // // dbo_UpdateReferencesTest_TestAction // dbo_UpdateReferencesTest_TestAction.Conditions.Add(updateReferenceCount); @@ -752,8 +760,8 @@ private void InitializeComponent() { // // dbo_UpdateExtendedAttributesTestData // - this.dbo_UpdateExtendedAttributesTestData.PosttestAction = null; - this.dbo_UpdateExtendedAttributesTestData.PretestAction = null; + this.dbo_UpdateExtendedAttributesTestData.PosttestAction = dbo_UpdateExtendedAttributesTest_PosttestAction; + this.dbo_UpdateExtendedAttributesTestData.PretestAction = dbo_UpdateExtendedAttributesTest_PretestAction; this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction; // // dbo_UpdateReferencesTestData @@ -790,6 +798,55 @@ private void InitializeComponent() { postUpdateTopicCount.Name = "postUpdateTopicCount"; postUpdateTopicCount.ResultSet = 1; postUpdateTopicCount.RowCount = 0; + // + // dbo_UpdateExtendedAttributesTest_PretestAction + // + dbo_UpdateExtendedAttributesTest_PretestAction.Conditions.Add(preUpdateExtendedAttributeCount); + resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PretestAction, "dbo_UpdateExtendedAttributesTest_PretestAction"); + // + // preUpdateExtendedAttributeCount + // + preUpdateExtendedAttributeCount.Enabled = true; + preUpdateExtendedAttributeCount.Name = "preUpdateExtendedAttributeCount"; + preUpdateExtendedAttributeCount.ResultSet = 1; + preUpdateExtendedAttributeCount.RowCount = 1; + // + // updateExtendedAttributeCount + // + updateExtendedAttributeCount.Enabled = true; + updateExtendedAttributeCount.Name = "updateExtendedAttributeCount"; + updateExtendedAttributeCount.ResultSet = 1; + updateExtendedAttributeCount.RowCount = 2; + // + // updateExtendedAttributeValue + // + updateExtendedAttributeValue.ColumnNumber = 1; + updateExtendedAttributeValue.Enabled = true; + updateExtendedAttributeValue.ExpectedValue = "New"; + updateExtendedAttributeValue.Name = "updateExtendedAttributeValue"; + updateExtendedAttributeValue.NullExpected = false; + updateExtendedAttributeValue.ResultSet = 1; + updateExtendedAttributeValue.RowNumber = 2; + // + // dbo_UpdateExtendedAttributesTest_PosttestAction + // + dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeTopicCount); + dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeCount); + resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PosttestAction, "dbo_UpdateExtendedAttributesTest_PosttestAction"); + // + // postUpdateExtendedAttributeTopicCount + // + postUpdateExtendedAttributeTopicCount.Enabled = true; + postUpdateExtendedAttributeTopicCount.Name = "postUpdateExtendedAttributeTopicCount"; + postUpdateExtendedAttributeTopicCount.ResultSet = 1; + postUpdateExtendedAttributeTopicCount.RowCount = 0; + // + // postUpdateExtendedAttributeCount + // + postUpdateExtendedAttributeCount.Enabled = true; + postUpdateExtendedAttributeCount.Name = "postUpdateExtendedAttributeCount"; + postUpdateExtendedAttributeCount.ResultSet = 1; + postUpdateExtendedAttributeCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 6ae5fb69..805c4851 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -337,26 +337,48 @@ WHERE TopicID = @TopicID ORDER BY AttributeKey ASC; - -- database unit test for dbo.UpdateExtendedAttributes -DECLARE @RC AS INT, + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), @TopicID AS INT, @ExtendedAttributes AS XML, @Version AS DATETIME, @DeleteUnmatched AS BIT; -SELECT @RC = 0, - @TopicID = 0, - @ExtendedAttributes = NULL, +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateExtendedAttributesTest', + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>New</Attribute></Attributes>', @Version = GETUTCDATE(), @DeleteUnmatched = 0; -EXECUTE @RC = [dbo].[UpdateExtendedAttributes] +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[UpdateExtendedAttributes] + @TopicID, + @ExtendedAttributes, + @Version, + @DeleteUnmatched; + +EXECUTE [dbo].[UpdateExtendedAttributes] @TopicID, @ExtendedAttributes, @Version, @DeleteUnmatched; -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT AttributesXml +FROM ExtendedAttributes +ORDER BY Version ASC -------------------------------------------------------------------------------------------------------------------------------- @@ -1294,6 +1316,12 @@ WHERE RelationshipKey = 'UpdateRelationshipsTest%'; -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT; + + +-------------------------------------------------------------------------------------------------------------------------------- -- DELETE TEST DATA -------------------------------------------------------------------------------------------------------------------------------- DELETE @@ -1344,6 +1372,77 @@ EXECUTE @TopicID = [dbo].[CreateTopic] SELECT * FROM Topics WHERE TopicKey = @Key; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateExtendedAttributesTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXEC [dbo].[DeleteTopic] + @TopicID + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey = @Key + +SELECT * +FROM ExtendedAttributes + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @ExtendedAttributes AS XML; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM ExtendedAttributes + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateExtendedAttributesTest', + @ContentType = 'Test', + @ParentID = NULL, + @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key = @Key, + @ContentType = @ContentType, + @ParentID = @ParentID, + @ExtendedAttributes = @ExtendedAttributes; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM ExtendedAttributes True From d235125401bc64c51a9459b37a928ff4a55b9952 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 17:35:21 -0800 Subject: [PATCH 317/778] Fixed bug where unit tests would occassionally introduce duplicates Fast running unit tests and imprecise date/time values could result in primary key violations on versioned data due to the pre-test script inserting values with the same version as the main test script, even though they're intended to be distinct. --- OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 805c4851..555d2c3d 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -309,7 +309,7 @@ SELECT @TopicID = TopicID FROM Topics WHERE TopicKey = 'UpdateAttributesTest'; -SELECT @Version = GETUTCDATE(); +SELECT @Version = DATEADD(day, 1, GETUTCDATE()); INSERT INTO @Attributes @@ -351,7 +351,7 @@ DECLARE @Key AS VARCHAR(128), -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'UpdateExtendedAttributesTest', @ExtendedAttributes = '<Attributes><Attribute key=''Body''>New</Attribute></Attributes>', - @Version = GETUTCDATE(), + @Version = DATEADD(day, 1, GETUTCDATE()), @DeleteUnmatched = 0; SELECT @TopicID = TopicID @@ -401,7 +401,7 @@ SELECT @TargetID = TopicID FROM Topics WHERE TopicKey = 'UpdateReferencesTestTarget2'; -SELECT @Version = GETUTCDATE(), +SELECT @Version = DATEADD(day, 1, GETUTCDATE()), @DeleteUnmatched = 1; INSERT @@ -445,7 +445,7 @@ DECLARE @TopicID AS INT, -- SET VARIABLES -------------------------------------------------------------------------------------------------------------------------------- SELECT @RelationshipKey = 'UpdateRelationshipsTest', - @Version = GETUTCDATE(), + @Version = DATEADD(day, 1, GETUTCDATE()), @DeleteUnmatched = 1; SELECT @TopicID = TopicID From 58856eff3afbde276f72ba8166e0b0a4a1cd7970 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 16 Jan 2021 17:37:51 -0800 Subject: [PATCH 318/778] =?UTF-8?q?Set=20SQL=20unit=20tests=20to=20compile?= =?UTF-8?q?=20during=20`debug`=E2=80=94but=20not=20`release`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local SQL server won't be available on Azure DevOps. As such, we don't want the SQL server unit tests project to compile—and, thus, have its unit tests discovered—when running as part of continuous integration. We do want updates to the unit tests to be automatically compiled when running the SQL unit tests in Visual Studio, however. Since Visual Studio's unit tests run in the `debug` configuration and Azure DevOps' run in the `release` configuration, we can hack around this by limiting the compilation of the unit tests project to the `debug` configuration. --- OnTopic.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic.sln b/OnTopic.sln index e6c0a9df..b29473fa 100644 --- a/OnTopic.sln +++ b/OnTopic.sln @@ -79,6 +79,7 @@ Global {FE175884-59C1-4C4D-A663-4CC570432ECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE175884-59C1-4C4D-A663-4CC570432ECC}.Release|Any CPU.Build.0 = Release|Any CPU {D7FE876D-A75F-4493-8283-B316271FD5AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7FE876D-A75F-4493-8283-B316271FD5AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7FE876D-A75F-4493-8283-B316271FD5AE}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution From 4803fbbb2868f89cbb6dde54dd2bf1a41b6c0c82 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 12:07:53 -0800 Subject: [PATCH 319/778] Updated casing of `AttributesXml` Element names should be lower case. --- .../StoredProcedures.cs | 148 +++++++++--------- .../StoredProcedures.resx | 74 ++++----- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 12ac3a38..7b670361 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -62,6 +62,8 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateExtendedAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateReferenceCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateReferenceValue; @@ -70,6 +72,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateRelationshipValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_CreateTopicTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postCreateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_DeleteTopicTest_PretestAction; @@ -104,14 +107,11 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateTopicTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateTopicValue; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition updateExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition updateExtendedAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeCount; @@ -155,6 +155,8 @@ private void InitializeComponent() { updateAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); updateAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateExtendedAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + updateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateReferencesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); updateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); updateReferenceValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); @@ -163,6 +165,7 @@ private void InitializeComponent() { updateRelationshipValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); updateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + updateTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_CreateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postCreateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -197,14 +200,11 @@ private void InitializeComponent() { preUpdateRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_UpdateRelationshipsTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_UpdateTopicTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_UpdateTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - updateTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); - postUpdateTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_UpdateExtendedAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - updateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - updateExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_UpdateExtendedAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postUpdateExtendedAttributeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); postUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); @@ -438,6 +438,23 @@ private void InitializeComponent() { dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(updateExtendedAttributeValue); resources.ApplyResources(dbo_UpdateExtendedAttributesTest_TestAction, "dbo_UpdateExtendedAttributesTest_TestAction"); // + // updateExtendedAttributeCount + // + updateExtendedAttributeCount.Enabled = true; + updateExtendedAttributeCount.Name = "updateExtendedAttributeCount"; + updateExtendedAttributeCount.ResultSet = 1; + updateExtendedAttributeCount.RowCount = 2; + // + // updateExtendedAttributeValue + // + updateExtendedAttributeValue.ColumnNumber = 1; + updateExtendedAttributeValue.Enabled = true; + updateExtendedAttributeValue.ExpectedValue = "New"; + updateExtendedAttributeValue.Name = "updateExtendedAttributeValue"; + updateExtendedAttributeValue.NullExpected = false; + updateExtendedAttributeValue.ResultSet = 1; + updateExtendedAttributeValue.RowNumber = 2; + // // dbo_UpdateReferencesTest_TestAction // dbo_UpdateReferencesTest_TestAction.Conditions.Add(updateReferenceCount); @@ -497,6 +514,16 @@ private void InitializeComponent() { updateTopicCount.ResultSet = 1; updateTopicCount.RowCount = 1; // + // updateTopicValue + // + updateTopicValue.ColumnNumber = 1; + updateTopicValue.Enabled = true; + updateTopicValue.ExpectedValue = "TestNew"; + updateTopicValue.Name = "updateTopicValue"; + updateTopicValue.NullExpected = false; + updateTopicValue.ResultSet = 1; + updateTopicValue.RowNumber = 1; + // // dbo_CreateTopicTest_PosttestAction // dbo_CreateTopicTest_PosttestAction.Conditions.Add(postCreateTopicCount); @@ -710,6 +737,13 @@ private void InitializeComponent() { dbo_UpdateTopicTest_PosttestAction.Conditions.Add(postUpdateTopicCount); resources.ApplyResources(dbo_UpdateTopicTest_PosttestAction, "dbo_UpdateTopicTest_PosttestAction"); // + // postUpdateTopicCount + // + postUpdateTopicCount.Enabled = true; + postUpdateTopicCount.Name = "postUpdateTopicCount"; + postUpdateTopicCount.ResultSet = 1; + postUpdateTopicCount.RowCount = 0; + // // dbo_UpdateTopicTest_PretestAction // dbo_UpdateTopicTest_PretestAction.Conditions.Add(preUpdateTopicCount); @@ -722,6 +756,38 @@ private void InitializeComponent() { preUpdateTopicCount.ResultSet = 1; preUpdateTopicCount.RowCount = 1; // + // dbo_UpdateExtendedAttributesTest_PretestAction + // + dbo_UpdateExtendedAttributesTest_PretestAction.Conditions.Add(preUpdateExtendedAttributeCount); + resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PretestAction, "dbo_UpdateExtendedAttributesTest_PretestAction"); + // + // preUpdateExtendedAttributeCount + // + preUpdateExtendedAttributeCount.Enabled = true; + preUpdateExtendedAttributeCount.Name = "preUpdateExtendedAttributeCount"; + preUpdateExtendedAttributeCount.ResultSet = 1; + preUpdateExtendedAttributeCount.RowCount = 1; + // + // dbo_UpdateExtendedAttributesTest_PosttestAction + // + dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeTopicCount); + dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeCount); + resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PosttestAction, "dbo_UpdateExtendedAttributesTest_PosttestAction"); + // + // postUpdateExtendedAttributeTopicCount + // + postUpdateExtendedAttributeTopicCount.Enabled = true; + postUpdateExtendedAttributeTopicCount.Name = "postUpdateExtendedAttributeTopicCount"; + postUpdateExtendedAttributeTopicCount.ResultSet = 1; + postUpdateExtendedAttributeTopicCount.RowCount = 0; + // + // postUpdateExtendedAttributeCount + // + postUpdateExtendedAttributeCount.Enabled = true; + postUpdateExtendedAttributeCount.Name = "postUpdateExtendedAttributeCount"; + postUpdateExtendedAttributeCount.ResultSet = 1; + postUpdateExtendedAttributeCount.RowCount = 0; + // // dbo_CreateTopicTestData // this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; @@ -781,72 +847,6 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = dbo_UpdateTopicTest_PosttestAction; this.dbo_UpdateTopicTestData.PretestAction = dbo_UpdateTopicTest_PretestAction; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; - // - // updateTopicValue - // - updateTopicValue.ColumnNumber = 1; - updateTopicValue.Enabled = true; - updateTopicValue.ExpectedValue = "TestNew"; - updateTopicValue.Name = "updateTopicValue"; - updateTopicValue.NullExpected = false; - updateTopicValue.ResultSet = 1; - updateTopicValue.RowNumber = 1; - // - // postUpdateTopicCount - // - postUpdateTopicCount.Enabled = true; - postUpdateTopicCount.Name = "postUpdateTopicCount"; - postUpdateTopicCount.ResultSet = 1; - postUpdateTopicCount.RowCount = 0; - // - // dbo_UpdateExtendedAttributesTest_PretestAction - // - dbo_UpdateExtendedAttributesTest_PretestAction.Conditions.Add(preUpdateExtendedAttributeCount); - resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PretestAction, "dbo_UpdateExtendedAttributesTest_PretestAction"); - // - // preUpdateExtendedAttributeCount - // - preUpdateExtendedAttributeCount.Enabled = true; - preUpdateExtendedAttributeCount.Name = "preUpdateExtendedAttributeCount"; - preUpdateExtendedAttributeCount.ResultSet = 1; - preUpdateExtendedAttributeCount.RowCount = 1; - // - // updateExtendedAttributeCount - // - updateExtendedAttributeCount.Enabled = true; - updateExtendedAttributeCount.Name = "updateExtendedAttributeCount"; - updateExtendedAttributeCount.ResultSet = 1; - updateExtendedAttributeCount.RowCount = 2; - // - // updateExtendedAttributeValue - // - updateExtendedAttributeValue.ColumnNumber = 1; - updateExtendedAttributeValue.Enabled = true; - updateExtendedAttributeValue.ExpectedValue = "New"; - updateExtendedAttributeValue.Name = "updateExtendedAttributeValue"; - updateExtendedAttributeValue.NullExpected = false; - updateExtendedAttributeValue.ResultSet = 1; - updateExtendedAttributeValue.RowNumber = 2; - // - // dbo_UpdateExtendedAttributesTest_PosttestAction - // - dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeTopicCount); - dbo_UpdateExtendedAttributesTest_PosttestAction.Conditions.Add(postUpdateExtendedAttributeCount); - resources.ApplyResources(dbo_UpdateExtendedAttributesTest_PosttestAction, "dbo_UpdateExtendedAttributesTest_PosttestAction"); - // - // postUpdateExtendedAttributeTopicCount - // - postUpdateExtendedAttributeTopicCount.Enabled = true; - postUpdateExtendedAttributeTopicCount.Name = "postUpdateExtendedAttributeTopicCount"; - postUpdateExtendedAttributeTopicCount.ResultSet = 1; - postUpdateExtendedAttributeTopicCount.RowCount = 0; - // - // postUpdateExtendedAttributeCount - // - postUpdateExtendedAttributeCount.Enabled = true; - postUpdateExtendedAttributeCount.Name = "postUpdateExtendedAttributeCount"; - postUpdateExtendedAttributeCount.ResultSet = 1; - postUpdateExtendedAttributeCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 555d2c3d..74a044ae 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -350,7 +350,7 @@ DECLARE @Key AS VARCHAR(128), -- SET VARIABLES -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'UpdateExtendedAttributesTest', - @ExtendedAttributes = '<Attributes><Attribute key=''Body''>New</Attribute></Attributes>', + @ExtendedAttributes = '<attributes><attribute key=''Body''>New</attribute></attributes>', @Version = DATEADD(day, 1, GETUTCDATE()), @DeleteUnmatched = 0; @@ -697,7 +697,7 @@ WHERE TopicKey LIKE 'GetTopics%' SELECT @Key = 'GetTopicsTest', @ContentType = 'Test', @ParentID = NULL, - @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>', @Version = GETUTCDATE(); INSERT @@ -831,7 +831,7 @@ WHERE TopicKey LIKE 'GetTopicVersion%' SELECT @Key = 'GetTopicVersionTest', @ContentType = 'Test', @ParentID = NULL, - @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', + @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>', @Version = '2020-01-01 12:00:00:000', @NewVersion = '2021-01-01 12:00:00:000'; @@ -893,7 +893,7 @@ VALUES ( @ParentID, -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE VARIABLES -------------------------------------------------------------------------------------------------------------------------------- -SELECT @ExtendedAttributes = '<Attributes><Attribute key=''Body''>New Test</Attribute></Attributes>'; +SELECT @ExtendedAttributes = '<attributes><attribute key=''Body''>New Test</attribute></attributes>'; UPDATE @Attributes SET AttributeValue = 'NewValue' @@ -1372,38 +1372,6 @@ EXECUTE @TopicID = [dbo].[CreateTopic] SELECT * FROM Topics WHERE TopicKey = @Key; - - - -------------------------------------------------------------------------------------------------------------------------------- --- DECLARE VARIABLES --------------------------------------------------------------------------------------------------------------------------------- -DECLARE @Key AS VARCHAR(128), - @TopicID AS INT; - --------------------------------------------------------------------------------------------------------------------------------- --- SET VARIABLES --------------------------------------------------------------------------------------------------------------------------------- -SELECT @Key = 'UpdateExtendedAttributesTest'; - -SELECT @TopicID = TopicID -FROM Topics -WHERE TopicKey = @Key; - --------------------------------------------------------------------------------------------------------------------------------- --- DELETE TEST DATA --------------------------------------------------------------------------------------------------------------------------------- -EXEC [dbo].[DeleteTopic] - @TopicID - --------------------------------------------------------------------------------------------------------------------------------- --- VERIFY RESULTS --------------------------------------------------------------------------------------------------------------------------------- -SELECT * -FROM Topics -WHERE TopicKey = @Key - -SELECT * -FROM ExtendedAttributes -------------------------------------------------------------------------------------------------------------------------------- @@ -1427,7 +1395,7 @@ FROM ExtendedAttributes SELECT @Key = 'UpdateExtendedAttributesTest', @ContentType = 'Test', @ParentID = NULL, - @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>'; + @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>'; -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA @@ -1441,6 +1409,38 @@ EXECUTE @TopicID = [dbo].[CreateTopic] -------------------------------------------------------------------------------------------------------------------------------- -- VERIFY RESULTS -------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM ExtendedAttributes + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'UpdateExtendedAttributesTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXEC [dbo].[DeleteTopic] + @TopicID + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey = @Key + SELECT * FROM ExtendedAttributes From 156e734af443ebbe275d2dcabe489bd250c2fbcc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 12:09:24 -0800 Subject: [PATCH 320/778] Implement basic unit test for the `GetAttributes` function The `GetAttributesTest` creates a new topic with indexed and extended attributes, and then retrieves those attributes using the `GetAttributes` function. It confirms that both the indexed and extended attributes are correctly retrieved. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 63 +++++++-- .../Functions.resx | 120 +++++++++++++++++- 2 files changed, 172 insertions(+), 11 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 74e53a78..5dd3a8c3 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -43,9 +43,14 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition6; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetChildTopicIDsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postGetAttributeCount; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -64,9 +69,14 @@ private void InitializeComponent() { dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition6 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetChildTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + dbo_GetAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -120,13 +130,26 @@ private void InitializeComponent() { // // dbo_GetAttributesTest_TestAction // - dbo_GetAttributesTest_TestAction.Conditions.Add(inconclusiveCondition6); + dbo_GetAttributesTest_TestAction.Conditions.Add(getAttributeCount); + dbo_GetAttributesTest_TestAction.Conditions.Add(getAttributeValue); resources.ApplyResources(dbo_GetAttributesTest_TestAction, "dbo_GetAttributesTest_TestAction"); // - // inconclusiveCondition6 + // getAttributeCount // - inconclusiveCondition6.Enabled = true; - inconclusiveCondition6.Name = "inconclusiveCondition6"; + getAttributeCount.Enabled = true; + getAttributeCount.Name = "getAttributeCount"; + getAttributeCount.ResultSet = 1; + getAttributeCount.RowCount = 4; + // + // getAttributeValue + // + getAttributeValue.ColumnNumber = 1; + getAttributeValue.Enabled = true; + getAttributeValue.ExpectedValue = "GetAttributesTest4"; + getAttributeValue.Name = "getAttributeValue"; + getAttributeValue.NullExpected = false; + getAttributeValue.ResultSet = 1; + getAttributeValue.RowNumber = 4; // // dbo_GetChildTopicIDsTest_TestAction // @@ -138,6 +161,18 @@ private void InitializeComponent() { inconclusiveCondition7.Enabled = true; inconclusiveCondition7.Name = "inconclusiveCondition7"; // + // dbo_GetAttributesTest_PretestAction + // + dbo_GetAttributesTest_PretestAction.Conditions.Add(preGetAttributeCount); + resources.ApplyResources(dbo_GetAttributesTest_PretestAction, "dbo_GetAttributesTest_PretestAction"); + // + // preGetAttributeCount + // + preGetAttributeCount.Enabled = true; + preGetAttributeCount.Name = "preGetAttributeCount"; + preGetAttributeCount.ResultSet = 1; + preGetAttributeCount.RowCount = 3; + // // dbo_GetExtendedAttributeTestData // this.dbo_GetExtendedAttributeTestData.PosttestAction = null; @@ -170,8 +205,8 @@ private void InitializeComponent() { // // dbo_GetAttributesTestData // - this.dbo_GetAttributesTestData.PosttestAction = null; - this.dbo_GetAttributesTestData.PretestAction = null; + this.dbo_GetAttributesTestData.PosttestAction = dbo_GetAttributesTest_PosttestAction; + this.dbo_GetAttributesTestData.PretestAction = dbo_GetAttributesTest_PretestAction; this.dbo_GetAttributesTestData.TestAction = dbo_GetAttributesTest_TestAction; // // dbo_GetChildTopicIDsTestData @@ -179,6 +214,18 @@ private void InitializeComponent() { this.dbo_GetChildTopicIDsTestData.PosttestAction = null; this.dbo_GetChildTopicIDsTestData.PretestAction = null; this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; + // + // dbo_GetAttributesTest_PosttestAction + // + dbo_GetAttributesTest_PosttestAction.Conditions.Add(postGetAttributeCount); + resources.ApplyResources(dbo_GetAttributesTest_PosttestAction, "dbo_GetAttributesTest_PosttestAction"); + // + // postGetAttributeCount + // + postGetAttributeCount.Enabled = true; + postGetAttributeCount.Name = "postGetAttributeCount"; + postGetAttributeCount.ResultSet = 1; + postGetAttributeCount.RowCount = 0; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 0695d55a..79ac8c0a 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -200,11 +200,24 @@ FROM [dbo].[FindTopicIDs]( ); - -- database unit test for dbo.GetAttributes -DECLARE @TopicID AS INT; + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT; -SELECT @TopicID = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetAttributesTest' + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM [dbo].[GetAttributes]( @TopicID @@ -221,6 +234,107 @@ FROM [dbo].[GetChildTopicIDs]( @TopicID ); + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT, + @AttributesXml AS XML; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetAttributesTest', + @AttributesXml = '<attributes><attribute key=''GetAttributesTest4''>New</attribute></attributes>'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE TopicID = @TopicID; + +DELETE +FROM ExtendedAttributes +WHERE TopicID = @TopicID; + +DELETE +FROM Topics +WHERE TopicID = @TopicID; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + 'Test', + NULL; + +INSERT +INTO Attributes ( + TopicID, + AttributeKey, + AttributeValue + ) +VALUES ( @TopicID, + 'GetAttributesTest1', + 'Value' + ), + ( @TopicID, + 'GetAttributesTest2', + 'Value' + ), + ( @TopicID, + 'GetAttributesTest3', + 'Value' + ); + +INSERT +INTO ExtendedAttributes ( + TopicID, + AttributesXML + ) +VALUES ( @TopicID, + @AttributesXml + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetAttributesTest%'; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetAttributesTest' + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +EXEC [dbo].[DeleteTopic] @TopicID + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetAttributesTest%' + True From 4618bb3355e8fc120812e11d3de462a9b1f841cc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 12:14:48 -0800 Subject: [PATCH 321/778] Updated casing of `AttributesXml` (cont.) Element names should be lower case. I missed a post-condition during the original update, thus breaking the `UpdateExtendedAttributes` test (4803fbb). --- OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 7b670361..8a07c889 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -449,7 +449,7 @@ private void InitializeComponent() { // updateExtendedAttributeValue.ColumnNumber = 1; updateExtendedAttributeValue.Enabled = true; - updateExtendedAttributeValue.ExpectedValue = "New"; + updateExtendedAttributeValue.ExpectedValue = "New"; updateExtendedAttributeValue.Name = "updateExtendedAttributeValue"; updateExtendedAttributeValue.NullExpected = false; updateExtendedAttributeValue.ResultSet = 1; From 2bac9f69a3d24f71c6fc0bf62b7a8c4a98ca4b1e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 13:30:23 -0800 Subject: [PATCH 322/778] Established shared data structure for (remaining) function tests Most of the function tests operate against a hierarchy, so establishing an initial hierarchy of topic data will reduce the amount of setup and teardown required for each individual unit test. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 37 ++++++ .../Functions.resx | 112 +++++++++++++++++- 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 5dd3a8c3..d6edb6ba 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -51,6 +51,10 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postGetAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testInitializeAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFunctionTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testCleanupAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postFunctionTopicCount; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -77,6 +81,10 @@ private void InitializeComponent() { preGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + testInitializeAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + testCleanupAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -226,6 +234,35 @@ private void InitializeComponent() { postGetAttributeCount.Name = "postGetAttributeCount"; postGetAttributeCount.ResultSet = 1; postGetAttributeCount.RowCount = 0; + // + // testInitializeAction + // + testInitializeAction.Conditions.Add(preFunctionTopicCount); + resources.ApplyResources(testInitializeAction, "testInitializeAction"); + // + // preFunctionTopicCount + // + preFunctionTopicCount.Enabled = true; + preFunctionTopicCount.Name = "preFunctionTopicCount"; + preFunctionTopicCount.ResultSet = 1; + preFunctionTopicCount.RowCount = 7; + // + // testCleanupAction + // + testCleanupAction.Conditions.Add(postFunctionTopicCount); + resources.ApplyResources(testCleanupAction, "testCleanupAction"); + // + // postFunctionTopicCount + // + postFunctionTopicCount.Enabled = true; + postFunctionTopicCount.Name = "postFunctionTopicCount"; + postFunctionTopicCount.ResultSet = 1; + postFunctionTopicCount.RowCount = 0; + // + // Functions + // + this.TestCleanupAction = testCleanupAction; + this.TestInitializeAction = testInitializeAction; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 79ac8c0a..be33521e 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -238,7 +238,8 @@ FROM [dbo].[GetChildTopicIDs]( -------------------------------------------------------------------------------------------------------------------------------- -- DECLARE VARIABLES -------------------------------------------------------------------------------------------------------------------------------- -DECLARE @Key AS VARCHAR(128), +DECLARE @RootTopicID AS INT, + @Key AS VARCHAR(128), @TopicID AS INT, @AttributesXml AS XML; @@ -248,6 +249,10 @@ DECLARE @Key AS VARCHAR(128), SELECT @Key = 'GetAttributesTest', @AttributesXml = '<attributes><attribute key=''GetAttributesTest4''>New</attribute></attributes>'; +SELECT @RootTopicID = TopicID +FROM Topics +WHERE TopicKey = 'FunctionTests'; + SELECT @TopicID = TopicID FROM Topics WHERE TopicKey = @Key; @@ -273,7 +278,7 @@ WHERE TopicID = @TopicID; EXECUTE @TopicID = [dbo].[CreateTopic] @Key, 'Test', - NULL; + @RootTopicID; INSERT INTO Attributes ( @@ -334,6 +339,109 @@ EXEC [dbo].[DeleteTopic] @TopicID SELECT * FROM Attributes WHERE AttributeKey LIKE 'GetAttributesTest%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @RootTopicID AS INT, + @RootTopicKey AS VARCHAR(128), + @ContentType AS VARCHAR(128), + @TopicID1 AS INT, + @TopicID2 AS INT, + @TopicID3 AS INT; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @RootTopicKey = 'FunctionTests', + @ContentType = 'Test'; + +SELECT @RootTopicID = TopicID +FROM Topics +WHERE TopicKey = @RootTopicKey; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +IF @RootTopicID IS NOT NULL + BEGIN + EXECUTE [dbo].[DeleteTopic] @RootTopicID; + END + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @RootTopicID = [dbo].[CreateTopic] + @RootTopicKey, + @ContentType, + NULL; + +EXECUTE @TopicID1 = [dbo].[CreateTopic] + 'Topic_1', + @ContentType, + @RootTopicID; + +EXECUTE @TopicID2 = [dbo].[CreateTopic] + 'Topic_1_1', + @ContentType, + @TopicID1; + +EXECUTE @TopicID3 = [dbo].[CreateTopic] + 'Topic_1_1_1', + @ContentType, + @TopicID2; + +EXECUTE [dbo].[CreateTopic] + 'Topic_1_1_1_1', + @ContentType, + @TopicID3; + +EXECUTE [dbo].[CreateTopic] + 'Topic_1_1_1_2', + @ContentType, + @TopicID3; + +EXECUTE [dbo].[CreateTopic] + 'Topic_1_1_1_3', + @ContentType, + @TopicID3; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @RootTopicID AS INT, + @RootTopicKey AS VARCHAR(128); + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @RootTopicKey = 'FunctionTests'; + +SELECT @RootTopicID = TopicID +FROM Topics +WHERE TopicKey = @RootTopicKey; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +IF @RootTopicID IS NOT NULL + BEGIN + EXECUTE [dbo].[DeleteTopic] @RootTopicID; + END + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics True From 6d69fa6431b05e754bd892d89e43c8424b088864 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:05:26 -0800 Subject: [PATCH 323/778] Implement basic unit test for the `FindTopicIDs` function The `FindTopicIDsTest` adds an attribute and an extended attribute to two existing topics, and then retrieves those topics using the `FindTopicIDs` function. It confirms that both the indexed and extended attributes are correctly retrieved. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 104 ++++++++++------- .../Functions.resx | 110 +++++++++++++++++- 2 files changed, 166 insertions(+), 48 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index d6edb6ba..cf343bbe 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -41,7 +41,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition5; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition findTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getAttributeValue; @@ -55,6 +55,8 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFunctionTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testCleanupAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postFunctionTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFindAttributeCount; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -71,7 +73,7 @@ private void InitializeComponent() { dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition5 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); + findTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); @@ -85,6 +87,8 @@ private void InitializeComponent() { preFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); testCleanupAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_FindTopicIDsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -128,13 +132,15 @@ private void InitializeComponent() { // // dbo_FindTopicIDsTest_TestAction // - dbo_FindTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition5); + dbo_FindTopicIDsTest_TestAction.Conditions.Add(findTopicCount); resources.ApplyResources(dbo_FindTopicIDsTest_TestAction, "dbo_FindTopicIDsTest_TestAction"); // - // inconclusiveCondition5 + // findTopicCount // - inconclusiveCondition5.Enabled = true; - inconclusiveCondition5.Name = "inconclusiveCondition5"; + findTopicCount.Enabled = true; + findTopicCount.Name = "findTopicCount"; + findTopicCount.ResultSet = 1; + findTopicCount.RowCount = 2; // // dbo_GetAttributesTest_TestAction // @@ -181,6 +187,54 @@ private void InitializeComponent() { preGetAttributeCount.ResultSet = 1; preGetAttributeCount.RowCount = 3; // + // dbo_GetAttributesTest_PosttestAction + // + dbo_GetAttributesTest_PosttestAction.Conditions.Add(postGetAttributeCount); + resources.ApplyResources(dbo_GetAttributesTest_PosttestAction, "dbo_GetAttributesTest_PosttestAction"); + // + // postGetAttributeCount + // + postGetAttributeCount.Enabled = true; + postGetAttributeCount.Name = "postGetAttributeCount"; + postGetAttributeCount.ResultSet = 1; + postGetAttributeCount.RowCount = 0; + // + // testInitializeAction + // + testInitializeAction.Conditions.Add(preFunctionTopicCount); + resources.ApplyResources(testInitializeAction, "testInitializeAction"); + // + // preFunctionTopicCount + // + preFunctionTopicCount.Enabled = true; + preFunctionTopicCount.Name = "preFunctionTopicCount"; + preFunctionTopicCount.ResultSet = 1; + preFunctionTopicCount.RowCount = 7; + // + // testCleanupAction + // + testCleanupAction.Conditions.Add(postFunctionTopicCount); + resources.ApplyResources(testCleanupAction, "testCleanupAction"); + // + // postFunctionTopicCount + // + postFunctionTopicCount.Enabled = true; + postFunctionTopicCount.Name = "postFunctionTopicCount"; + postFunctionTopicCount.ResultSet = 1; + postFunctionTopicCount.RowCount = 0; + // + // dbo_FindTopicIDsTest_PretestAction + // + dbo_FindTopicIDsTest_PretestAction.Conditions.Add(preFindAttributeCount); + resources.ApplyResources(dbo_FindTopicIDsTest_PretestAction, "dbo_FindTopicIDsTest_PretestAction"); + // + // preFindAttributeCount + // + preFindAttributeCount.Enabled = true; + preFindAttributeCount.Name = "preFindAttributeCount"; + preFindAttributeCount.ResultSet = 1; + preFindAttributeCount.RowCount = 3; + // // dbo_GetExtendedAttributeTestData // this.dbo_GetExtendedAttributeTestData.PosttestAction = null; @@ -208,7 +262,7 @@ private void InitializeComponent() { // dbo_FindTopicIDsTestData // this.dbo_FindTopicIDsTestData.PosttestAction = null; - this.dbo_FindTopicIDsTestData.PretestAction = null; + this.dbo_FindTopicIDsTestData.PretestAction = dbo_FindTopicIDsTest_PretestAction; this.dbo_FindTopicIDsTestData.TestAction = dbo_FindTopicIDsTest_TestAction; // // dbo_GetAttributesTestData @@ -223,42 +277,6 @@ private void InitializeComponent() { this.dbo_GetChildTopicIDsTestData.PretestAction = null; this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; // - // dbo_GetAttributesTest_PosttestAction - // - dbo_GetAttributesTest_PosttestAction.Conditions.Add(postGetAttributeCount); - resources.ApplyResources(dbo_GetAttributesTest_PosttestAction, "dbo_GetAttributesTest_PosttestAction"); - // - // postGetAttributeCount - // - postGetAttributeCount.Enabled = true; - postGetAttributeCount.Name = "postGetAttributeCount"; - postGetAttributeCount.ResultSet = 1; - postGetAttributeCount.RowCount = 0; - // - // testInitializeAction - // - testInitializeAction.Conditions.Add(preFunctionTopicCount); - resources.ApplyResources(testInitializeAction, "testInitializeAction"); - // - // preFunctionTopicCount - // - preFunctionTopicCount.Enabled = true; - preFunctionTopicCount.Name = "preFunctionTopicCount"; - preFunctionTopicCount.ResultSet = 1; - preFunctionTopicCount.RowCount = 7; - // - // testCleanupAction - // - testCleanupAction.Conditions.Add(postFunctionTopicCount); - resources.ApplyResources(testCleanupAction, "testCleanupAction"); - // - // postFunctionTopicCount - // - postFunctionTopicCount.Enabled = true; - postFunctionTopicCount.Name = "postFunctionTopicCount"; - postFunctionTopicCount.ResultSet = 1; - postFunctionTopicCount.RowCount = 0; - // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index be33521e..4c47fbbb 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -177,19 +177,31 @@ SELECT @RC = [dbo].[GetUniqueKey]( SELECT @RC AS RC; - -- database unit test for dbo.FindTopicIDs + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- DECLARE @TopicID AS INT, @AttributeKey AS VARCHAR (255), @AttributeValue AS NVARCHAR (255), @IsExtendedAttribute AS BIT, @UsePartialMatch AS BIT; -SELECT @TopicID = 0, - @AttributeKey = NULL, - @AttributeValue = NULL, - @IsExtendedAttribute = 0, +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @AttributeKey = 'FindTopicIDsTest', + @AttributeValue = 'Test', + @IsExtendedAttribute = NULL, @UsePartialMatch = 0; +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1' + + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM [dbo].[FindTopicIDs]( @TopicID, @@ -442,6 +454,94 @@ IF @RootTopicID IS NOT NULL -------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @Value AS VARCHAR(128), + @TopicID1 AS INT, + @TopicID2 AS INT, + @TopicID3 AS INT, + @TopicID4 AS INT, + @Attributes AS AttributeValues, + @ExtendedAttributes AS XML; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'FindTopicIDsTest', + @Value = 'Test', + @ExtendedAttributes = '<attributes><attribute key=''' + @Key + '''>' + @Value + '</attribute></attributes>'; + +SELECT @TopicID1 = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_1'; + +SELECT @TopicID2 = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_2'; + +SELECT @TopicID3 = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_3'; + +SELECT @TopicID4 = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO ExtendedAttributes ( + TopicID, + AttributesXML + ) +VALUES ( @TopicID1, + @ExtendedAttributes + ); + +INSERT +INTO Attributes ( + TopicID, + AttributeKey, + AttributeValue + ) +VALUES ( @TopicID2, + @Key, + @Value + ); + +INSERT +INTO Attributes ( + TopicID, + AttributeKey, + AttributeValue + ) +VALUES ( @TopicID3, + @Key, + 'Invalid' + ); + +INSERT +INTO Attributes ( + TopicID, + AttributeKey, + AttributeValue + ) +VALUES ( @TopicID4, + @Key, + @Value + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey = @Key True From 4c80bb029cc456c5393721e8f684ab22e3de15b4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:13:40 -0800 Subject: [PATCH 324/778] Implement basic unit test for the `GetChildTopicIDs` function The `GetChildTopicsIDsTest` attempts to load the children of an existing topic using the `GetChildTopicIDs` function. It confirms that the correct number of children are located. Note: This currently fails due to a bug in the `GethildTopicIDs` function, which this exposed. This bug will be resolved in a subsequent commit. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 18 ++++++++++-------- OnTopic.Data.Sql.Database.Tests/Functions.resx | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index cf343bbe..93d7a892 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -46,7 +46,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetChildTopicIDsTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition7; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PosttestAction; @@ -57,6 +56,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postFunctionTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFindAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -78,7 +78,6 @@ private void InitializeComponent() { getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetChildTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition7 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -89,6 +88,7 @@ private void InitializeComponent() { postFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_FindTopicIDsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -167,14 +167,9 @@ private void InitializeComponent() { // // dbo_GetChildTopicIDsTest_TestAction // - dbo_GetChildTopicIDsTest_TestAction.Conditions.Add(inconclusiveCondition7); + dbo_GetChildTopicIDsTest_TestAction.Conditions.Add(getChildTopicCount); resources.ApplyResources(dbo_GetChildTopicIDsTest_TestAction, "dbo_GetChildTopicIDsTest_TestAction"); // - // inconclusiveCondition7 - // - inconclusiveCondition7.Enabled = true; - inconclusiveCondition7.Name = "inconclusiveCondition7"; - // // dbo_GetAttributesTest_PretestAction // dbo_GetAttributesTest_PretestAction.Conditions.Add(preGetAttributeCount); @@ -277,6 +272,13 @@ private void InitializeComponent() { this.dbo_GetChildTopicIDsTestData.PretestAction = null; this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; // + // getChildTopicCount + // + getChildTopicCount.Enabled = true; + getChildTopicCount.Name = "getChildTopicCount"; + getChildTopicCount.ResultSet = 1; + getChildTopicCount.RowCount = 3; + // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 4c47fbbb..1360ac23 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -236,11 +236,21 @@ FROM [dbo].[GetAttributes]( ); - -- database unit test for dbo.GetChildTopicIDs + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- DECLARE @TopicID AS INT; -SELECT @TopicID = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1'; +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM [dbo].[GetChildTopicIDs]( @TopicID From eb03f08dc3e21adc9b3505b2c9b2798f220dd06f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:15:49 -0800 Subject: [PATCH 325/778] Fixed bug in `GetChildTopicIDs` function The unit test for the `GetChildTopicIDs` function (4c80bb0) exposed that it was not correctly behaving, as it was still written to query the `Attributes` table, even though `ParentID` had been moved to `Topics` along with other core, non-versioned attributes. This fixes the implemention of `GetChildTopicIDs`. (Though, it could be argued, that with the current implementation having a function might not provide much benefit.) --- OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql index f8209653..d72e9372 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql @@ -23,9 +23,8 @@ BEGIN INSERT INTO @Topics SELECT TopicID - FROM Attributes - WHERE AttributeKey = 'ParentID' - AND AttributeValue = @TopicID + FROM Topics + WHERE ParentID = @TopicID ------------------------------------------------------------------------------------------------------------------------------ -- RETURN From b08055e46f5d066cbf52cc4ae38b929b22f13086 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:25:08 -0800 Subject: [PATCH 326/778] Implement basic unit test for the `GetParentID` function The `GetParentIDTest` attempts to load the parent of an existing topic using the `GetParentID` function. It confirms that the correct parent is returned. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 21 +++++++----- .../Functions.resx | 32 +++++++++++++++---- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 93d7a892..485a47e0 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -35,7 +35,6 @@ private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Functions)); Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition2; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; @@ -57,6 +56,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFindAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -67,7 +67,6 @@ private void InitializeComponent() { dbo_GetExtendedAttributeTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition2 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -89,6 +88,7 @@ private void InitializeComponent() { dbo_FindTopicIDsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -102,14 +102,9 @@ private void InitializeComponent() { // // dbo_GetParentIDTest_TestAction // - dbo_GetParentIDTest_TestAction.Conditions.Add(inconclusiveCondition2); + dbo_GetParentIDTest_TestAction.Conditions.Add(getParentIDValue); resources.ApplyResources(dbo_GetParentIDTest_TestAction, "dbo_GetParentIDTest_TestAction"); // - // inconclusiveCondition2 - // - inconclusiveCondition2.Enabled = true; - inconclusiveCondition2.Name = "inconclusiveCondition2"; - // // dbo_GetTopicIDTest_TestAction // dbo_GetTopicIDTest_TestAction.Conditions.Add(inconclusiveCondition3); @@ -279,6 +274,16 @@ private void InitializeComponent() { getChildTopicCount.ResultSet = 1; getChildTopicCount.RowCount = 3; // + // getParentIDValue + // + getParentIDValue.ColumnNumber = 1; + getParentIDValue.Enabled = true; + getParentIDValue.ExpectedValue = "0"; + getParentIDValue.Name = "getParentIDValue"; + getParentIDValue.NullExpected = false; + getParentIDValue.ResultSet = 1; + getParentIDValue.RowNumber = 1; + // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 1360ac23..734397b8 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -135,18 +135,36 @@ SELECT @RC = [dbo].[GetExtendedAttribute]( SELECT @RC AS RC; - -- database unit test for dbo.GetParentID -DECLARE @RC AS INT, - @TopicID AS INT; + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @ExpectedParentID AS INT, + @ParentID AS INT; -SELECT @RC = NULL, - @TopicID = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ExpectedParentID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_1'; -SELECT @RC = [dbo].[GetParentID]( +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ParentID = [dbo].[GetParentID]( @TopicID ); -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- EVALUATE RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ExpectedParentID - @ParentID AS RC; + -- database unit test for dbo.GetTopicID From d6991e7352b9f85106f1b39120a0c864a834ab4c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:50:38 -0800 Subject: [PATCH 327/778] Implement basic unit test for the `GetTopicID` function The `GetTopicID` attempts to load an existing topic using the `GetTopicID` function with a fully-qualified unique key. It confirms that the correct topic is returned. As part of this, ensured that a `Root` topic with a `TopicID` of `1` is first established, as this is a dependency of `GetTopicID`. (We may want to revisit this dependency, but it remains a current dependency nevertheless.) --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 25 +++++--- .../Functions.resx | 61 ++++++++++++++++--- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 485a47e0..870fd9af 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -36,7 +36,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition3; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; @@ -57,6 +56,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFindAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getIDTopicValue; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -68,7 +68,6 @@ private void InitializeComponent() { inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition3 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -89,6 +88,7 @@ private void InitializeComponent() { preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getIDTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -107,14 +107,9 @@ private void InitializeComponent() { // // dbo_GetTopicIDTest_TestAction // - dbo_GetTopicIDTest_TestAction.Conditions.Add(inconclusiveCondition3); + dbo_GetTopicIDTest_TestAction.Conditions.Add(getIDTopicValue); resources.ApplyResources(dbo_GetTopicIDTest_TestAction, "dbo_GetTopicIDTest_TestAction"); // - // inconclusiveCondition3 - // - inconclusiveCondition3.Enabled = true; - inconclusiveCondition3.Name = "inconclusiveCondition3"; - // // dbo_GetUniqueKeyTest_TestAction // dbo_GetUniqueKeyTest_TestAction.Conditions.Add(inconclusiveCondition4); @@ -199,7 +194,7 @@ private void InitializeComponent() { preFunctionTopicCount.Enabled = true; preFunctionTopicCount.Name = "preFunctionTopicCount"; preFunctionTopicCount.ResultSet = 1; - preFunctionTopicCount.RowCount = 7; + preFunctionTopicCount.RowCount = 8; // // testCleanupAction // @@ -211,7 +206,7 @@ private void InitializeComponent() { postFunctionTopicCount.Enabled = true; postFunctionTopicCount.Name = "postFunctionTopicCount"; postFunctionTopicCount.ResultSet = 1; - postFunctionTopicCount.RowCount = 0; + postFunctionTopicCount.RowCount = 1; // // dbo_FindTopicIDsTest_PretestAction // @@ -284,6 +279,16 @@ private void InitializeComponent() { getParentIDValue.ResultSet = 1; getParentIDValue.RowNumber = 1; // + // getIDTopicValue + // + getIDTopicValue.ColumnNumber = 1; + getIDTopicValue.Enabled = true; + getIDTopicValue.ExpectedValue = "0"; + getIDTopicValue.Name = "getIDTopicValue"; + getIDTopicValue.NullExpected = false; + getIDTopicValue.ResultSet = 1; + getIDTopicValue.RowNumber = 1; + // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 734397b8..7522a133 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -167,18 +167,33 @@ SELECT @ExpectedParentID - @ParentID AS RC; - -- database unit test for dbo.GetTopicID -DECLARE @RC AS INT, - @UniqueKey AS NVARCHAR (2500); + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @ExpectedTopicID AS INT, + @UniqueKey AS NVARCHAR(2500); -SELECT @RC = NULL, - @UniqueKey = NULL; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = 'Root:FunctionTests:Topic_1:Topic_1_1:Topic_1_1_1:Topic_1_1_1_2'; -SELECT @RC = [dbo].[GetTopicID]( +SELECT @ExpectedTopicID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_2'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = [dbo].[GetTopicID]( @UniqueKey ); -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- EVALUATE RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ExpectedTopicID - @TopicID AS RC; -- database unit test for dbo.GetUniqueKey @@ -409,13 +424,43 @@ IF @RootTopicID IS NOT NULL EXECUTE [dbo].[DeleteTopic] @RootTopicID; END +-------------------------------------------------------------------------------------------------------------------------------- +-- ENSURE GLOBAL ROOT +-------------------------------------------------------------------------------------------------------------------------------- +IF NOT EXISTS (SELECT * FROM Topics WHERE TopicID = 1) + BEGIN + + SET IDENTITY_INSERT [dbo].[Topics] ON; + + INSERT + INTO Topics ( + TopicID, + TopicKey, + ContentType, + ParentID, + RangeLeft, + RangeRight + ) + VALUES ( + 1, + 'Root', + 'Test', + NULL, + 1, + 2 + ) + + SET IDENTITY_INSERT [dbo].[Topics] OFF; + + END + -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA -------------------------------------------------------------------------------------------------------------------------------- EXECUTE @RootTopicID = [dbo].[CreateTopic] @RootTopicKey, @ContentType, - NULL; + 1; EXECUTE @TopicID1 = [dbo].[CreateTopic] 'Topic_1', From 40b50faeaa79c745413d3d8b239916d3d6a69d3f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 14:55:10 -0800 Subject: [PATCH 328/778] Implement basic unit test for the `GetUniqueKey` function The `GetUniqueKeyTest` attempts to determine the fully-qualified unique key of an existing topic using the `GetUniqueKey` function. It confirms that the correct key is returned. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 21 +++++++++------ .../Functions.resx | 26 ++++++++++++++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 870fd9af..e775dfe6 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -37,7 +37,6 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition4; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition findTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_TestAction; @@ -57,6 +56,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getIDTopicValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getUniqueKeyValue; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -69,7 +69,6 @@ private void InitializeComponent() { dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition4 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); findTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -89,6 +88,7 @@ private void InitializeComponent() { getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); getIDTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + getUniqueKeyValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_GetExtendedAttributeTest_TestAction // @@ -112,14 +112,9 @@ private void InitializeComponent() { // // dbo_GetUniqueKeyTest_TestAction // - dbo_GetUniqueKeyTest_TestAction.Conditions.Add(inconclusiveCondition4); + dbo_GetUniqueKeyTest_TestAction.Conditions.Add(getUniqueKeyValue); resources.ApplyResources(dbo_GetUniqueKeyTest_TestAction, "dbo_GetUniqueKeyTest_TestAction"); // - // inconclusiveCondition4 - // - inconclusiveCondition4.Enabled = true; - inconclusiveCondition4.Name = "inconclusiveCondition4"; - // // dbo_FindTopicIDsTest_TestAction // dbo_FindTopicIDsTest_TestAction.Conditions.Add(findTopicCount); @@ -289,6 +284,16 @@ private void InitializeComponent() { getIDTopicValue.ResultSet = 1; getIDTopicValue.RowNumber = 1; // + // getUniqueKeyValue + // + getUniqueKeyValue.ColumnNumber = 1; + getUniqueKeyValue.Enabled = true; + getUniqueKeyValue.ExpectedValue = "Root:FunctionTests:Topic_1:Topic_1_1:Topic_1_1_1:Topic_1_1_1_2"; + getUniqueKeyValue.Name = "getUniqueKeyValue"; + getUniqueKeyValue.NullExpected = false; + getUniqueKeyValue.ResultSet = 1; + getUniqueKeyValue.RowNumber = 1; + // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 7522a133..5e1445a2 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -196,18 +196,30 @@ SELECT @TopicID = [dbo].[GetTopicID]( SELECT @ExpectedTopicID - @TopicID AS RC; - -- database unit test for dbo.GetUniqueKey -DECLARE @RC AS VARCHAR (MAX), - @TopicID AS INT; + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @UniqueKey AS NVARCHAR(2500); -SELECT @RC = NULL, - @TopicID = 0; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = 'Topic_1_1_1_2'; -SELECT @RC = [dbo].[GetUniqueKey]( +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = [dbo].[GetUniqueKey]( @TopicID ); -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- EVALUATE RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey AS RC; -------------------------------------------------------------------------------------------------------------------------------- From 6d461d39f9684391bb867daf5d4f55108076c4fe Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 15:06:41 -0800 Subject: [PATCH 329/778] Ensured implicit `Root` topic (with `TopicID` of `1`) Before executing the stored procedure tests, ensure there's a root topic with a `TopicID` set to `1`. Most tests don't depend on this, but the `CreateTopic` stored procedure doesn't (yet) handle parentless topics well (in that it will assign them to overlapping ranges), and some functions (such as `GetUniqueKey`) still expect a topic with a `TopicKey` set to `Root` and a `TopicID` set to `1`. --- .../StoredProcedures.cs | 10 +++ .../StoredProcedures.resx | 75 ++++++++++++------- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 8a07c889..10d15527 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -115,6 +115,7 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testInitializeAction; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -208,6 +209,7 @@ private void InitializeComponent() { dbo_UpdateExtendedAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postUpdateExtendedAttributeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); postUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + testInitializeAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); // // dbo_CreateTopicTest_TestAction // @@ -847,6 +849,14 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PosttestAction = dbo_UpdateTopicTest_PosttestAction; this.dbo_UpdateTopicTestData.PretestAction = dbo_UpdateTopicTest_PretestAction; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; + // + // testInitializeAction + // + resources.ApplyResources(testInitializeAction, "testInitializeAction"); + // + // StoredProcedures + // + this.TestInitializeAction = testInitializeAction; } #endregion diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 74a044ae..9a9eda85 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -136,7 +136,7 @@ DECLARE @RC AS INT, SELECT @RC = 0, @Key = 'CreateTopicTest', @ContentType = 'Test', - @ParentID = NULL, + @ParentID = 1, @ExtendedAttributes = NULL, @Version = GETUTCDATE(); @@ -498,8 +498,7 @@ DECLARE @TopicID AS INT, @Key AS VARCHAR (128), @ContentType AS VARCHAR (128), @NewKey AS VARCHAR (128), - @NewContentType AS VARCHAR (128), - @ParentID AS INT; + @NewContentType AS VARCHAR (128); -------------------------------------------------------------------------------------------------------------------------------- -- SET VARIABLES @@ -507,8 +506,7 @@ DECLARE @TopicID AS INT, SELECT @Key = 'UpdateTopicTest', @ContentType = 'Test', @NewKey = 'UpdateTopicTestNew', - @NewContentType = 'TestNew', - @ParentID = NULL; + @NewContentType = 'TestNew'; SELECT @TopicID = TopicID FROM Topics @@ -536,17 +534,13 @@ WHERE TopicKey = @NewKey DELETE FROM Topics WHERE TopicKey = 'CreateTopicTest' - AND ContentType = 'Test' - AND ParentID is NULL -------------------------------------------------------------------------------------------------------------------------------- -- VERIFY RESULTS -------------------------------------------------------------------------------------------------------------------------------- SELECT * FROM Topics -WHERE TopicKey = 'CreateTopicTest' - AND ContentType = 'Test' - AND ParentID is NULL +WHERE TopicKey = 'CreateTopicTest' -------------------------------------------------------------------------------------------------------------------------------- @@ -585,7 +579,7 @@ WHERE TopicKey LIKE 'DeleteTopic%' -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'DeleteTopicTest', @ContentType = 'Test', - @ParentID = NULL, + @ParentID = 1, @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>', @Version = GETUTCDATE(); @@ -696,7 +690,7 @@ WHERE TopicKey LIKE 'GetTopics%' -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'GetTopicsTest', @ContentType = 'Test', - @ParentID = NULL, + @ParentID = 1, @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>', @Version = GETUTCDATE(); @@ -830,7 +824,7 @@ WHERE TopicKey LIKE 'GetTopicVersion%' -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'GetTopicVersionTest', @ContentType = 'Test', - @ParentID = NULL, + @ParentID = 1, @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>', @Version = '2020-01-01 12:00:00:000', @NewVersion = '2021-01-01 12:00:00:000'; @@ -1003,7 +997,7 @@ SELECT @ContentType = 'Test'; EXECUTE @RootTopicID = [dbo].[CreateTopic] 'MoveTopicTest', @ContentType, - NULL; + 1; EXECUTE @ParentID1 = [dbo].[CreateTopic] 'MoveTopicTest1', @@ -1080,7 +1074,7 @@ WHERE TopicKey = 'UpdateAttributesTest'; EXECUTE @TopicID = [dbo].[CreateTopic] 'UpdateAttributesTest', 'Test', - NULL; + 1; -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA @@ -1154,17 +1148,17 @@ WHERE TopicKey LIKE 'UpdateReferencesTest%'; EXECUTE @TopicID = [dbo].[CreateTopic] 'UpdateReferencesTest', 'Test', - NULL; + 1; EXECUTE @TargetID = [dbo].[CreateTopic] 'UpdateReferencesTestTarget1', 'Test', - NULL; + 1; EXECUTE [dbo].[CreateTopic] 'UpdateReferencesTestTarget2', 'Test', - NULL; + 1; -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA @@ -1244,27 +1238,27 @@ SELECT @RelationshipKey = 'UpdateRelationshipsTest' EXECUTE @TopicID = [dbo].[CreateTopic] @RelationshipKey, 'Test', - NULL; + 1; EXECUTE @TargetID1 = [dbo].[CreateTopic] 'UpdateRelationshipsTestTarget1', 'Test', - NULL; + 1; EXECUTE @TargetID2 = [dbo].[CreateTopic] 'UpdateRelationshipsTestTarget2', 'Test', - NULL; + 1; EXECUTE @TargetID3 = [dbo].[CreateTopic] 'UpdateRelationshipsTestTarget3', 'Test', - NULL; + 1; EXECUTE [dbo].[CreateTopic] 'UpdateRelationshipsTestTarget4', 'Test', - NULL; + 1; -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA @@ -1356,7 +1350,7 @@ WHERE TopicKey LIKE 'UpdateTopicTest%'; -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'UpdateTopicTest', @ContentType = 'Test', - @ParentID = NULL; + @ParentID = 1; -------------------------------------------------------------------------------------------------------------------------------- -- ESTABLISH TEST DATA @@ -1394,7 +1388,7 @@ FROM ExtendedAttributes -------------------------------------------------------------------------------------------------------------------------------- SELECT @Key = 'UpdateExtendedAttributesTest', @ContentType = 'Test', - @ParentID = NULL, + @ParentID = 1, @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>'; -------------------------------------------------------------------------------------------------------------------------------- @@ -1443,6 +1437,37 @@ WHERE TopicKey = @Key SELECT * FROM ExtendedAttributes + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ENSURE GLOBAL ROOT +-------------------------------------------------------------------------------------------------------------------------------- +IF NOT EXISTS (SELECT * FROM Topics WHERE TopicID = 1) + BEGIN + + SET IDENTITY_INSERT [dbo].[Topics] ON; + + INSERT + INTO Topics ( + TopicID, + TopicKey, + ContentType, + ParentID, + RangeLeft, + RangeRight + ) + VALUES ( + 1, + 'Root', + 'Test', + NULL, + 1, + 2 + ) + + SET IDENTITY_INSERT [dbo].[Topics] OFF; + + END True From dd5e015382eb9c23031df95058354c8f6014d6f1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 17 Jan 2021 15:22:30 -0800 Subject: [PATCH 330/778] Implement basic unit test for the `GetExtendedAttribute` function The `GetExtendedAttributeTest` attempts to extract a single extended attribute value from a topic using the `GetExtendedAttribute` function. It confirms that the correct value is returned. --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 39 ++++++--- .../Functions.resx | 80 ++++++++++++++++--- 2 files changed, 101 insertions(+), 18 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index e775dfe6..662dce8c 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -33,7 +33,6 @@ public void TestCleanup() { private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_TestAction; System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Functions)); - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition inconclusiveCondition1; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; @@ -57,6 +56,9 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getIDTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getUniqueKeyValue; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getExtendedAttributeValue; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -65,7 +67,6 @@ private void InitializeComponent() { this.dbo_GetAttributesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetChildTopicIDsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); dbo_GetExtendedAttributeTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); - inconclusiveCondition1 = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.InconclusiveCondition(); dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -89,17 +90,15 @@ private void InitializeComponent() { getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); getIDTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); getUniqueKeyValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); + dbo_GetExtendedAttributeTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preGetExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_GetExtendedAttributeTest_TestAction // - dbo_GetExtendedAttributeTest_TestAction.Conditions.Add(inconclusiveCondition1); + dbo_GetExtendedAttributeTest_TestAction.Conditions.Add(getExtendedAttributeValue); resources.ApplyResources(dbo_GetExtendedAttributeTest_TestAction, "dbo_GetExtendedAttributeTest_TestAction"); // - // inconclusiveCondition1 - // - inconclusiveCondition1.Enabled = true; - inconclusiveCondition1.Name = "inconclusiveCondition1"; - // // dbo_GetParentIDTest_TestAction // dbo_GetParentIDTest_TestAction.Conditions.Add(getParentIDValue); @@ -218,7 +217,7 @@ private void InitializeComponent() { // dbo_GetExtendedAttributeTestData // this.dbo_GetExtendedAttributeTestData.PosttestAction = null; - this.dbo_GetExtendedAttributeTestData.PretestAction = null; + this.dbo_GetExtendedAttributeTestData.PretestAction = dbo_GetExtendedAttributeTest_PretestAction; this.dbo_GetExtendedAttributeTestData.TestAction = dbo_GetExtendedAttributeTest_TestAction; // // dbo_GetParentIDTestData @@ -294,6 +293,28 @@ private void InitializeComponent() { getUniqueKeyValue.ResultSet = 1; getUniqueKeyValue.RowNumber = 1; // + // dbo_GetExtendedAttributeTest_PretestAction + // + dbo_GetExtendedAttributeTest_PretestAction.Conditions.Add(preGetExtendedAttributeCount); + resources.ApplyResources(dbo_GetExtendedAttributeTest_PretestAction, "dbo_GetExtendedAttributeTest_PretestAction"); + // + // preGetExtendedAttributeCount + // + preGetExtendedAttributeCount.Enabled = true; + preGetExtendedAttributeCount.Name = "preGetExtendedAttributeCount"; + preGetExtendedAttributeCount.ResultSet = 1; + preGetExtendedAttributeCount.RowCount = 1; + // + // getExtendedAttributeValue + // + getExtendedAttributeValue.ColumnNumber = 1; + getExtendedAttributeValue.Enabled = true; + getExtendedAttributeValue.ExpectedValue = "Value2"; + getExtendedAttributeValue.Name = "getExtendedAttributeValue"; + getExtendedAttributeValue.NullExpected = false; + getExtendedAttributeValue.ResultSet = 1; + getExtendedAttributeValue.RowNumber = 1; + // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index 5e1445a2..a32a37d1 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -118,21 +118,36 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - -- database unit test for dbo.GetExtendedAttribute -DECLARE @RC AS NVARCHAR (MAX), + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), @TopicID AS INT, - @AttributeKey AS NVARCHAR (255); + @AttributeKey AS VARCHAR(128), + @AttributeValue AS NVARCHAR(2000); -SELECT @RC = NULL, - @TopicID = 0, - @AttributeKey = NULL; +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'Topic_1_1', + @AttributeKey = 'GetExtendedAttributeTest2'; -SELECT @RC = [dbo].[GetExtendedAttribute]( - @TopicID, +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE FUNCTION +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @AttributeValue = [dbo].[GetExtendedAttribute]( + @TopicID, @AttributeKey ); -SELECT @RC AS RC; +-------------------------------------------------------------------------------------------------------------------------------- +-- VALIDATE RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @AttributeValue AS RC; -------------------------------------------------------------------------------------------------------------------------------- @@ -627,6 +642,53 @@ VALUES ( @TopicID4, SELECT * FROM Attributes WHERE AttributeKey = @Key + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @Key AS VARCHAR(128), + @TopicID AS INT, + @AttributesXml AS XML; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'Topic_1_1', + @AttributesXml = '<attributes>' + + ' <attribute key=''GetExtendedAttributeTest1''>Value1</attribute>' + + ' <attribute key=''GetExtendedAttributeTest2''>Value2</attribute>' + + '</attributes>'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @Key; + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM ExtendedAttributes +WHERE TopicID = @TopicID; + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +INSERT +INTO ExtendedAttributes ( + TopicID, + AttributesXML + ) +VALUES ( @TopicID, + @AttributesXml + ); + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM ExtendedAttributes +WHERE TopicID = @TopicID; True From dc27f2bb18a8e54f2e7c0f5c15c5215db052abb3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 18 Jan 2021 13:51:43 -0800 Subject: [PATCH 331/778] Treat `TokenizedTopicList` as a `Relationship` or a `Reference` Previously, the `TokenizedTopicList` defaulted to being treated as an attribute, unless it was explicitly set to `SaveAsRelationship`. Instead, it should _always_ default to a relationship, unless the `TokenLimit` is `1`, in which case it should be saved as a topic reference. Pointers to topics shoud never be saved as an attribute, as that prevents any sort of referential integrity or reciprocal relationship. --- OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs index 48cd341d..85377c84 100644 --- a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs @@ -42,7 +42,7 @@ public TokenizedTopicListAttribute( \-------------------------------------------------------------------------------------------------------------------------*/ /// public override ModelType ModelType => - Attributes.GetBoolean("SaveAsRelationship")? ModelType.Relationship : ModelType.ScalarValue; + Attributes.GetInteger("TokenLimit") is 1 ? ModelType.Reference : ModelType.Relationship; } //Class } //Namespace \ No newline at end of file From afaf60f46815d7b2f6ca6c760d413d50a0e530b8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 18 Jan 2021 13:59:10 -0800 Subject: [PATCH 332/778] Treat `TopicList` as a `Reference` if `ValueProperty` is set to `TopicId` Previously, the TopicList` was always treated as an attribute. Now, we can save it as a topic reference if the `ValueProperty` is set to `TopicId`. Pointers to topics should never be saved as an attribute, as that prevents any sort of referential integrity or reciprocal relationship. --- OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs index 7804ab52..0ff1d47a 100644 --- a/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs +++ b/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; namespace OnTopic.Metadata.AttributeTypes { @@ -36,5 +37,13 @@ public TopicListAttribute( ) { } + /*========================================================================================================================== + | PROPERTY: MODEL TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override ModelType ModelType + => Attributes.GetValue("ValueProperty", "").Equals("TopicID", StringComparison.OrdinalIgnoreCase)? + ModelType.Reference : ModelType.ScalarValue; + } //Class } //Namespace \ No newline at end of file From b0afb908a56f5f8f946e3a52f7097d3a0d0eeb1b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 18 Jan 2021 14:05:48 -0800 Subject: [PATCH 333/778] Migrated `TopicFactory` to use `DynamicTopicLookupService` The `DynamicTopicLookupService` was originally intended for use with the `TopicFactory`, but we ended up using the `DefaultTopicLookupService` instead since we were moving away from `Topic` derivatives, and didn't expect to be creating any more. As such, dynamically assessing them was a bit of overhead considering we only expected `Topic`, `ContentTypeDescriptor`, and `AttributeDescriptor`. That changes a bit with the introduction of strongly typed attribute descriptors (e.g., `TextAttribute`). And as we plan on migrating these to the OnTopic Editor, where they make more logical sense, we need to make sure that the `TopicFactory` can discover these, ideally, without needing to be manually registered. Hopefully, this will allow us to optionally move attribute types to their own projects or assemblies. --- OnTopic/TopicFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 75e403dd..d4fa17e7 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -26,7 +26,7 @@ public static class TopicFactory { /// /// Establishes static variables for the . /// - public static ITypeLookupService TypeLookupService { get; set; } = new DefaultTopicLookupService(); + public static ITypeLookupService TypeLookupService { get; set; } = new DynamicTopicLookupService(); /*========================================================================================================================== | METHOD: CREATE From a380d90b85268fbdec861e5c6892fa4957deb006 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 18 Jan 2021 15:28:58 -0800 Subject: [PATCH 334/778] Remove Topic Editor attribute types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ideally, the attribute type descriptors should be registered alongside their corresponding attributes in the OnTopic Editor. By having them in the OnTopic Library, we create an awkward dependency between the Editor and core library which makes maintenance difficult. In the previous commit, we migrated `TopicFactory` to use the `DynamicTopicLookupService` (b0afb90) with the expectation that it will then be able to find these in the OnTopic Editor, assuming it's in scope. In a corresponding update, we'll add these back as part of the OnTopic Editor itself. As part of this, five of these—`BooleanAttribute`, `NestedTopicListAttribute`, `RelationshipAttribute`, `TextAttribute`, `TopicReferenceAttribute`—have been moved to `OnTopic.TestDoubles` so that the unit tests that use them as examples can continue to operate. As these are bare-bones classes, this duplication isn't considered much of a concern. As part of this, namespaces in unit tests needed to be updated to point to `OnTopic.TestDoubles.Metadata` where appropriate. --- .../Metadata}/BooleanAttribute.cs | 3 +- .../Metadata}/NestedTopicListAttribute.cs | 3 +- .../Metadata}/RelationshipAttribute.cs | 5 +- .../Metadata}/TextAttribute.cs | 3 +- .../Metadata}/TopicReferenceAttribute.cs | 3 +- .../TextAttributeTopicBindingModel.cs | 3 +- OnTopic.Tests/Properties/AssemblyInfo.cs | 8 +++ .../ReverseTopicMappingServiceTest.cs | 2 +- OnTopic.Tests/TopicMappingServiceTest.cs | 4 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 2 +- .../Metadata/TextAttributeTopicViewModel.cs | 2 +- .../TopicReferenceAttributeTopicViewModel.cs | 2 +- OnTopic/GlobalSuppressions.cs | 1 - OnTopic/Lookup/DefaultTopicLookupService.cs | 19 ------- .../AttributeTypes/AttributeTypeDescriptor.cs | 54 ------------------- .../AttributeTypes/DateTimeAttribute.cs | 40 -------------- .../AttributeTypes/FileListAttribute.cs | 40 -------------- .../AttributeTypes/FilePathAttribute.cs | 40 -------------- .../Metadata/AttributeTypes/HtmlAttribute.cs | 46 ---------------- .../IncomingRelationshipAttribute.cs | 40 -------------- .../AttributeTypes/InstructionAttribute.cs | 39 -------------- .../AttributeTypes/LastModifiedAttribute.cs | 40 -------------- .../AttributeTypes/LastModifiedByAttribute.cs | 40 -------------- .../AttributeTypes/NumberAttribute.cs | 40 -------------- .../QueryableTopicListAttribute.cs | 40 -------------- .../AttributeTypes/TextAreaAttribute.cs | 46 ---------------- .../TokenizedTopicListAttribute.cs | 48 ----------------- .../AttributeTypes/TopicListAttribute.cs | 49 ----------------- OnTopic/Repositories/TopicRepositoryBase.cs | 3 +- 29 files changed, 27 insertions(+), 638 deletions(-) rename {OnTopic/Metadata/AttributeTypes => OnTopic.TestDoubles/Metadata}/BooleanAttribute.cs (96%) rename {OnTopic/Metadata/AttributeTypes => OnTopic.TestDoubles/Metadata}/NestedTopicListAttribute.cs (96%) rename {OnTopic/Metadata/AttributeTypes => OnTopic.TestDoubles/Metadata}/RelationshipAttribute.cs (94%) rename {OnTopic/Metadata/AttributeTypes => OnTopic.TestDoubles/Metadata}/TextAttribute.cs (96%) rename {OnTopic/Metadata/AttributeTypes => OnTopic.TestDoubles/Metadata}/TopicReferenceAttribute.cs (96%) delete mode 100644 OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/FileListAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/NumberAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs delete mode 100644 OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs diff --git a/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs b/OnTopic.TestDoubles/Metadata/BooleanAttribute.cs similarity index 96% rename from OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs rename to OnTopic.TestDoubles/Metadata/BooleanAttribute.cs index 72ca8584..767114a5 100644 --- a/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/BooleanAttribute.cs @@ -3,8 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; -namespace OnTopic.Metadata.AttributeTypes { +namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ | CLASS: BOOLEAN ATTRIBUTE (DESCRIPTOR) diff --git a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs b/OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs similarity index 96% rename from OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs rename to OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs index a77f68e1..20ede770 100644 --- a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs @@ -3,8 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; -namespace OnTopic.Metadata.AttributeTypes { +namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ | CLASS: NESTED TOPIC LIST ATTRIBUTE (DESCRIPTOR) diff --git a/OnTopic/Metadata/AttributeTypes/RelationshipAttribute.cs b/OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs similarity index 94% rename from OnTopic/Metadata/AttributeTypes/RelationshipAttribute.cs rename to OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs index da9551ce..eafa88e4 100644 --- a/OnTopic/Metadata/AttributeTypes/RelationshipAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs @@ -3,8 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; -namespace OnTopic.Metadata.AttributeTypes { +namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ | CLASS: RELATIONSHIP ATTRIBUTE (DESCRIPTOR) @@ -17,7 +18,7 @@ namespace OnTopic.Metadata.AttributeTypes { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class RelationshipAttribute : QueryableTopicListAttribute { + public class RelationshipAttribute : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Metadata/AttributeTypes/TextAttribute.cs b/OnTopic.TestDoubles/Metadata/TextAttribute.cs similarity index 96% rename from OnTopic/Metadata/AttributeTypes/TextAttribute.cs rename to OnTopic.TestDoubles/Metadata/TextAttribute.cs index 2a0d2b97..8724f188 100644 --- a/OnTopic/Metadata/AttributeTypes/TextAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/TextAttribute.cs @@ -3,8 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; -namespace OnTopic.Metadata.AttributeTypes { +namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ | CLASS: TEXT ATTRIBUTE (DESCRIPTOR) diff --git a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs b/OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs similarity index 96% rename from OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs rename to OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs index 5082b713..3ee437c1 100644 --- a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs @@ -3,8 +3,9 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.Metadata; -namespace OnTopic.Metadata.AttributeTypes { +namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ | CLASS: TOPIC REFERENCE ATTRIBUTE (DESCRIPTOR) diff --git a/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs index 0bd3db84..530877cc 100644 --- a/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs @@ -3,8 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; +using OnTopic.TestDoubles.Metadata; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/Properties/AssemblyInfo.cs b/OnTopic.Tests/Properties/AssemblyInfo.cs index cd19192f..3505cb26 100644 --- a/OnTopic.Tests/Properties/AssemblyInfo.cs +++ b/OnTopic.Tests/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; /*============================================================================================================================== @@ -14,3 +15,10 @@ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] [assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")] + +/*============================================================================================================================== +| HANDLE SUPPRESSIONS +>=============================================================================================================================== +| Suppress warnings from code analysis that are either false positives or not relevant for this assembly. +\-----------------------------------------------------------------------------------------------------------------------------*/ +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Tests")] \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index a90b6f69..34995c9a 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -14,10 +14,10 @@ using OnTopic.Mapping.Annotations; using OnTopic.Mapping.Reverse; using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; using OnTopic.Models; using OnTopic.Repositories; using OnTopic.TestDoubles; +using OnTopic.TestDoubles.Metadata; using OnTopic.Tests.BindingModels; using OnTopic.ViewModels; diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 833237c1..89f34514 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -13,12 +13,12 @@ using OnTopic.Data.Caching; using OnTopic.Lookup; using OnTopic.Mapping; -using OnTopic.Mapping.Internal; using OnTopic.Mapping.Annotations; +using OnTopic.Mapping.Internal; using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; using OnTopic.Repositories; using OnTopic.TestDoubles; +using OnTopic.TestDoubles.Metadata; using OnTopic.Tests.TestDoubles; using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index d645c36a..f7122d16 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -9,9 +9,9 @@ using OnTopic.Attributes; using OnTopic.Data.Caching; using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; using OnTopic.Repositories; using OnTopic.TestDoubles; +using OnTopic.TestDoubles.Metadata; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs index 420bb160..3671a1b6 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; +using OnTopic.TestDoubles.Metadata; namespace OnTopic.Tests.ViewModels.Metadata { diff --git a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs index 0744ab1a..fe089233 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; +using OnTopic.TestDoubles.Metadata; namespace OnTopic.Tests.ViewModels.Metadata { diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs index f323eb98..d7e85b98 100644 --- a/OnTopic/GlobalSuppressions.cs +++ b/OnTopic/GlobalSuppressions.cs @@ -5,4 +5,3 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic")] \ No newline at end of file diff --git a/OnTopic/Lookup/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs index 41159e2f..7e752fa3 100644 --- a/OnTopic/Lookup/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Reflection; using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; namespace OnTopic.Lookup { @@ -40,24 +39,6 @@ public DefaultTopicLookupService(IEnumerable? types = null, Type? defaultT \-----------------------------------------------------------------------------------------------------------------------*/ TryAdd(typeof(ContentTypeDescriptor)); TryAdd(typeof(AttributeDescriptor)); - TryAdd(typeof(BooleanAttribute)); - TryAdd(typeof(DateTimeAttribute)); - TryAdd(typeof(FileListAttribute)); - TryAdd(typeof(FilePathAttribute)); - TryAdd(typeof(HtmlAttribute)); - TryAdd(typeof(IncomingRelationshipAttribute)); - TryAdd(typeof(InstructionAttribute)); - TryAdd(typeof(LastModifiedAttribute)); - TryAdd(typeof(LastModifiedByAttribute)); - TryAdd(typeof(NestedTopicListAttribute)); - TryAdd(typeof(NumberAttribute)); - TryAdd(typeof(QueryableTopicListAttribute)); - TryAdd(typeof(RelationshipAttribute)); - TryAdd(typeof(TextAreaAttribute)); - TryAdd(typeof(TextAttribute)); - TryAdd(typeof(TokenizedTopicListAttribute)); - TryAdd(typeof(TopicListAttribute)); - TryAdd(typeof(TopicReferenceAttribute)); } diff --git a/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs b/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs deleted file mode 100644 index 5e53a933..00000000 --- a/OnTopic/Metadata/AttributeTypes/AttributeTypeDescriptor.cs +++ /dev/null @@ -1,54 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: ATTRIBUTE TYPE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a base class for attribute type classes. - /// - /// - /// In the OnTopic Editor, every attribute is assigned a type, which represents how the data should be presented in the - /// editor, what constraints those data have, and how those data should be stored in the data source. In Version 4.0.0 and - /// above, these attribute types are described by their own s, which offer a strongly - /// typed representation of those properties. This class provides a base for those representations. - /// - public abstract class AttributeTypeDescriptor : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - protected AttributeTypeDescriptor( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType => ModelType.ScalarValue; - - /*========================================================================================================================== - | PROPERTY: EDITOR TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override string EditorType => GetType().Name.Replace("Attribute", "", StringComparison.OrdinalIgnoreCase); - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs b/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs deleted file mode 100644 index 5775eac2..00000000 --- a/OnTopic/Metadata/AttributeTypes/DateTimeAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: DATE/TIME ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a Date/Time attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class DateTimeAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public DateTimeAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs b/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs deleted file mode 100644 index 36da5ac0..00000000 --- a/OnTopic/Metadata/AttributeTypes/FileListAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: FILE LIST ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a file list attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class FileListAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public FileListAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs b/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs deleted file mode 100644 index 41be86ee..00000000 --- a/OnTopic/Metadata/AttributeTypes/FilePathAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: FILE PATH ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a file path attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class FilePathAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public FilePathAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs b/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs deleted file mode 100644 index 6675f0f4..00000000 --- a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: HTML ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing an HTML attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class HtmlAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public HtmlAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - /*========================================================================================================================== - | PROPERTY: IS EXTENDED ATTRIBUTE? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override bool IsExtendedAttribute => true; - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs b/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs deleted file mode 100644 index e6cc2156..00000000 --- a/OnTopic/Metadata/AttributeTypes/IncomingRelationshipAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: INCOMING RELATIONSHIP ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing an incoming relationship attribute type, including information on how it will be - /// presented and validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class IncomingRelationshipAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public IncomingRelationshipAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs b/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs deleted file mode 100644 index d2944298..00000000 --- a/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs +++ /dev/null @@ -1,39 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: INSTRUCTION ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing an instruction attribute type. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class InstructionAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public InstructionAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs b/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs deleted file mode 100644 index a10e80c9..00000000 --- a/OnTopic/Metadata/AttributeTypes/LastModifiedAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: LAST MODIFIED ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for descripting the last modified attribute type, including information on how it will be presented - /// and validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class LastModifiedAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public LastModifiedAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs b/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs deleted file mode 100644 index 6a128979..00000000 --- a/OnTopic/Metadata/AttributeTypes/LastModifiedByAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: LAST MODIFIED BY ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for descripting the last modified by attribute type, including information on how it will be - /// presented and validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class LastModifiedByAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public LastModifiedByAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs b/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs deleted file mode 100644 index 880c66ae..00000000 --- a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: NUMBER ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a number attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class NumberAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public NumberAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs deleted file mode 100644 index e8248649..00000000 --- a/OnTopic/Metadata/AttributeTypes/QueryableTopicListAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: QUERYABLE TOPIC LIST ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a queryable topic list attribute types, including information on how they will be - /// presented and validated in the editor. Acts as a base class for other topic list attribute types. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class QueryableTopicListAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public QueryableTopicListAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs b/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs deleted file mode 100644 index efc03230..00000000 --- a/OnTopic/Metadata/AttributeTypes/TextAreaAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: TEXT AREA ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a text area attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class TextAreaAttribute : AttributeDescriptor { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public TextAreaAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - /*========================================================================================================================== - | PROPERTY: IS EXTENDED ATTRIBUTE? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override bool IsExtendedAttribute => true; - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs deleted file mode 100644 index 85377c84..00000000 --- a/OnTopic/Metadata/AttributeTypes/TokenizedTopicListAttribute.cs +++ /dev/null @@ -1,48 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using OnTopic.Attributes; - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: TOKENIZED TOPIC LIST ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a tokenized topic list attribute type, including information on how it will be - /// presented and validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class TokenizedTopicListAttribute : QueryableTopicListAttribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public TokenizedTopicListAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType => - Attributes.GetInteger("TokenLimit") is 1 ? ModelType.Reference : ModelType.Relationship; - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs b/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs deleted file mode 100644 index 0ff1d47a..00000000 --- a/OnTopic/Metadata/AttributeTypes/TopicListAttribute.cs +++ /dev/null @@ -1,49 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; - -namespace OnTopic.Metadata.AttributeTypes { - - /*============================================================================================================================ - | CLASS: TOPIC LIST ATTRIBUTE (DESCRIPTOR) - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents metadata for describing a topic list attribute type, including information on how it will be presented and - /// validated in the editor. - /// - /// - /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the - /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. - /// - public class TopicListAttribute : QueryableTopicListAttribute { - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public TopicListAttribute( - string key, - string contentType, - Topic parent, - int id = -1 - ) : base( - key, - contentType, - parent, - id - ) { - } - - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType - => Attributes.GetValue("ValueProperty", "").Equals("TopicID", StringComparison.OrdinalIgnoreCase)? - ModelType.Reference : ModelType.ScalarValue; - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index c09538d2..7c77794d 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -10,7 +10,6 @@ using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; -using OnTopic.Metadata.AttributeTypes; using OnTopic.Querying; #pragma warning disable CS0618 // Type or member is obsolete; used to hide known deprecation of events until v5.0.0 @@ -668,7 +667,7 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { .Union(topic.Attributes.DeletedAttributes); foreach (var attributeKey in attributeKeys) { if (!attributes.Contains(attributeKey)) { - attributes.Add((TextAttribute)TopicFactory.Create(attributeKey, "TextAttribute")); + attributes.Add((AttributeDescriptor)TopicFactory.Create(attributeKey, "TextAttribute")); } } From 5ba0a09ebccc3928d48ed00171037f1012cce01d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 18 Jan 2021 15:58:17 -0800 Subject: [PATCH 335/778] Moved suppression of CA1711 Normally, in .NET, classes should only end in `Attribute` if they inherit from the `Attribute` class. In this case, the naming conventions of OnTopic Editor conflict with the naming conventions of .NET. That's unfortunate, but well-established. As such, we're suppressing this warning since this is a deliberate design decision. This suppression was originally defined on OnTopic, then moved to OnTopic Tests. With the attribute type descriptors finally moved to Test Doubles (a380d90), the suppression should follow. --- OnTopic.TestDoubles/Properties/AssemblyInfo.cs | 8 ++++++++ OnTopic.Tests/Properties/AssemblyInfo.cs | 10 +--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs index 770c70b0..22db4487 100644 --- a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs +++ b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; /*============================================================================================================================== @@ -14,3 +15,10 @@ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] [assembly: Guid("FE175884-59C1-4C4D-A663-4CC570432ECC")] + +/*============================================================================================================================== +| HANDLE SUPPRESSIONS +>=============================================================================================================================== +| Suppress warnings from code analysis that are either false positives or not relevant for this assembly. +\-----------------------------------------------------------------------------------------------------------------------------*/ +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Tests")] \ No newline at end of file diff --git a/OnTopic.Tests/Properties/AssemblyInfo.cs b/OnTopic.Tests/Properties/AssemblyInfo.cs index 3505cb26..475bbcfc 100644 --- a/OnTopic.Tests/Properties/AssemblyInfo.cs +++ b/OnTopic.Tests/Properties/AssemblyInfo.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; /*============================================================================================================================== @@ -14,11 +13,4 @@ \-----------------------------------------------------------------------------------------------------------------------------*/ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] -[assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")] - -/*============================================================================================================================== -| HANDLE SUPPRESSIONS ->=============================================================================================================================== -| Suppress warnings from code analysis that are either false positives or not relevant for this assembly. -\-----------------------------------------------------------------------------------------------------------------------------*/ -[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Tests")] \ No newline at end of file +[assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")] \ No newline at end of file From e0bd5df08f7fd9fc053a4a612ff494ce12c800a9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 10:44:07 -0800 Subject: [PATCH 336/778] Introduce `IsLoaded` property to `TopicRelationshipMultiMap` The `IsLoaded` property allows us to track if a topic was not able to be fully loaded during `ITopicRepository.Load()` due to the `Target_TopicId` not existing in the current topic graph. This generally occurs because a) the load is limited to an individual topic or branch, and b) the `referenceTopic` was not passed to `Load()`, or itself represents an incomplete topic graph. --- .../References/TopicRelationshipMultiMap.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 50a50b4d..4864f5b2 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using OnTopic.Collections; using OnTopic.Internal.Diagnostics; +using OnTopic.Repositories; namespace OnTopic.References { @@ -210,6 +211,28 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo } + /*========================================================================================================================== + | IS LOADED? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether or not the collection was fully loaded from the persistence store. + /// + /// + /// + /// When loading an individual or branch from the persistence store, it is possible that the + /// relationships may not be fully available. In this scenario, updating relationships while e.g. deleting unmatched + /// relationships can result in unintended data loss. To account for this, the property tracks + /// whether a collection was fully loaded from the persistence store; if it wasn't, the + /// should not deleted unmatched relationships. + /// + /// + /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to + /// a valid reference in memory. + /// + /// + public bool IsLoaded { get; set; } = true; + /*========================================================================================================================== | METHOD: IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ From 3c4dcddc925f35f0474e653225eec04cb090fa6b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 10:44:46 -0800 Subject: [PATCH 337/778] Introduce `IsLoaded` property to `TopicReferenceDictionary` The `IsLoaded` property allows us to track if a topic was not able to be fully loaded during `ITopicRepository.Load()` due to the `Target_TopicId` not existing in the current topic graph. This generally occurs because a) the load is limited to an individual topic or branch, and b) the `referenceTopic` was not passed to `Load()`, or itself represents an incomplete topic graph. --- .../References/TopicReferenceDictionary.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index 71c7b0dc..ef22d72f 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -9,6 +9,7 @@ using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; +using OnTopic.Repositories; namespace OnTopic.References { @@ -61,11 +62,33 @@ public TopicReferenceDictionary(Topic parent) { public int Count => _storage.Count; /*========================================================================================================================== - | IsReadOnly + | IS READ ONLY? \-------------------------------------------------------------------------------------------------------------------------*/ /// public bool IsReadOnly => false; + /*========================================================================================================================== + | IS LOADED? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether or not the collection was fully loaded from the persistence store. + /// + /// + /// + /// When loading an individual or branch from the persistence store, it is possible that topic + /// references may not be fully available. In this scenario, updating topic references while e.g. deleting unmatched + /// relationships can result in unintended data loss. To account for this, the property tracks + /// whether a collection was fully loaded from the persistence store; if it wasn't, the + /// should not deleted unmatched topic references. + /// + /// + /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to + /// a valid reference in memory. + /// + /// + public bool IsLoaded { get; set; } = true; + /*========================================================================================================================== | ITEM \-------------------------------------------------------------------------------------------------------------------------*/ From 9a407c4e1036c68d0bc5ae0d8098a0cbd8e38799 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 10:46:23 -0800 Subject: [PATCH 338/778] Set `IsLoaded` if a(ny) target topic cannot be loaded This is a bit of a broad brush, and especially for relationships, where all relationships are effectively marked as `!IsLoaded` if any topic in any relationship fails to be found. That said, this is considered a rare scenario, so shouldn't be a particularly high risk. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index d5f2fcc3..378a40e0 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -364,8 +364,11 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic related = topics[targetTopicId]; } - // Bypass if either of the objects are missing - if (related is null) return; + // Bypass if the target object is missing + if (related is null) { + current.Relationships.IsLoaded = false; + return; + } /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object @@ -417,8 +420,11 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics, referenced = topics[targetTopicId.Value]; } - // Bypass if either of the objects are missing - if (referenced is null) return; + // Bypass if the target object is missing + if (referenced is null) { + current.References.IsLoaded = false; + return; + } /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object From 2416a243320631c6035a63a1022b1bcaef0458da Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 10:49:56 -0800 Subject: [PATCH 339/778] Disable `DeleteUnmatched` if `!IsLoaded` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a collection has not been completely loaded—i.e., some of the target topics from the relationship could not be found in the scoped topic graph—then `DeleteUnmatched` should be disabled on `Save()`, as otherwise we risk inadvertantly deleting those relationships or references from the persistence store. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 08dbef8d..4250fe6c 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -684,7 +684,7 @@ private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnec command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); command.AddParameter("Version", version.Value); - command.AddParameter("DeleteUnmatched", true); + command.AddParameter("DeleteUnmatched", topic.Relationships.IsLoaded); command.ExecuteNonQuery(); @@ -743,7 +743,7 @@ private static void PersistReferences(Topic topic, SqlDateTime version, SqlConne command.AddParameter("TopicID", topicId); command.AddParameter("ReferencedTopics", references); command.AddParameter("Version", version.Value); - command.AddParameter("DeleteUnmatched", true); + command.AddParameter("DeleteUnmatched", topic.References.IsLoaded); command.ExecuteNonQuery(); From 20f2fc5f926d4acbfb5c7dbb86079051cba08d7a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 10:53:15 -0800 Subject: [PATCH 340/778] Renamed `IsLoaded` to `IsFullyLoaded` to better articulate condition A topic can be loaded (e.g. from the persistence store) without being _fully_ loaded. While a less common identifier in the .NET landscape, this better articulates the condition we're modeling. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 4 ++-- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- OnTopic/References/TopicReferenceDictionary.cs | 12 ++++++------ OnTopic/References/TopicRelationshipMultiMap.cs | 12 ++++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 378a40e0..db7df6a1 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -366,7 +366,7 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic // Bypass if the target object is missing if (related is null) { - current.Relationships.IsLoaded = false; + current.Relationships.IsFullyLoaded = false; return; } @@ -422,7 +422,7 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics, // Bypass if the target object is missing if (referenced is null) { - current.References.IsLoaded = false; + current.References.IsFullyLoaded = false; return; } diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 4250fe6c..5b7ea6e7 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -684,7 +684,7 @@ private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnec command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); command.AddParameter("Version", version.Value); - command.AddParameter("DeleteUnmatched", topic.Relationships.IsLoaded); + command.AddParameter("DeleteUnmatched", topic.Relationships.IsFullyLoaded); command.ExecuteNonQuery(); @@ -743,7 +743,7 @@ private static void PersistReferences(Topic topic, SqlDateTime version, SqlConne command.AddParameter("TopicID", topicId); command.AddParameter("ReferencedTopics", references); command.AddParameter("Version", version.Value); - command.AddParameter("DeleteUnmatched", topic.References.IsLoaded); + command.AddParameter("DeleteUnmatched", topic.References.IsFullyLoaded); command.ExecuteNonQuery(); diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index ef22d72f..ad9b7c7b 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -68,7 +68,7 @@ public TopicReferenceDictionary(Topic parent) { public bool IsReadOnly => false; /*========================================================================================================================== - | IS LOADED? + | IS FULLY LOADED? \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Determines whether or not the collection was fully loaded from the persistence store. @@ -77,17 +77,17 @@ public TopicReferenceDictionary(Topic parent) { /// /// When loading an individual or branch from the persistence store, it is possible that topic /// references may not be fully available. In this scenario, updating topic references while e.g. deleting unmatched - /// relationships can result in unintended data loss. To account for this, the property tracks - /// whether a collection was fully loaded from the persistence store; if it wasn't, the - /// should not deleted unmatched topic references. + /// relationships can result in unintended data loss. To account for this, the property ' + /// tracks whether a collection was fully loaded from the persistence store; if it wasn't, the should not deleted unmatched topic references. /// /// - /// The property defaults to true. It should be set to false during the property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to /// a valid reference in memory. /// /// - public bool IsLoaded { get; set; } = true; + public bool IsFullyLoaded { get; set; } = true; /*========================================================================================================================== | ITEM diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 4864f5b2..ad41f997 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -212,7 +212,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo } /*========================================================================================================================== - | IS LOADED? + | IS FULLY LOADED? \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Determines whether or not the collection was fully loaded from the persistence store. @@ -221,17 +221,17 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo /// /// When loading an individual or branch from the persistence store, it is possible that the /// relationships may not be fully available. In this scenario, updating relationships while e.g. deleting unmatched - /// relationships can result in unintended data loss. To account for this, the property tracks - /// whether a collection was fully loaded from the persistence store; if it wasn't, the - /// should not deleted unmatched relationships. + /// relationships can result in unintended data loss. To account for this, the property + /// tracks whether a collection was fully loaded from the persistence store; if it wasn't, the should not deleted unmatched relationships. /// /// - /// The property defaults to true. It should be set to false during the property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to /// a valid reference in memory. /// /// - public bool IsLoaded { get; set; } = true; + public bool IsFullyLoaded { get; set; } = true; /*========================================================================================================================== | METHOD: IS DIRTY? From 03a35824918df3c2ca003f6e47c2a922a37bfd75 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 11:13:02 -0800 Subject: [PATCH 341/778] Improved documentation for `ITopicRepository` interface --- OnTopic/Repositories/ITopicRepository.cs | 48 ++++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 3d89daea..a430a3d1 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -50,7 +50,8 @@ public interface ITopicRepository { \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier. + /// Loads a (and, optionally, all of its descendants) based on the specified . /// /// The topic identifier. /// @@ -62,7 +63,8 @@ public interface ITopicRepository { Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true); /// - /// Loads a topic (and, optionally, all of its descendants) based on the specified key name. + /// Loads a (and, optionally, all of its descendants) based on the specified . /// /// The fully-qualified unique topic key. /// @@ -74,7 +76,8 @@ public interface ITopicRepository { Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); /// - /// Loads a specific version of a topic based on its version. + /// Loads a specific version of a based on its and . /// /// /// This overload does not accept an argument for recursion; it will only load a single instance of a version. Further, @@ -96,10 +99,10 @@ public interface ITopicRepository { | METHOD: ROLLBACK \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Rolls back the current topic to a particular point in its version history by reloading legacy attributes and then - /// saving the new version. + /// Rolls back the supplied to a particular point in its version history by reloading legacy + /// attributes and then saving the new version. /// - /// The current version of the topic to rollback. + /// The current version of the to rollback. /// The selected Date/Time for the version to which to roll back. /// - /// Interface method that saves topic attributes; also used for renaming a topic since name is stored as an attribute. + /// Saves the (and, optionally, its descendants) to the persistence store. /// - /// The topic object. + /// The object. /// /// Boolean indicator nothing whether to recurse through the topic's descendants and save them as well. /// @@ -128,15 +131,19 @@ public interface ITopicRepository { | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Interface method that supports moving a topic from one position to another. + /// Moves the from its existing parent to a parent, optionally placing + /// it after a supplied topic. /// /// - /// May optionally specify a sibling. If specified, it is expected that the topic will be placed immediately after the - /// topic. + /// May optionally specify a topic. If specified, it is expected that the + /// will be placed immediately after the . /// - /// The topic object to be moved. - /// A topic object under which to move the source topic. - /// A topic object representing a sibling adjacent to which the topic should be moved. + /// The object to be moved. + /// The target object under which to move the source . + /// + /// An optional object representing a sibling adjacent to which the source should + /// be moved. + /// /// Boolean value representing whether the operation completed successfully. /// - /// Interface method that deletes the provided topic from the tree + /// Deletes the provided from the topic tree. /// - /// - /// Prior to OnTopic 4.5.0, the defaulted to false. Unfortunately, a bug in the - /// implementation of resulted in this not being validated, and - /// thus it operated as though it were true. This was fixed in OnTopic 4.5.0. As this bug fix potentially - /// breaks prior implementations, however, the default for was changed to true in - /// order to maintain backward compatibility. In OnTopic 5.0.0, this will be changed back to false. - /// - /// The topic object to delete. + /// The object to delete. /// - /// Boolean indicator nothing whether to recurse through the topic's descendants and delete them as well. If set to false + /// Boolean indicator nothing whether to recurse through the 's descendants and delete them as well. If set to false /// and the topic has children, including any nested topics, an exception will be thrown. The default is false. /// /// From 79cf630cca60a4bc5c823bdf403dac045004bc9d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 11:18:38 -0800 Subject: [PATCH 342/778] Fixed namespace scope in Test Double suppression This was incorrectly updated during a previous commit (5ba0a09). My bad! --- OnTopic.TestDoubles/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs index 22db4487..4bb6a499 100644 --- a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs +++ b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ >=============================================================================================================================== | Suppress warnings from code analysis that are either false positives or not relevant for this assembly. \-----------------------------------------------------------------------------------------------------------------------------*/ -[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic.Tests")] \ No newline at end of file +[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Expected by convention for OnTopic Editor", Scope = "namespaceanddescendants", Target = "~N:OnTopic.TestDoubles")] \ No newline at end of file From 3d51b14337af3d96736e00e099010796104864ac Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 11:34:50 -0800 Subject: [PATCH 343/778] Introduced new `ITopicRepository.Refresh()` method The `ITopicRepository.Refresh()` method will query the database for any updates since a given date, and update the supplied topic graph with those updates. This is not yet implemented, but the interface signature is established and placeholder methods are in place. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 6 ++++++ OnTopic.Data.Sql/SqlTopicRepository.cs | 8 ++++++++ OnTopic.TestDoubles/DummyTopicRepository.cs | 6 ++++++ OnTopic.TestDoubles/StubTopicRepository.cs | 6 ++++++ OnTopic/Repositories/ITopicRepository.cs | 14 ++++++++++++++ 5 files changed, 40 insertions(+) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index cbef86b2..e8a843c8 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -131,6 +131,12 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void Refresh(Topic referenceTopic, DateTime since) => _dataProvider.Refresh(referenceTopic, since); + /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 5b7ea6e7..93a6b16c 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -276,6 +276,14 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic } + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void Refresh(Topic referenceTopic, DateTime since) { + + } + /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index c55ef0b1..63b22b89 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -38,6 +38,12 @@ public DummyTopicRepository() : base() { } /// public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) => throw new NotImplementedException(); + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void Refresh(Topic referenceTopic, DateTime since) => throw new NotImplementedException(); + /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index b530ca8b..ae15bfd5 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -101,6 +101,12 @@ public StubTopicRepository() : base() { } + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void Refresh(Topic referenceTopic, DateTime since) { } + /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index a430a3d1..ab499e80 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -111,6 +111,20 @@ public interface ITopicRepository { /// void Rollback(Topic topic, DateTime version); + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Updates the topic graph represented by the by loading any changes the specified . + /// + /// + /// The method is intended to provide basic synchronization of core attributes, + /// indexed attributes, extended attributes, relationships, and topic references. It is not expected to handle deletes + /// or reordering of topics. + /// + void Refresh(Topic referenceTopic, DateTime since); + /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ From 3e95d8dad7930b3b5ed2b008208d5ea486d1fa49 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 11:45:53 -0800 Subject: [PATCH 344/778] Introduced `Topics.LastModified` column This will usually be set to `Version`. Since core `Topic` attributes aren't versioned, however, I'm naming it `LastModified` to help avoid confusion. This will be used for update tracking and cache refresh. --- OnTopic.Data.Sql.Database/Tables/Topics.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index d94b8c4d..9430375a 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -11,7 +11,8 @@ TABLE [dbo].[Topics] ( [RangeRight] INT NOT NULL, [TopicKey] VARCHAR(128) NOT NULL, [ContentType] VARCHAR(128) NOT NULL, - [ParentID] INT NULL + [ParentID] INT NULL, + [LastModified] DATETIME NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Topics] PRIMARY KEY CLUSTERED ( [TopicID] ASC ), From 0d82bcc8fe47c8ce9bc21caa75df70032200f062 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 11:46:12 -0800 Subject: [PATCH 345/778] Update `Topics.LastModified` on `CreateTopic` or `UpdateTopic` --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 6 ++++-- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index ffff5706..eefb0dc4 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -59,14 +59,16 @@ INSERT INTO Topics ( RangeRight, TopicKey, ContentType, - ParentID + ParentID, + LastModified ) Values ( @RangeRight, @RangeRight + 1, @Key, @ContentType, - @ParentID + @ParentID, + @Version ) DECLARE @TopicID INT diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 46e65049..a6d1f9b2 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -37,7 +37,8 @@ IF @Key IS NOT NULL OR @ContentType IS NOT NULL WHEN @ContentType IS NULL THEN TopicKey ELSE @ContentType - END + END, + LastModified = @Version WHERE TopicID = @TopicID END From bc8725a399fd165e9d88481135e901740c035927 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 12:36:25 -0800 Subject: [PATCH 346/778] Introduced new `TopicRepositoryBase.Refresh()` method This was missed as part of the previous commit which introduced the `Refresh()` method (3d51b14). --- OnTopic/Repositories/TopicRepositoryBase.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 7c77794d..6055bee4 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -232,6 +232,12 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont /// public abstract Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); + /*========================================================================================================================== + | METHOD: LOAD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Refresh(Topic referenceTopic, DateTime since); + /*========================================================================================================================== | METHOD: ROLLBACK \-------------------------------------------------------------------------------------------------------------------------*/ From 0f369474a0b87d53e7f6c8c3d43716414923cc5e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 12:50:21 -0800 Subject: [PATCH 347/778] Introduced new `GetTopicUpdates` stored procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This retrieves any updates to the database—including new or modified `Topics`, `Attributes`, `ExtendedAttributes`, `Relationships`, or `TopicReferences`—since a given date, assuming that date is within the last twenty four hours. --- .../OnTopic.Data.Sql.Database.sqlproj | 1 + .../Stored Procedures/GetTopicUpdates.sql | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql diff --git a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj index 2367e5f0..631bb12e 100644 --- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj +++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj @@ -127,6 +127,7 @@ + diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql new file mode 100644 index 00000000..8a2e0096 --- /dev/null +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql @@ -0,0 +1,121 @@ +-------------------------------------------------------------------------------------------------------------------------------- +-- GET TOPIC UPDATES +-------------------------------------------------------------------------------------------------------------------------------- +-- Retrieves any data persisted to the database since the last query. +-------------------------------------------------------------------------------------------------------------------------------- + +CREATE PROCEDURE [dbo].[GetTopicUpdates] + @Since DATETIME +AS + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT KEY ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT TopicID, + ContentType, + ParentID, + TopicKey, + 0 AS SortOrder +FROM Topics +WHERE LastModified > @Since + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT TOPIC ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +;WITH TopicAttributes +AS ( + SELECT TopicID, + AttributeKey, + AttributeValue, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY TopicID, + AttributeKey + ORDER BY Version DESC + ) + FROM Attributes + WHERE Version > @Since +) +SELECT TopicID, + AttributeKey, + AttributeValue, + Version +FROM TopicAttributes +WHERE RowNumber = 1 + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT EXTENDED ATTRIBUTES +-------------------------------------------------------------------------------------------------------------------------------- +;WITH TopicExtendedAttributes +AS ( + SELECT TopicID, + AttributesXml, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY TopicID + ORDER BY Version DESC + ) + FROM ExtendedAttributes + WHERE Version > @Since +) +SELECT TopicID, + AttributesXml, + Version +FROM TopicExtendedAttributes +WHERE RowNumber = 1 + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT RELATIONSHIPS +-------------------------------------------------------------------------------------------------------------------------------- +;WITH Relationships AS ( + SELECT Source_TopicID, + RelationshipKey, + Target_TopicID, + IsDeleted, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + RelationshipKey + ORDER BY Version DESC + ) + FROM [dbo].[Relationships] + WHERE Version > @Since +) +SELECT Relationships.Source_TopicID, + Relationships.RelationshipKey, + Relationships.Target_TopicID, + Relationships.IsDeleted, + Relationships.Version +FROM Relationships +WHERE RowNumber = 1 + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT REFERENCES +-------------------------------------------------------------------------------------------------------------------------------- +;WITH TopicReferences AS ( + SELECT Source_TopicID, + ReferenceKey, + Target_TopicID, + Version, + RowNumber = ROW_NUMBER() OVER ( + PARTITION BY Source_TopicID, + ReferenceKey + ORDER BY Version DESC + ) + FROM [dbo].[TopicReferences] + WHERE Version > @Since +) +SELECT TopicReferences.Source_TopicID, + TopicReferences.ReferenceKey, + TopicReferences.Target_TopicID, + TopicReferences.Version +FROM TopicReferences +WHERE RowNumber = 1 + +-------------------------------------------------------------------------------------------------------------------------------- +-- SELECT HISTORY +-------------------------------------------------------------------------------------------------------------------------------- +SELECT TopicID, + Version +FROM VersionHistoryIndex +WHERE Version > @Since \ No newline at end of file From 84d957d63c81fa1230e147ce0e59ba1adc4eef43 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 12:55:42 -0800 Subject: [PATCH 348/778] Implement basic unit test for the `GetTopicUpdates` function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `GetTopicUpdates` test creates two topics with attributes, extended attributes, relationships, and topic references, and then updates them, while also adding a new third topic. It then uses the `GetTopicUpdates` to confirm that the new updates—and only the updates—are returned. --- .../StoredProcedures.cs | 161 ++++++++++++- .../StoredProcedures.resx | 225 ++++++++++++++++++ 2 files changed, 384 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index 10d15527..bb2d20ba 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -116,6 +116,20 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testInitializeAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_PretestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_PosttestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postGetUpdatesAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesVersionHistoryCount; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -126,6 +140,7 @@ private void InitializeComponent() { this.dbo_UpdateReferencesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_UpdateRelationshipsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_UpdateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); + this.dbo_GetTopicUpdatesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); dbo_CreateTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); createTopicTotal = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_DeleteTopicTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -210,6 +225,20 @@ private void InitializeComponent() { postUpdateExtendedAttributeTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); postUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); testInitializeAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_GetTopicUpdatesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + dbo_GetTopicUpdatesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + preGetUpdatesTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetUpdatesRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + preGetUpdatesReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + dbo_GetTopicUpdatesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + postGetUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); // // dbo_CreateTopicTest_TestAction // @@ -790,6 +819,42 @@ private void InitializeComponent() { postUpdateExtendedAttributeCount.ResultSet = 1; postUpdateExtendedAttributeCount.RowCount = 0; // + // testInitializeAction + // + resources.ApplyResources(testInitializeAction, "testInitializeAction"); + // + // dbo_GetTopicUpdatesTest_TestAction + // + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesTopicCount); + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesAttributeCount); + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesExtendedAttributeCount); + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesRelationshipCount); + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesReferenceCount); + dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesVersionHistoryCount); + resources.ApplyResources(dbo_GetTopicUpdatesTest_TestAction, "dbo_GetTopicUpdatesTest_TestAction"); + // + // dbo_GetTopicUpdatesTest_PretestAction + // + dbo_GetTopicUpdatesTest_PretestAction.Conditions.Add(preGetUpdatesTopicCount); + dbo_GetTopicUpdatesTest_PretestAction.Conditions.Add(preGetUpdatesAttributeCount); + dbo_GetTopicUpdatesTest_PretestAction.Conditions.Add(preGetUpdatesRelationshipCount); + dbo_GetTopicUpdatesTest_PretestAction.Conditions.Add(preGetUpdatesReferenceCount); + resources.ApplyResources(dbo_GetTopicUpdatesTest_PretestAction, "dbo_GetTopicUpdatesTest_PretestAction"); + // + // preGetUpdatesTopicCount + // + preGetUpdatesTopicCount.Enabled = true; + preGetUpdatesTopicCount.Name = "preGetUpdatesTopicCount"; + preGetUpdatesTopicCount.ResultSet = 1; + preGetUpdatesTopicCount.RowCount = 3; + // + // preGetUpdatesAttributeCount + // + preGetUpdatesAttributeCount.Enabled = true; + preGetUpdatesAttributeCount.Name = "preGetUpdatesAttributeCount"; + preGetUpdatesAttributeCount.ResultSet = 2; + preGetUpdatesAttributeCount.RowCount = 8; + // // dbo_CreateTopicTestData // this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; @@ -850,9 +915,79 @@ private void InitializeComponent() { this.dbo_UpdateTopicTestData.PretestAction = dbo_UpdateTopicTest_PretestAction; this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction; // - // testInitializeAction + // dbo_GetTopicUpdatesTestData // - resources.ApplyResources(testInitializeAction, "testInitializeAction"); + this.dbo_GetTopicUpdatesTestData.PosttestAction = dbo_GetTopicUpdatesTest_PosttestAction; + this.dbo_GetTopicUpdatesTestData.PretestAction = dbo_GetTopicUpdatesTest_PretestAction; + this.dbo_GetTopicUpdatesTestData.TestAction = dbo_GetTopicUpdatesTest_TestAction; + // + // preGetUpdatesRelationshipCount + // + preGetUpdatesRelationshipCount.Enabled = true; + preGetUpdatesRelationshipCount.Name = "preGetUpdatesRelationshipCount"; + preGetUpdatesRelationshipCount.ResultSet = 3; + preGetUpdatesRelationshipCount.RowCount = 2; + // + // preGetUpdatesReferenceCount + // + preGetUpdatesReferenceCount.Enabled = true; + preGetUpdatesReferenceCount.Name = "preGetUpdatesReferenceCount"; + preGetUpdatesReferenceCount.ResultSet = 3; + preGetUpdatesReferenceCount.RowCount = 2; + // + // getUpdatesTopicCount + // + getUpdatesTopicCount.Enabled = true; + getUpdatesTopicCount.Name = "getUpdatesTopicCount"; + getUpdatesTopicCount.ResultSet = 5; + getUpdatesTopicCount.RowCount = 1; + // + // getUpdatesAttributeCount + // + getUpdatesAttributeCount.Enabled = true; + getUpdatesAttributeCount.Name = "getUpdatesAttributeCount"; + getUpdatesAttributeCount.ResultSet = 2; + getUpdatesAttributeCount.RowCount = 4; + // + // getUpdatesExtendedAttributeCount + // + getUpdatesExtendedAttributeCount.Enabled = true; + getUpdatesExtendedAttributeCount.Name = "getUpdatesExtendedAttributeCount"; + getUpdatesExtendedAttributeCount.ResultSet = 3; + getUpdatesExtendedAttributeCount.RowCount = 2; + // + // getUpdatesRelationshipCount + // + getUpdatesRelationshipCount.Enabled = true; + getUpdatesRelationshipCount.Name = "getUpdatesRelationshipCount"; + getUpdatesRelationshipCount.ResultSet = 4; + getUpdatesRelationshipCount.RowCount = 1; + // + // dbo_GetTopicUpdatesTest_PosttestAction + // + dbo_GetTopicUpdatesTest_PosttestAction.Conditions.Add(postGetUpdatesAttributeCount); + resources.ApplyResources(dbo_GetTopicUpdatesTest_PosttestAction, "dbo_GetTopicUpdatesTest_PosttestAction"); + // + // postGetUpdatesAttributeCount + // + postGetUpdatesAttributeCount.Enabled = true; + postGetUpdatesAttributeCount.Name = "postGetUpdatesAttributeCount"; + postGetUpdatesAttributeCount.ResultSet = 1; + postGetUpdatesAttributeCount.RowCount = 0; + // + // getUpdatesReferenceCount + // + getUpdatesReferenceCount.Enabled = true; + getUpdatesReferenceCount.Name = "getUpdatesReferenceCount"; + getUpdatesReferenceCount.ResultSet = 5; + getUpdatesReferenceCount.RowCount = 1; + // + // getUpdatesVersionHistoryCount + // + getUpdatesVersionHistoryCount.Enabled = true; + getUpdatesVersionHistoryCount.Name = "getUpdatesVersionHistoryCount"; + getUpdatesVersionHistoryCount.ResultSet = 6; + getUpdatesVersionHistoryCount.RowCount = 2; // // StoredProcedures // @@ -1085,6 +1220,27 @@ public void dbo_UpdateTopicTest() { SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); } } + [TestMethod()] + public void dbo_GetTopicUpdatesTest() { + SqlDatabaseTestActions testActions = this.dbo_GetTopicUpdatesTestData; + // Execute the pre-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PretestAction != null), "Executing pre-test script..."); + SqlExecutionResult[] pretestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PretestAction); + try { + // Execute the test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.TestAction != null), "Executing test script..."); + SqlExecutionResult[] testResults = TestService.Execute(this.ExecutionContext, this.PrivilegedContext, testActions.TestAction); + } + finally { + // Execute the post-test script + // + System.Diagnostics.Trace.WriteLineIf((testActions.PosttestAction != null), "Executing post-test script..."); + SqlExecutionResult[] posttestResults = TestService.Execute(this.PrivilegedContext, this.PrivilegedContext, testActions.PosttestAction); + } + } + private SqlDatabaseTestActions dbo_CreateTopicTestData; private SqlDatabaseTestActions dbo_DeleteTopicTestData; private SqlDatabaseTestActions dbo_GetTopicVersionTestData; @@ -1095,5 +1251,6 @@ public void dbo_UpdateTopicTest() { private SqlDatabaseTestActions dbo_UpdateReferencesTestData; private SqlDatabaseTestActions dbo_UpdateRelationshipsTestData; private SqlDatabaseTestActions dbo_UpdateTopicTestData; + private SqlDatabaseTestActions dbo_GetTopicUpdatesTestData; } } diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index 9a9eda85..c21e279e 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -1468,6 +1468,231 @@ IF NOT EXISTS (SELECT * FROM Topics WHERE TopicID = 1) SET IDENTITY_INSERT [dbo].[Topics] OFF; END + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Since AS DATETIME; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Since = '2022-01-01 00:00:00:000'; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[GetTopicUpdates] + @Since; + + + -------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @Key AS VARCHAR (128), + @ContentType AS VARCHAR (128), + @ParentID AS INT, + @Attributes AS [dbo].[AttributeValues], + @ExtendedAttributes AS XML, + @References AS [dbo].[TopicReferences], + @Version AS DATETIME, + @NewVersion AS DATETIME; + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @Key = 'GetTopicUpdatesTest', + @ContentType = 'Test', + @ParentID = 1, + @ExtendedAttributes = '<attributes><attribute key=''Body''>Test</attribute></attributes>', + @Version = '2020-01-01 12:00:00:000', + @NewVersion = '2022-01-01 12:00:00:000'; + +INSERT +INTO @Attributes +VALUES ( 'GetTopicUpdatesTest1', 'Value' ), + ( 'GetTopicUpdatesTest2', 'Value' ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- DELETE TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +DELETE +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicUpdatesTest%' + +DELETE +FROM ExtendedAttributes +WHERE Version = @Version +OR Version = @NewVersion + +DELETE +FROM Relationships +WHERE RelationshipKey = 'GetTopicUpdatesTest' + +DELETE +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicUpdatesTest' + +DELETE +FROM Topics +WHERE TopicKey LIKE 'GetTopicUpdates%' + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH TEST DATA +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +SELECT @Key = 'GetTopicUpdatesChildTest', + @ParentID = @TopicID; + +EXECUTE @TopicID = [dbo].[CreateTopic] + @Key, + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @Version; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicUpdatesTest', + @TopicID, + @Version + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicUpdatesTest', + @TopicID, + @Version + ) + +-------------------------------------------------------------------------------------------------------------------------------- +-- UPDATE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @ExtendedAttributes = '<attributes><attribute key=''Body''>New Test</attribute></attributes>'; + +UPDATE @Attributes +SET AttributeValue = 'NewValue' + +-------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH NEW VERSION +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE @TopicID = [dbo].[UpdateTopic] + @TopicID = @TopicID, + @Key = 'GetTopicUpdatesModifiedTest', + @ContentType = @ContentType, + @Attributes = @Attributes, + @ExtendedAttributes = @ExtendedAttributes, + @Version = @NewVersion; + +INSERT +INTO Relationships ( + Source_TopicID, + RelationshipKey, + Target_TopicID, + IsDeleted, + Version + ) +VALUES ( @ParentID, + 'GetTopicUpdatesTest', + @TopicID, + 1, + @NewVersion + ) + +INSERT +INTO TopicReferences ( + Source_TopicID, + ReferenceKey, + Target_TopicID, + Version + ) +VALUES ( @ParentID, + 'GetTopicUpdatesTest', + NULL, + @NewVersion + ) + +EXECUTE [dbo].[CreateTopic] + 'GetTopicUpdatesTestNew', + @ContentType, + @ParentID, + @Attributes, + @ExtendedAttributes, + @References, + @NewVersion; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Topics +WHERE TopicKey LIKE 'GetTopicUpdates%' + +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicUpdatesTest%' + +SELECT * +FROM Relationships +WHERE RelationshipKey = 'GetTopicUpdatesTest' + +SELECT * +FROM TopicReferences +WHERE ReferenceKey = 'GetTopicUpdatesTest' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- ESTABLISH VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @TopicID AS INT, + @UniqueKey AS NVARCHAR (255); + +-------------------------------------------------------------------------------------------------------------------------------- +-- SET VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +SELECT @UniqueKey = 'GetTopicUpdatesTest'; + +SELECT @TopicID = TopicID +FROM Topics +WHERE TopicKey = @UniqueKey + +-------------------------------------------------------------------------------------------------------------------------------- +-- EXECUTE PROCEDURE +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [dbo].[DeleteTopic] + @TopicID; + +-------------------------------------------------------------------------------------------------------------------------------- +-- VERIFY RESULTS +-------------------------------------------------------------------------------------------------------------------------------- +SELECT * +FROM Attributes +WHERE AttributeKey LIKE 'GetTopicUpdatesTest%' True From 5bbb1e8bb01ffddee590ddb870c199b4c34a9c2a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 13:31:13 -0800 Subject: [PATCH 349/778] Added missing `override` to `Refresh()` implementations Since implementing the base `Refresh()` method on `TopicRepositoryBase()` (bc8725a), we must update the derived implementations to use the `override` keyword. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index e8a843c8..eb5714d0 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -135,7 +135,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { | METHOD: REFRESH \-------------------------------------------------------------------------------------------------------------------------*/ /// - public void Refresh(Topic referenceTopic, DateTime since) => _dataProvider.Refresh(referenceTopic, since); + public override void Refresh(Topic referenceTopic, DateTime since) => _dataProvider.Refresh(referenceTopic, since); /*========================================================================================================================== | METHOD: SAVE diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 93a6b16c..ab1e555a 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -280,7 +280,7 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic | METHOD: REFRESH \-------------------------------------------------------------------------------------------------------------------------*/ /// - public void Refresh(Topic referenceTopic, DateTime since) { + public override void Refresh(Topic referenceTopic, DateTime since) { } diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 63b22b89..524d1014 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -42,7 +42,7 @@ public DummyTopicRepository() : base() { } | METHOD: REFRESH \-------------------------------------------------------------------------------------------------------------------------*/ /// - public void Refresh(Topic referenceTopic, DateTime since) => throw new NotImplementedException(); + public override void Refresh(Topic referenceTopic, DateTime since) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: SAVE diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index ae15bfd5..007ebdb2 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -105,7 +105,7 @@ public StubTopicRepository() : base() { | METHOD: REFRESH \-------------------------------------------------------------------------------------------------------------------------*/ /// - public void Refresh(Topic referenceTopic, DateTime since) { } + public override void Refresh(Topic referenceTopic, DateTime since) { } /*========================================================================================================================== | METHOD: SAVE From 6dbf9738f5d11b701a3e264484ea9f357afbeaef Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 13:31:34 -0800 Subject: [PATCH 350/778] Implemented `SqlTopicRepository.Refresh()` method This wires up the new `GetTopicUpdates` stored procedure (0f36947) to retrieve updates to topics and merge them with the topic graph. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index ab1e555a..a99a0c4a 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -282,6 +282,47 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic /// public override void Refresh(Topic referenceTopic, DateTime since) { + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(referenceTopic, "A referenceTopic from the topic graph must be provided."); + Contract.Requires( + since.Date >= DateTime.Now.AddHours(-24), + "The since date is expected to be within the last twenty four hours." + ); + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish database connection + \-----------------------------------------------------------------------------------------------------------------------*/ + using var connection = new SqlConnection(_connectionString); + using var command = new SqlCommand("GetTopicUpdates", connection) { + CommandType = CommandType.StoredProcedure, + CommandTimeout = 120 + }; + + command.CommandType = CommandType.StoredProcedure; + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish query parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + command.AddParameter("Since", since); + + /*------------------------------------------------------------------------------------------------------------------------ + | Process database query + \-----------------------------------------------------------------------------------------------------------------------*/ + try { + connection.Open(); + using var reader = command.ExecuteReader(); + reader.LoadTopicGraph(referenceTopic.GetRootTopic(), false); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Catch exception + \-----------------------------------------------------------------------------------------------------------------------*/ + catch (SqlException exception) { + throw new TopicRepositoryException($"Topics failed to update: '{exception.Message}'", exception); + } + } /*========================================================================================================================== From 22760836550d76d6998e349aef7fe4808b7ca749 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 13:52:22 -0800 Subject: [PATCH 351/778] Added in basic support for cache updates This approach isn't perfect in that it prevents the request from being completed until the refresh is finished, instead of being performed on a background thread. A more sophisticated version would operate as a background task. Alternatively, in the future, we'll likely introduce a queue via e.g. Azure Service Bus to track events, and rely on those to trigger `Refresh()`. Until then, however, this provides a simple approach for satisfying pulling in occassional cache updates. --- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 81eec5ee..60b9228f 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -34,6 +34,7 @@ public class SampleActivator : IControllerActivator, IViewComponentActivator { private readonly ITypeLookupService _typeLookupService = null; private readonly ITopicMappingService _topicMappingService = null; private readonly ITopicRepository _topicRepository = null; + private DateTime _cacheLastUpdated = DateTime.UtcNow; /*========================================================================================================================== | HIERARCHICAL TOPIC MAPPING SERVICE @@ -94,6 +95,15 @@ public object Create(ControllerContext context) { \-----------------------------------------------------------------------------------------------------------------------*/ var type = context.ActionDescriptor.ControllerTypeInfo.AsType(); + /*------------------------------------------------------------------------------------------------------------------------ + | Periodically update cache + \-----------------------------------------------------------------------------------------------------------------------*/ + if (DateTime.UtcNow > _cacheLastUpdated.AddMinutes(1)) { + var currentUpdate = DateTime.UtcNow; + _topicRepository.Refresh(_topicRepository.Load(), _cacheLastUpdated); + _cacheLastUpdated = currentUpdate; + } + /*------------------------------------------------------------------------------------------------------------------------ | Configure and return appropriate controller \-----------------------------------------------------------------------------------------------------------------------*/ From 2d0047ed8c9bd4a5ecf8287b9756f5163d43440e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 14:38:49 -0800 Subject: [PATCH 352/778] Introduced new `InvalidTypeException` The `InvalidTypeException` is thrown is the `TopicMappingService` attempts to map to a type (such as a `TopicViewModel`) which cannot be located in the associated `ITypeLookupService`. This can happen, for instance, if the topic to be mapped has a content type that doesn't have a corresponding `TopicViewModel`, or if that type isn't properly registered with the `ITypeLookupService`. This inherits from the existing `TopicMappingException`, thus allowing callers to catch the general exception, or hone in on this specific exception if appropriate. --- OnTopic/Mapping/InvalidTypeException.cs | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 OnTopic/Mapping/InvalidTypeException.cs diff --git a/OnTopic/Mapping/InvalidTypeException.cs b/OnTopic/Mapping/InvalidTypeException.cs new file mode 100644 index 00000000..6c342594 --- /dev/null +++ b/OnTopic/Mapping/InvalidTypeException.cs @@ -0,0 +1,60 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.Serialization; +using OnTopic.Internal.Diagnostics; +using OnTopic.Lookup; + +namespace OnTopic.Mapping { + + /*============================================================================================================================ + | CLASS: INVALID TYPE EXCEPTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The is thrown when an implementation requests a + /// target type that cannot be located using the supplied . + /// + /// + /// Having one (base) class used for all expected exceptions from the and other mapping + /// service interfaces allows implementors to capture all exceptions—while, potentially, catching more specific exceptions + /// based on derived classes, if we discover the need for more specific exceptions. + /// + [Serializable] + public class InvalidTypeException: TopicMappingException { + + /*========================================================================================================================== + | CONSTRUCTOR: INVALID TYPE EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance. + /// + public InvalidTypeException() : base() { } + + /// + /// Initializes a new instance with a specific error message. + /// + /// The message to display for this exception. + public InvalidTypeException(string message) : base(message) { } + + /// + /// Initializes a new instance with a specific error message and nested exception. + /// + /// The message to display for this exception. + /// The reference to the original, underlying exception. + public InvalidTypeException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Instantiates a new instance for serialization. + /// + /// A instance with details about the serialization requirements. + /// A instance with details about the request context. + /// A new instance. + protected InvalidTypeException(SerializationInfo info, StreamingContext context) : base(info, context) { + Contract.Requires(info); + } + + } //Class +} //Namespace \ No newline at end of file From d1a17ccc5986c249a0ee348309cf0dd6aaa72c79 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 14:39:11 -0800 Subject: [PATCH 353/778] Implement the new `InvalidTypeException` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new `InvalidTypeException` is used to replace the legacy `TypeLoadException` (2d0047e). This ensures that callers can catch the general `TopicMappingException`, which this derives from, in order to catch these errors—whereas `TypeLoadException` derived from a different exception—while still allowing callers to catch this specific exception, if appropriate. --- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 19922658..ade30345 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -121,7 +121,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); if (viewModelType is null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { - throw new TypeLoadException( + throw new InvalidTypeException( $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + $"to map the topic '{topic.GetUniqueKey()}'." ); @@ -755,7 +755,7 @@ MappedTopicCache cache try { topicDto = await MapAsync(source, configuration.CrawlRelationships, cache).ConfigureAwait(false); } - catch (TypeLoadException) { + catch (InvalidTypeException) { //Disregard errors caused by unmapped view models; those are functionally equivalent to IsAssignableFrom() mismatches } if (topicDto is not null && configuration.Property.PropertyType.IsAssignableFrom(topicDto.GetType())) { From 9c2baf9b0a25aa5985a7bd2ba9d956b244ee244a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 14:47:26 -0800 Subject: [PATCH 354/778] Introduced new `MappingModelValidationException` The `MappingModelValidationException` is thrown a mapping service attempts to map to a model which cannot be mapped to. This is especially important with the `ReverseTopicMappingService`, since it takes data integrity very seriously when writing back to the database and, thus, fails if any members don't align with the `ContentTypeDescriptor`. By contrast, this won't generally be used with the read-only mapping services, such as `TopicMappingService`, since it is generally designed to be forgiving. This inherits from the existing `TopicMappingException`, thus allowing callers to catch the general exception, or hone in on this specific exception if appropriate. --- .../MappingModelValidationException.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 OnTopic/Mapping/MappingModelValidationException.cs diff --git a/OnTopic/Mapping/MappingModelValidationException.cs b/OnTopic/Mapping/MappingModelValidationException.cs new file mode 100644 index 00000000..c4099c2a --- /dev/null +++ b/OnTopic/Mapping/MappingModelValidationException.cs @@ -0,0 +1,64 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.Serialization; +using OnTopic.Internal.Diagnostics; +using OnTopic.Mapping.Reverse; +using OnTopic.Metadata; + +namespace OnTopic.Mapping { + + /*============================================================================================================================ + | CLASS: MAPPING MODEL VALIDATION EXCEPTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The is thrown when an + /// implementation is provided a model that cannot be reliably mapped back to the target , thus + /// introducing potential data integrity issues. + /// + /// + /// The read-only mapping services, such as , are generally designed to be forgiving of + /// mismatches. By contrast, because the is intended to update the persistence + /// store, it is generally designed to fail if there are any discrepancies between the source model and the target . Given this, the is expected to be thrown for + /// validation errors caused by e.g. the . + /// + [Serializable] + public class MappingModelValidationException: TopicMappingException { + + /*========================================================================================================================== + | CONSTRUCTOR: MAPPING MODEL VALIDATION EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance. + /// + public MappingModelValidationException() : base() { } + + /// + /// Initializes a new instance with a specific error message. + /// + /// The message to display for this exception. + public MappingModelValidationException(string message) : base(message) { } + + /// + /// Initializes a new instance with a specific error message and nested exception. + /// + /// The message to display for this exception. + /// The reference to the original, underlying exception. + public MappingModelValidationException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Instantiates a new instance for serialization. + /// + /// A instance with details about the serialization requirements. + /// A instance with details about the request context. + /// A new instance. + protected MappingModelValidationException(SerializationInfo info, StreamingContext context) : base(info, context) { + Contract.Requires(info); + } + + } //Class +} //Namespace \ No newline at end of file From 7a6b2f3ddf1905540116b54ca6c75a4605c775ef Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 14:54:47 -0800 Subject: [PATCH 355/778] Implement the new `MappingModelValidationException` The new `MappingModelValidationException` provides a more specific exception that the general `TopicMappingException` (9c2baf9). This ensures that callers can catch the general `TopicMappingException`, which this derives from, in order to catch these errors, while still allowing callers to catch this specific exception, if appropriate. --- OnTopic/Mapping/Reverse/BindingModelValidator.cs | 16 ++++++++-------- .../Reverse/ReverseTopicMappingService.cs | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 1f012f51..df270fda 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -198,7 +198,7 @@ static internal void ValidateProperty( | Handle children \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.RelationshipType is RelationshipType.Children) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The {nameof(ReverseTopicMappingService)} does not support mapping child topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + $"it from being evaluated by the {nameof(ReverseTopicMappingService)}. If children must be mapped, then the " + @@ -211,7 +211,7 @@ static internal void ValidateProperty( | Handle parent \-----------------------------------------------------------------------------------------------------------------------*/ if (configuration.AttributeKey is "Parent") { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The {nameof(ReverseTopicMappingService)} does not support mapping Parent topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + $"it from being evaluated by the {nameof(ReverseTopicMappingService)}." @@ -222,7 +222,7 @@ static internal void ValidateProperty( | Validate attribute type \-----------------------------------------------------------------------------------------------------------------------*/ if (attributeDescriptor is null) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"A '{nameof(sourceType)}' object was provided with a content type set to '{contentTypeDescriptor.Key}'. This " + $"content type does not contain an attribute named '{compositeAttributeKey}', as requested by the " + $"'{configuration.Property.Name}' property. If this property is not intended to be mapped by the " + @@ -245,7 +245,7 @@ attributeDescriptor.ModelType is ModelType.NestedTopic && !typeof(ITopicBindingModel).IsAssignableFrom(listType) && listType is not null ) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.RelationshipType}, but the generic type '{listType.Name}' does not implement the " + $"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " + @@ -262,7 +262,7 @@ listType is not null attributeDescriptor.ModelType is ModelType.Reference && !typeof(IRelatedTopicBindingModel).IsAssignableFrom(propertyType) ) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " + $"{nameof(IRelatedTopicBindingModel)} interface. This is required for references. If this property is not intended " + @@ -315,7 +315,7 @@ [AllowNull]Type listType | Validate list \-----------------------------------------------------------------------------------------------------------------------*/ if (!typeof(IList).IsAssignableFrom(property.PropertyType)) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but does not implement {nameof(IList)}. Relationships must implement " + $"{nameof(IList)} or derive from a collection that does." @@ -326,7 +326,7 @@ [AllowNull]Type listType | Validate relationship type \-----------------------------------------------------------------------------------------------------------------------*/ if (!new[] { RelationshipType.Any, RelationshipType.Relationship }.Contains(configuration.RelationshipType)) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + $"'{attributeDescriptor.Key}', but is configured as a {configuration.RelationshipType}. The property should be " + $"flagged as either {nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}." @@ -337,7 +337,7 @@ [AllowNull]Type listType | Validate the correct base class for relationships \-----------------------------------------------------------------------------------------------------------------------*/ if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.RelationshipType}, but the generic type '{listType?.Name}' does not implement the " + $"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 26c5bc32..6bc401cd 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -145,7 +145,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the content type is valid if (!_contentTypeDescriptors.Contains(source.ContentType)) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}'. There " + $"are no matching content types in the ITopicRepository provided. This suggests that the binding model is invalid. " + $"If this is expected—e.g., if the content type is being added as part of this operation—then it needs to be added " + @@ -155,7 +155,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the content types match if (source.ContentType != target.ContentType) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The {nameof(source)} object (with the key '{source.Key}') has a content type of '{source.ContentType}', while " + $"the {nameof(target)} object (with the key '{target.Key}') has a content type of '{target.ContentType}'. It is not" + $"permitted to change the topic's content type during a mapping operation, as this interferes with the validation. " + @@ -165,7 +165,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { //Ensure the keys match if (source.Key != target.Key && !String.IsNullOrEmpty(source.Key)) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The {nameof(source)} object has a key of '{source.Key}', while the {nameof(target)} object has a key of " + $"'{target.Key}'. It is not permitted to change the topic's key during a mapping operation, as this suggests an " + $"invalid target. If this is by design, change the key on the target topic prior to invoking MapAsync()." @@ -293,7 +293,7 @@ await MapAsync( var attributeType = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey); if (attributeType is null) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The attribute '{configuration.AttributeKey}' mapped by the {source.GetType()} could not be found on the " + $"'{contentTypeDescriptor.Key}' content type."); } @@ -436,7 +436,7 @@ PropertyConfiguration configuration foreach (IRelatedTopicBindingModel relationship in sourceList) { var targetTopic = _topicRepository.Load(relationship.UniqueKey, target); if (targetTopic is null) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The relationship '{relationship.UniqueKey}' mapped in the '{configuration.Property.Name}' property could not " + $"be located in the repository." ); @@ -542,7 +542,7 @@ PropertyConfiguration configuration | Provide error handling \-----------------------------------------------------------------------------------------------------------------------*/ if (topicReference is null) { - throw new TopicMappingException( + throw new MappingModelValidationException( $"The topic '{modelReference.UniqueKey}' referenced by the '{source.GetType()}' model's " + $"'{configuration.Property.Name}' property could not be found." ); From 4d235116965b3b929b1336060ae5d18ec3166da0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 15:10:06 -0800 Subject: [PATCH 356/778] Updated documentation to include new exception types --- OnTopic/Mapping/README.md | 8 +++++++- OnTopic/Mapping/Reverse/README.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 91ee1d8e..120041dc 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -20,6 +20,7 @@ The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract inte - [Internal Caching](#internal-caching) - [`CachedTopicMappingService`](#cachedtopicmappingservice) - [Limitations](#limitations) +- [Exceptions](#exceptions) ## `TopicMappingService` 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 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. 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. -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. \ No newline at end of file +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. + +## Exceptions +The topic mapping services will throw a [`TopicMappingException`](TopicMappingException.cs) if a foreseeable exception occurs. Specifically, the exceptions expected will be: +- **[`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`. +- **[`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. \ No newline at end of file diff --git a/OnTopic/Mapping/Reverse/README.md b/OnTopic/Mapping/Reverse/README.md index 14b3f31b..03f78806 100644 --- a/OnTopic/Mapping/Reverse/README.md +++ b/OnTopic/Mapping/Reverse/README.md @@ -11,7 +11,7 @@ The [`IReverseTopicMappingService`](IReverseTopicMappingService.cs) and its conc 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. ## Model Validation -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. +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. > _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. From 1574b90aa994c0adf094882e0aa73601e161b497 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 15:39:30 -0800 Subject: [PATCH 357/778] Migrated topic view and binding models to records The new C# 9.0 record feature is optimal for view models and binding models, which we don't expect to change after they've been generated. Records are also potentially a bit faster, since they're immutable, and help ensure that they're read-only. Finally, the record syntax makes it easier to generate new versions should we need to make modifications via the `with` syntax. The nature of records mandates that all downstream implementations of view and binding models are also converted to records, so this is (very much!) a breaking change. --- .../BindingModels/BasicTopicBindingModel.cs | 4 ++-- .../DescendentSpecializedTopicViewModel.cs | 2 +- .../ViewModels/DescendentTopicViewModel.cs | 2 +- .../BindingModels/RelatedTopicBindingModel.cs | 4 ++-- .../ContentItemTopicViewModel.cs | 10 ++++----- .../ContentListTopicViewModel.cs | 8 +++---- OnTopic.ViewModels/IndexTopicViewModel.cs | 2 +- OnTopic.ViewModels/Internal/IsExternalInit.cs | 19 ++++++++++++++++ OnTopic.ViewModels/ItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/ListTopicViewModel.cs | 2 +- .../LookupListItemTopicViewModel.cs | 2 +- .../NavigationTopicViewModel.cs | 4 ++-- OnTopic.ViewModels/PageGroupTopicViewModel.cs | 2 +- OnTopic.ViewModels/PageTopicViewModel.cs | 16 +++++++------- OnTopic.ViewModels/SectionTopicViewModel.cs | 4 ++-- OnTopic.ViewModels/SlideTopicViewModel.cs | 2 +- OnTopic.ViewModels/SlideshowTopicViewModel.cs | 4 ++-- OnTopic.ViewModels/TopicViewModel.cs | 22 +++++++++---------- OnTopic.ViewModels/VideoTopicViewModel.cs | 6 ++--- OnTopic/Internal/Runtime/IsExternalInit.cs | 2 +- .../Models/INavigationTopicViewModel{T}.cs | 2 +- OnTopic/Models/IPageTopicViewModel.cs | 4 ++-- OnTopic/Models/IRelatedTopicBindingModel.cs | 2 +- OnTopic/Models/ITopicBindingModel.cs | 4 ++-- OnTopic/Models/ITopicViewModel.cs | 16 +++++++------- 25 files changed, 83 insertions(+), 64 deletions(-) create mode 100644 OnTopic.ViewModels/Internal/IsExternalInit.cs diff --git a/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs b/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs index c50c8f98..35a9cd07 100644 --- a/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs @@ -26,10 +26,10 @@ public BasicTopicBindingModel(string? key, string? contentType) { ContentType = contentType; } - public string? Key { get; set; } + public string? Key { get; init; } [Required] - public string? ContentType { get; set; } + public string? ContentType { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs index e4bcd63b..d472d9a3 100644 --- a/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentSpecializedTopicViewModel.cs @@ -24,7 +24,7 @@ namespace OnTopic.Tests.ViewModels { /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// /// - public class DescendentSpecializedTopicViewModel: DescendentTopicViewModel { + public record DescendentSpecializedTopicViewModel: DescendentTopicViewModel { public bool IsLeaf { get; set; } diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index 29f216f7..ac3f7e68 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -23,7 +23,7 @@ namespace OnTopic.Tests.ViewModels { /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// /// - public class DescendentTopicViewModel: TopicViewModel { + public record DescendentTopicViewModel: TopicViewModel { [Follow(Relationships.Children)] public TopicViewModelCollection Children { get; } = new(); diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs index 1870d665..480bea9f 100644 --- a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs @@ -21,7 +21,7 @@ namespace OnTopic.ViewModels.BindingModels { /// cref="ReverseTopicMappingService"/>. The only reason to implement a custom definition is if the caller needs additional /// metadata for separate validation or processing. /// - public class RelatedTopicBindingModel : IRelatedTopicBindingModel { + public record RelatedTopicBindingModel : IRelatedTopicBindingModel { /*========================================================================================================================== | PROPERTY: UNIQUE KEY @@ -33,7 +33,7 @@ public class RelatedTopicBindingModel : IRelatedTopicBindingModel { /// value is not null /// [Required] - public string? UniqueKey { get; set; } + public string? UniqueKey { get; init; } } //Class } //Namespaces \ No newline at end of file diff --git a/OnTopic.ViewModels/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/ContentItemTopicViewModel.cs index 3a8987eb..b084d6b1 100644 --- a/OnTopic.ViewModels/ContentItemTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentItemTopicViewModel.cs @@ -18,7 +18,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class ContentItemTopicViewModel: ItemTopicViewModel { + public record ContentItemTopicViewModel: ItemTopicViewModel { /*========================================================================================================================== | DESCRIPTION @@ -26,7 +26,7 @@ public class ContentItemTopicViewModel: ItemTopicViewModel { /// /// Gets the description; for Content Items, this is effectively the body. /// - public string Description { get; set; } = default!; + public string Description { get; init; } = default!; /*========================================================================================================================== | LEARN MORE URL @@ -34,7 +34,7 @@ public class ContentItemTopicViewModel: ItemTopicViewModel { /// /// Gets an optional URL for additional information that should be linked to. /// - public Uri? LearnMoreUrl { get; set; } + public Uri? LearnMoreUrl { get; init; } /*========================================================================================================================== | THUMBNAIL IMAGE @@ -42,7 +42,7 @@ public class ContentItemTopicViewModel: ItemTopicViewModel { /// /// Gets an optional path to a thumbnail image that should accompany the content item. /// - public Uri? ThumbnailImage { get; set; } + public Uri? ThumbnailImage { get; init; } /*========================================================================================================================== | CATEGORY @@ -50,7 +50,7 @@ public class ContentItemTopicViewModel: ItemTopicViewModel { /// /// Gets the category that the content item should be grouped under. /// - public string? Category { get; set; } + public string? Category { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/ContentListTopicViewModel.cs index 505624cd..82b91b5d 100644 --- a/OnTopic.ViewModels/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentListTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class ContentListTopicViewModel: PageTopicViewModel { + public record ContentListTopicViewModel: PageTopicViewModel { /*========================================================================================================================== | CONTENT ITEMS @@ -51,7 +51,7 @@ public class ContentListTopicViewModel: PageTopicViewModel { /// corresponding attribute, and so this can easily be hidden or disabled globally via the editor. /// /// True if the content list should be indexed; false otherwise. - public bool IsIndexed { get; set; } + public bool IsIndexed { get; init; } /*========================================================================================================================== | INDEX LABEL @@ -64,8 +64,8 @@ public class ContentListTopicViewModel: PageTopicViewModel { /// "IndexLabel"/> allows that to be optionally set on a per topic basis. The default value is "Contents", though it is up /// to view implementors and editor configurations as to whether this option is exposed or honored. /// - /// Returns the value set; defaults to "Contents". - public string IndexLabel { get; set; } = "Contents"; + /// Returns the value init; defaults to "Contents". + public string IndexLabel { get; init; } = "Contents"; } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/IndexTopicViewModel.cs b/OnTopic.ViewModels/IndexTopicViewModel.cs index e2941843..9c1cde35 100644 --- a/OnTopic.ViewModels/IndexTopicViewModel.cs +++ b/OnTopic.ViewModels/IndexTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class IndexTopicViewModel: PageTopicViewModel { + public record IndexTopicViewModel: PageTopicViewModel { } //Class diff --git a/OnTopic.ViewModels/Internal/IsExternalInit.cs b/OnTopic.ViewModels/Internal/IsExternalInit.cs new file mode 100644 index 00000000..2bd828ad --- /dev/null +++ b/OnTopic.ViewModels/Internal/IsExternalInit.cs @@ -0,0 +1,19 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace System.Runtime.CompilerServices { + + /*============================================================================================================================ + | CLASS: IS EXTERNAL INIT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. + /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. + /// + internal static class IsExternalInit { + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/ItemTopicViewModel.cs b/OnTopic.ViewModels/ItemTopicViewModel.cs index a7e1f4da..4a1e3411 100644 --- a/OnTopic.ViewModels/ItemTopicViewModel.cs +++ b/OnTopic.ViewModels/ItemTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class ItemTopicViewModel : TopicViewModel { + public record ItemTopicViewModel : TopicViewModel { } //Class diff --git a/OnTopic.ViewModels/ListTopicViewModel.cs b/OnTopic.ViewModels/ListTopicViewModel.cs index 1948be01..79631df1 100644 --- a/OnTopic.ViewModels/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/ListTopicViewModel.cs @@ -25,7 +25,7 @@ namespace OnTopic.ViewModels { /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// /// - public class ListTopicViewModel: ContentItemTopicViewModel { + public record ListTopicViewModel: ContentItemTopicViewModel { } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/LookupListItemTopicViewModel.cs b/OnTopic.ViewModels/LookupListItemTopicViewModel.cs index 9e357bed..57224494 100644 --- a/OnTopic.ViewModels/LookupListItemTopicViewModel.cs +++ b/OnTopic.ViewModels/LookupListItemTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class LookupListItemTopicViewModel: ItemTopicViewModel { + public record LookupListItemTopicViewModel: ItemTopicViewModel { } //Class diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 29f72ada..873c9fee 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -28,7 +28,7 @@ namespace OnTopic.ViewModels { /// cref="NavigationTopicViewModel"/> class is marked as sealed. /// /// - public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicViewModel { + public sealed record NavigationTopicViewModel : TopicViewModel, INavigationTopicViewModel { /*========================================================================================================================== | SHORT TITLE @@ -36,7 +36,7 @@ public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicV /// /// Provides a short title to be used in the navigation, for cases where the normal title is too long. /// - public string? ShortTitle { get; set; } + public string? ShortTitle { get; init; } /*========================================================================================================================== | CHILDREN diff --git a/OnTopic.ViewModels/PageGroupTopicViewModel.cs b/OnTopic.ViewModels/PageGroupTopicViewModel.cs index b1562426..4b5435b7 100644 --- a/OnTopic.ViewModels/PageGroupTopicViewModel.cs +++ b/OnTopic.ViewModels/PageGroupTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class PageGroupTopicViewModel : SectionTopicViewModel { + public record PageGroupTopicViewModel : SectionTopicViewModel { } //Class diff --git a/OnTopic.ViewModels/PageTopicViewModel.cs b/OnTopic.ViewModels/PageTopicViewModel.cs index 5cc1a0c2..2e56d20c 100644 --- a/OnTopic.ViewModels/PageTopicViewModel.cs +++ b/OnTopic.ViewModels/PageTopicViewModel.cs @@ -18,7 +18,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { + public record PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /*========================================================================================================================== | SUBTITLE @@ -26,7 +26,7 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /// /// Provides an optional subtitle which will typically be displayed under the title. /// - public string? Subtitle { get; set; } + public string? Subtitle { get; init; } /*========================================================================================================================== | META TITLE @@ -34,19 +34,19 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /// /// Provides an optional title to be used in page's metadata, if it differs from the . /// - public string? MetaTitle { get; set; } + public string? MetaTitle { get; init; } /*========================================================================================================================== | META DESCRIPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? MetaDescription { get; set; } + public string? MetaDescription { get; init; } /*========================================================================================================================== | META KEYWORDS \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? MetaKeywords { get; set; } + public string? MetaKeywords { get; init; } /*========================================================================================================================== | META KEYWORDS @@ -54,7 +54,7 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /// /// Determines whether or not search engines are expected to index the page. /// - public bool? NoIndex { get; set; } + public bool? NoIndex { get; init; } /*========================================================================================================================== | SHORT TITLE @@ -62,7 +62,7 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /// /// Provides a short title to be used in the navigation, for cases where the normal title is too long. /// - public string? ShortTitle { get; set; } + public string? ShortTitle { get; init; } /*========================================================================================================================== | BODY @@ -70,7 +70,7 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel { /// /// Provides the primary content for the page, which is typically in HTML format. /// - public string? Body { get; set; } + public string? Body { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/SectionTopicViewModel.cs b/OnTopic.ViewModels/SectionTopicViewModel.cs index 146554df..7f39c501 100644 --- a/OnTopic.ViewModels/SectionTopicViewModel.cs +++ b/OnTopic.ViewModels/SectionTopicViewModel.cs @@ -19,7 +19,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class SectionTopicViewModel : TopicViewModel { + public record SectionTopicViewModel : TopicViewModel { /*========================================================================================================================== | HEADER IMAGE @@ -27,7 +27,7 @@ public class SectionTopicViewModel : TopicViewModel { /// /// Provides a header image which may be displayed at the top of a section. /// - public Uri? HeaderImageUrl { get; set; } + public Uri? HeaderImageUrl { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/SlideTopicViewModel.cs b/OnTopic.ViewModels/SlideTopicViewModel.cs index 7f1e4af8..770d86b9 100644 --- a/OnTopic.ViewModels/SlideTopicViewModel.cs +++ b/OnTopic.ViewModels/SlideTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class SlideTopicViewModel: ContentItemTopicViewModel { + public record SlideTopicViewModel: ContentItemTopicViewModel { } //Class diff --git a/OnTopic.ViewModels/SlideshowTopicViewModel.cs b/OnTopic.ViewModels/SlideshowTopicViewModel.cs index d88c3774..7ec11449 100644 --- a/OnTopic.ViewModels/SlideshowTopicViewModel.cs +++ b/OnTopic.ViewModels/SlideshowTopicViewModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class SlideshowTopicViewModel: ContentListTopicViewModel { + public record SlideshowTopicViewModel: ContentListTopicViewModel { /*========================================================================================================================== | TRANSITION EFFECT @@ -30,7 +30,7 @@ public class SlideshowTopicViewModel: ContentListTopicViewModel { /// slideshow. Typically, they will map to standard HTML5/CSS3 transition effects, but they could differ depending on the /// implementation. /// - public string? TransitionEffect { get; set; } + public string? TransitionEffect { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 796f2d9a..0f43f34a 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -20,55 +20,55 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class TopicViewModel: ITopicViewModel { + public record TopicViewModel: ITopicViewModel { /*========================================================================================================================== | ID \-------------------------------------------------------------------------------------------------------------------------*/ /// - public int Id { get; set; } + public int Id { get; init; } /*========================================================================================================================== | KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? Key { get; set; } + public string? Key { get; init; } /*========================================================================================================================== | CONTENT TYPE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? ContentType { get; set; } + public string? ContentType { get; init; } /*========================================================================================================================== | UNIQUE KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? UniqueKey { get; set; } + public string? UniqueKey { get; init; } /*========================================================================================================================== | WEB PATH \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? WebPath { get; set; } + public string? WebPath { get; init; } /*========================================================================================================================== | VIEW \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? View { get; set; } + public string? View { get; init; } /*========================================================================================================================== | TITLE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public string? Title { get; set; } + public string? Title { get; init; } /*========================================================================================================================== | IS HIDDEN? \-------------------------------------------------------------------------------------------------------------------------*/ /// - public bool IsHidden { get; set; } + public bool IsHidden { get; init; } /*========================================================================================================================== | LAST MODIFIED @@ -76,7 +76,7 @@ public class TopicViewModel: ITopicViewModel { /// /// The date that the topic was last modified on. /// - public DateTime LastModified { get; set; } + public DateTime LastModified { get; init; } /*========================================================================================================================== | PARENT @@ -92,7 +92,7 @@ public class TopicViewModel: ITopicViewModel { /// they are annotated with a . /// [Follow(Relationships.Parents)] - public TopicViewModel? Parent { get; set; } + public TopicViewModel? Parent { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.ViewModels/VideoTopicViewModel.cs b/OnTopic.ViewModels/VideoTopicViewModel.cs index 720b1042..842a946f 100644 --- a/OnTopic.ViewModels/VideoTopicViewModel.cs +++ b/OnTopic.ViewModels/VideoTopicViewModel.cs @@ -19,7 +19,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class VideoTopicViewModel: PageTopicViewModel { + public record VideoTopicViewModel: PageTopicViewModel { /*========================================================================================================================== | VIDEO URL @@ -27,7 +27,7 @@ public class VideoTopicViewModel: PageTopicViewModel { /// /// Provides a URL reference to a video to display on the page. /// - public Uri? VideoUrl { get; set; } + public Uri? VideoUrl { get; init; } /*========================================================================================================================== | POSTER URL @@ -35,7 +35,7 @@ public class VideoTopicViewModel: PageTopicViewModel { /// /// Provides a URL reference to an image to display prior to playing the video. /// - public Uri? PosterUrl { get; set; } + public Uri? PosterUrl { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Runtime/IsExternalInit.cs b/OnTopic/Internal/Runtime/IsExternalInit.cs index 1ba62b43..26713de0 100644 --- a/OnTopic/Internal/Runtime/IsExternalInit.cs +++ b/OnTopic/Internal/Runtime/IsExternalInit.cs @@ -16,4 +16,4 @@ namespace System.Runtime.CompilerServices { internal class IsExternalInit { } //Class -} //Namespace +} //Namespace \ No newline at end of file diff --git a/OnTopic/Models/INavigationTopicViewModel{T}.cs b/OnTopic/Models/INavigationTopicViewModel{T}.cs index e2eff2dc..140b5ceb 100644 --- a/OnTopic/Models/INavigationTopicViewModel{T}.cs +++ b/OnTopic/Models/INavigationTopicViewModel{T}.cs @@ -28,7 +28,7 @@ public interface INavigationTopicViewModel : /// In addition to the Title, a site may opt to define a Short Title used exclusively in the navigation. If present, this /// value should be used instead of Title. /// - string? ShortTitle { get; set; } + string? ShortTitle { get; init; } /*========================================================================================================================== | METHOD: ISSELECTED diff --git a/OnTopic/Models/IPageTopicViewModel.cs b/OnTopic/Models/IPageTopicViewModel.cs index ee053cef..a63f9ab7 100644 --- a/OnTopic/Models/IPageTopicViewModel.cs +++ b/OnTopic/Models/IPageTopicViewModel.cs @@ -30,7 +30,7 @@ public interface IPageTopicViewModel : ITopicViewModel { /// Gets or sets the Meta Keywords attribute, which represents the HTML metadata that will be presented alongside the /// page. /// - string? MetaKeywords { get; set; } + string? MetaKeywords { get; init; } /*========================================================================================================================== | PROPERTY: META DESCRIPTION @@ -39,7 +39,7 @@ public interface IPageTopicViewModel : ITopicViewModel { /// Gets or sets the Meta Description attribute, which represents the HTML metadata that will be presented alongside the /// page. /// - string? MetaDescription { get; set; } + string? MetaDescription { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Models/IRelatedTopicBindingModel.cs b/OnTopic/Models/IRelatedTopicBindingModel.cs index 3a47d1b5..45473dcc 100644 --- a/OnTopic/Models/IRelatedTopicBindingModel.cs +++ b/OnTopic/Models/IRelatedTopicBindingModel.cs @@ -31,7 +31,7 @@ public interface IRelatedTopicBindingModel { /// value is not null /// [Required] - string? UniqueKey { get; set; } + string? UniqueKey { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index 8220e507..83b976ef 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -36,7 +36,7 @@ public interface ITopicBindingModel { /// !value.Contains(" ") /// [Required] - string? Key { get; set; } + string? Key { get; init; } /*========================================================================================================================== | PROPERTY: CONTENT TYPE @@ -49,7 +49,7 @@ public interface ITopicBindingModel { /// Editor (via the property). /// [Required] - string? ContentType { get; set; } + string? ContentType { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index d5a53343..d5e8109d 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -36,7 +36,7 @@ public interface ITopicViewModel { /// /// Gets or sets the topic's ID attribute, the primary unique identifier for the topic. /// - int Id { get; set; } + int Id { get; init; } /*========================================================================================================================== | PROPERTY: KEY @@ -44,7 +44,7 @@ public interface ITopicViewModel { /// /// Gets or sets the topic's Key attribute, the primary text identifier for the topic. /// - string? Key { get; set; } + string? Key { get; init; } /*========================================================================================================================== | PROPERTY: UNIQUE KEY @@ -52,7 +52,7 @@ public interface ITopicViewModel { /// /// Gets or sets the topic's attribute, the unique text identifier for the topic. /// - string? UniqueKey { get; set; } + string? UniqueKey { get; init; } /*========================================================================================================================== | PROPERTY: WEB PATH @@ -61,7 +61,7 @@ public interface ITopicViewModel { /// Gets or sets the topic's attribute, which represents the in its URL /// format. /// - string? WebPath { get; set; } + string? WebPath { get; init; } /*========================================================================================================================== | PROPERTY: CONTENT TYPE @@ -73,7 +73,7 @@ public interface ITopicViewModel { /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics /// Editor (via the property). /// - string? ContentType { get; set; } + string? ContentType { get; init; } /*========================================================================================================================== | PROPERTY: VIEW @@ -88,7 +88,7 @@ public interface ITopicViewModel { /// Content Type is "Page", then the view will be "Page". This will cause the TopicViewResultExecutor to look /// for a view at, for instance, /Views/Page/Page.cshtml. /// - string? View { get; set; } + string? View { get; init; } /*========================================================================================================================== | PROPERTY: IS HIDDEN @@ -96,7 +96,7 @@ public interface ITopicViewModel { /// /// Gets or sets whether the current topic is hidden. /// - bool IsHidden { get; set; } + bool IsHidden { get; init; } /*========================================================================================================================== | PROPERTY: TITLE @@ -109,7 +109,7 @@ public interface ITopicViewModel { /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for /// referencing topics. /// - string? Title { get; set; } + string? Title { get; init; } } //Class } //Namespace \ No newline at end of file From 1872dda09b2dedd309d6668ef430b13eb215a0bc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 19 Jan 2021 15:40:23 -0800 Subject: [PATCH 358/778] Updated unit tests to expect the correct exception types This should have been caught as part of the previous merge request (bbc035d), but was missed. Whoops. --- OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 34995c9a..dac5e63d 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -369,7 +369,7 @@ public async Task Map_ExceedsMinimumValue_ThrowsValidationException() { /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -387,7 +387,7 @@ public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException() /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -408,7 +408,7 @@ public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() { /// . /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -426,7 +426,7 @@ public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() { /// is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -446,7 +446,7 @@ public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationExceptio /// cref="InvalidOperationException"/>. /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -465,7 +465,7 @@ public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException() /// cref="IList"/>. This is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); @@ -484,7 +484,7 @@ public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationExceptio /// cref="IRelatedTopicBindingModel"/>. This is invalid, and expected to throw an . /// [TestMethod] - [ExpectedException(typeof(TopicMappingException))] + [ExpectedException(typeof(MappingModelValidationException))] public async Task Map_InvalidTopicReferenceType_ThrowsInvalidOperationException() { var mappingService = new ReverseTopicMappingService(_topicRepository); From afee4a2b8f64927ddccc26d0330b28c4e158156c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 21:22:54 -0800 Subject: [PATCH 359/778] Unobsoleted `ITopicRepository` events Previously, the `DeleteEvent`, `MoveEvent`, and `RenameEvent`, as well as their respective `EventArgs` classes, had all been marked as deprecated. That was premature. While, admittedly, they aren't currently in use, they continue to provide potential value, and don't introduce much overhead to maintain. We can reevaluate these in the future, if we wish, but for now they'll be offerred a reprieve. --- OnTopic/Repositories/DeleteEventArgs.cs | 1 - OnTopic/Repositories/ITopicRepository.cs | 3 --- OnTopic/Repositories/MoveEventArgs.cs | 1 - OnTopic/Repositories/RenameEventArgs.cs | 1 - OnTopic/Repositories/TopicRepositoryBase.cs | 9 +-------- 5 files changed, 1 insertion(+), 14 deletions(-) diff --git a/OnTopic/Repositories/DeleteEventArgs.cs b/OnTopic/Repositories/DeleteEventArgs.cs index 7527ba25..147679e1 100644 --- a/OnTopic/Repositories/DeleteEventArgs.cs +++ b/OnTopic/Repositories/DeleteEventArgs.cs @@ -13,7 +13,6 @@ namespace OnTopic.Repositories { /// /// The DeleteEventArgs class defines an event argument type specific to deletion events /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public class DeleteEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index ab499e80..11c5f18a 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -22,19 +22,16 @@ public interface ITopicRepository { /// /// Instantiates the event handler. /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] event EventHandler DeleteEvent; /// /// Instantiates the event handler. /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] event EventHandler MoveEvent; /// /// Instantiates the event handler. /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] event EventHandler RenameEvent; /*========================================================================================================================== diff --git a/OnTopic/Repositories/MoveEventArgs.cs b/OnTopic/Repositories/MoveEventArgs.cs index d78fb5fc..2fbe6c03 100644 --- a/OnTopic/Repositories/MoveEventArgs.cs +++ b/OnTopic/Repositories/MoveEventArgs.cs @@ -17,7 +17,6 @@ namespace OnTopic.Repositories { /// /// Allows tracking of the source and destination topics. /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public class MoveEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/Repositories/RenameEventArgs.cs b/OnTopic/Repositories/RenameEventArgs.cs index 94d03e19..2714f64c 100644 --- a/OnTopic/Repositories/RenameEventArgs.cs +++ b/OnTopic/Repositories/RenameEventArgs.cs @@ -13,7 +13,6 @@ namespace OnTopic.Repositories { /// /// The RenameEventArgs object defines an event argument type specific to rename events. /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public class RenameEventArgs : EventArgs { /*========================================================================================================================== diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 6055bee4..10bf3e3c 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -12,8 +12,6 @@ using OnTopic.Metadata; using OnTopic.Querying; -#pragma warning disable CS0618 // Type or member is obsolete; used to hide known deprecation of events until v5.0.0 - namespace OnTopic.Repositories { /*============================================================================================================================ @@ -33,15 +31,12 @@ public abstract class TopicRepositoryBase : ITopicRepository { | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public event EventHandler? DeleteEvent; /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public event EventHandler? MoveEvent; /// - [Obsolete("The TopicRepository events will be removed in OnTopic Library 5.0.", false)] public event EventHandler? RenameEvent; /*========================================================================================================================== @@ -755,6 +750,4 @@ private static void ResetAttributeDescriptors(Topic topic) { } } //Class -} //Namespace - -#pragma warning restore CS0618 // Type or member is obsolete \ No newline at end of file +} //Namespace \ No newline at end of file From 59add98c3e259e7dc5b8cef4af20e467e9c7abc2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 21:27:44 -0800 Subject: [PATCH 360/778] Updated event definitions to be `virtual` Updated the event declarations on `TopicRepositoryBase` to be `virtual`. As part of this, I updated these to use the full-bodied event declaration syntax, instead of the more common `field-like` syntax. There are, apparently, issues with virtual events when used with the field-like syntax, thus raising the CA1070 warning. --- OnTopic/Repositories/TopicRepositoryBase.cs | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 10bf3e3c..8f0c2693 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -25,19 +25,32 @@ public abstract class TopicRepositoryBase : ITopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); + private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); + private EventHandler? _deleteEvent; + private EventHandler? _moveEvent; + private EventHandler? _renameEvent; /*========================================================================================================================== | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ + /// - public event EventHandler? DeleteEvent; + public virtual event EventHandler? DeleteEvent { + add => _deleteEvent += value; + remove => _deleteEvent -= value; + } /// - public event EventHandler? MoveEvent; + public virtual event EventHandler? MoveEvent { + add => _moveEvent += value; + remove => _moveEvent -= value; + } /// - public event EventHandler? RenameEvent; + public virtual event EventHandler? RenameEvent { + add => _renameEvent += value; + remove => _renameEvent -= value; + } /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS @@ -301,7 +314,7 @@ public virtual void Save([ValidatedNotNull]Topic topic, bool isRecursive = false \-----------------------------------------------------------------------------------------------------------------------*/ if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { var args = new RenameEventArgs(topic); - RenameEvent?.Invoke(this, args); + _renameEvent?.Invoke(this, args); } /*---------------------------------------------------------------------------------------------------------------------- @@ -385,7 +398,7 @@ topic.Parent is not null && | Perform base logic \-----------------------------------------------------------------------------------------------------------------------*/ var previousParent = topic.Parent; - MoveEvent?.Invoke(this, new MoveEventArgs(topic, target)); + _moveEvent?.Invoke(this, new MoveEventArgs(topic, target)); if (sibling is null) { topic.SetParent(target); } @@ -447,7 +460,7 @@ public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ var args = new DeleteEventArgs(topic); - DeleteEvent?.Invoke(this, args); + _deleteEvent?.Invoke(this, args); /*------------------------------------------------------------------------------------------------------------------------ | Remove from parent From ed14bb6af80d74f89b3f60df19d5866c8e8f5cec Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 21:29:02 -0800 Subject: [PATCH 361/778] Established unit test for basic event format Previously, the events were not covered by any unit tests. This unit tests confirms that, at minimum, the `DeleteEvent` can be wired up to an event handler and is properly fired when the `TopicRepositoryBase.Delete()` method is called. --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index f7122d16..13fab89e 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -636,5 +636,28 @@ public void Delete_AttributeDescriptor_UpdatesContentTypeCache() { } + /*========================================================================================================================== + | TEST: DELETE: DELETE EVENT: IS FIRED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates a and then immediately deletes it. Ensures that the is fired. + /// + [TestMethod] + public void Delete_DeleteEvent_IsFired() { + + var topic = TopicFactory.Create("Test", "Page"); + var hasFired = false; + + _topicRepository.Save(topic); + _topicRepository.DeleteEvent += eventHandler; + _topicRepository.Delete(topic); + + Assert.IsTrue(hasFired); + + void eventHandler(object? sender, DeleteEventArgs eventArgs) => hasFired = true; + + } + } //Class } //Namespace \ No newline at end of file From 99e54e7903b11ce7e6732a7dc9963e7604cf069e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 21:32:33 -0800 Subject: [PATCH 362/778] Added event passthroughs for the `CachedTopicRepository` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if a caller added an event handler to the `CachedTopicRepository`, it would never get called. That's because the actual `Delete()`, `Move()`, and `Save()` methods were relayed to the underlying `ITopicRepository` instance—likely a `SqlTopicRepository`. As a result, the `DeleteEvent`, `MoveEvent`, and `RenameEvent` would never be fired on the `CachedTopicRepository` itself. To mitigate that, the `CachedTopicRepository` now includes a passthrough for the event definitions to subscribe to and relay the events off of the underlying `_dataProvider` which the decorated `ITopicRepository` instance is assigned to. That way, when the underlying `ITopicRepository` fires that event, it is properly "bubbled up" to the `CachedTopicRepository`, and any of its subscribers will be correctly notified. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index eb5714d0..4d8f1b7d 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -69,6 +69,28 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } + /*========================================================================================================================== + | EVENT PASSTHROUGHS + \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public override event EventHandler? DeleteEvent { + add => _dataProvider.DeleteEvent += value; + remove => _dataProvider.DeleteEvent -= value; + } + + /// + public override event EventHandler? MoveEvent { + add => _dataProvider.MoveEvent += value; + remove => _dataProvider.MoveEvent -= value; + } + + /// + public override event EventHandler? RenameEvent { + add => _dataProvider.RenameEvent += value; + remove => _dataProvider.RenameEvent -= value; + } + /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS \-------------------------------------------------------------------------------------------------------------------------*/ From 4c11dd06e412146657229e76215317949ac028f3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 21:34:15 -0800 Subject: [PATCH 363/778] Established unit test for event bubbling This unit test is identical to the main event unit test on `TopicRepositoryBase` (ed14bb6), except that it operates against a `CachedTopicRepository` decorator, as implemented on the `ITopicRepositoryTest` class. That confirms that the event bubbling enabled via the event passthroughs (99e54e7) is working properly. --- OnTopic.Tests/ITopicRepositoryTest.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index be558f38..a7ab13aa 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -224,5 +224,30 @@ public void Delete_Topic_Removed() { } + /*========================================================================================================================== + | TEST: DELETE: DELETE EVENT: IS FIRED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Creates a and then immediately deletes it. Ensures that the is fired, even though the original event is fired from the underlying + /// and not the immediate . + /// + [TestMethod] + public void Delete_DeleteEvent_IsFired() { + + var topic = TopicFactory.Create("Test", "Page"); + var hasFired = false; + + _topicRepository.Save(topic); + _topicRepository.DeleteEvent += eventHandler; + _topicRepository.Delete(topic); + + Assert.IsTrue(hasFired); + + void eventHandler(object? sender, DeleteEventArgs eventArgs) => hasFired = true; + + } + + } //Class } //Namespace \ No newline at end of file From 63b71dea61ae80ff53377c85e1c8d57a23bcce24 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 20 Jan 2021 22:01:25 -0800 Subject: [PATCH 364/778] Generalize `SqlDataReader` extensions to operate off `IDataReader` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `SqlDataReader` can only be created from a `SqlConnection` object, and cannot be separately constructed. That makes it difficult to write unit tests against. By generalizing the `SqlDataReader` extensions to actually target the underlying `IDataReader`, it still works successfully against the target `SqlDataReader`, but _also_ works against e.g. the `DataTableReader` class, which can easily be constructed for the purpose of unit tests. There is one method—the `SetExtendedAttributes()` method—which cannot be successfully migrated to use `IDataReader`. That's because it relies on `GetSqlXml()`, which is specific to `SqlDataReader`. Given that, I've added a wrapper around that call so that it's only called if the `IDataReader` can be cast as a `SqlDataReader`. In practice, we expact that to be in all cases _except_ for the unit tests. Because the class is still _intended_ to target the `SqlDataReader`, and because it still contains at least one method that is specific to `SqlDataReader`, I'm keeping the name as `SqlDataReaderExtensions`, instead of renaming it to e.g. `DataReaderExtensions`. We may reevaluate that in the future—though it doesn't matter _too_ much since it's an internal class. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 58 +++++++++++---------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index db7df6a1..9be90194 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Data; using System.Diagnostics; using System.Linq; using System.Net; @@ -19,7 +20,7 @@ namespace OnTopic.Data.Sql { | CLASS: SQL DATA READER EXTENSIONS \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Extension methods for the class. + /// Extension methods for the class. /// /// /// Most of the extensions are optimized for reading the data returned from the GetTopics and GetTopicVersion @@ -36,10 +37,10 @@ internal static class SqlDataReaderExtensions { | METHOD: LOAD TOPIC GRAPH \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a from a call to the GetTopics stored procedure, will extract a list of + /// Given a from a call to the GetTopics stored procedure, will extract a list of /// topics and populate their attributes, relationships, and children. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references /// and relationships, including , are integrated with existing entities. @@ -56,7 +57,7 @@ internal static class SqlDataReaderExtensions { /// thus external references aren't likely to be available. /// internal static Topic LoadTopicGraph( - this SqlDataReader reader, + this IDataReader reader, Topic? referenceTopic = null, bool? markDirty = null, bool includeExternalReferences = true @@ -65,6 +66,7 @@ internal static Topic LoadTopicGraph( /*---------------------------------------------------------------------------------------------------------------------- | Establish topic index \---------------------------------------------------------------------------------------------------------------------*/ + var sqlDataReader = reader as SqlDataReader; var topics = referenceTopic is not null? referenceTopic.GetRootTopic().GetTopicIndex() : new(); var rootTopicId = -1; @@ -101,7 +103,9 @@ internal static Topic LoadTopicGraph( // Loop through each extended attribute record associated with a specific topic while (reader.Read()) { - reader.SetExtendedAttributes(topics, markDirty); + if (sqlDataReader is not null) { + sqlDataReader.SetExtendedAttributes(topics, markDirty); + } } /*---------------------------------------------------------------------------------------------------------------------- @@ -162,7 +166,7 @@ internal static Topic LoadTopicGraph( /// Given the primary topic attributes from the TopicIndex view, establishes a barebones /// instance and adds it to the collection. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. /// /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it @@ -170,7 +174,7 @@ internal static Topic LoadTopicGraph( /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update /// from being persisted to the data store on . /// - private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { + private static void AddTopic(this IDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -217,7 +221,7 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool? /// Given an attribute record from the AttributeIndex view, finds the associated in the /// collection, and sets the corresponding value. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. /// /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it @@ -225,7 +229,7 @@ private static void AddTopic(this SqlDataReader reader, TopicIndex topics, bool? /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update /// from being persisted to the data store on . /// - private static void SetIndexedAttributes(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { + private static void SetIndexedAttributes(this IDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -335,7 +339,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex /// Topics can be cross-referenced with each other via a many-to-many relationships. Once the topics are populated in /// memory, loop through the data to create these associations. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. /// /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it @@ -343,7 +347,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update /// from being persisted to the data store on . /// - private static void SetRelationships(this SqlDataReader reader, TopicIndex topics, bool? isDirty = false) { + private static void SetRelationships(this IDataReader reader, TopicIndex topics, bool? isDirty = false) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -392,7 +396,7 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic /// Topics can be cross-referenced with each other topics via a one-to-one relationships. Once the topics are populated in /// memory, loop through the data to create these associations. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. /// /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it @@ -400,7 +404,7 @@ private static void SetRelationships(this SqlDataReader reader, TopicIndex topic /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update /// from being persisted to the data store on . /// - private static void SetReferences(this SqlDataReader reader, TopicIndex topics, bool? markDirty) { + private static void SetReferences(this IDataReader reader, TopicIndex topics, bool? markDirty) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -444,9 +448,9 @@ private static void SetReferences(this SqlDataReader reader, TopicIndex topics, /// version history is aggregated per topic to allow topic information to be rolled back to a specific date.While version /// content is not exposed directly via the Load() method, the metadata is. /// - /// The with output from the GetTopics stored procedure. + /// The with output from the GetTopics stored procedure. /// A of topics to be loaded. - private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topics) { + private static void SetVersionHistory(this IDataReader reader, TopicIndex topics) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -474,9 +478,9 @@ private static void SetVersionHistory(this SqlDataReader reader, TopicIndex topi /// /// Retrieves a string value by column name. /// - /// The object. + /// The object. /// The name of the column to retrieve the value from. - private static string GetString(this SqlDataReader reader, string columnName) => + private static string GetString(this IDataReader reader, string columnName) => reader.GetString(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -485,9 +489,9 @@ private static string GetString(this SqlDataReader reader, string columnName) => /// /// Retrieves a boolean value by column name. /// - /// The object. + /// The object. /// The name of the column to retrieve the value from. - private static bool GetBoolean(this SqlDataReader reader, string columnName) => + private static bool GetBoolean(this IDataReader reader, string columnName) => reader.GetBoolean(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -496,9 +500,9 @@ private static bool GetBoolean(this SqlDataReader reader, string columnName) => /// /// Retrieves an integer value by column name. /// - /// The object. + /// The object. /// The name of the column to retrieve the value from. - private static int GetInteger(this SqlDataReader reader, string columnName) => + private static int GetInteger(this IDataReader reader, string columnName) => Int32.TryParse(reader.GetValue(reader.GetOrdinal(columnName)).ToString(), out var output)? output : -1; /*========================================================================================================================== @@ -507,9 +511,9 @@ private static int GetInteger(this SqlDataReader reader, string columnName) => /// /// Retrieves a value by column name. /// - /// The object. + /// The object. /// The name of the column to retrieve the value from. - private static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") => + private static int GetTopicId(this IDataReader reader, string columnName = "TopicID") => reader.GetInt32(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -518,9 +522,9 @@ private static int GetTopicId(this SqlDataReader reader, string columnName = "To /// /// Retrieves a value by column name, while accepting null values. /// - /// The object. + /// The object. /// The name of the column to retrieve the value from. - private static int? GetNullableTopicId(this SqlDataReader reader, string columnName = "TopicID") => + private static int? GetNullableTopicId(this IDataReader reader, string columnName = "TopicID") => reader.IsDBNull(reader.GetOrdinal(columnName))? null : reader.GetInt32(reader.GetOrdinal(columnName)); /*========================================================================================================================== @@ -529,8 +533,8 @@ private static int GetTopicId(this SqlDataReader reader, string columnName = "To /// /// Retrieves the version column, with precisions appropriate for setting the . /// - /// The object. - private static DateTime GetVersion(this SqlDataReader reader) => + /// The object. + private static DateTime GetVersion(this IDataReader reader) => reader.GetDateTime(reader.GetOrdinal("Version")); } //Class From c209f39f8b0c8e42e559fd5d6133681a331440c8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 00:52:47 -0800 Subject: [PATCH 365/778] Setup `SqlTopicRepository` for unit testing This includes marking the `SqlTopicRepository` internals as visible to `OnTopic.Tests`, referencing the `SqlTopicRepository` project from `OnTopic.Tests`, and establishing a basic placeholder for a `SqlTopicRepositoryTest` class. --- OnTopic.Data.Sql/Properties/AssemblyInfo.cs | 2 ++ OnTopic.Tests/OnTopic.Tests.csproj | 1 + OnTopic.Tests/SqlTopicRepositoryTest.cs | 27 +++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 OnTopic.Tests/SqlTopicRepositoryTest.cs diff --git a/OnTopic.Data.Sql/Properties/AssemblyInfo.cs b/OnTopic.Data.Sql/Properties/AssemblyInfo.cs index b8a1bad9..638c33b8 100644 --- a/OnTopic.Data.Sql/Properties/AssemblyInfo.cs +++ b/OnTopic.Data.Sql/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; /*============================================================================================================================== @@ -13,4 +14,5 @@ \-----------------------------------------------------------------------------------------------------------------------------*/ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] +[assembly: InternalsVisibleTo("OnTopic.Tests")] [assembly: Guid("1de1f923-c7c2-435b-b49a-975acbcb5ff0")] diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 7e2ef426..6ff2e2a1 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -40,6 +40,7 @@ + diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs new file mode 100644 index 00000000..bea088d7 --- /dev/null +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -0,0 +1,27 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using System.Linq; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.Data.Sql; +using OnTopic.References; +using OnTopic.Tests.Schemas; + +namespace OnTopic.Tests { + + /*============================================================================================================================ + | CLASS: SQL TOPIC REPOSITORY TEST + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides unit tests for the class. + /// + [TestClass] + public class SqlTopicRepositoryTest { + + } //Class +} //Namespace \ No newline at end of file From c951fab82b1d93c6dd1737f7fe3d490d9448ef5d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:03:11 -0800 Subject: [PATCH 366/778] Established `TopicsDataTable` for unit testing While we can't easily test `SqlDataReader`, we can easily test a `DataTableReader`, which operates off of the same `IDataReader` interface as `SqlDataReader`. Given this, the `TopicsDataTable` provides a simple way of mimicking SQL response data for the `IDataReader` extension methods used by `SqlTopicRepository` to operate off of, thus allowing us to unit test these methods without using a SQL database. The `TopicsDataTable` class includes not only the basic schema definition for the `Topics` table, but also an `AddRow()` method with strongly typed parameters to simplify creating a new record. --- OnTopic.Tests/Schemas/TopicsDataTable.cs | 102 +++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 OnTopic.Tests/Schemas/TopicsDataTable.cs diff --git a/OnTopic.Tests/Schemas/TopicsDataTable.cs b/OnTopic.Tests/Schemas/TopicsDataTable.cs new file mode 100644 index 00000000..199f37bc --- /dev/null +++ b/OnTopic.Tests/Schemas/TopicsDataTable.cs @@ -0,0 +1,102 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Schemas { + + /*============================================================================================================================ + | CLASS: TOPICS DATA TABLE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a which maps to the expected schema of the Topics table. + /// + /// + /// This allows testing of the via its methods. + /// + public class TopicsDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the . + /// + /// A new instance of the . + public TopicsDataTable() : base("Topics") { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "TopicId", + Unique = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add TopicKey column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "TopicKey" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add ContentType column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "ContentType" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add ParentId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "ParentId", + AllowDBNull = true + }); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to the . + /// + public void AddRow(int topicId, string topicKey, string contentType, int? parentId = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Verify parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topicId, nameof(topicId)); + Contract.Requires(topicKey, nameof(topicKey)); + Contract.Requires(contentType, nameof(contentType)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new row + \-----------------------------------------------------------------------------------------------------------------------*/ + var row = NewRow(); + + row["TopicId"] = topicId; + row["TopicKey"] = topicKey; + row["ContentType"] = contentType; + row["ParentId"] = parentId.HasValue? (object)parentId : DBNull.Value; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add row to table + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(row); + + } + + } //Class +} //Namespace \ No newline at end of file From 824bf335dbf675766ca3cfa546040a8b7c2ec9ba Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:03:53 -0800 Subject: [PATCH 367/778] Established `AttributesDataTable` for unit testing While we can't easily test `SqlDataReader`, we can easily test a `DataTableReader`, which operates off of the same `IDataReader` interface as `SqlDataReader`. Given this, the `AttributesDataTable` provides a simple way of mimicking SQL response data for the `IDataReader` extension methods used by `SqlTopicRepository` to operate off of, thus allowing us to unit test these methods without using a SQL database. The `AttributesDataTable` class includes not only the basic schema definition for the `Attributes` table, but also an `AddRow()` method with strongly typed parameters to simplify creating a new record. --- OnTopic.Tests/Schemas/AttributesDataTable.cs | 101 +++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 OnTopic.Tests/Schemas/AttributesDataTable.cs diff --git a/OnTopic.Tests/Schemas/AttributesDataTable.cs b/OnTopic.Tests/Schemas/AttributesDataTable.cs new file mode 100644 index 00000000..2571698c --- /dev/null +++ b/OnTopic.Tests/Schemas/AttributesDataTable.cs @@ -0,0 +1,101 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Schemas { + + /*============================================================================================================================ + | CLASS: ATTRIBUTES DATA TABLE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a which maps to the expected schema of the Attributes table. + /// + /// + /// This allows testing of the via its methods. + /// + public class AttributesDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the . + /// + /// A new instance of the . + public AttributesDataTable() : base("Attributes") { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "TopicId", + Unique = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add AttributeKey column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "AttributeKey" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add AttributeValue column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "AttributeValue", + AllowDBNull = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Version column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(DateTime), + ColumnName = "Version" + }); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to the . + /// + public void AddRow(int topicId, string attributeKey, string? attributeValue, DateTime? version = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Verify parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topicId, nameof(topicId)); + Contract.Requires(attributeKey, nameof(attributeKey)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new row + \-----------------------------------------------------------------------------------------------------------------------*/ + var row = NewRow(); + + row["TopicId"] = topicId; + row["AttributeKey"] = attributeKey; + row["AttributeValue"] = attributeValue is null? DBNull.Value : attributeValue; + row["Version"] = version?? DateTime.UtcNow; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add row to table + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(row); + + } + + } //Class +} //Namespace \ No newline at end of file From 792f0ba1070a1946c3a630d28adc58bf34ae54e9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:06:55 -0800 Subject: [PATCH 368/778] Established `ExtendedAttributesDataTable` for unit testing While we can't easily test `SqlDataReader`, we can easily test a `DataTableReader`, which operates off of the same `IDataReader` interface as `SqlDataReader`. Given this, the `ExtendedAttributesDataTable` provides a simple way of mimicking SQL response data for the `IDataReader` extension methods used by `SqlTopicRepository` to operate off of, thus allowing us to unit test these methods without using a SQL database. The `ExtendedAttributesDataTable` class includes not only the basic schema definition for the `ExtendedAttributes` table, but also an `AddRow()` method with strongly typed parameters to simplify creating a new record. Note: Because the `SetExtendedAttributes()` extension method relies upon the `GetSqlXml()` method, which is not available on the `IDataReader` interface, we may not be able to actually unit test the extended attributes using the same technique. That said, we're creating this data table schema in case we can identify a way of mitigating that. --- .../Schemas/ExtendedAttributesDataTable.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs diff --git a/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs b/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs new file mode 100644 index 00000000..9405bad3 --- /dev/null +++ b/OnTopic.Tests/Schemas/ExtendedAttributesDataTable.cs @@ -0,0 +1,92 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using System.Xml; +using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Schemas { + + /*============================================================================================================================ + | CLASS: EXTENDED ATTRIBUTES DATA TABLE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a which maps to the expected schema of the ExtendedAttributes table. + /// + /// + /// This allows testing of the via its methods. + /// + public class ExtendedAttributesDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the . + /// + /// A new instance of the . + public ExtendedAttributesDataTable() : base("ExtendedAttributes") { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "TopicId", + Unique = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add AttributesXml column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(XmlDocument), + ColumnName = "AttributesXml" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Version column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(DateTime), + ColumnName = "Version" + }); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to the . + /// + public void AddRow(int topicId, XmlDocument xml, DateTime? version = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Verify parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topicId, nameof(topicId)); + Contract.Requires(xml, nameof(xml)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new row + \-----------------------------------------------------------------------------------------------------------------------*/ + var row = NewRow(); + + row["TopicId"] = topicId; + row["AttributesXml"] = xml; + row["Version"] = version?? DateTime.UtcNow; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add row to table + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(row); + + } + + } //Class +} //Namespace \ No newline at end of file From d67e8ed4682f6e0802f1fe5f3b01c376fe037dc1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:07:36 -0800 Subject: [PATCH 369/778] Established `RelationshipsDataTable` for unit testing While we can't easily test `SqlDataReader`, we can easily test a `DataTableReader`, which operates off of the same `IDataReader` interface as `SqlDataReader`. Given this, the `RelationshipsDataTable` provides a simple way of mimicking SQL response data for the `IDataReader` extension methods used by `SqlTopicRepository` to operate off of, thus allowing us to unit test these methods without using a SQL database. The `RelationshipsDataTable` class includes not only the basic schema definition for the `Relationships` table, but also an `AddRow()` method with strongly typed parameters to simplify creating a new record. --- .../Schemas/RelationshipsDataTable.cs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 OnTopic.Tests/Schemas/RelationshipsDataTable.cs diff --git a/OnTopic.Tests/Schemas/RelationshipsDataTable.cs b/OnTopic.Tests/Schemas/RelationshipsDataTable.cs new file mode 100644 index 00000000..1bea4f7f --- /dev/null +++ b/OnTopic.Tests/Schemas/RelationshipsDataTable.cs @@ -0,0 +1,110 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Schemas { + + /*============================================================================================================================ + | CLASS: RELATIONSHIPS DATA TABLE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a which maps to the expected schema of the Relationships table. + /// + /// + /// This allows testing of the via its methods. + /// + public class RelationshipsDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the . + /// + /// A new instance of the . + public RelationshipsDataTable() : base("Relationships") { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Source_TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "Source_TopicId", + Unique = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add RelationshipKey column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "RelationshipKey" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Target_TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "Target_TopicId" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add IsDeleted column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(bool), + ColumnName = "IsDeleted" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add ParentId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(DateTime), + ColumnName = "Version" + }); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to the . + /// + public void AddRow(int sourceTopicId, string relationshipKey, int targetTopicId, bool isDeleted, DateTime? version = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Verify parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(sourceTopicId, nameof(sourceTopicId)); + Contract.Requires(relationshipKey, nameof(relationshipKey)); + Contract.Requires(targetTopicId, nameof(targetTopicId)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new row + \-----------------------------------------------------------------------------------------------------------------------*/ + var row = NewRow(); + + row["Source_TopicId"] = sourceTopicId; + row["RelationshipKey"] = relationshipKey; + row["Target_TopicId"] = targetTopicId; + row["IsDeleted"] = isDeleted; + row["Version"] = version?? DateTime.UtcNow; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add row to table + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(row); + + } + + } //Class +} //Namespace \ No newline at end of file From e5f02c5f4646a267c4ad71921156f4cbe3b43e64 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:08:55 -0800 Subject: [PATCH 370/778] Established `TopicReferencesDataTable` for unit testing While we can't easily test `SqlDataReader`, we can easily test a `DataTableReader`, which operates off of the same `IDataReader` interface as `SqlDataReader`. Given this, the `TopicReferencesDataTable` provides a simple way of mimicking SQL response data for the `IDataReader` extension methods used by `SqlTopicRepository` to operate off of, thus allowing us to unit test these methods without using a SQL database. The `TopicReferencesDataTable` class includes not only the basic schema definition for the `TopicReferences` table, but also an `AddRow()` method with strongly typed parameters to simplify creating a new record. --- .../Schemas/TopicReferencesDataTable.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 OnTopic.Tests/Schemas/TopicReferencesDataTable.cs diff --git a/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs b/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs new file mode 100644 index 00000000..3e843abf --- /dev/null +++ b/OnTopic.Tests/Schemas/TopicReferencesDataTable.cs @@ -0,0 +1,102 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Data; +using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Tests.Schemas { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCES DATA TABLE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a which maps to the expected schema of the TopicReferences table. + /// + /// + /// This allows testing of the via its methods. + /// + public class TopicReferencesDataTable: DataTable { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the . + /// + /// A new instance of the . + public TopicReferencesDataTable() : base("TopicReferences") { + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Source_TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "Source_TopicId", + Unique = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add RelationshipKey column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(string), + ColumnName = "ReferenceKey" + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add Target_TopicId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(int), + ColumnName = "Target_TopicId", + AllowDBNull = true + }); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add ParentId column + \-----------------------------------------------------------------------------------------------------------------------*/ + Columns.Add(new DataColumn() { + DataType = typeof(DateTime), + ColumnName = "Version" + }); + + } + + /*========================================================================================================================== + | ADD ROW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to the . + /// + public void AddRow(int sourceTopicId, string referenceKey, int? targetTopicId, DateTime? version = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Verify parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(sourceTopicId, nameof(sourceTopicId)); + Contract.Requires(referenceKey, nameof(referenceKey)); + Contract.Requires(targetTopicId, nameof(targetTopicId)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new row + \-----------------------------------------------------------------------------------------------------------------------*/ + var row = NewRow(); + + row["Source_TopicId"] = sourceTopicId; + row["ReferenceKey"] = referenceKey; + row["Target_TopicId"] = targetTopicId.HasValue ? (object)targetTopicId : DBNull.Value; + row["Version"] = version?? DateTime.UtcNow; + + /*------------------------------------------------------------------------------------------------------------------------ + | Add row to table + \-----------------------------------------------------------------------------------------------------------------------*/ + Rows.Add(row); + + } + + } //Class +} //Namespace \ No newline at end of file From 6dff467412c306d0d2e4d0858ddac79e29a3498f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:10:40 -0800 Subject: [PATCH 371/778] Introduced unit test for validating the `AddTopic()` extension method This unit test creates a `TopicsDataTable` and passed it to the `LoadTopicGraph()` extension method, ensuring that the (private) `AddTopic()` extension method correctly utilizes it to create a new `Topic` entity. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index bea088d7..d79049aa 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -23,5 +23,28 @@ namespace OnTopic.Tests { [TestClass] public class SqlTopicRepositoryTest { + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH TOPIC: RETURNS TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a record and confirms that a topic with those values is returned. + /// + [TestMethod] + public void LoadTopicGraph_WithTopic_ReturnsTopic() { + + using var topics = new TopicsDataTable(); + + topics.AddRow(1, "Root", "Container", null); + + using var tableReader = new DataTableReader(topics); + + var topic = tableReader.LoadTopicGraph(); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + + } + } //Class } //Namespace \ No newline at end of file From c9d3ef5070e00c82ccffda953c26bb14c8c68ff3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:12:24 -0800 Subject: [PATCH 372/778] Introduced unit test for validating the `SetIndexedAttributes()` extension This unit test creates a `TopicsDataTable` and an `AttributesDataTable` and passes them to the `LoadTopicGraph()` extension method, ensuring that the (private) `SetIndexedAttributes()` extension method correctly utilizes the data tables to set the specified attribute on the new `Topic` entity. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index d79049aa..de4e3715 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -46,5 +46,31 @@ public void LoadTopicGraph_WithTopic_ReturnsTopic() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH ATTRIBUTES: RETURNS ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with an record and confirms that a topic with those values is returned. + /// + [TestMethod] + public void LoadTopicGraph_WithAttributes_ReturnsAttributes() { + + using var topics = new TopicsDataTable(); + using var attributes = new AttributesDataTable(); + + topics.AddRow(1, "Root", "Container", null); + attributes.AddRow(1, "Test", "Value"); + + using var tableReader = new DataTableReader(new DataTable[] { topics, attributes }); + + var topic = tableReader.LoadTopicGraph(); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + Assert.AreEqual("Value", topic.Attributes.GetValue("Test")); + + } + } //Class } //Namespace \ No newline at end of file From e512f34caa4c025f23987de213a7149d8f4ba1ec Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:13:26 -0800 Subject: [PATCH 373/778] Introduced unit test for validating the `SetRelationships()` extension This unit test creates a `TopicsDataTable` and a `RelationshipsDataTable` and passes them to the `LoadTopicGraph()` extension method, ensuring that the (private) `SetRelationships()` extension method correctly utilizes the data tables to set the specified relationship on the new `Topic` entity. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index de4e3715..6235631a 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -72,5 +72,33 @@ public void LoadTopicGraph_WithAttributes_ReturnsAttributes() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH RELATIONSHIP: RETURNS RELATIONSHIP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a record and confirms that a topic with those values is returned. + /// + [TestMethod] + public void LoadTopicGraph_WithRelationship_ReturnsRelationship() { + + using var topics = new TopicsDataTable(); + using var empty = new AttributesDataTable(); + using var relationships = new RelationshipsDataTable(); + + topics.AddRow(1, "Root", "Container", null); + topics.AddRow(2, "Web", "Container", 1); + relationships.AddRow(1, "Test", 2, false); + + using var tableReader = new DataTableReader(new DataTable[] { topics, empty, empty, relationships }); + + var topic = tableReader.LoadTopicGraph(); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + Assert.AreEqual(2, topic.Relationships.GetTopics("Test").FirstOrDefault()?.Id); + + } + } //Class } //Namespace \ No newline at end of file From 6bd7f955695e663dd2feeb5725d0e6740cdd1f5e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:15:20 -0800 Subject: [PATCH 374/778] Introduced unit test for validating the `SetReferences()` extension This unit test creates a `TopicsDataTable` and a `TopicReferencesDataTable` and passes them to the `LoadTopicGraph()` extension method, ensuring that the (private) `SetReferences()` extension method correctly utilizes the data tables to set the specified topic reference on the new `Topic` entity. While it's at it, it also confirms that the collection is correctly marked as `IsDirty()`, since it has changed. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 6235631a..4e309060 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -100,5 +100,34 @@ public void LoadTopicGraph_WithRelationship_ReturnsRelationship() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH REFERENCE: RETURNS REFERENCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a record and confirms that a topic with those values is returned. + /// + [TestMethod] + public void LoadTopicGraph_WithReference_ReturnsReference() { + + using var topics = new TopicsDataTable(); + using var empty = new AttributesDataTable(); + using var references = new TopicReferencesDataTable(); + + topics.AddRow(1, "Root", "Container", null); + topics.AddRow(2, "Web", "Container", 1); + references.AddRow(1, "Test", 2); + + using var tableReader = new DataTableReader(new DataTable[] { topics, empty, empty, empty, references }); + + var topic = tableReader.LoadTopicGraph(); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + Assert.AreEqual(2, topic.References.GetTopic("Test")?.Id); + Assert.IsTrue(topic.References.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file From 56d98cc6e62c895630edcab00ea3757d4837fbf6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:17:24 -0800 Subject: [PATCH 375/778] Introduced unit test for validating `referenceTopic` behavior This unit test creates a `TopicsDataTable` and a `TopicReferencesDataTable` and passes them to the `LoadTopicGraph()` extension method, ensuring that the (private) `SetReferences()` extension method correctly utilizes the data tables along with a `referenceTopic` to set the reference to the existing `Topic` on the new `Topic` entity. While it's at it, it also confirms that the collection is correctly marked as `IsDirty()`, since it has changed, as well as `IsFullyLoaded`, since all references should be resolved. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 4e309060..3aae2674 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -129,5 +129,36 @@ public void LoadTopicGraph_WithReference_ReturnsReference() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH EXTERNAL REFERENCE: RETURNS REFERENCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a record and confirms that a topic with those values is returned. + /// + [TestMethod] + public void LoadTopicGraph_WithExternalReference_ReturnsReference() { + + using var topics = new TopicsDataTable(); + using var empty = new AttributesDataTable(); + using var references = new TopicReferencesDataTable(); + + var referenceTopic = TopicFactory.Create("Web", "Container", 2); + + topics.AddRow(1, "Root", "Container", null); + references.AddRow(1, "Test", 2); + + using var tableReader = new DataTableReader(new DataTable[] { topics, empty, empty, empty, references }); + + var topic = tableReader.LoadTopicGraph(referenceTopic, false); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + Assert.AreEqual(2, topic.References.GetTopic("Test")?.Id); + Assert.IsTrue(topic.References.IsFullyLoaded); + Assert.IsFalse(topic.References.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file From fc5bb9a74a81a06a171e42e347f7312b044d9715 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:20:41 -0800 Subject: [PATCH 376/778] Introduced unit test for validating `IsFullyLoaded` behavior This unit test creates a `TopicsDataTable` as well as a `TopicReferencesDataTable` with an invalid topic reference, passing them to the `LoadTopicGraph()` extension method, and ensuring that the (private) `SetReferences()` extension method correctly sets the `TopicReferenceDictionary` as `!IsFullyLoaded` since one of the relationships could not be correctly set. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 3aae2674..8a84e13e 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -160,5 +160,34 @@ public void LoadTopicGraph_WithExternalReference_ReturnsReference() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH MISSING REFERENCE: NOT FULLY LOADED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a record that is missing and confirms that returns false. + /// + [TestMethod] + public void LoadTopicGraph_WithMissingReference_NotFullyLoaded() { + + using var topics = new TopicsDataTable(); + using var empty = new AttributesDataTable(); + using var references = new TopicReferencesDataTable(); + + topics.AddRow(1, "Root", "Container", null); + references.AddRow(1, "Test", 2); + + using var tableReader = new DataTableReader(new DataTable[] { topics, empty, empty, empty, references }); + + var topic = tableReader.LoadTopicGraph(); + + Assert.IsNotNull(topic); + Assert.AreEqual(1, topic.Id); + Assert.AreEqual(0, topic.References.Count); + Assert.IsFalse(topic.References.IsFullyLoaded); + + } + } //Class } //Namespace \ No newline at end of file From 9bc4c57fb8c946ae86f73780a7a9b768ac9bd85c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 01:25:25 -0800 Subject: [PATCH 377/778] Introduced unit test for validating the `IsDeleted` handling This unit test creates a `TopicsDataTable` and a `RelationshipsDataTable` and passes them to the `LoadTopicGraph()` extension method, ensuring that the (private) `SetRelationships()` extension method correctly utilizes the data tables to _delete_ the specified relationship on the new `Topic` entity, since it is marked as `IsDeleted`. --- OnTopic.Tests/SqlTopicRepositoryTest.cs | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 8a84e13e..b8679dd4 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -189,5 +189,34 @@ public void LoadTopicGraph_WithMissingReference_NotFullyLoaded() { } + /*========================================================================================================================== + | TEST: LOAD TOPIC GRAPH: WITH DELETED RELATIONSHIP: REMOVES RELATIONSHIP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Calls with a deleted record and confirms that it is deleted from the referenceTopic graph. + /// + [TestMethod] + public void LoadTopicGraph_WithDeletedRelationship_RemovesRelationship() { + + var topic = TopicFactory.Create("Test", "Container", 1); + var child = TopicFactory.Create("Child", "Container", 2, topic); + var related = TopicFactory.Create("Related", "Container", 3, topic); + + child.Relationships.SetTopic("Test", related); + + using var empty = new AttributesDataTable(); + using var relationships = new RelationshipsDataTable(); + + relationships.AddRow(2, "Test", 3, true); + + using var tableReader = new DataTableReader(new DataTable[] { empty, empty, empty, relationships }); + + tableReader.LoadTopicGraph(related); + + Assert.AreEqual(0, topic.Relationships.GetTopics("Test").Count); + + } + } //Class } //Namespace \ No newline at end of file From 713051cde93a0b5a785bca8e4d93663be244f915 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:08:36 -0800 Subject: [PATCH 378/778] Upgraded to .NET 5.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgraded projects from either .NET Core 3.1 or .NET Standard 2.1 to .NET 5.0. (There is no differentiation between .NET, ASP.NET Core, or .NET Standard in .NET 5.0, so these all end up using the same `` moniker.) This is a breaking change in that .NET Core 3.x projects will no longer be able to reference OnTopic after this. That said, most .NET Core projects should be able to painlessly upgrade to .NET 5.0, and we expect any new projects to be targeting .NET 5.0 from the start. In return, this allows us to take advantage of some new capabilities—most notably return type covariance. --- OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj | 2 +- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 2 +- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index 64c69ddc..3c442e05 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 62eb85bf-f802-4afd-8bec-3d344e1cfc79 false diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index 68fc1b07..bbeb86bf 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false 9.0 diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 1a22f769..54d6acb8 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -3,7 +3,7 @@ {B7F136A1-C86D-4A74-AC4F-3693CD1358A4} OnTopic.AspNetCore.Mvc - netcoreapp3.1 + net5.0 True False 9.0 diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 4c8d9740..66f16ff0 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -3,7 +3,7 @@ {206B7F91-CA25-4E9D-9576-60D2E54A2C0A} OnTopic.Data.Caching - netstandard2.1 + net5.0 True False 9.0 diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 8b1d7fe9..b98075de 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -3,8 +3,8 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} OnTopic.Data.Sql - netstandard2.1 9.0 + net5.0 enable latest AllEnabledByDefault diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index e2975435..91efc809 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -1,8 +1,8 @@ - netstandard2.1 9.0 + net5.0 enable latest AllEnabledByDefault diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 6ff2e2a1..7c8098dd 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 9.0 diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index a6c4a912..25b6f928 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -3,7 +3,7 @@ {E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7} OnTopic.ViewModels - netstandard2.1 + net5.0 True False 9.0 diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 550355c7..f32aed5c 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -3,7 +3,7 @@ {B8D5B290-4451-4C3B-AE9E-0FF075958A74} OnTopic - netstandard2.1 + net5.0 True False 9.0 From a8716db57bc0893b6568d8cc47e8c79fd24cf0fa Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:09:00 -0800 Subject: [PATCH 379/778] Fallback to implicit default of C# 9.0 in .NET 5.0 In .NET Core 3.x, the default language version was C# 8.0, and so in order to take advantage of new C# 9.0 features, we had to explicitly set the `` in the `csproj` files. Now that we've upgraded to .NET 5.0 (085bf16), the default language is C# 9.0, and so this isn't necessary. --- OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj | 1 - OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 1 - OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 1 - OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 1 - OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 1 - OnTopic.Tests/OnTopic.Tests.csproj | 1 - OnTopic.ViewModels/OnTopic.ViewModels.csproj | 1 - OnTopic/OnTopic.csproj | 1 - 8 files changed, 8 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index bbeb86bf..c4494b35 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -3,7 +3,6 @@ net5.0 false - 9.0 diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 54d6acb8..68c90bb9 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -6,7 +6,6 @@ net5.0 True False - 9.0 enable latest AllEnabledByDefault diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 66f16ff0..a4dce954 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -6,7 +6,6 @@ net5.0 True False - 9.0 enable latest AllEnabledByDefault diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index b98075de..0e7ac5cd 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -3,7 +3,6 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} OnTopic.Data.Sql - 9.0 net5.0 enable latest diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 91efc809..dbd8dbbf 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -1,7 +1,6 @@ - 9.0 net5.0 enable latest diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 7c8098dd..bb2af2bf 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -4,7 +4,6 @@ net5.0 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 - 9.0 enable latest AllEnabledByDefault diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 25b6f928..5bfe21f8 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -6,7 +6,6 @@ net5.0 True False - 9.0 enable latest AllEnabledByDefault diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index f32aed5c..2b9564df 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -6,7 +6,6 @@ net5.0 True False - 9.0 enable latest AllEnabledByDefault From 31631f487dffc6fe9463bfdc2cd75b60c14617f8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:10:29 -0800 Subject: [PATCH 380/778] Fallback to implicit default analysis level in .NET 5.0 In .NET Core 3.x, we needed to explicitly enable the latest `` in the `csproj` files in order to take advantage of the most recent code analysis. That's no longer required in .NET 5.0, which already uses the latest code analysis level. As such, these instructions can be removed. --- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 1 - OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 1 - OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 1 - OnTopic.Tests/OnTopic.Tests.csproj | 1 - OnTopic.ViewModels/OnTopic.ViewModels.csproj | 1 - OnTopic/OnTopic.csproj | 1 - 6 files changed, 6 deletions(-) diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index a4dce954..2cc7f27e 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -7,7 +7,6 @@ True False enable - latest AllEnabledByDefault diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 0e7ac5cd..6c42aa79 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -5,7 +5,6 @@ OnTopic.Data.Sql net5.0 enable - latest AllEnabledByDefault diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index dbd8dbbf..480b68f1 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -3,7 +3,6 @@ net5.0 enable - latest AllEnabledByDefault diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index bb2af2bf..515c56d2 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -5,7 +5,6 @@ false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 enable - latest AllEnabledByDefault diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 5bfe21f8..e7131b83 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -7,7 +7,6 @@ True False enable - latest AllEnabledByDefault diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 2b9564df..98c22797 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -7,7 +7,6 @@ True False enable - latest AllEnabledByDefault From af76cf01a418ed47290161be1e347dc5ea70b7de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:11:15 -0800 Subject: [PATCH 381/778] New year, new year: Updated copyright from 2020 to 2021 --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 68c90bb9..1bef46d7 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -16,7 +16,7 @@ Ignia OnTopic Provides presentation-layer support for the ASP.NET Core Framework. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 2cc7f27e..c759c1a5 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -15,7 +15,7 @@ Ignia OnTopic Provides a caching decorator for ITopicRepository implementations. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 6c42aa79..42520afb 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -13,7 +13,7 @@ Ignia OnTopic Provides Microsoft SQL Server support for persisting the OnTopic graph to a database. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 480b68f1..a1c6c13b 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -11,7 +11,7 @@ Ignia OnTopic Test doubles, such as dummies and stubs, useful in setting up unit and integration tests for OnTopic. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 515c56d2..503102a6 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -13,7 +13,7 @@ Ignia Ignia OnTopic Library Provides unit tests for the OnTopic library. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index e7131b83..0eb1bc16 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -15,7 +15,7 @@ Ignia OnTopic Provides view models that map to the factory default content type schemas. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 98c22797..b438347f 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -15,7 +15,7 @@ Ignia OnTopic Libraries for supporting Ignia Topics, a content management system (CMS) based on structured, hierarchical data. - ©2020 Ignia, LLC + ©2021 Ignia, LLC bin\$(Configuration)\ Ignia From b9857c859f851004b78c38245cbd409ed0306fc0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:15:06 -0800 Subject: [PATCH 382/778] Removed `IsExternalInit` polyfills The .NET Core 3.x SDK didn't include the `IsExternalInit` class needed to enable C# 9.0's `init` property initializers, which are especially useful on C# 9.0's `record` types, although can also be used elsewhere. Now that we have upgraded to .NET 5.0 (713051c), this polyfill is no longer needed, and can be deleted. (And, in fact, keeping it actually breaks the implementation.) --- OnTopic.ViewModels/Internal/IsExternalInit.cs | 19 ------------------- OnTopic/Internal/Runtime/IsExternalInit.cs | 19 ------------------- 2 files changed, 38 deletions(-) delete mode 100644 OnTopic.ViewModels/Internal/IsExternalInit.cs delete mode 100644 OnTopic/Internal/Runtime/IsExternalInit.cs diff --git a/OnTopic.ViewModels/Internal/IsExternalInit.cs b/OnTopic.ViewModels/Internal/IsExternalInit.cs deleted file mode 100644 index 2bd828ad..00000000 --- a/OnTopic.ViewModels/Internal/IsExternalInit.cs +++ /dev/null @@ -1,19 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace System.Runtime.CompilerServices { - - /*============================================================================================================================ - | CLASS: IS EXTERNAL INIT - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. - /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. - /// - internal static class IsExternalInit { - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Runtime/IsExternalInit.cs b/OnTopic/Internal/Runtime/IsExternalInit.cs deleted file mode 100644 index 26713de0..00000000 --- a/OnTopic/Internal/Runtime/IsExternalInit.cs +++ /dev/null @@ -1,19 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace System.Runtime.CompilerServices { - - /*============================================================================================================================ - | CLASS: IS EXTERNAL INIT - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. - /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. - /// - internal class IsExternalInit { - - } //Class -} //Namespace \ No newline at end of file From 6fd467b49b1d27cf4d07aa9f0919408ad8dcedcd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:27:12 -0800 Subject: [PATCH 383/778] CS8600: Converting (possible) null value to non-nullable type .NET 5.0 includes comprehensive nullability annotations which were missing from .NET Core 3.x. As a result, additional potential null reference issues have been identified. In this set, we address `CS8600`, "converting null literal or possible null value to non-nullable type." In particular, this occurs when dynamically evaluating objects (e.g., using reflection) which could potentially return null, but casting them to a non-nullable type (e.g., `(IList)`). In most of these cases, we already had subsequent null checks, and simply weren't properly casting to a nullable type. In the case of the `TopicFactory.Create()` method, this required a bit more invasive changes, with a `Contract.Assume()` to validate the result of the `ITypeLookupService`, and then a `!` to validate that the return value will never be null. (In practice, it _could_ be null, but in that case an exception would have been thrown by the `Activator.CreateInstance()` method.) --- .../TopicRouteValueTransformer.cs | 8 ++++---- .../TopicViewLocationExpander.cs | 6 +++--- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic/Internal/Diagnostics/Contract.cs | 2 +- .../Mapping/Internal/PropertyConfiguration.cs | 2 +- .../Reverse/ReverseTopicMappingService.cs | 6 +++--- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- OnTopic/TopicFactory.cs | 16 ++++++++++++++-- 8 files changed, 29 insertions(+), 17 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs b/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs index 270cc396..499322b1 100644 --- a/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs +++ b/OnTopic.AspNetCore.Mvc/TopicRouteValueTransformer.cs @@ -43,8 +43,8 @@ public override async ValueTask TransformAsync(HttpContext | If the area is set, but not the controller, assume that the controller is named after the area by convention. If the | controller is being set in the route pattern, this won't change that. \-----------------------------------------------------------------------------------------------------------------------*/ - var controller = (string)values["controller"]; - var area = (string)values["area"]; + var controller = (string?)values["controller"]; + var area = (string?)values["area"]; if (area is not null && controller is null) { values["controller"] = area; } @@ -54,7 +54,7 @@ public override async ValueTask TransformAsync(HttpContext >------------------------------------------------------------------------------------------------------------------------- | If the action isn't defined in the route, assume Index—which is the default action for the TopicController. \-----------------------------------------------------------------------------------------------------------------------*/ - var action = (string)values["action"]; + var action = (string?)values["action"]; if (action is null) { action = "Index"; values["action"] = action; @@ -67,7 +67,7 @@ public override async ValueTask TransformAsync(HttpContext | required by the TopicRepositoryExtensions to create a fully qualified topic path, and correctly identify the topic | based on the path. It is not needed when routing by controller/action pairs. \-----------------------------------------------------------------------------------------------------------------------*/ - var path = (string)values["path"]; + var path = (string?)values["path"]; if (path is not null || action.Equals("Index", StringComparison.OrdinalIgnoreCase)) { values["rootTopic"] = area; } diff --git a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs index 337b40e5..7c832456 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs @@ -82,7 +82,7 @@ public void PopulateValues(ViewLocationExpanderContext context) { Contract.Requires(context, nameof(context)); context.Values["action_displayname"] = context.ActionContext.ActionDescriptor.DisplayName; context.ActionContext.RouteData.Values.TryGetValue("contenttype", out var contentType); - context.Values["content_type"] = (string)contentType; + context.Values["content_type"] = (string?)contentType; } /*========================================================================================================================== @@ -109,14 +109,14 @@ public IEnumerable ExpandViewLocations(ViewLocationExpanderContext conte | Yield view locations \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var location in ViewLocations) { - yield return location.Replace(@"{3}", (string)contentType, StringComparison.InvariantCulture); + yield return location.Replace(@"{3}", (string?)contentType, StringComparison.InvariantCulture); } /*------------------------------------------------------------------------------------------------------------------------ | Yield area view locations \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var location in AreaViewLocations) { - yield return location.Replace(@"{3}", (string)contentType, StringComparison.InvariantCulture); + yield return location.Replace(@"{3}", (string?)contentType, StringComparison.InvariantCulture); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 9be90194..f9485d9f 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -307,7 +307,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex /*---------------------------------------------------------------------------------------------------------------------- | Identify attributes \---------------------------------------------------------------------------------------------------------------------*/ - var attributeKey = (string)xmlReader.GetAttribute("key"); + var attributeKey = (string?)xmlReader.GetAttribute("key"); var attributeValue = WebUtility.HtmlDecode(xmlReader.ReadInnerXml()); /*---------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index ebbc74c3..50a5a415 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -97,7 +97,7 @@ public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, s throw new(); } try { - throw (T)Activator.CreateInstance(typeof(T), new object[] { errorMessage }); + throw (T?)Activator.CreateInstance(typeof(T), new object[] { errorMessage }); } catch (Exception ex) when ( ex is MissingMethodException diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index eb2b582c..6332cdeb 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -447,7 +447,7 @@ public void Validate(object target) { /// The instance to pull the attribute from. /// The to execute on the attribute. private static void GetAttributeValue(PropertyInfo property, Action action) where T : Attribute { - var attribute = (T)property.GetCustomAttribute(typeof(T), true); + var attribute = (T?)property.GetCustomAttribute(typeof(T), true); if (attribute is not null) { action(attribute); } diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 6bc401cd..781812d8 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -419,7 +419,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source list \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = (IList)configuration.Property.GetValue(source, null); + var sourceList = (IList?)configuration.Property.GetValue(source, null); if (sourceList is null) { sourceList = new List(); @@ -476,7 +476,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source list \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = (IList)configuration.Property.GetValue(source, null) ?? new List(); + var sourceList = (IList?)configuration.Property.GetValue(source, null) ?? new List(); /*------------------------------------------------------------------------------------------------------------------------ | Establish target collection to store mapped topics @@ -524,7 +524,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source value \-----------------------------------------------------------------------------------------------------------------------*/ - var modelReference = (IRelatedTopicBindingModel)configuration.Property.GetValue(source); + var modelReference = (IRelatedTopicBindingModel?)configuration.Property.GetValue(source); /*------------------------------------------------------------------------------------------------------------------------ | Bypass if reference (or value) is null (or empty) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index ade30345..345ca6a3 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -448,9 +448,9 @@ MappedTopicCache cache /*------------------------------------------------------------------------------------------------------------------------ | Ensure target list is created \-----------------------------------------------------------------------------------------------------------------------*/ - var targetList = (IList)configuration.Property.GetValue(target, null); + var targetList = (IList?)configuration.Property.GetValue(target, null); if (targetList is null) { - targetList = (IList)Activator.CreateInstance(configuration.Property.PropertyType); + targetList = (IList?)Activator.CreateInstance(configuration.Property.PropertyType); configuration.Property.SetValue(target, targetList); } diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index d4fa17e7..8e1f9682 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -78,10 +78,16 @@ public static Topic Create(string key, string contentType, Topic? parent = null) \-----------------------------------------------------------------------------------------------------------------------*/ var targetType = TypeLookupService.Lookup(contentType); + Contract.Assume( + targetType, + $"The content type {contentType} could not be located in the ITypeLookupService, and no fallback could be " + + $"identified." + ); + /*------------------------------------------------------------------------------------------------------------------------ | Identify the appropriate topic \-----------------------------------------------------------------------------------------------------------------------*/ - return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1); + return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1)!; } @@ -119,10 +125,16 @@ public static Topic Create(string key, string contentType, int id, Topic? parent \-----------------------------------------------------------------------------------------------------------------------*/ var targetType = TypeLookupService.Lookup(contentType); + Contract.Assume( + targetType, + $"The content type {contentType} could not be located in the ITypeLookupService, and no fallback could be " + + $"identified." + ); + /*------------------------------------------------------------------------------------------------------------------------ | Identify the appropriate topic \---------------------------------------------------------------------------------------------------------------------*/ - return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, id); + return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, id)!; } From 1444363fb10699cc551a8b7acd72ebef6569d1b6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:28:49 -0800 Subject: [PATCH 384/778] CS8597: Thrown value may be null In practice, this should never happen since the `Activator.CreateInstance()` would throw an exception. As a result, I'm suppressing the error with a `!`. --- OnTopic/Internal/Diagnostics/Contract.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Internal/Diagnostics/Contract.cs b/OnTopic/Internal/Diagnostics/Contract.cs index 50a5a415..9a1bd177 100644 --- a/OnTopic/Internal/Diagnostics/Contract.cs +++ b/OnTopic/Internal/Diagnostics/Contract.cs @@ -97,7 +97,7 @@ public static void Requires([ValidatedNotNull, NotNull]object? requiredObject, s throw new(); } try { - throw (T?)Activator.CreateInstance(typeof(T), new object[] { errorMessage }); + throw (T?)Activator.CreateInstance(typeof(T), new object[] { errorMessage })!; } catch (Exception ex) when ( ex is MissingMethodException From 1b9cae1095ed7943f702c746e8c3594c1d2e6918 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:33:58 -0800 Subject: [PATCH 385/778] CS8601: Possible null reference assignment In practice, the values in the underlying `_storage` dictionary will never be `null`, as they are validated on entry, and deleted if the values are set to `null`. As such, we can be confident that if `TryGetValue()` is successful, then the output value will not be `null`. Note: This is required to satisfy the expectations of `IDictionary`. Given that, it's peculiar that this throws a CS8601. --- OnTopic/References/TopicReferenceDictionary.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index ad9b7c7b..2a40f4bb 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -349,7 +349,7 @@ public bool Remove(string key) { | TRY/GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value); + public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value!); /*========================================================================================================================== | GET TOPIC From 2d426fbff09de1343af4dccb26f1666334b633b0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:36:00 -0800 Subject: [PATCH 386/778] CS8602: Dereference of a possibly null reference This just requires use of the null-conditional operator (`.?`) to acknowledge that a value could be null, and to bubble that up before dereferencing it. --- OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs | 2 +- OnTopic/Lookup/DynamicTypeLookupService.cs | 2 +- OnTopic/Mapping/TopicMappingService.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs index f9951dbe..afd90f5e 100644 --- a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs +++ b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs @@ -135,7 +135,7 @@ public override void OnActionExecuting(ActionExecutingContext context) { \-----------------------------------------------------------------------------------------------------------------------*/ if (currentTopic.ContentType is "PageGroup") { context.Result = controller.Redirect( - currentTopic.Children.Where(t => t.IsVisible()).FirstOrDefault().GetWebPath() + currentTopic.Children.Where(t => t.IsVisible()).FirstOrDefault()?.GetWebPath() ); return; } diff --git a/OnTopic/Lookup/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs index ce58aabe..7f873450 100644 --- a/OnTopic/Lookup/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -36,7 +36,7 @@ public DynamicTypeLookupService(Func predicate, Type? defaultType = .GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => t.IsClass && predicate(t)) - .OrderBy(t => t.Namespace.StartsWith("OnTopic", StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(t => t.Namespace?.StartsWith("OnTopic", StringComparison.InvariantCultureIgnoreCase)) .ToList(); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 345ca6a3..e072aea4 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -555,7 +555,7 @@ protected IList GetSourceCollection(Topic source, Relationships relations if ( sourceProperty.GetValue(source) is IList sourcePropertyValue && sourcePropertyValue.Count > 0 && - typeof(Topic).IsAssignableFrom(sourcePropertyValue[0].GetType()) + typeof(Topic).IsAssignableFrom(sourcePropertyValue[0]?.GetType()) ) { listSource = GetRelationship( RelationshipType.MappedCollection, From cfa8ac960ea5cb14a01c3421db62c2e556a8f8e2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:38:34 -0800 Subject: [PATCH 387/778] CS8603: Possible null reference return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In each of these cases, the return values _could_ potentially be null, and callers should be aware of that. In most of these cases, which were in `TopicMappingServiceTest`, the caller was an `Assert.IsNotNull()`, and was thus already _expecting_ a potentially null reference—otherwise there'd be no point in testing for it. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.Tests/TopicMappingServiceTest.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index f9485d9f..4f5bd1cf 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -56,7 +56,7 @@ internal static class SqlDataReaderExtensions { /// cref="Topic.DerivedTopic"/>. This is useful for cases where it's known that a shallow copy is being retrieved, and /// thus external references aren't likely to be available. /// - internal static Topic LoadTopicGraph( + internal static Topic? LoadTopicGraph( this IDataReader reader, Topic? referenceTopic = null, bool? markDirty = null, diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 89f34514..37365bf3 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -734,7 +734,7 @@ public async Task Map_TopicEntities_ReturnsTopics() { Assert.AreEqual(relatedTopic3.Key, relatedTopic3copy.Key); - Topic getRelatedTopic(RelatedEntityTopicViewModel topic, string key) + Topic? getRelatedTopic(RelatedEntityTopicViewModel topic, string key) => topic.RelatedTopics.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); } @@ -1042,10 +1042,10 @@ public async Task Map_CachedTopic_ReturnsCachedModel() { /// /// A helper function which retrieves a child topic based on the key. /// - public static KeyOnlyTopicViewModel GetChildTopic(IEnumerable topicCollection, string key) + public static KeyOnlyTopicViewModel? GetChildTopic(IEnumerable topicCollection, string key) => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); - public static TopicViewModel GetChildTopic(IEnumerable topicCollection, string key) + public static TopicViewModel? GetChildTopic(IEnumerable topicCollection, string key) => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); } //Class From 1d15dbabe748b820194ec1f3c20eb9eebabf580e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:40:31 -0800 Subject: [PATCH 388/778] CS8600: Converting (possible) null value to non-nullable type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I previously patched several CS8600s (6fd467b), but code analysis discovered a couple of more in a test file. These are easily fixed by casting the return value as nullable—a condition that's already being accounted for in the code. --- OnTopic.Tests/TopicMappingServiceTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 37365bf3..d4c8911e 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -492,11 +492,11 @@ public async Task Map_Children_ReturnsMappedModel() { Assert.IsNotNull(GetChildTopic(target.Children, "ChildTopic2")); Assert.IsNotNull(GetChildTopic(target.Children, "ChildTopic3")); Assert.IsNotNull(GetChildTopic(target.Children, "ChildTopic4")); - Assert.IsTrue(((DescendentSpecializedTopicViewModel)GetChildTopic(target.Children, "ChildTopic4")).IsLeaf); + Assert.IsTrue(((DescendentSpecializedTopicViewModel?)GetChildTopic(target.Children, "ChildTopic4")).IsLeaf); Assert.IsNull(GetChildTopic(target.Children, "invalidChildTopic")); Assert.IsNull(GetChildTopic(target.Children, "GrandchildTopic")); Assert.IsNotNull(GetChildTopic( - ((DescendentTopicViewModel)GetChildTopic(target.Children, "ChildTopic3")).Children, + ((DescendentTopicViewModel?)GetChildTopic(target.Children, "ChildTopic3")).Children, "GrandchildTopic" )); } From eee7e5d246148d00a452bc48c16bee2c49da298c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 11:47:38 -0800 Subject: [PATCH 389/778] CS8602: Dereference of a possibly null reference I previously patched several CS8602s (2d426fb ), but the fix of CS8603 (cfa8ac9) introduced a new one. This possibility was already accounted for in the return condition, but not properly accounted for prior to introducing new code for deleting unmatched attributes. Given that, I've moved the validation up from the return condition to above the orphaned attribute handling, so it addresses both scenarios. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index a99a0c4a..454982ca 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -256,6 +256,13 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic throw new TopicRepositoryException($"Topics failed to load: '{exception.Message}'", exception); } + /*------------------------------------------------------------------------------------------------------------------------ + | Validate result + \-----------------------------------------------------------------------------------------------------------------------*/ + if (topic is null) { + throw new TopicNotFoundException(topicId); + } + /*------------------------------------------------------------------------------------------------------------------------ | Delete orphaned attributes >------------------------------------------------------------------------------------------------------------------------- @@ -272,7 +279,7 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ - return topic?? throw new TopicNotFoundException(topicId); + return topic; } From c3ed628f93e7d6ea37724c0cd01d03559e046206 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 12:03:41 -0800 Subject: [PATCH 390/778] CS8604: Possible null reference argument for parameter Where possible, a default was provided using the null-coalescing operator (`??`). In most cases, however, I had to add a `Contract.Assume()` or manually check and throw an exception. In one case, I needed to suppress the warning, unfortunately, as otherwise we'll need to rewrite that logic to account for the scenario. (In practice, the target accepts nulls, despite the warning.) --- .../Controllers/SitemapController.cs | 2 ++ .../ReadOnlyKeyedTopicCollection{T}.cs | 2 +- .../Collections/ReadOnlyTopicCollection.cs | 2 +- .../Reflection/TopicPropertyDispatcher.cs | 5 +++- OnTopic/Mapping/TopicMappingService.cs | 30 ++++++++++++++----- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index fb4b82c1..4461b082 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -194,6 +194,7 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false new XElement(_sitemapNamespace + "changefreq", "monthly"), new XElement(_sitemapNamespace + "lastmod", lastModified.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), new XElement(_sitemapNamespace + "priority", 1), + #pragma warning disable CS8604 // Possible null reference argument. includeMetadata? new XElement(_pagemapNamespace + "PageMap", new XElement(_pagemapNamespace + "DataObject", new XAttribute("type", topic.ContentType?? "Page"), @@ -201,6 +202,7 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false ), getRelationships() ) : null + #pragma warning restore CS8604 // Possible null reference argument. ); if ( !SkippedContentTypes.Any(c => topic.ContentType?.Equals(c, StringComparison.OrdinalIgnoreCase)?? false) && diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index 60d72702..e7a141d5 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -30,7 +30,7 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T /// Establishes a new based on an existing . /// /// The underlying . - public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection) { + public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection?? new List()) { Contract.Requires(innerCollection, "innerCollection should not be null"); _innerCollection = innerCollection as KeyedTopicCollection?? new(innerCollection); } diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs index a57638fb..aee0f619 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -23,7 +23,7 @@ public class ReadOnlyTopicCollection : ReadOnlyCollection { /// Establishes a new based on an existing . /// /// The underlying . - public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection) { + public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection?? new List()) { } } //Class diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index ae63cfcc..fdbb35ca 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -276,7 +276,10 @@ internal bool Enforce(string itemKey, TValueType? initialObject) { if (PropertyCache.ContainsKey(itemKey)) { PropertyCache.Remove(itemKey); } - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + if (ex.InnerException is not null) { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + throw; } _setCounter = 0; return false; diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index e072aea4..eaecfcf4 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -129,6 +129,12 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService target = Activator.CreateInstance(viewModelType); + Contract.Assume( + target, + $"The target type '{viewModelType}' could not be properly constructed, as required to map the topic " + + $"'{topic.GetUniqueKey()}'." + ); + } /*------------------------------------------------------------------------------------------------------------------------ @@ -319,13 +325,17 @@ protected async Task SetPropertyAsync( } } else if (configuration.MapToParent) { - await MapAsync( - source, - property.GetValue(target), - relationships, - cache, - configuration.AttributePrefix - ).ConfigureAwait(false); + var targetProperty = property.GetValue(target); + if (targetProperty is not null) { + await MapAsync( + source, + targetProperty, + relationships, + cache, + configuration.AttributePrefix + ).ConfigureAwait(false); + } + } /*------------------------------------------------------------------------------------------------------------------------ @@ -454,6 +464,12 @@ MappedTopicCache cache configuration.Property.SetValue(target, targetList); } + Contract.Assume( + targetList, + $"The target list type, '{configuration.Property.PropertyType}', could not be properly constructed, as required to " + + $"map the '{configuration.Property.Name}' property on the '{target?.GetType().Name}' object." + ); + /*------------------------------------------------------------------------------------------------------------------------ | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ From 8022a965b53057b24c728402ece0b81e6888c41e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 12:06:47 -0800 Subject: [PATCH 391/778] CS8762: Parameter must have a non-null value when exiting with true This is similar to CS8601 (1b9cae1), and has a similar resolution, since the underlying storage will always have a non-nullable value. I'm not a fan of using the null-forgiving operator, but since the `TryGetValue()` doesn't honor the nullability of the target type, this is a necessity. --- OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index fdbb35ca..71779630 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -219,7 +219,7 @@ internal bool Register(string itemKey, TValueType? initialValue) { /// The object which is being inserted. /// Returns true if the has been registered, otherwise false. internal bool IsRegistered(string itemKey, [NotNullWhen(true)] out TValueType? initialObject) => - PropertyCache.TryGetValue(itemKey, out initialObject); + PropertyCache.TryGetValue(itemKey, out initialObject!); /*========================================================================================================================== | METHOD: ENFORCE From 5853e8dd305e99d5bbbc674f9ff0793fcbaa1e5e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 12:20:51 -0800 Subject: [PATCH 392/778] Removed legacy `FromList()` method One of these versions was marked as obsolete, but overlooked when deleting the obsolete members. The other version was not, but should have been. Regardless, neither are used or even make much sense, as they effectively do the same thing as the primary constructor. --- .../Collections/ReadOnlyKeyedTopicCollection.cs | 16 ---------------- .../ReadOnlyKeyedTopicCollection{T}.cs | 16 ---------------- 2 files changed, 32 deletions(-) diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs index 3e5fdc45..c93b0173 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.Collections.Generic; -using OnTopic.Internal.Diagnostics; namespace OnTopic.Collections { @@ -26,20 +25,5 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyKeyedTopicCollection public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection) { } - /*========================================================================================================================== - | FACTORY METHOD: FROM LIST - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a new based on an existing . - /// - /// - /// The will be converted to a . - /// - /// The underlying . - public new static ReadOnlyKeyedTopicCollection FromList(IList innerCollection) { - Contract.Requires(innerCollection, "innerCollection should not be null"); - return new(innerCollection); - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index e7a141d5..be052b4b 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -49,22 +49,6 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T return null; } - /*========================================================================================================================== - | FACTORY METHOD: FROM LIST - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Establishes a new based on an existing . - /// - /// - /// The will be converted to a . - /// - /// The underlying . - [Obsolete("This is effectively satisfied by the related overload, and will be removed in OnTopic 5.0.0.", true)] - public ReadOnlyKeyedTopicCollection FromList(IList innerCollection) { - Contract.Requires(innerCollection, "innerCollection should not be null"); - return new(innerCollection); - } - /*========================================================================================================================== | INDEXER \-------------------------------------------------------------------------------------------------------------------------*/ From 868a42358da1a4023b325d77df59bd67e37f8f08 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 12:35:37 -0800 Subject: [PATCH 393/778] Moved `[ReferenceSetter]` attribute to `OnTopic.References` It was previously placed in `OnTopic`, but mistakenly maintained a namespace of `OnTopic.Attributes`. Whoops. --- OnTopic.Tests/Entities/CustomTopic.cs | 1 + OnTopic/{ => References}/ReferenceSetterAttribute.cs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) rename OnTopic/{ => References}/ReferenceSetterAttribute.cs (97%) diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index ea5ecc35..8a92653b 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -7,6 +7,7 @@ using System.Globalization; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; +using OnTopic.References; namespace OnTopic.Tests.Entities { diff --git a/OnTopic/ReferenceSetterAttribute.cs b/OnTopic/References/ReferenceSetterAttribute.cs similarity index 97% rename from OnTopic/ReferenceSetterAttribute.cs rename to OnTopic/References/ReferenceSetterAttribute.cs index 22c0b3d2..0abab0bc 100644 --- a/OnTopic/ReferenceSetterAttribute.cs +++ b/OnTopic/References/ReferenceSetterAttribute.cs @@ -4,9 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.References; -namespace OnTopic.Attributes { +namespace OnTopic.References { /*============================================================================================================================ | CLASS: REFERENCE SETTER [ATTRIBUTE] From a60216cf9c6077e59e945d4d682be97d7abcc7a5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 12:35:46 -0800 Subject: [PATCH 394/778] Removed outdated developer note --- OnTopic/Metadata/ContentTypeDescriptor.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index f1c29fb6..bb6320f9 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -176,14 +176,7 @@ public AttributeDescriptorCollection AttributeDescriptors { _attributeDescriptors = new(); /*-------------------------------------------------------------------------------------------------------------------- - | Get values from self - >--------------------------------------------------------------------------------------------------------------------- - | ### NOTE KLT052015: The (ContentType)Topic.Attributes property is an AttributeValue collection, not an Attribute - | collection. - >--------------------------------------------------------------------------------------------------------------------- - | ### NOTE KLT052015: The only place this is really used (and where the strongly-typed Attribute is needed) is in - | SqlTopicDataProvider.cs (lines 408 - 422), where it is used to add Attributes to the null Attributes collection; the - | Type property is used for determining whether the Attribute Topic is a Relationships definition or Nested Topic. + | Get values from nested topics \-------------------------------------------------------------------------------------------------------------------*/ if (Children.Contains("Attributes")) { foreach (AttributeDescriptor attribute in Children["Attributes"].Children) { From fdb9d1928e0bf56aac7b766e8a95ec178a20e17a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 13:34:44 -0800 Subject: [PATCH 395/778] Reintroduced .NET Standard 2.1, .NET Core 3.1 support This effectively rolls back the upgrade to .NET 5.0 (713051c). --- OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj | 2 +- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 2 +- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 3 ++- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index 3c442e05..e03c93ef 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -1,7 +1,7 @@  - net5.0 + netcoreapp3.1 62eb85bf-f802-4afd-8bec-3d344e1cfc79 false diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index c4494b35..bf27ffbf 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + netcoreapp3.1 false diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 1bef46d7..ed6b3063 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -3,7 +3,7 @@ {B7F136A1-C86D-4A74-AC4F-3693CD1358A4} OnTopic.AspNetCore.Mvc - net5.0 + netcoreapp3.1 True False enable diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index c759c1a5..1adaf28a 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -3,10 +3,11 @@ {206B7F91-CA25-4E9D-9576-60D2E54A2C0A} OnTopic.Data.Caching - net5.0 + netstandard2.1 True False enable + latest AllEnabledByDefault diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 42520afb..66503e9a 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -3,7 +3,7 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} OnTopic.Data.Sql - net5.0 + netstandard2.1 enable AllEnabledByDefault diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index a1c6c13b..b909c568 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -1,7 +1,7 @@ - net5.0 + netstandard2.1 enable AllEnabledByDefault diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 503102a6..18ff6219 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + netcoreapp3.1 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 enable diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 0eb1bc16..a4aa429e 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -3,7 +3,7 @@ {E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7} OnTopic.ViewModels - net5.0 + netstandard2.1 True False enable diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index b438347f..3417b060 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -3,7 +3,7 @@ {B8D5B290-4451-4C3B-AE9E-0FF075958A74} OnTopic - net5.0 + netstandard2.1 True False enable From 25c5dda98e26c51bf8c21516573ba019d77627b0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 13:36:43 -0800 Subject: [PATCH 396/778] Reintroduced explicit setting of C# 9.0 This effectively rolls back the implicit use of C# 9.0 (a8716db), which was not necessary with .NET 5.0, but is now necessary since we've rolled back to .NET Standard 2.1 and .NET Core 3.1. --- OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj | 1 + OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj | 1 + OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 1 + OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 1 + OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 1 + OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 1 + OnTopic.Tests/OnTopic.Tests.csproj | 1 + OnTopic.ViewModels/OnTopic.ViewModels.csproj | 1 + OnTopic/OnTopic.csproj | 1 + 9 files changed, 9 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index e03c93ef..c50a72ae 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + 9.0 62eb85bf-f802-4afd-8bec-3d344e1cfc79 false diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index bf27ffbf..c1fbcb27 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + 9.0 false diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index ed6b3063..9d13ba09 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -4,6 +4,7 @@ {B7F136A1-C86D-4A74-AC4F-3693CD1358A4} OnTopic.AspNetCore.Mvc netcoreapp3.1 + 9.0 True False enable diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 1adaf28a..7fb0c90a 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -4,6 +4,7 @@ {206B7F91-CA25-4E9D-9576-60D2E54A2C0A} OnTopic.Data.Caching netstandard2.1 + 9.0 True False enable diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 66503e9a..f8a1d720 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -4,6 +4,7 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} OnTopic.Data.Sql netstandard2.1 + 9.0 enable AllEnabledByDefault diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index b909c568..5b373f1a 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -2,6 +2,7 @@ netstandard2.1 + 9.0 enable AllEnabledByDefault diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 18ff6219..a2526759 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + 9.0 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 enable diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index a4aa429e..1990b4d5 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -4,6 +4,7 @@ {E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7} OnTopic.ViewModels netstandard2.1 + 9.0 True False enable diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 3417b060..a022631a 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -4,6 +4,7 @@ {B8D5B290-4451-4C3B-AE9E-0FF075958A74} OnTopic netstandard2.1 + 9.0 True False enable From 8f7fba6d9eef20dc25da25bbcb26ed388084c765 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 13:37:48 -0800 Subject: [PATCH 397/778] Reintroduced explicit setting of code analysis mode This effectively rolls back the implicit use of latest code analysis level (31631f4), which was not necessary with .NET 5.0, but is now necessary again with the rollback to .NET Standard 2.1 and .NET Core 3.1. --- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 1 + OnTopic.TestDoubles/OnTopic.TestDoubles.csproj | 1 + OnTopic.Tests/OnTopic.Tests.csproj | 1 + OnTopic.ViewModels/OnTopic.ViewModels.csproj | 1 + OnTopic/OnTopic.csproj | 1 + 5 files changed, 5 insertions(+) diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index f8a1d720..185fdb5c 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -6,6 +6,7 @@ netstandard2.1 9.0 enable + latest AllEnabledByDefault diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 5b373f1a..220bef6f 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -4,6 +4,7 @@ netstandard2.1 9.0 enable + latest AllEnabledByDefault diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index a2526759..13037233 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -6,6 +6,7 @@ false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 enable + latest AllEnabledByDefault diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 1990b4d5..f6cfb432 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -8,6 +8,7 @@ True False enable + latest AllEnabledByDefault diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index a022631a..c47946f5 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -8,6 +8,7 @@ True False enable + latest AllEnabledByDefault From 4bc95e109a57fdcc662df0545d328f7c3e34a8c4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 13:39:00 -0800 Subject: [PATCH 398/778] Reintroduced the `IsExternal` polyfills This effectively rolls back the deletion of the `IsExternalInit` polyfills (b9857c8), which are not necessary when targeting .NET 5.0, but are necessary now that we've rolled back to targeting .NET Standard 3.1 and .NET Core 3.1. --- OnTopic.ViewModels/Internal/IsExternalInit.cs | 19 +++++++++++++++++++ OnTopic/Internal/Runtime/IsExternalInit.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 OnTopic.ViewModels/Internal/IsExternalInit.cs create mode 100644 OnTopic/Internal/Runtime/IsExternalInit.cs diff --git a/OnTopic.ViewModels/Internal/IsExternalInit.cs b/OnTopic.ViewModels/Internal/IsExternalInit.cs new file mode 100644 index 00000000..2bd828ad --- /dev/null +++ b/OnTopic.ViewModels/Internal/IsExternalInit.cs @@ -0,0 +1,19 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace System.Runtime.CompilerServices { + + /*============================================================================================================================ + | CLASS: IS EXTERNAL INIT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. + /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. + /// + internal static class IsExternalInit { + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Internal/Runtime/IsExternalInit.cs b/OnTopic/Internal/Runtime/IsExternalInit.cs new file mode 100644 index 00000000..2bd828ad --- /dev/null +++ b/OnTopic/Internal/Runtime/IsExternalInit.cs @@ -0,0 +1,19 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace System.Runtime.CompilerServices { + + /*============================================================================================================================ + | CLASS: IS EXTERNAL INIT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. + /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. + /// + internal static class IsExternalInit { + + } //Class +} //Namespace \ No newline at end of file From 318943ab9ace9a1d3ab0fb1caa859d5e52416ff6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 13:41:22 -0800 Subject: [PATCH 399/778] Removed suppression of CS8604 This partially rolls back one of the changes made when addressing CS8604 (c3ed628), which is the suppression of a warning introduced by improved nullability annotations in .NET 5.0. Now that we're targeting .NET Core 3.1, this suppression is no longer relevant. We can revisit at the point that we upgrade OnTopic. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 4461b082..fb4b82c1 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -194,7 +194,6 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false new XElement(_sitemapNamespace + "changefreq", "monthly"), new XElement(_sitemapNamespace + "lastmod", lastModified.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), new XElement(_sitemapNamespace + "priority", 1), - #pragma warning disable CS8604 // Possible null reference argument. includeMetadata? new XElement(_pagemapNamespace + "PageMap", new XElement(_pagemapNamespace + "DataObject", new XAttribute("type", topic.ContentType?? "Page"), @@ -202,7 +201,6 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false ), getRelationships() ) : null - #pragma warning restore CS8604 // Possible null reference argument. ); if ( !SkippedContentTypes.Any(c => topic.ContentType?.Equals(c, StringComparison.OrdinalIgnoreCase)?? false) && From 6fdcdf163e86e68e2b555a5ff59f41a708dd3775 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 14:11:00 -0800 Subject: [PATCH 400/778] Reorganized view model dependencies Moved item types (derived from `ItemTopicViewModel`) to the new `OnTopic.ViewModels.Items` namespace. Moved the `TopicViewModelCollection` to a new `OnTopic.ViewModels.Collections` namespace. This helps declutter the main `OnTopic.ViewModels` namespace, while focusing it on the main entry points. --- .../ViewModels/DescendentTopicViewModel.cs | 1 + .../FilteredContentTypeTopicViewModel.cs | 1 + .../ViewModels/FilteredTopicViewModel.cs | 1 + .../MetadataLookupTopicViewModel.cs | 1 + .../TopicViewModelCollection.cs | 2 +- .../ContentListTopicViewModel.cs | 2 ++ .../{ => Items}/ContentItemTopicViewModel.cs | 2 +- .../{ => Items}/ItemTopicViewModel.cs | 2 +- .../{ => Items}/ListTopicViewModel.cs | 2 +- .../LookupListItemTopicViewModel.cs | 2 +- .../{ => Items}/SlideTopicViewModel.cs | 2 +- OnTopic.ViewModels/README.md | 14 +++++++------- .../TopicViewModelLookupService.cs | 19 ++++++++++++++----- 13 files changed, 33 insertions(+), 18 deletions(-) rename OnTopic.ViewModels/{ => Collections}/TopicViewModelCollection.cs (98%) rename OnTopic.ViewModels/{ => Items}/ContentItemTopicViewModel.cs (98%) rename OnTopic.ViewModels/{ => Items}/ItemTopicViewModel.cs (97%) rename OnTopic.ViewModels/{ => Items}/ListTopicViewModel.cs (97%) rename OnTopic.ViewModels/{ => Items}/LookupListItemTopicViewModel.cs (97%) rename OnTopic.ViewModels/{ => Items}/SlideTopicViewModel.cs (97%) diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index ac3f7e68..2c982d55 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; +using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs index 0c613841..355eeb8e 100644 --- a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; +using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs index 4b87fc24..9fb9bb11 100644 --- a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; +using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs index cce94c39..eb3867aa 100644 --- a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; +using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.ViewModels/TopicViewModelCollection.cs b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs similarity index 98% rename from OnTopic.ViewModels/TopicViewModelCollection.cs rename to OnTopic.ViewModels/Collections/TopicViewModelCollection.cs index 94c079b6..a42338e5 100644 --- a/OnTopic.ViewModels/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs @@ -10,7 +10,7 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Models; -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Collections { /*============================================================================================================================ | VIEW MODEL: TOPIC COLLECTION diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/ContentListTopicViewModel.cs index 82b91b5d..6849325d 100644 --- a/OnTopic.ViewModels/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentListTopicViewModel.cs @@ -3,6 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using OnTopic.ViewModels.Collections; +using OnTopic.ViewModels.Items; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs similarity index 98% rename from OnTopic.ViewModels/ContentItemTopicViewModel.cs rename to OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs index b084d6b1..b7185e34 100644 --- a/OnTopic.ViewModels/ContentItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Items { /*============================================================================================================================ | VIEW MODEL: CONTENT ITEM TOPIC diff --git a/OnTopic.ViewModels/ItemTopicViewModel.cs b/OnTopic.ViewModels/Items/ItemTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/ItemTopicViewModel.cs rename to OnTopic.ViewModels/Items/ItemTopicViewModel.cs index 4a1e3411..723d7fcc 100644 --- a/OnTopic.ViewModels/ItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ItemTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Items { /*============================================================================================================================ | VIEW MODEL: ITEM TOPIC diff --git a/OnTopic.ViewModels/ListTopicViewModel.cs b/OnTopic.ViewModels/Items/ListTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/ListTopicViewModel.cs rename to OnTopic.ViewModels/Items/ListTopicViewModel.cs index 79631df1..c8b5a4e4 100644 --- a/OnTopic.ViewModels/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ListTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Items { /*============================================================================================================================ | VIEW MODEL: LIST diff --git a/OnTopic.ViewModels/LookupListItemTopicViewModel.cs b/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/LookupListItemTopicViewModel.cs rename to OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs index 57224494..7004c32e 100644 --- a/OnTopic.ViewModels/LookupListItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Items { /*============================================================================================================================ | VIEW MODEL: LOOKUP LIST ITEM TOPIC diff --git a/OnTopic.ViewModels/SlideTopicViewModel.cs b/OnTopic.ViewModels/Items/SlideTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/SlideTopicViewModel.cs rename to OnTopic.ViewModels/Items/SlideTopicViewModel.cs index 770d86b9..77927f07 100644 --- a/OnTopic.ViewModels/SlideTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/SlideTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels { +namespace OnTopic.ViewModels.Items { /*============================================================================================================================ | VIEW MODEL: SLIDE TOPIC diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md index 12196e82..69277a62 100644 --- a/OnTopic.ViewModels/README.md +++ b/OnTopic.ViewModels/README.md @@ -31,19 +31,19 @@ Installation can be performed by providing a ` to the `OnTo ## Inventory - [`TopicViewModel`](TopicViewModel.cs) - [`PageTopicViewModel`](PageTopicViewModel.cs) - - [`ContentListTopicViewModel`](ContentListTopicViewModel.cs) ([`ContentItemTopicViewModel`](ContentItemTopicViewModel.cs)) + - [`ContentListTopicViewModel`](ContentListTopicViewModel.cs) ([`ContentItemTopicViewModel`](Items/ContentItemTopicViewModel.cs)) - [`IndexTopicViewModel`](IndexTopicViewModel.cs) - - [`SlideshowTopicViewModel`](SlideshowTopicViewModel.cs) ([`SlideTopicViewModel`](SlideTopicViewModel.cs)) + - [`SlideshowTopicViewModel`](SlideshowTopicViewModel.cs) ([`SlideTopicViewModel`](Items/SlideTopicViewModel.cs)) - [`VideoTopicViewModel`](VideoTopicViewModel.cs) - [`SectionTopicViewModel`](SectionTopicViewModel.cs) - [`PageGroupTopicViewModel`](PageGroupTopicViewModel.cs) - [`NavigationTopicViewModel`](NavigationTopicViewModel.cs) - - [`ItemTopicViewModel`](ItemTopicViewModel.cs) - - [`ContentItemTopicViewModel`](ContentItemTopicViewModel.cs) - - [`LookupListItemTopicViewModel`](LookupListItemTopicViewModel.cs) - - [`SlideTopicViewModel`](SlideTopicViewModel.cs) + - [`ItemTopicViewModel`](Items/ItemTopicViewModel.cs) + - [`ContentItemTopicViewModel`](Items/ContentItemTopicViewModel.cs) + - [`LookupListItemTopicViewModel`](Items/LookupListItemTopicViewModel.cs) + - [`SlideTopicViewModel`](Items/SlideTopicViewModel.cs) - [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) -- [`TopicViewModelCollection<>`](TopicViewModelCollection.cs) +- [`TopicViewModelCollection<>`](Collections/TopicViewModelCollection.cs) ## Usage By default, the [`OnTopic.AspNetCore.Mvc`](../OnTopic.AspNetCore.Mvc/README.md)'s [`TopicController`](../OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs) uses the out-of-the-box [`TopicMappingService`](../OnTopic/Mapping) to map topics to view models. For applications primarily relying on the out-of-the-box view models, it is recommended that the [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) be used; this includes all of the out-of-the-box view models, and can be derived to add application-specific view models. diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index d15b0704..80bc2a48 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using OnTopic.Lookup; +using OnTopic.ViewModels.Items; namespace OnTopic.ViewModels { @@ -36,21 +37,29 @@ public TopicViewModelLookupService(IEnumerable? types = null, Type? defaul /*------------------------------------------------------------------------------------------------------------------------ | Ensure local view models are accounted for \-----------------------------------------------------------------------------------------------------------------------*/ - TryAdd(typeof(ContentItemTopicViewModel)); TryAdd(typeof(ContentListTopicViewModel)); TryAdd(typeof(IndexTopicViewModel)); - TryAdd(typeof(ItemTopicViewModel)); - TryAdd(typeof(ListTopicViewModel)); - TryAdd(typeof(LookupListItemTopicViewModel)); TryAdd(typeof(NavigationTopicViewModel)); TryAdd(typeof(PageGroupTopicViewModel)); TryAdd(typeof(PageTopicViewModel)); TryAdd(typeof(SectionTopicViewModel)); - TryAdd(typeof(SlideTopicViewModel)); TryAdd(typeof(SlideshowTopicViewModel)); TryAdd(typeof(TopicViewModel)); TryAdd(typeof(VideoTopicViewModel)); + /*------------------------------------------------------------------------------------------------------------------------ + | Add item types + \-----------------------------------------------------------------------------------------------------------------------*/ + TryAdd(typeof(ItemTopicViewModel)); + TryAdd(typeof(ContentItemTopicViewModel)); + TryAdd(typeof(ListTopicViewModel)); + TryAdd(typeof(LookupListItemTopicViewModel)); + TryAdd(typeof(SlideTopicViewModel)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Add support types + \-----------------------------------------------------------------------------------------------------------------------*/ + } } //Class From 0d34a81aa68c057ac4c9c94aa809048faa155adb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 14:19:52 -0800 Subject: [PATCH 401/778] Removed trailing spaces in views --- OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml | 2 +- .../Views/ContentList/ContentList.cshtml | 2 +- .../Views/ContentList/IndexedList.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Page.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/PageGroup.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Video.cshtml | 2 +- .../Views/Shared/Components/Menu/Default.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/Shared/_PageAttributes.cshtml | 2 +- .../Views/Shared/_TopicAttributes.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/_ViewImports.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/_ViewStart.cshtml | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml index ce32ef5b..052906ea 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml @@ -4,4 +4,4 @@ Content Type: Content List View Type: Accordion View Location: ~/Views/ContentList/Accordion.cshtml ---> +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/ContentList.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/ContentList.cshtml index 36e6cb78..aab88b85 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/ContentList.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/ContentList.cshtml @@ -27,4 +27,4 @@ Content Type: Content List View Type: Accordion View Location: ~/Views/ContentList/Accordion.cshtml ---> +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml index 31b611e7..c77d20ea 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml @@ -4,4 +4,4 @@ Content Type: Content List View Type: Indexed List View Location: ~/Views/ContentList/IndexedList.cshtml ---> +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml index 9d08976a..cf2d6365 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml @@ -4,4 +4,4 @@ Content Type: Content List View Type: Linked List View Location: ~/Views/ContentList/LinkedList.cshtml ---> +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Page.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Page.cshtml index 007d839a..c1a9dcd3 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Page.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Page.cshtml @@ -5,4 +5,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/PageGroup.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/PageGroup.cshtml index c16cb146..d8e5bcb3 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/PageGroup.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/PageGroup.cshtml @@ -3,4 +3,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Video.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Video.cshtml index 0958f061..056e37ec 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Video.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Video.cshtml @@ -11,4 +11,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml index 5a7cd088..99aa3589 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml @@ -32,4 +32,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_PageAttributes.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_PageAttributes.cshtml index 6b6dac84..014bde4d 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_PageAttributes.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_PageAttributes.cshtml @@ -16,4 +16,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml index ab100265..464de203 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml @@ -14,4 +14,4 @@ +--> \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/_ViewImports.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/_ViewImports.cshtml index e0ba14b7..852bc1b7 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/_ViewImports.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/_ViewImports.cshtml @@ -9,4 +9,4 @@ @using OnTopic.ViewModels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, OnTopic.AspNetCore.Mvc.Host +@addTagHelper *, OnTopic.AspNetCore.Mvc.Host \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/_ViewStart.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/_ViewStart.cshtml index 914e7249..9b511c61 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/_ViewStart.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ Layout = "~/Views/Layout/_Layout.cshtml"; -} +} \ No newline at end of file From 2b17f6e909f178cd8ba69d8d3cbbda2136148681 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 14:27:57 -0800 Subject: [PATCH 402/778] Fixed partial references The previous `PartialAsync` call was missing the necessary `await` keyword, and was thus returning a `Task` instead of the rendered content. Whoops. This is a good opportunity to update the markup to use the new, preferred tag helper syntax. --- OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml | 2 +- .../Views/ContentList/IndexedList.cshtml | 2 +- OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml index 052906ea..04b3d1d4 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml @@ -1,4 +1,4 @@ -@Html.PartialAsync("~/Views/ContentList/ContentList.cshtml") + \ No newline at end of file From 73e7eb7aee48adda87e283a2052f48508c6f70cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 15:16:50 -0800 Subject: [PATCH 405/778] Removed trailing space --- OnTopic.ViewModels/Properties/AssemblyInfo.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OnTopic.ViewModels/Properties/AssemblyInfo.cs b/OnTopic.ViewModels/Properties/AssemblyInfo.cs index ddc26641..97e2bcdd 100644 --- a/OnTopic.ViewModels/Properties/AssemblyInfo.cs +++ b/OnTopic.ViewModels/Properties/AssemblyInfo.cs @@ -13,5 +13,4 @@ \-----------------------------------------------------------------------------------------------------------------------------*/ [assembly: ComVisible(false)] [assembly: CLSCompliant(true)] -[assembly: Guid("e52fc633-b4c5-4a2b-8caf-30e756d7a6a7")] - +[assembly: Guid("e52fc633-b4c5-4a2b-8caf-30e756d7a6a7")] \ No newline at end of file From 2f79cbbac7948331833a504d0c9b2042a0703797 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 15:17:10 -0800 Subject: [PATCH 406/778] Removed unused suppressions file This was previously used, but all suppressions have sense been removed --- OnTopic/GlobalSuppressions.cs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 OnTopic/GlobalSuppressions.cs diff --git a/OnTopic/GlobalSuppressions.cs b/OnTopic/GlobalSuppressions.cs deleted file mode 100644 index d7e85b98..00000000 --- a/OnTopic/GlobalSuppressions.cs +++ /dev/null @@ -1,7 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - From f6865c08c61eb09a5be950d567b5a3163e277b12 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 15:17:23 -0800 Subject: [PATCH 407/778] Removed unused `using` statements --- OnTopic/Attributes/AttributeValue.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index b727346f..d9786460 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Collections; using OnTopic.Metadata; using OnTopic.Repositories; From b2d31ae048335e0d398775a7b22651fa78425409 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 21 Jan 2021 15:28:11 -0800 Subject: [PATCH 408/778] Added missing `using` statement This issue was introduced when refactoring the `OnTopic.ViewModels` project, but not immediately picked up since it was due to an Xml Doc reference. --- OnTopic.Tests/TopicMappingServiceTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index d4c8911e..b5d62013 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -23,6 +23,7 @@ using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; using OnTopic.ViewModels; +using OnTopic.ViewModels.Items; namespace OnTopic.Tests { From 1668ae906d4af6f2a4b29aa309f4971f539f75ab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 14:56:37 -0800 Subject: [PATCH 409/778] Removed unnecessary suppressions It's not entirely clear what these suppressions were for originally, but they no longer appear to be necessary. (Today, we prefer code-based suppressions along with either local context or, otherwise, an explanation. We only use the project based suppressions when absolutely necessary.) --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 -- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 -- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 -- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 -- OnTopic/OnTopic.csproj | 2 -- 5 files changed, 10 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 9d13ba09..6b267fde 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -32,11 +32,9 @@ full false latest - 1701;1702;CA1303; pdbonly - 1701;1702;CA1303 diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 7fb0c90a..7fafb2c7 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -32,12 +32,10 @@ full false latest - 1701;1702;CA1303 pdbonly false - 1701;1702;CA1303 diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 185fdb5c..626add6f 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -30,11 +30,9 @@ full false latest - 1701;1702;CA1303 pdbonly - 1701;1702;CA1303 diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index f6cfb432..6c0a7309 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -30,13 +30,11 @@ full - CS1591,CA1056,CA1303 false latest pdbonly - CA1303 diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index c47946f5..608c14d4 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -32,12 +32,10 @@ full bin\$(Configuration)\OnTopic.XML false - 1701;1702;CA1303 pdbonly false - 1701;1702;CA1303 From 9ba3359f2980bbfba1891595e3bf8d9becbd2a20 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 15:08:35 -0800 Subject: [PATCH 410/778] Renamed attribute types descriptors to include `Descriptor` suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the content types for attribute types, such as `TextAttribute`, ended with the `Attribute` suffix. This introduces two points of confusion. First, they are actually attribute _descriptors_—and, indeed, derive from `AttributeDescriptor`. Second, in their C# implementation, this causes problems because the `Attribute` suffix is usually reserved for actual `[Attribute]`s. To remedy these issues, the content type is renamed to use the `AttributeDescriptor` suffix (e.g., `TextAttributeDescriptor`). This requires updating the keys of the appropriate `ContentTypeDescriptor`s, as well as the content types of the corresponding implementations of those attribute descriptors in the database. --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 1d4a638b..e48fa802 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -120,4 +120,21 @@ WHERE AttributeKey LIKE '%ID' UPDATE TopicReferences SET ReferenceKey = 'DerivedTopic' -WHERE ReferenceKey = 'Topic' \ No newline at end of file +WHERE ReferenceKey = 'Topic' + +-------------------------------------------------------------------------------------------------------------------------------- +-- MIGRATE ATTRIBUTE KEYS +-------------------------------------------------------------------------------------------------------------------------------- +-- In OnTopic 5, attribute content types have been renamed to have the suffix "AttributeDescriptor" instead of just "Attribute". +-- This has a number of benefits, including consistency with the base "AttributeDescriptor" content type, and avoiding a naming +-- conflict with .NET's own "*Attribute" convention (which is usually reserved for actual attributes). +-------------------------------------------------------------------------------------------------------------------------------- + +UPDATE Topics +SET TopicKey = TopicKey + 'Descriptor' +WHERE TopicKey LIKE '%Attribute' +AND ContentType = 'ContentTypeDescriptor' + +UPDATE Topics +SET ContentType = ContentType + 'Descriptor' +WHERE ContentType LIKE '%Attribute' \ No newline at end of file From 3ec62f1388050308fbfd0a02071798c0d3978838 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 15:13:15 -0800 Subject: [PATCH 411/778] Strip `AttributeDescriptor` from `EditorType` value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `EditorType` is intended to enforce the convention of converting the current type name—representing a content type—to the identifier used by the OnTopic Editor. Previously, this involved stripping the `Attribute` suffix from the type name. Now that we use the more descriptive and accurate `AttributeDescriptor` suffix (9ba3359), this needs to be updated to account for that. --- OnTopic/Metadata/AttributeDescriptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index e469eeae..400df943 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -109,7 +109,7 @@ protected AttributeDescriptor( /// !value.Contains(" ") && !value.Contains("/") /// [AttributeSetter] - public virtual string EditorType => GetType().Name.Replace("Attribute", "", StringComparison.OrdinalIgnoreCase); + public virtual string EditorType => GetType().Name.Replace("AttributeDescriptor", "", StringComparison.OrdinalIgnoreCase); /*========================================================================================================================== | PROPERTY: DISPLAY GROUP From 69c8286fc740f74ebd93aa8df5a6c2f6ccaa9f3c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 15:43:45 -0800 Subject: [PATCH 412/778] Update unit tests, test doubles to use `AttributeDescriptor` suffix To accurately test the updated naming convention for attribute types, the test doubles and models need to be updated to use the `AttributeDescriptor` suffix, and the unit tests need to be updated to expect them. --- ...ibute.cs => BooleanAttributeDescriptor.cs} | 6 ++-- ... => NestedTopicListAttributeDescriptor.cs} | 6 ++-- ....cs => RelationshipAttributeDescriptor.cs} | 6 ++-- ...ttribute.cs => TextAttributeDescriptor.cs} | 6 ++-- ...s => TopicReferenceAttributeDescriptor.cs} | 6 ++-- OnTopic.TestDoubles/StubTopicRepository.cs | 34 +++++++++---------- .../ReferenceTopicBindingModel.cs | 2 +- .../TextAttributeTopicBindingModel.cs | 4 +-- .../ReverseTopicMappingServiceTest.cs | 26 +++++++------- .../TestDoubles/FakeViewModelLookupService.cs | 4 +-- OnTopic.Tests/TopicMappingServiceTest.cs | 2 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 +-- ... TextAttributeDescriptorTopicViewModel.cs} | 6 ++-- ...renceAttributeDescriptorTopicViewModel.cs} | 6 ++-- 14 files changed, 59 insertions(+), 59 deletions(-) rename OnTopic.TestDoubles/Metadata/{BooleanAttribute.cs => BooleanAttributeDescriptor.cs} (91%) rename OnTopic.TestDoubles/Metadata/{NestedTopicListAttribute.cs => NestedTopicListAttributeDescriptor.cs} (92%) rename OnTopic.TestDoubles/Metadata/{RelationshipAttribute.cs => RelationshipAttributeDescriptor.cs} (92%) rename OnTopic.TestDoubles/Metadata/{TextAttribute.cs => TextAttributeDescriptor.cs} (92%) rename OnTopic.TestDoubles/Metadata/{TopicReferenceAttribute.cs => TopicReferenceAttributeDescriptor.cs} (92%) rename OnTopic.Tests/ViewModels/Metadata/{TextAttributeTopicViewModel.cs => TextAttributeDescriptorTopicViewModel.cs} (86%) rename OnTopic.Tests/ViewModels/Metadata/{TopicReferenceAttributeTopicViewModel.cs => TopicReferenceAttributeDescriptorTopicViewModel.cs} (84%) diff --git a/OnTopic.TestDoubles/Metadata/BooleanAttribute.cs b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs similarity index 91% rename from OnTopic.TestDoubles/Metadata/BooleanAttribute.cs rename to OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs index 767114a5..4d20e6d0 100644 --- a/OnTopic.TestDoubles/Metadata/BooleanAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs @@ -8,7 +8,7 @@ namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ - | CLASS: BOOLEAN ATTRIBUTE (DESCRIPTOR) + | CLASS: BOOLEAN (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents metadata for describing an boolean attribute type, including information on how it will be presented and @@ -18,13 +18,13 @@ namespace OnTopic.TestDoubles.Metadata { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class BooleanAttribute : AttributeDescriptor { + public class BooleanAttributeDescriptor : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public BooleanAttribute( + public BooleanAttributeDescriptor( string key, string contentType, Topic parent, diff --git a/OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs similarity index 92% rename from OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs rename to OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs index 20ede770..de21ed9f 100644 --- a/OnTopic.TestDoubles/Metadata/NestedTopicListAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs @@ -8,7 +8,7 @@ namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ - | CLASS: NESTED TOPIC LIST ATTRIBUTE (DESCRIPTOR) + | CLASS: NESTED TOPIC LIST (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents metadata for describing a nested topic list attribute type, including information on how it will be presented @@ -18,13 +18,13 @@ namespace OnTopic.TestDoubles.Metadata { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class NestedTopicListAttribute : AttributeDescriptor { + public class NestedTopicListAttributeDescriptor : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public NestedTopicListAttribute( + public NestedTopicListAttributeDescriptor( string key, string contentType, Topic parent, diff --git a/OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs similarity index 92% rename from OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs rename to OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs index eafa88e4..d87c6f15 100644 --- a/OnTopic.TestDoubles/Metadata/RelationshipAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs @@ -8,7 +8,7 @@ namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ - | CLASS: RELATIONSHIP ATTRIBUTE (DESCRIPTOR) + | CLASS: RELATIONSHIP (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents metadata for describing a relationship attribute type, including information on how it will be presented and @@ -18,13 +18,13 @@ namespace OnTopic.TestDoubles.Metadata { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class RelationshipAttribute : AttributeDescriptor { + public class RelationshipAttributeDescriptor : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public RelationshipAttribute( + public RelationshipAttributeDescriptor( string key, string contentType, Topic parent, diff --git a/OnTopic.TestDoubles/Metadata/TextAttribute.cs b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs similarity index 92% rename from OnTopic.TestDoubles/Metadata/TextAttribute.cs rename to OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs index 8724f188..8411f658 100644 --- a/OnTopic.TestDoubles/Metadata/TextAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs @@ -8,7 +8,7 @@ namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ - | CLASS: TEXT ATTRIBUTE (DESCRIPTOR) + | CLASS: TEXT (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents metadata for describing a text attribute type, including information on how it will be presented and @@ -18,13 +18,13 @@ namespace OnTopic.TestDoubles.Metadata { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class TextAttribute : AttributeDescriptor { + public class TextAttributeDescriptor : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public TextAttribute( + public TextAttributeDescriptor( string key, string contentType, Topic parent, diff --git a/OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs similarity index 92% rename from OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs rename to OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs index 3ee437c1..e8381267 100644 --- a/OnTopic.TestDoubles/Metadata/TopicReferenceAttribute.cs +++ b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs @@ -8,7 +8,7 @@ namespace OnTopic.TestDoubles.Metadata { /*============================================================================================================================ - | CLASS: TOPIC REFERENCE ATTRIBUTE (DESCRIPTOR) + | CLASS: TOPIC REFERENCE (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents metadata for describing a topic reference attribute type, including information on how it will be presented @@ -18,13 +18,13 @@ namespace OnTopic.TestDoubles.Metadata { /// This class is primarily used by the Topic Editor interface to determine how attributes are displayed as part of the /// CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// - public class TopicReferenceAttribute : AttributeDescriptor { + public class TopicReferenceAttributeDescriptor : AttributeDescriptor { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public TopicReferenceAttribute( + public TopicReferenceAttributeDescriptor( string key, string contentType, Topic parent, diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 007ebdb2..2e2c982f 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -219,15 +219,15 @@ private Topic CreateFakeData() { var configuration = TopicFactory.Create("Configuration", "Container", rootTopic); var contentTypes = TopicFactory.Create("ContentTypes", "ContentTypeDescriptor", configuration); - addAttribute(contentTypes, "Key", "TextAttribute", false, true); - addAttribute(contentTypes, "ContentType", "TextAttribute", false, true); - addAttribute(contentTypes, "Title", "TextAttribute", true, true); - addAttribute(contentTypes, "DerivedTopic", "TopicReferenceAttribute", false); + addAttribute(contentTypes, "Key", "TextAttributeDescriptor", false, true); + addAttribute(contentTypes, "ContentType", "TextAttributeDescriptor", false, true); + addAttribute(contentTypes, "Title", "TextAttributeDescriptor", true, true); + addAttribute(contentTypes, "DerivedTopic", "TopicReferenceAttributeDescriptor", false); var contentTypeDescriptor = TopicFactory.Create("ContentTypeDescriptor", "ContentTypeDescriptor", contentTypes); - addAttribute(contentTypeDescriptor, "ContentTypes", "RelationshipAttribute"); - addAttribute(contentTypeDescriptor, "Attributes", "NestedTopicListAttribute"); + addAttribute(contentTypeDescriptor, "ContentTypes", "RelationshipAttributeDescriptor"); + addAttribute(contentTypeDescriptor, "Attributes", "NestedTopicListAttributeDescriptor"); TopicFactory.Create("Container", "ContentTypeDescriptor", contentTypes); TopicFactory.Create("Lookup", "ContentTypeDescriptor", contentTypes); @@ -236,22 +236,22 @@ private Topic CreateFakeData() { var attributeDescriptor = (ContentTypeDescriptor)TopicFactory.Create("AttributeDescriptor", "ContentTypeDescriptor", contentTypes); - addAttribute(attributeDescriptor, "DefaultValue", "TextAttribute", false, true); - addAttribute(attributeDescriptor, "IsRequired", "TextAttribute", false, true); + addAttribute(attributeDescriptor, "DefaultValue", "TextAttributeDescriptor", false, true); + addAttribute(attributeDescriptor, "IsRequired", "TextAttributeDescriptor", false, true); - TopicFactory.Create("BooleanAttribute", "ContentTypeDescriptor", attributeDescriptor); - TopicFactory.Create("NestedTopicListAttribute", "ContentTypeDescriptor", attributeDescriptor); - TopicFactory.Create("NumberAttribute", "ContentTypeDescriptor", attributeDescriptor); - TopicFactory.Create("RelationshipAttribute", "ContentTypeDescriptor", attributeDescriptor); - TopicFactory.Create("TextAttribute", "ContentTypeDescriptor", attributeDescriptor); - TopicFactory.Create("TopicReferenceAttribute", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("BooleanAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("NestedTopicListAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("NumberAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("RelationshipAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("TextAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); + TopicFactory.Create("TopicReferenceAttributeDescriptor", "ContentTypeDescriptor", attributeDescriptor); var pageContentType = TopicFactory.Create("Page", "ContentTypeDescriptor", contentTypes); addAttribute(pageContentType, "MetaTitle"); addAttribute(pageContentType, "MetaDescription"); - addAttribute(pageContentType, "IsHidden", "TextAttribute", false); - addAttribute(pageContentType, "TopicReference", "TopicReferenceAttribute", false); + addAttribute(pageContentType, "IsHidden", "TextAttributeDescriptor", false); + addAttribute(pageContentType, "TopicReference", "TopicReferenceAttributeDescriptor", false); pageContentType.Relationships.SetTopic("ContentTypes", pageContentType); pageContentType.Relationships.SetTopic("ContentTypes", contentTypeDescriptor); @@ -268,7 +268,7 @@ private Topic CreateFakeData() { AttributeDescriptor addAttribute( Topic contentType, string attributeKey, - string editorType = "TextAttribute", + string editorType = "TextAttributeDescriptor", bool isExtended = true, bool isRequired = false ) { diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index 88bf5b57..60f186fd 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -20,7 +20,7 @@ namespace OnTopic.Tests.BindingModels { /// public class ReferenceTopicBindingModel : BasicTopicBindingModel { - public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttribute") { } + public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttributeDescriptor") { } public RelatedTopicBindingModel? DerivedTopic { get; set; } diff --git a/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs index 530877cc..d1b87d78 100644 --- a/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs @@ -12,14 +12,14 @@ namespace OnTopic.Tests.BindingModels { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a minimal implementation of a custom topic binding model with a couple of scalar values mapping to properties - /// on the content type. + /// on the content type. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// public class TextAttributeTopicBindingModel : AttributeDescriptorTopicBindingModel { - public TextAttributeTopicBindingModel(string? key = null) : base(key, "TextAttribute") { } + public TextAttributeTopicBindingModel(string? key = null) : base(key, "TextAttributeDescriptor") { } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index dac5e63d..e0a55c67 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -67,16 +67,16 @@ public async Task Map_Generic_ReturnsNewTopic() { var bindingModel = new TextAttributeTopicBindingModel() { Key = "Test", - ContentType = "TextAttribute", + ContentType = "TextAttributeDescriptor", Title = "Test Attribute", DefaultValue = "Hello", IsRequired = true }; - var target = await mappingService.MapAsync(bindingModel).ConfigureAwait(false); + var target = await mappingService.MapAsync(bindingModel).ConfigureAwait(false); Assert.AreEqual("Test", target.Key); - Assert.AreEqual("TextAttribute", target.ContentType); + Assert.AreEqual("TextAttributeDescriptor", target.ContentType); Assert.AreEqual("Test Attribute", target.Title); Assert.AreEqual("Hello", target.DefaultValue); Assert.AreEqual(true, target.IsRequired); @@ -97,17 +97,17 @@ public async Task Map_Dynamic_ReturnsNewTopic() { var bindingModel = new TextAttributeTopicBindingModel { Key = "Test", - ContentType = "TextAttribute", + ContentType = "TextAttributeDescriptor", Title = "Test Attribute", DefaultValue = "Hello", IsRequired = true }; - var target = (TextAttribute?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); + var target = (TextAttributeDescriptor?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); Assert.IsNotNull(target); Assert.AreEqual("Test", target.Key); - Assert.AreEqual("TextAttribute", target.ContentType); + Assert.AreEqual("TextAttributeDescriptor", target.ContentType); Assert.AreEqual("Test Attribute", target.Title); Assert.AreEqual("Hello", target.DefaultValue); Assert.AreEqual(true, target.IsRequired); @@ -128,13 +128,13 @@ public async Task Map_Existing_ReturnsUpdatedTopic() { var bindingModel = new TextAttributeTopicBindingModel() { Key = "Test", - ContentType = "TextAttribute", + ContentType = "TextAttributeDescriptor", Title = null, DefaultValue = "World", IsRequired = false }; - var target = (TextAttribute?)TopicFactory.Create("Test", "TextAttribute"); + var target = (TextAttributeDescriptor?)TopicFactory.Create("Test", "TextAttributeDescriptor"); target.Title = "Original Attribute"; target.DefaultValue = "Hello"; @@ -143,10 +143,10 @@ public async Task Map_Existing_ReturnsUpdatedTopic() { target.Attributes.SetValue("Description", "Original Description"); - target = (TextAttribute?)await mappingService.MapAsync(bindingModel, target).ConfigureAwait(false); + target = (TextAttributeDescriptor?)await mappingService.MapAsync(bindingModel, target).ConfigureAwait(false); Assert.AreEqual("Test", target.Key); - Assert.AreEqual("TextAttribute", target.ContentType); + Assert.AreEqual("TextAttributeDescriptor", target.ContentType); Assert.AreEqual("Test", target.Title); //Should inherit from "Key" since it will be null Assert.AreEqual("World", target.DefaultValue); Assert.AreEqual(false, target.IsRequired); @@ -262,8 +262,8 @@ public async Task Map_NestedTopics_ReturnsMappedTopic() { var topic = TopicFactory.Create("Test", "ContentTypeDescriptor"); var attributes = TopicFactory.Create("Attributes", "List", topic); - var attribute3 = (AttributeDescriptor)TopicFactory.Create("Attribute3", "TextAttribute", attributes); - var attribute4 = TopicFactory.Create("Attribute4", "TextAttribute", attributes); + var attribute3 = (AttributeDescriptor)TopicFactory.Create("Attribute3", "TextAttributeDescriptor", attributes); + var attribute4 = TopicFactory.Create("Attribute4", "TextAttributeDescriptor", attributes); attribute3.DefaultValue = "Original Value"; @@ -295,7 +295,7 @@ public async Task Map_TopicReferences_ReturnsMappedTopic() { } }; - var target = (TopicReferenceAttribute?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); + var target = (TopicReferenceAttributeDescriptor?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); Assert.IsNotNull(target.DerivedTopic); Assert.AreEqual("Title", target.DerivedTopic.Key); diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index 6266632b..4e9fb046 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -55,7 +55,7 @@ public FakeViewModelLookupService() : base(null, typeof(object)) { Add(typeof(RelationWithChildrenTopicViewModel)); Add(typeof(RequiredObjectTopicViewModel)); Add(typeof(RequiredTopicViewModel)); - Add(typeof(TopicReferenceAttributeTopicViewModel)); + Add(typeof(TopicReferenceAttributeDescriptorTopicViewModel)); Add(typeof(TopicReferenceTopicViewModel)); /*------------------------------------------------------------------------------------------------------------------------ @@ -64,7 +64,7 @@ public FakeViewModelLookupService() : base(null, typeof(object)) { Add(typeof(AttributeDescriptorTopicViewModel)); Add(typeof(ContentTypeDescriptorTopicViewModel)); Add(typeof(MetadataLookupTopicViewModel)); - Add(typeof(TextAttributeTopicViewModel)); + Add(typeof(TextAttributeDescriptorTopicViewModel)); } diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index b5d62013..808a9a41 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -835,7 +835,7 @@ public async Task Map_GetterMethods_MapMethodOutput() { [TestMethod] public async Task Map_CompatibleProperties_MapObjectReference() { - var topic = (TextAttribute)TopicFactory.Create("Attribute", "TextAttribute"); + var topic = (TextAttributeDescriptor)TopicFactory.Create("Attribute", "TextAttributeDescriptor"); topic.VersionHistory.Add(new(1976, 10, 15, 9, 30, 00)); diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 13fab89e..e9566759 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -605,7 +605,7 @@ public void Save_AttributeDescriptor_UpdatesContentType() { var childContentType = TopicFactory.Create("Child", "ContentTypeDescriptor", contentType) as ContentTypeDescriptor; var attributeCount = childContentType.AttributeDescriptors.Count; - var newAttribute = TopicFactory.Create("NewAttribute", "BooleanAttribute", attributeList) as BooleanAttribute; + var newAttribute = TopicFactory.Create("NewAttribute", "BooleanAttributeDescriptor", attributeList) as BooleanAttributeDescriptor; _topicRepository.Save(newAttribute); @@ -626,7 +626,7 @@ public void Delete_AttributeDescriptor_UpdatesContentTypeCache() { var contentType = TopicFactory.Create("Parent", "ContentTypeDescriptor") as ContentTypeDescriptor; var attributeList = TopicFactory.Create("Attributes", "List", contentType); - var newAttribute = TopicFactory.Create("NewAttribute", "BooleanAttribute", attributeList) as BooleanAttribute; + var newAttribute = TopicFactory.Create("NewAttribute", "BooleanAttributeDescriptor", attributeList) as BooleanAttributeDescriptor; var childContentType = TopicFactory.Create("Child", "ContentTypeDescriptor", contentType) as ContentTypeDescriptor; var attributeCount = childContentType.AttributeDescriptors.Count; diff --git a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs similarity index 86% rename from OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs rename to OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs index 3671a1b6..87c6542d 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs @@ -9,16 +9,16 @@ namespace OnTopic.Tests.ViewModels.Metadata { /*============================================================================================================================ - | VIEW MODEL: TEXT ATTRIBUTE (DESCRIPTOR) + | VIEW MODEL: TEXT (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a dummy implementation of a view model for an view model, in order to - /// allow the dynamic resolution of mapping topics to view models. + /// allow the dynamic resolution of mapping topics to view models. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// - public class TextAttributeTopicViewModel : AttributeDescriptorTopicViewModel { + public class TextAttributeDescriptorTopicViewModel : AttributeDescriptorTopicViewModel { } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeDescriptorTopicViewModel.cs similarity index 84% rename from OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs rename to OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeDescriptorTopicViewModel.cs index fe089233..c058b7b9 100644 --- a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeDescriptorTopicViewModel.cs @@ -9,16 +9,16 @@ namespace OnTopic.Tests.ViewModels.Metadata { /*============================================================================================================================ - | VIEW MODEL: TOPIC REFERENCE ATTRIBUTE (DESCRIPTOR) + | VIEW MODEL: TOPIC REFERENCE (ATTRIBUTE DESCRIPTOR) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a dummy implementation of a view model for an view model, in order to - /// allow the dynamic resolution of mapping topics to view models. + /// allow the dynamic resolution of mapping topics to view models. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// - public class TopicReferenceAttributeTopicViewModel : AttributeDescriptorTopicViewModel { + public class TopicReferenceAttributeDescriptorTopicViewModel : AttributeDescriptorTopicViewModel { } //Class } //Namespace \ No newline at end of file From 2f41af3812cddad04cf37c8452c223a50ea4f9ff Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 15:46:30 -0800 Subject: [PATCH 413/778] Create arbitrary attributes as `TextAttributeDescriptor` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When arbitrary attributes—i.e., attributes that don't map to any attributes registered with the current `ContentTypeDescriptor`—are discovered by `TopicRepositoryBase.GetUnmatchedAttributes()`, they default to use the `TextAttributeDescriptor`. This needed to be updated to reflect the change from `TextAttribute` to `TextAttributeDescriptor`. --- OnTopic/Repositories/TopicRepositoryBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 8f0c2693..2c301ce4 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -681,7 +681,7 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { .Union(topic.Attributes.DeletedAttributes); foreach (var attributeKey in attributeKeys) { if (!attributes.Contains(attributeKey)) { - attributes.Add((AttributeDescriptor)TopicFactory.Create(attributeKey, "TextAttribute")); + attributes.Add((AttributeDescriptor)TopicFactory.Create(attributeKey, "TextAttributeDescriptor")); } } From 9f7a58734ae96bece8d85b82057daf565d03b8f8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 16:04:18 -0800 Subject: [PATCH 414/778] Update `Topic.ParentID` when a topic is moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, when a topic was moved, the `ParentID` was being updated in the `Attribute` table. With the migration of core attributes to the `Topics` table, this is no longer necessary—and, worse, leaves the core `Topics` record in an invalid state (with the `ParentID` pointing to a different parent than the hierarchy represents). This is fixed by updating the logic in `MoveTopic` to correctly update the `Topics` table. --- OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index 4535bdee..3367d111 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -299,10 +299,9 @@ END -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE PARENT ID -------------------------------------------------------------------------------------------------------------------------------- -UPDATE Attributes -SET AttributeValue = CONVERT(NVARCHAR(255), @ParentID) +UPDATE Topics +SET ParentID = @ParentID WHERE TopicID = @TopicID - AND AttributeKey = 'ParentID' -------------------------------------------------------------------------------------------------------------------------------- -- DEBUGGING DATA From 0b0b0e7462c09259279dac746146ee946746d00a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 16:06:19 -0800 Subject: [PATCH 415/778] Remove `MarkClean()` during `TopicRepositoryBase.Move()` This doesn't _hurt_ anything, but it also doesn't _do_ anything since the `ParentId` is no longer stored as an attribute and, therefore, the `Attributes` collection should no longer be marked as `IsDirty()` do to the `ParentId` having been modified. (There's an argument that this _should_ mark the main `Topic` as clean. Currently, the main topic doesn't get marked as `IsDirty()` when the `Parent` changes. We may need to revisit that later.) --- OnTopic.Data.Sql/SqlTopicRepository.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 454982ca..6bdc25eb 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -646,11 +646,6 @@ public override void Move(Topic topic, Topic target, Topic? sibling) { ); } - /*------------------------------------------------------------------------------------------------------------------------ - | Reset dirty status - \-----------------------------------------------------------------------------------------------------------------------*/ - topic.Attributes.MarkClean("ParentId"); - } /*========================================================================================================================== From 49ef2e72deea0c24ae1b6937026f616544a9e4cc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 16:30:29 -0800 Subject: [PATCH 416/778] Ensured `NULL` attribute values are converted to empty values The `AttributeValue` column is not nullable, and `NULL` values (e.g., to overwrite previous versions) are expected to be submitted as empty strings. To make the interface fool-proof, this is enforced within the `UpdateAttributes` stored procedure, thus allowing `NULL` attributes to be submitted, but ensuring they're properly converted to an empty string. --- .../Stored Procedures/UpdateAttributes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql index a40604ba..5547a30f 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql @@ -24,7 +24,7 @@ INTO Attributes ( ) SELECT @TopicID, AttributeKey, - AttributeValue, + ISNULL(AttributeValue, ''), @Version FROM @Attributes New OUTER APPLY ( From 9f420858fd6011c97725c6c01da8c2789d523f08 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 16:31:50 -0800 Subject: [PATCH 417/778] Ensured `GetChildTopicIDs` can accept a `NULL` parent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `ParentID` can be null in the `Topics` table—though this is usually needed exclusively for the `Root` node. Previously, this wasn't accounted for in the `GetChildTopicIDs` function, thus preventing it from e.g. retrieving the root node by including a `@ParentID` parameter of `NULL`. This is fixed in this update. --- .../Functions/GetChildTopicIDs.sql | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql index d72e9372..a4a043f2 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql @@ -17,6 +17,14 @@ AS BEGIN + ------------------------------------------------------------------------------------------------------------------------------ + -- SET DEFAULTS + ------------------------------------------------------------------------------------------------------------------------------ + IF (@TopicID IS NULL) + BEGIN + SET @TopicID = '' + END + ------------------------------------------------------------------------------------------------------------------------------ -- RETRIEVE VALUES ------------------------------------------------------------------------------------------------------------------------------ @@ -24,7 +32,7 @@ BEGIN INTO @Topics SELECT TopicID FROM Topics - WHERE ParentID = @TopicID + WHERE ISNULL(ParentID, '') = @TopicID ------------------------------------------------------------------------------------------------------------------------------ -- RETURN From 90e849a6c0cbccfc2ddfed9fc2e716ad5bae5377 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 16:35:08 -0800 Subject: [PATCH 418/778] Ensured `FindTopicIDs` can accept a `@AttributeValue` of `NULL` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most attribute value columns are not nullable. As such, if a `NULL` value is submitted, it won't match anything. That's fine, given the nature of the procedure. But, normally, we'd expect it to return empty values in that case—e.g., attributes that were overwritten. (These are always treated as empty.) Further, for the core attributes, the `ParentID` column _can_ be `NULL`. As such, this _also_ ensures that `FindTopicIDs` can find, if appropriate, the root node, similar to a previous update to `GetChildTopicIDs` (9f42085). --- OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql index e83980fe..33669f57 100644 --- a/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql +++ b/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql @@ -32,6 +32,14 @@ BEGIN FROM Topics WHERE TopicID = @TopicID + ------------------------------------------------------------------------------------------------------------------------------ + -- SET DEFAULTS + ------------------------------------------------------------------------------------------------------------------------------ + IF (@AttributeValue IS NULL) + BEGIN + SET @AttributeValue = '' + END + ------------------------------------------------------------------------------------------------------------------------------ -- RETRIEVE KEY ATTRIBUTES ------------------------------------------------------------------------------------------------------------------------------ @@ -46,7 +54,7 @@ BEGIN OR @AttributeKey = 'ContentType' AND ContentType = @AttributeValue OR @AttributeKey = 'ParentID' - AND ParentID = @AttributeValue + AND ISNULL(ParentID, '') = @AttributeValue ) RETURN END From 4799a84b8b16a161b6cc250c71a888496c2d485a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 23 Jan 2021 17:00:23 -0800 Subject: [PATCH 419/778] Ensured transaction is completed on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, any errors would result in an immediate return. This left the transaction incomplete, however. To fix this, the transaction—in which nothing has actually been done—will be immediately committed prior to returning. That resolves a bug in which case the following call to `MoveTopic` would fail due to the previous transaction being incomplete. --- OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index 3367d111..40286bac 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -100,6 +100,7 @@ IF @TopicID IS NULL OR @OriginalLeft IS NULL OR @OriginalRight IS NULL 1, -- State, @TopicID ); + COMMIT RETURN END @@ -111,6 +112,7 @@ IF @ParentID IS NULL OR @InsertionPoint IS NULL 1, -- State, @ParentID ); + COMMIT RETURN END @@ -123,6 +125,7 @@ IF @InsertionPoint >= @OriginalLeft AND @InsertionPoint <= @OriginalRight @TopicID, @ParentID ); + COMMIT RETURN END From e657c7bf6e13b157644d265e9248613b4cbb35fd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 25 Jan 2021 13:11:00 -0800 Subject: [PATCH 420/778] Established common `TopicEventArgs` base All of the event args related to topics share a common element: `Topic`. Some _only_ need that element, such as the delete event. Instead of maintaining a `DeleteEventArgs` and then repeating the `Topic` property for each topic-related event args class, instead base those classes off this `TopicEventArgs` class. This will also allow us to introduce additional events, should we choose, without needing to create new event args classes. For instance, if we introduce a `TopicLoaded`, `TopicRefreshed`, or `TopicSaved` event in the future, those could likely just use this `TopicEventArgs` class. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 2 +- OnTopic.Tests/ITopicRepositoryTest.cs | 2 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 2 +- OnTopic/Repositories/ITopicRepository.cs | 4 ++-- OnTopic/Repositories/MoveEventArgs.cs | 12 ++---------- OnTopic/Repositories/RenameEventArgs.cs | 17 ++--------------- .../{DeleteEventArgs.cs => TopicEventArgs.cs} | 19 +++++++++++++------ OnTopic/Repositories/TopicRepositoryBase.cs | 6 +++--- 8 files changed, 25 insertions(+), 39 deletions(-) rename OnTopic/Repositories/{DeleteEventArgs.cs => TopicEventArgs.cs} (60%) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 4d8f1b7d..0b22c466 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -74,7 +74,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override event EventHandler? DeleteEvent { + public override event EventHandler? DeleteEvent { add => _dataProvider.DeleteEvent += value; remove => _dataProvider.DeleteEvent -= value; } diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index a7ab13aa..681ef1a8 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -244,7 +244,7 @@ public void Delete_DeleteEvent_IsFired() { Assert.IsTrue(hasFired); - void eventHandler(object? sender, DeleteEventArgs eventArgs) => hasFired = true; + void eventHandler(object? sender, TopicEventArgs eventArgs) => hasFired = true; } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index e9566759..4dbbabab 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -655,7 +655,7 @@ public void Delete_DeleteEvent_IsFired() { Assert.IsTrue(hasFired); - void eventHandler(object? sender, DeleteEventArgs eventArgs) => hasFired = true; + void eventHandler(object? sender, TopicEventArgs eventArgs) => hasFired = true; } diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 11c5f18a..fd561b43 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -20,9 +20,9 @@ public interface ITopicRepository { | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Instantiates the event handler. + /// Instantiates the event handler. /// - event EventHandler DeleteEvent; + event EventHandler DeleteEvent; /// /// Instantiates the event handler. diff --git a/OnTopic/Repositories/MoveEventArgs.cs b/OnTopic/Repositories/MoveEventArgs.cs index 2fbe6c03..07a72cc5 100644 --- a/OnTopic/Repositories/MoveEventArgs.cs +++ b/OnTopic/Repositories/MoveEventArgs.cs @@ -17,7 +17,7 @@ namespace OnTopic.Repositories { /// /// Allows tracking of the source and destination topics. /// - public class MoveEventArgs : EventArgs { + public class MoveEventArgs : TopicEventArgs { /*========================================================================================================================== | CONSTRUCTOR: TAXONOMY MOVE EVENT ARGS @@ -39,7 +39,7 @@ public class MoveEventArgs : EventArgs { /// /// topic != target /// - public MoveEventArgs(Topic topic, Topic target) { + public MoveEventArgs(Topic topic, Topic target): base(topic) { Contract.Requires(topic, "topic"); Contract.Requires(target, "target"); Contract.Requires(topic != target, "The topic cannot be its own parent."); @@ -47,14 +47,6 @@ public MoveEventArgs(Topic topic, Topic target) { Target = target; } - /*========================================================================================================================== - | PROPERTY: EVENT TOPIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the Topic object associated with the event. - /// - public Topic Topic { get; set; } - /*========================================================================================================================== | PROPERTY: TARGET \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/RenameEventArgs.cs b/OnTopic/Repositories/RenameEventArgs.cs index 2714f64c..c64b0289 100644 --- a/OnTopic/Repositories/RenameEventArgs.cs +++ b/OnTopic/Repositories/RenameEventArgs.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; namespace OnTopic.Repositories { @@ -13,7 +12,7 @@ namespace OnTopic.Repositories { /// /// The RenameEventArgs object defines an event argument type specific to rename events. /// - public class RenameEventArgs : EventArgs { + public class RenameEventArgs : TopicEventArgs { /*========================================================================================================================== | CONSTRUCTOR: TAXONOMY RENAME EVENT ARGS @@ -23,20 +22,8 @@ public class RenameEventArgs : EventArgs { /// on the specified object. /// /// The topic object associated with the rename event. - public RenameEventArgs(Topic topic) { - Topic = topic; + public RenameEventArgs(Topic topic): base(topic) { } - /*========================================================================================================================== - | PROPERTY: TOPIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the Topic object associated with the event. - /// - /// - /// The topic. - /// - public Topic Topic { get; } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/DeleteEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs similarity index 60% rename from OnTopic/Repositories/DeleteEventArgs.cs rename to OnTopic/Repositories/TopicEventArgs.cs index 147679e1..e9e6cc73 100644 --- a/OnTopic/Repositories/DeleteEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -8,21 +8,28 @@ namespace OnTopic.Repositories { /*============================================================================================================================ - | CLASS: TAXONOMY DELETE EVENT ARGS + | CLASS: TOPIC EVENT ARGS \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// The DeleteEventArgs class defines an event argument type specific to deletion events + /// The class defines an event argument type shared among + /// events. It contains the being operated against. /// - public class DeleteEventArgs : EventArgs { + /// + /// All events share at least one shared element: the being operated + /// against. Some, such as the , only relate to that information. Others, + /// such as , need additional information, and thus offer derived classes, such as + /// , to capture additional information. + /// + public class TopicEventArgs : EventArgs { /*========================================================================================================================== | CONSTRUCTOR: TAXONOMY DELETE EVENT ARGS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The topic. - public DeleteEventArgs(Topic topic) : base() { + /// The being operated against. + public TopicEventArgs(Topic topic) : base() { Topic = topic; } diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 2c301ce4..2f0d5a54 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -26,7 +26,7 @@ public abstract class TopicRepositoryBase : ITopicRepository { | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); - private EventHandler? _deleteEvent; + private EventHandler? _deleteEvent; private EventHandler? _moveEvent; private EventHandler? _renameEvent; @@ -35,7 +35,7 @@ public abstract class TopicRepositoryBase : ITopicRepository { \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual event EventHandler? DeleteEvent { + public virtual event EventHandler? DeleteEvent { add => _deleteEvent += value; remove => _deleteEvent -= value; } @@ -459,7 +459,7 @@ public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { /*------------------------------------------------------------------------------------------------------------------------ | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ - var args = new DeleteEventArgs(topic); + var args = new TopicEventArgs(topic); _deleteEvent?.Invoke(this, args); /*------------------------------------------------------------------------------------------------------------------------ From 565be78858270f9621959fe5f305e3f02c415f36 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 25 Jan 2021 13:48:19 -0800 Subject: [PATCH 421/778] Renamed and improved `TopicMoveEventArgs` Renamed `MoveEventArgs` to `TopicMoveEventArgs` to better clarify that it's intended to map to topic operations; this is consistent with .NET CLR conventions. Added the `Source` topic and `Sibling` topic as parameters, properties to fully map the range of options available for move events. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 2 +- OnTopic/Repositories/ITopicRepository.cs | 4 +- OnTopic/Repositories/MoveEventArgs.cs | 59 ------------ OnTopic/Repositories/TopicMoveEventArgs.cs | 91 +++++++++++++++++++ OnTopic/Repositories/TopicRepositoryBase.cs | 6 +- 5 files changed, 97 insertions(+), 65 deletions(-) delete mode 100644 OnTopic/Repositories/MoveEventArgs.cs create mode 100644 OnTopic/Repositories/TopicMoveEventArgs.cs diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 0b22c466..3564a836 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -80,7 +80,7 @@ public override event EventHandler? DeleteEvent { } /// - public override event EventHandler? MoveEvent { + public override event EventHandler? MoveEvent { add => _dataProvider.MoveEvent += value; remove => _dataProvider.MoveEvent -= value; } diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index fd561b43..d4d06d37 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -25,9 +25,9 @@ public interface ITopicRepository { event EventHandler DeleteEvent; /// - /// Instantiates the event handler. + /// Instantiates the event handler. /// - event EventHandler MoveEvent; + event EventHandler MoveEvent; /// /// Instantiates the event handler. diff --git a/OnTopic/Repositories/MoveEventArgs.cs b/OnTopic/Repositories/MoveEventArgs.cs deleted file mode 100644 index 07a72cc5..00000000 --- a/OnTopic/Repositories/MoveEventArgs.cs +++ /dev/null @@ -1,59 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia -| Project Topics Library -\=============================================================================================================================*/ -using System; -using OnTopic.Internal.Diagnostics; - -namespace OnTopic.Repositories { - - /*============================================================================================================================ - | CLASS: MOVE EVENT ARGS - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The MoveEventArgs object defines an event argument type specific to move events. - /// - /// - /// Allows tracking of the source and destination topics. - /// - public class MoveEventArgs : TopicEventArgs { - - /*========================================================================================================================== - | CONSTRUCTOR: TAXONOMY MOVE EVENT ARGS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class and sets the and - /// properties based on the specified objects. - /// - /// The topic object associated with the move event. - /// The parent topic object targeted by the move event. - /// - /// topic is not null - /// - /// - /// target is not null - /// - /// - /// topic != target - /// - public MoveEventArgs(Topic topic, Topic target): base(topic) { - Contract.Requires(topic, "topic"); - Contract.Requires(target, "target"); - Contract.Requires(topic != target, "The topic cannot be its own parent."); - Topic = topic; - Target = target; - } - - /*========================================================================================================================== - | PROPERTY: TARGET - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the new parent that the topic will be moved to. - /// - public Topic Target { get; set; } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/TopicMoveEventArgs.cs b/OnTopic/Repositories/TopicMoveEventArgs.cs new file mode 100644 index 00000000..6150714a --- /dev/null +++ b/OnTopic/Repositories/TopicMoveEventArgs.cs @@ -0,0 +1,91 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC MOVE EVENT ARGUMENTS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines an event arguments relevant to a operation. + /// + /// + /// Allows tracking of the source and destination topics. + /// + public class TopicMoveEventArgs : TopicEventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class and sets the and properties based on the specified objects. + /// + /// The object being moved. + /// The original of the . + /// The new of the . + /// The optional the will be moved after. + /// + /// is not null + /// + /// + /// is not null + /// + /// + /// != + /// + public TopicMoveEventArgs(Topic topic, Topic? source, Topic target, Topic? sibling = null): base(topic) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Vaidate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topic, "topic"); + Contract.Requires(target, "target"); + Contract.Requires(topic != target, "The topic cannot be its own parent."); + Contract.Requires(topic != source, "The topic cannot be its own parent."); + Contract.Requires(topic != sibling, "The topic cannot be moved next to itself."); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Topic = topic; + Source = source; + Target = target; + Sibling = sibling; + + } + + /*========================================================================================================================== + | PROPERTY: SOURCE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the new parent that the is being moved from. + /// + public Topic? Source { get; set; } + + /*========================================================================================================================== + | PROPERTY: TARGET + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the new parent that the is being moved to. + /// + public Topic Target { get; set; } + + /*========================================================================================================================== + | PROPERTY: SIBLING + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the sibling that the is being moved after, if specified. + /// + public Topic? Sibling { get; set; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 2f0d5a54..59acf831 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -27,7 +27,7 @@ public abstract class TopicRepositoryBase : ITopicRepository { \-------------------------------------------------------------------------------------------------------------------------*/ private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); private EventHandler? _deleteEvent; - private EventHandler? _moveEvent; + private EventHandler? _moveEvent; private EventHandler? _renameEvent; /*========================================================================================================================== @@ -41,7 +41,7 @@ public virtual event EventHandler? DeleteEvent { } /// - public virtual event EventHandler? MoveEvent { + public virtual event EventHandler? MoveEvent { add => _moveEvent += value; remove => _moveEvent -= value; } @@ -398,7 +398,7 @@ topic.Parent is not null && | Perform base logic \-----------------------------------------------------------------------------------------------------------------------*/ var previousParent = topic.Parent; - _moveEvent?.Invoke(this, new MoveEventArgs(topic, target)); + _moveEvent?.Invoke(this, new TopicMoveEventArgs(topic, previousParent, target, sibling)); if (sibling is null) { topic.SetParent(target); } From d18e0b49ca2b77f945751d2b39b29bc371085058 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 25 Jan 2021 14:00:55 -0800 Subject: [PATCH 422/778] Renamed and improved `TopicRenameEventArgs` Renamed `RenameEventArgs` to `TopicRenameEventArgs` to better clarify that it's intended to map to topic operations; this is consistent with .NET CLR conventions. Added the `OriginalKey` and `NewKey` parameters, properties to fully map the range of options available for rename events. Technically, this information can be inferred from the `Topic`, but this makes it more explicit. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 2 +- OnTopic/Repositories/ITopicRepository.cs | 4 +- OnTopic/Repositories/RenameEventArgs.cs | 29 --------- OnTopic/Repositories/TopicRenameEventArgs.cs | 64 +++++++++++++++++++ OnTopic/Repositories/TopicRepositoryBase.cs | 6 +- OnTopic/Topic.cs | 2 +- 6 files changed, 71 insertions(+), 36 deletions(-) delete mode 100644 OnTopic/Repositories/RenameEventArgs.cs create mode 100644 OnTopic/Repositories/TopicRenameEventArgs.cs diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 3564a836..90d6755a 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -86,7 +86,7 @@ public override event EventHandler? MoveEvent { } /// - public override event EventHandler? RenameEvent { + public override event EventHandler? RenameEvent { add => _dataProvider.RenameEvent += value; remove => _dataProvider.RenameEvent -= value; } diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index d4d06d37..b7109b56 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -30,9 +30,9 @@ public interface ITopicRepository { event EventHandler MoveEvent; /// - /// Instantiates the event handler. + /// Instantiates the event handler. /// - event EventHandler RenameEvent; + event EventHandler RenameEvent; /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS diff --git a/OnTopic/Repositories/RenameEventArgs.cs b/OnTopic/Repositories/RenameEventArgs.cs deleted file mode 100644 index c64b0289..00000000 --- a/OnTopic/Repositories/RenameEventArgs.cs +++ /dev/null @@ -1,29 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ - -namespace OnTopic.Repositories { - - /*============================================================================================================================ - | CLASS: RENAME EVENT ARGS - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The RenameEventArgs object defines an event argument type specific to rename events. - /// - public class RenameEventArgs : TopicEventArgs { - - /*========================================================================================================================== - | CONSTRUCTOR: TAXONOMY RENAME EVENT ARGS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class and sets the property based - /// on the specified object. - /// - /// The topic object associated with the rename event. - public RenameEventArgs(Topic topic): base(topic) { - } - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/TopicRenameEventArgs.cs b/OnTopic/Repositories/TopicRenameEventArgs.cs new file mode 100644 index 00000000..54451c23 --- /dev/null +++ b/OnTopic/Repositories/TopicRenameEventArgs.cs @@ -0,0 +1,64 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC RENAME EVENT ARGUMENTS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The RenameEventArgs object defines an event argument type specific to rename events. + /// + public class TopicRenameEventArgs : TopicEventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines the event arguments relevant to a operation where the has changed. + /// + /// The object associated with the rename event. + /// The original key of the prior to being renamed. + /// The new key of the after being renamed. + public TopicRenameEventArgs(Topic topic, string originalKey, string newKey): base(topic) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Vaidate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(originalKey, nameof(originalKey)); + Contract.Requires(newKey, nameof(newKey)); + Contract.Requires(originalKey != newKey, "The new key cannot be the same as the old key."); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Topic = topic; + OriginalKey = originalKey; + NewKey = newKey; + + } + + /*========================================================================================================================== + | PROPERTY: ORIGINAL KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the original of the that is being renamed. + /// + public string OriginalKey { get; set; } + + /*========================================================================================================================== + | PROPERTY: NEW KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the new of the that is being renamed. + /// + public string NewKey { get; set; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 59acf831..fc22877d 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -28,7 +28,7 @@ public abstract class TopicRepositoryBase : ITopicRepository { private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); private EventHandler? _deleteEvent; private EventHandler? _moveEvent; - private EventHandler? _renameEvent; + private EventHandler? _renameEvent; /*========================================================================================================================== | EVENT HANDLERS @@ -47,7 +47,7 @@ public virtual event EventHandler? MoveEvent { } /// - public virtual event EventHandler? RenameEvent { + public virtual event EventHandler? RenameEvent { add => _renameEvent += value; remove => _renameEvent -= value; } @@ -313,7 +313,7 @@ public virtual void Save([ValidatedNotNull]Topic topic, bool isRecursive = false | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { - var args = new RenameEventArgs(topic); + var args = new TopicRenameEventArgs(topic, topic.Key, topic.OriginalKey); _renameEvent?.Invoke(this, args); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 64a79c54..9eacd6ea 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -241,7 +241,7 @@ public string Key { /// /// /// The original key is automatically set by when its value is updated (assuming the original key isn't - /// already set). This is, in turn, used by the to represent the original + /// already set). This is, in turn, used by the to represent the original /// value, and thus allow the (or derived providers) from updating the data /// store appropriately. /// From 04395599ca29ef58a6f5a8f60c741f9d0190c89c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 25 Jan 2021 14:01:21 -0800 Subject: [PATCH 423/778] Updated formatting of `Topic` comments for consistency --- OnTopic/Topic.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 9eacd6ea..66d1fc7c 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -566,7 +566,8 @@ public string GetWebPath() { /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. /// /// - /// Returns true if the , , or, optionally, any collections have been modified. + /// Returns true if the , , or, optionally, any collections have been + /// modified. /// public bool IsDirty(bool checkCollections = false, bool excludeLastModified = false) { if (!_isDirty && checkCollections) { @@ -694,17 +695,17 @@ public Topic? DerivedTopic { /// The current 's relationships. public TopicReferenceDictionary References { get; } - /*=========================================================================================================================== + /*========================================================================================================================== | PROPERTY: INCOMING RELATIONSHIPS - \--------------------------------------------------------------------------------------------------------------------------*/ + \-------------------------------------------------------------------------------------------------------------------------*/ /// /// A façade for accessing related topics based on a relationship key; can be used for tags, related topics, etc. /// /// /// The incoming relationships property provides a reverse index of the property, in order to /// indicate which topics point to the current topic. This can be useful for traversing the topic tree as a network graph. - /// This is of particular use for tags, where the current topic represents a tag, and the incoming relationships represents - /// all topics associated with that tag. + /// This is of particular use for tags, where the current topic represents a tag, and the incoming relationships + /// represents all topics associated with that tag. /// /// The current 's incoming relationships. public TopicRelationshipMultiMap IncomingRelationships { get; } From f016318fedca7d9f91d265977c75465de3991571 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 25 Jan 2021 14:03:00 -0800 Subject: [PATCH 424/778] Updated comments in `TopicEventArgs` for consistency --- OnTopic/Repositories/TopicEventArgs.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs index e9e6cc73..f38a358d 100644 --- a/OnTopic/Repositories/TopicEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -8,7 +8,7 @@ namespace OnTopic.Repositories { /*============================================================================================================================ - | CLASS: TOPIC EVENT ARGS + | CLASS: TOPIC EVENT ARGUMENTS \---------------------------------------------------------------------------------------------------------------------------*/ /// /// The class defines an event argument type shared among @@ -18,12 +18,12 @@ namespace OnTopic.Repositories { /// All events share at least one shared element: the being operated /// against. Some, such as the , only relate to that information. Others, /// such as , need additional information, and thus offer derived classes, such as - /// , to capture additional information. + /// , to capture additional information. /// public class TopicEventArgs : EventArgs { /*========================================================================================================================== - | CONSTRUCTOR: TAXONOMY DELETE EVENT ARGS + | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Initializes a new instance of the class. From 2e09267ca507cfd0602c6a23edbeaaa0396e1b1b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:11:26 -0800 Subject: [PATCH 425/778] Updated SQL objects to use high-precision `DATETIME2` Microsoft recommends using `DATETIME2` over `DATETIME` in SQL because it has a broader range and more flexibility over precision. By using a precision of (7), we get the same precision as C#'s `DateTime` object, and thus avoid truncation issues which have caused problems in the past. This allows us to set the version with confidence in C# knowing the return value will be the same. It also allows us to not worry as much during unit tests or automated updates about version conflicts occuring due to overlapping timestamps. (In practice, we probably don't need versions to be _this_ precise, and we _may_ reevaluate that later. But we should definitely be using `DATETIME2`, and can revisit the precision in a future update if necessary.) --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 2 +- .../Stored Procedures/GetTopicUpdates.sql | 2 +- .../Stored Procedures/GetTopicVersion.sql | 2 +- .../Stored Procedures/UpdateAttributes.sql | 2 +- .../Stored Procedures/UpdateExtendedAttributes.sql | 2 +- .../Stored Procedures/UpdateReferences.sql | 2 +- .../Stored Procedures/UpdateRelationships.sql | 2 +- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Attributes.sql | 6 +++--- OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Relationships.sql | 2 +- OnTopic.Data.Sql.Database/Tables/TopicReferences.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Topics.sql | 2 +- .../Utilities/Stored Procedures/ConsolidateVersions.sql | 6 +++--- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index eefb0dc4..faaf78fd 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -11,7 +11,7 @@ CREATE PROCEDURE [dbo].[CreateTopic] @Attributes AttributeValues READONLY, @ExtendedAttributes XML = NULL, @References TopicReferences READONLY, - @Version DATETIME = NULL + @Version DATETIME2(7) = NULL AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql index 8a2e0096..98bba426 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicUpdates.sql @@ -5,7 +5,7 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [dbo].[GetTopicUpdates] - @Since DATETIME + @Since DATETIME2(7) AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql index 0398c7e1..0b37a0c8 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql @@ -6,7 +6,7 @@ CREATE PROCEDURE [dbo].[GetTopicVersion] @TopicID INT = -1, - @Version DATETIME = NULL + @Version DATETIME2(7) = NULL AS -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql index 5547a30f..ddfd65df 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql @@ -8,7 +8,7 @@ CREATE PROCEDURE [dbo].[UpdateAttributes] @TopicID INT, @Attributes AttributeValues READONLY , - @Version DATETIME = NULL , + @Version DATETIME2(7) = NULL , @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql index c6dcbab9..f438bd9b 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql @@ -7,7 +7,7 @@ CREATE PROCEDURE [dbo].[UpdateExtendedAttributes] @TopicID INT, @ExtendedAttributes XML = NULL , - @Version DATETIME = NULL , + @Version DATETIME2(7) = NULL , @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql index df36bbc3..2b30b90a 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -7,7 +7,7 @@ CREATE PROCEDURE [dbo].[UpdateReferences] @TopicID INT, @ReferencedTopics TopicReferences READONLY , - @Version DATETIME = NULL , + @Version DATETIME2(7) = NULL , @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index 5e43ab40..f660b963 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -8,7 +8,7 @@ CREATE PROCEDURE [dbo].[UpdateRelationships] @TopicID INT, @RelationshipKey VARCHAR(255), @RelatedTopics TopicList READONLY , - @Version DATETIME = NULL , + @Version DATETIME2(7) = NULL , @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index a6d1f9b2..8f3bf1a2 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -10,7 +10,7 @@ CREATE PROCEDURE [dbo].[UpdateTopic] @ContentType VARCHAR(128) = NULL , @Attributes AttributeValues READONLY , @ExtendedAttributes XML = NULL , - @Version DATETIME = NULL , + @Version DATETIME2(7) = NULL , @DeleteUnmatched BIT = 0 AS diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql index d9228666..af938cdf 100644 --- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql @@ -9,9 +9,9 @@ CREATE TABLE [dbo].[Attributes] ( [TopicID] INT NOT NULL, - [AttributeKey] VARCHAR (128) NOT NULL, - [AttributeValue] NVARCHAR (255) NOT NULL, - [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() + [AttributeKey] VARCHAR(128) NOT NULL, + [AttributeValue] NVARCHAR(255) NOT NULL, + [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Attributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [AttributeKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql index 5ee52570..24ee6fed 100644 --- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql @@ -9,7 +9,7 @@ CREATE TABLE [dbo].[ExtendedAttributes] ( [TopicID] INT NOT NULL, [AttributesXml] XML NOT NULL, - [Version] DATETIME NOT NULL DEFAULT GETUTCDATE(), + [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE(), CONSTRAINT [PK_ExtendedAttributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [Version] DESC diff --git a/OnTopic.Data.Sql.Database/Tables/Relationships.sql b/OnTopic.Data.Sql.Database/Tables/Relationships.sql index c20da308..facc2a8c 100644 --- a/OnTopic.Data.Sql.Database/Tables/Relationships.sql +++ b/OnTopic.Data.Sql.Database/Tables/Relationships.sql @@ -9,7 +9,7 @@ TABLE [dbo].[Relationships] ( [Source_TopicID] INT NOT NULL, [RelationshipKey] VARCHAR(255) NOT NULL, [IsDeleted] BIT NOT NULL DEFAULT 0, - [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() + [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Relationships] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, [RelationshipKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql index ff061fb5..6c40de5b 100644 --- a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql +++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql @@ -8,7 +8,7 @@ TABLE [dbo].[TopicReferences] ( [Source_TopicID] INT NOT NULL, [ReferenceKey] VARCHAR(128) NOT NULL, [Target_TopicID] INT NULL, - [Version] DATETIME NOT NULL DEFAULT GETUTCDATE() + [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_TopicReferences] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, [ReferenceKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index 9430375a..d5a510da 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -12,7 +12,7 @@ TABLE [dbo].[Topics] ( [TopicKey] VARCHAR(128) NOT NULL, [ContentType] VARCHAR(128) NOT NULL, [ParentID] INT NULL, - [LastModified] DATETIME NOT NULL DEFAULT GETUTCDATE() + [LastModified] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() CONSTRAINT [PK_Topics] PRIMARY KEY CLUSTERED ( [TopicID] ASC ), diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/ConsolidateVersions.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/ConsolidateVersions.sql index 631fdf08..dd4c56cb 100644 --- a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/ConsolidateVersions.sql +++ b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/ConsolidateVersions.sql @@ -8,8 +8,8 @@ -------------------------------------------------------------------------------------------------------------------------------- CREATE PROCEDURE [Utilities].[ConsolidateVersions] - @StartDate DATETIME = NULL, - @EndDate DATETIME = NULL + @StartDate DATETIME2(7) = NULL, + @EndDate DATETIME2(7) = NULL AS -------------------------------------------------------------------------------------------------------------------------------- @@ -37,7 +37,7 @@ ELSE -------------------------------------------------------------------------------------------------------------------------------- IF (@StartDate IS NULL) BEGIN - SET @StartDate = CONVERT(DATETIME, '2000-01-01') + SET @StartDate = CONVERT(DATETIME2(7), '2000-01-01') END -------------------------------------------------------------------------------------------------------------------------------- From 1ca4755982fb1595aa213506a806dc72058a7a59 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:14:28 -0800 Subject: [PATCH 426/778] Updated SQL calls to use `DATETIME2` This aligns them with the update of the stored procedures and underlying tables to use `DATETIME2(7)` (2e09267). --- OnTopic.Data.Sql/SqlCommandExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/SqlCommandExtensions.cs b/OnTopic.Data.Sql/SqlCommandExtensions.cs index 90ea3897..8ee245c5 100644 --- a/OnTopic.Data.Sql/SqlCommandExtensions.cs +++ b/OnTopic.Data.Sql/SqlCommandExtensions.cs @@ -79,7 +79,7 @@ internal static void AddParameter(this SqlCommand command, string sqlParameter, /// The SQL parameter. /// The SQL field value. internal static void AddParameter(this SqlCommand command, string sqlParameter, DateTime fieldValue) - => AddParameter(command, sqlParameter, fieldValue, SqlDbType.DateTime); + => AddParameter(command, sqlParameter, fieldValue, SqlDbType.DateTime2); /// /// Wrapper function that adds a SQL parameter to a command object. @@ -152,6 +152,7 @@ private static void AddParameter( parameter.Value = sqlDbType switch { SqlDbType.Bit => (bool)fieldValue, SqlDbType.DateTime => (DateTime)fieldValue, + SqlDbType.DateTime2 => (DateTime)fieldValue, SqlDbType.Int => (int)fieldValue, SqlDbType.Xml => (string)fieldValue, SqlDbType.Structured => (DataTable)fieldValue, From c34ee29186b1c0d40f7aa5a80a26dcfdcf391822 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:18:21 -0800 Subject: [PATCH 427/778] Prefer CLR's `DateTime` to `SqlDateTime` The `SqlDateTime` data type was used as a shorthand for rounding the precision to that which was expected by SQL's `DATETIME` data type. Now that we're using `DATETIME2(7)` in SQL (2e09267), that is no longer necessary; the two data types have the same precision. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 6bdc25eb..8d745a6d 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -341,7 +341,7 @@ public override void Save([NotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Establish dependencies \-----------------------------------------------------------------------------------------------------------------------*/ - var version = new SqlDateTime(DateTime.UtcNow); + var version = DateTime.UtcNow; var unresolvedTopics = new List(); using var connection = new SqlConnection(_connectionString); @@ -396,7 +396,7 @@ private void Save( bool isRecursive, SqlConnection connection, List unresolvedRelationships, - SqlDateTime version + DateTime version ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -531,7 +531,7 @@ SqlDateTime version command.AddParameter("Key", topic.Key); command.AddParameter("ContentType", topic.ContentType); } - command.AddParameter("Version", version.Value); + command.AddParameter("Version", version); if (areAttributesDirty) { command.AddParameter("Attributes", attributeValues); command.AddParameter("ExtendedAttributes", extendedAttributes); @@ -561,11 +561,11 @@ SqlDateTime version PersistReferences(topic, version, connection); } - if (!topic.VersionHistory.Contains(version.Value)) { - topic.VersionHistory.Insert(0, version.Value); + if (!topic.VersionHistory.Contains(version)) { + topic.VersionHistory.Insert(0, version); } - topic.Attributes.MarkClean(version.Value); + topic.Attributes.MarkClean(version); } @@ -700,7 +700,7 @@ public override void Delete(Topic topic, bool isRecursive = false) { /// /// The topic object whose relationships should be persisted. /// The SQL connection. - private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnection connection) { + private static void PersistRelations(Topic topic, DateTime version, SqlConnection connection) { /*------------------------------------------------------------------------------------------------------------------------ | Return blank if the topic has no relations. @@ -734,7 +734,7 @@ private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnec command.AddParameter("TopicID", topicId); command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); - command.AddParameter("Version", version.Value); + command.AddParameter("Version", version); command.AddParameter("DeleteUnmatched", topic.Relationships.IsFullyLoaded); command.ExecuteNonQuery(); @@ -773,7 +773,7 @@ private static void PersistRelations(Topic topic, SqlDateTime version, SqlConnec /// /// The topic object whose references should be persisted. /// The SQL connection. - private static void PersistReferences(Topic topic, SqlDateTime version, SqlConnection connection) { + private static void PersistReferences(Topic topic, DateTime version, SqlConnection connection) { /*------------------------------------------------------------------------------------------------------------------------ | Persist relations to database @@ -793,7 +793,7 @@ private static void PersistReferences(Topic topic, SqlDateTime version, SqlConne // Add Parameters command.AddParameter("TopicID", topicId); command.AddParameter("ReferencedTopics", references); - command.AddParameter("Version", version.Value); + command.AddParameter("Version", version); command.AddParameter("DeleteUnmatched", topic.References.IsFullyLoaded); command.ExecuteNonQuery(); From c45d0837b04bb9ab2e4604dc576d0c85c22f9f3f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:22:01 -0800 Subject: [PATCH 428/778] Set SQL defaults to use high-precision `SYSUTCDATETIME()` Previously, the SQL parameters and columns defaulted to `GETUTCDATE()`, which is lower precision than the `DATETIME2(7)` data type now used for those parameters and columns (2e09267). Updating to `SYSUTCDATETIME()` resolves this discrepancy, and takes full advantage of the available precision. --- .../Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql | 2 +- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 2 +- .../Stored Procedures/UpdateReferences.sql | 2 +- .../Stored Procedures/UpdateRelationships.sql | 2 +- OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Attributes.sql | 2 +- OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Relationships.sql | 2 +- OnTopic.Data.Sql.Database/Tables/TopicReferences.sql | 2 +- OnTopic.Data.Sql.Database/Tables/Topics.sql | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql index c6e2162c..480617d0 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql @@ -32,7 +32,7 @@ INTO topics_TopicAttributes SELECT SourceTopicID, 'Type', AttributeTypes.AttributeValue, - GETDATE() + SYSUTCDATETIME() FROM ( SELECT TopicID AS SourceTopicID, AttributeKey, diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index faaf78fd..b4a7bbce 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -18,7 +18,7 @@ AS -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = GETUTCDATE() +SET @Version = SYSUTCDATETIME() -------------------------------------------------------------------------------------------------------------------------------- -- DECLARE AND SET VARIABLES diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql index 2b30b90a..7db8b5d9 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql @@ -15,7 +15,7 @@ AS -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = GETUTCDATE() +SET @Version = SYSUTCDATETIME() -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NOVEL VALUES diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql index f660b963..86507b74 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql @@ -16,7 +16,7 @@ AS -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = GETUTCDATE() +SET @Version = SYSUTCDATETIME() -------------------------------------------------------------------------------------------------------------------------------- -- INSERT NOVEL VALUES diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql index 8f3bf1a2..8ac81acc 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql @@ -18,7 +18,7 @@ AS -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- IF @Version IS NULL -SET @Version = GETUTCDATE() +SET @Version = SYSUTCDATETIME() -------------------------------------------------------------------------------------------------------------------------------- -- UPDATE KEY ATTRIBUTES diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql index af938cdf..6949851f 100644 --- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql @@ -11,7 +11,7 @@ TABLE [dbo].[Attributes] ( [TopicID] INT NOT NULL, [AttributeKey] VARCHAR(128) NOT NULL, [AttributeValue] NVARCHAR(255) NOT NULL, - [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() + [Version] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME() CONSTRAINT [PK_Attributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [AttributeKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql index 24ee6fed..b64c64b6 100644 --- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql +++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql @@ -9,7 +9,7 @@ CREATE TABLE [dbo].[ExtendedAttributes] ( [TopicID] INT NOT NULL, [AttributesXml] XML NOT NULL, - [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE(), + [Version] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME(), CONSTRAINT [PK_ExtendedAttributes] PRIMARY KEY CLUSTERED ( [TopicID] ASC, [Version] DESC diff --git a/OnTopic.Data.Sql.Database/Tables/Relationships.sql b/OnTopic.Data.Sql.Database/Tables/Relationships.sql index facc2a8c..d62a56c0 100644 --- a/OnTopic.Data.Sql.Database/Tables/Relationships.sql +++ b/OnTopic.Data.Sql.Database/Tables/Relationships.sql @@ -9,7 +9,7 @@ TABLE [dbo].[Relationships] ( [Source_TopicID] INT NOT NULL, [RelationshipKey] VARCHAR(255) NOT NULL, [IsDeleted] BIT NOT NULL DEFAULT 0, - [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() + [Version] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME() CONSTRAINT [PK_Relationships] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, [RelationshipKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql index 6c40de5b..94cdc791 100644 --- a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql +++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql @@ -8,7 +8,7 @@ TABLE [dbo].[TopicReferences] ( [Source_TopicID] INT NOT NULL, [ReferenceKey] VARCHAR(128) NOT NULL, [Target_TopicID] INT NULL, - [Version] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() + [Version] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME() CONSTRAINT [PK_TopicReferences] PRIMARY KEY CLUSTERED ( [Source_TopicID] ASC, [ReferenceKey] ASC, diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql index d5a510da..c3de682e 100644 --- a/OnTopic.Data.Sql.Database/Tables/Topics.sql +++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql @@ -12,7 +12,7 @@ TABLE [dbo].[Topics] ( [TopicKey] VARCHAR(128) NOT NULL, [ContentType] VARCHAR(128) NOT NULL, [ParentID] INT NULL, - [LastModified] DATETIME2(7) NOT NULL DEFAULT GETUTCDATE() + [LastModified] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME() CONSTRAINT [PK_Topics] PRIMARY KEY CLUSTERED ( [TopicID] ASC ), From 1f078af7d75e3599be6a683b25d94908d698b5b2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:28:12 -0800 Subject: [PATCH 429/778] Picked up missing `DATETIME(2)` parameter This was missed as part of the original commit (2e09267). --- OnTopic.Data.Sql.Database/Functions/GetAttributes.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Functions/GetAttributes.sql b/OnTopic.Data.Sql.Database/Functions/GetAttributes.sql index a1ce1ab9..7b8801a7 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetAttributes.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetAttributes.sql @@ -13,7 +13,7 @@ RETURNS @Attributes TABLE AttributeKey NVARCHAR(255) NOT NULL, AttributeValue NVARCHAR(MAX) NOT NULL, IsExtendedAttribute BIT, - Version DATETIME + Version DATETIME2(7) ) AS From 78e69991e12a636dfbc62e8841316513c4701686 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:29:54 -0800 Subject: [PATCH 430/778] Fixed `UpdateReferences` signature The call to `UpdateReferences` accepts a third parameter of `@Version`, but this call was assuming the third parameter was `@DeleteUnmatched`. With `DATETIME`, the conversion from an integer was apparently accepted. With `DATETIME2(7)`, it was not, and caused an error. Whoops! --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index b4a7bbce..80437f0c 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -119,6 +119,7 @@ IF @ReferenceCount > 0 BEGIN EXEC UpdateReferences @TopicID, @References, + @Version, 1 END From 835fca0a36b3e69db76050c3d121c4a7f9e7eae4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 12:36:15 -0800 Subject: [PATCH 431/778] Use integer for `NULL`, not empty Since `dbo.Topics.ParentID` is nullable, we wrap it in an `ISNULL()` check. Previously, out of habit, I defaulted to returning a string for empty. This required doing something similar for `@TopicID`. But `@TopicID` is an `INT`, so converting from an empty string isn't explicit or predictable. Instead, preferably, we'll convert both to an explicit `INT` that's outside the expected range of the table. --- OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql index a4a043f2..337604d1 100644 --- a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql +++ b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql @@ -22,7 +22,7 @@ BEGIN ------------------------------------------------------------------------------------------------------------------------------ IF (@TopicID IS NULL) BEGIN - SET @TopicID = '' + SET @TopicID = -10 END ------------------------------------------------------------------------------------------------------------------------------ @@ -32,7 +32,7 @@ BEGIN INTO @Topics SELECT TopicID FROM Topics - WHERE ISNULL(ParentID, '') = @TopicID + WHERE ISNULL(ParentID, -10) = @TopicID ------------------------------------------------------------------------------------------------------------------------------ -- RETURN From 41166336d2bcc36761b747d9ffa703eb94ffc95e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 13:36:16 -0800 Subject: [PATCH 432/778] Allow `AttributeDescriptor` to be initialized directly Previously, `AttributeDescriptor` was marked as `public`, but we moved it to `abstract` in OnTopic 4.0.0 in preference for strongly typed `AttributeDescriptor` classes, such as `TextAttribute`. In OnTopic 5.0.0, we're moving those strongly typed `AttributeDescriptor` classes into their corresponding OnTopic Editor plugins (1d122e0). That allows `AttributeTypeDescriptor` instances to be defined dynamically by including external packages. As a result, however, that means that these types won't be locally available unless that plugin is included. If an application doesn't expose the OnTopic Editor, there isn't any reason to include that plugin. In that case, these attribute classes will fall back to being instantiated as a `Topic`. That prevents them from being used as `AttributeDescriptor`s during e.g. `SqlTopicRepository.Save()`. But, outside of OnTopic Editor, the properties most applications need to read for attribute descriptors are on the main `AttributeDescriptor` type itself. As such, we're making it `public` again so that it can be initialized as a fallback for cases where the strongly typed versions aren't in scope. A subsequent update will include this logic in the `DynamicTopicLookupService`. --- OnTopic/Metadata/AttributeDescriptor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 400df943..355f89a8 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -37,7 +37,7 @@ namespace OnTopic.Metadata { /// the CMS; except in very specific scenarios, it is not typically used elsewhere in the Topic Library itself. /// /// - public abstract class AttributeDescriptor : Topic { + public class AttributeDescriptor : Topic { /*========================================================================================================================== | CONSTRUCTOR @@ -62,7 +62,7 @@ public abstract class AttributeDescriptor : Topic { /// Thrown when the class representing the content type is found, but doesn't derive from . /// /// A strongly-typed instance of the class based on the target content type. - protected AttributeDescriptor( + public AttributeDescriptor( string key, string contentType, Topic? parent = null, From 4096f208424fd5519f1a6e6597d897619dd3ee56 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 13:38:28 -0800 Subject: [PATCH 433/778] Mark `StaticTypeLookupService`'s `Lookup()` function as `virtual` Most of the `ITypeLookupService` implementations derive from `StaticTypeLookupService`. It therefore acts as a base class in terms of core functionality. By marking `Lookup()` as `virtual`, we allow derived classes to customize the `Lookup()` logic by e.g. providing smart fallbacks based on known business logic of specialized types. --- OnTopic/Lookup/StaticTypeLookupService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index a8d91fb2..8c09fb5c 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -84,7 +84,7 @@ public StaticTypeLookupService( /// exception="T:System.ArgumentException"> /// !contentType.Contains(" ") /// - public Type? Lookup(string typeName) => Contains(typeName) ? _typeCollection[typeName] : DefaultType; + public virtual Type? Lookup(string typeName) => Contains(typeName) ? _typeCollection[typeName] : DefaultType; /*========================================================================================================================== | METHOD: ADD From cd9e5400061cd2c123fa1753d51fad8fc418e9c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 13:44:26 -0800 Subject: [PATCH 434/778] Added `AttributeDescriptor` fallback logic for `DynamicTopicLookupService` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed in a previous commit (4116633), the strongly typed versions of `AttributeDescriptor` are now contained within plugins for the OnTopic Editor (1d122e0), and will only be within scope if the OnTopic Editor is configured and that plugin is included. In absence of that, however, most other applications rely on members provided by the more general `AttributeDescriptor`. Given that, instead of falling back to the default of `Topic`, we want to fall back to `AttributeDescriptor` if the requested `typeName` ends in `AttributeDescriptor`, but the specialized type cannot be found. This ensures that e.g. the `ContentTypeCollection.AttributeTypes` collection is properly initialized, and that code that needs general access to attribute type configuration, such as `SqlTopicRepository`, can still get access to that, even if they don't have the more precise overrides from e.g. `EditorType` or `ModelType` typically exposed by the subclasses. This allows the attribute types to be decoupled from the OnTopic library—as well as the OnTopic Editor—and thus enabled plugins to be dynamically included without introducing problems for applications where those plugins aren't in scope. --- OnTopic/Lookup/DynamicTopicLookupService.cs | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/OnTopic/Lookup/DynamicTopicLookupService.cs b/OnTopic/Lookup/DynamicTopicLookupService.cs index b9995ff8..a5d975c6 100644 --- a/OnTopic/Lookup/DynamicTopicLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicLookupService.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Metadata; namespace OnTopic.Lookup { @@ -27,5 +28,31 @@ public DynamicTopicLookupService() : base( typeof(Topic) ) { } + /*========================================================================================================================== + | METHOD: LOOKUP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The version of will automatically fall back to + /// if the ends with AttributeDescriptor, but a + /// with the specified name cannot be found. This accounts for the fact that strongly + /// typed classes are expected to be in external plugins which may not be available + /// unless the current application is configured to use the OnTopic Editor. In that case, the base class will provide access to the attributes needed by most applications, including the core + /// OnTopic library. + /// + public override Type? Lookup(string typeName) { + if (typeName is null) { + return DefaultType; + } + else if (Contains(typeName)) { + return base.Lookup(typeName); + } + else if (typeName.EndsWith("AttributeDescriptor", StringComparison.OrdinalIgnoreCase)) { + return typeof(AttributeDescriptor); + } + return DefaultType; + } + } //Class } //Namespace \ No newline at end of file From 902b38081d9cb3458d44a563e7f83b2b43a95932 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 14:02:00 -0800 Subject: [PATCH 435/778] Added `ViewModel` fallback for `DynamicTopicViewModelLookupService` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicViewModel` suffix used by convention for topic view models can be a bit unwieldy—and, in some cases, not terribly logical. It also artificially prevents compatibility with more generally used `ViewModel` classes, since e.g. the `TopicMappingService` can map to any POCO. As such, the `Lookup()` method is being overwritten to fall back to `{ContentType}ViewModel` if `{ContentType}TopicViewModel` can't be found. The `TopicViewModel` is still the most specific and may be preferred, but the `ViewModel` option is completely acceptable, and may be preferrable in some cases. --- .../DynamicTopicViewModelLookupService.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index d5090955..b4f2cf8b 100644 --- a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Mapping; namespace OnTopic.Lookup { @@ -23,9 +24,34 @@ public class DynamicTopicViewModelLookupService : DynamicTypeLookupService { /// Establishes a new instance of a . /// public DynamicTopicViewModelLookupService() : base( - t => t.Name.EndsWith("TopicViewModel", StringComparison.InvariantCultureIgnoreCase), + t => t.Name.EndsWith("ViewModel", StringComparison.InvariantCultureIgnoreCase), typeof(object) ) { } + /*========================================================================================================================== + | METHOD: LOOKUP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The version of will first look for + /// the exact . If that fails, and the ends with TopicViewModel + /// , then it will automatically fall back to look for the with the ViewModel + /// suffix. If that cannot be found, it will return the of . This allows implementors to use the shorter name, if preferred, without breaking compatibility with + /// implementations which default to looking for TopicViewModel, such as the . + /// + public override Type? Lookup(string typeName) { + if (typeName is null) { + return DefaultType; + } + else if (Contains(typeName)) { + return base.Lookup(typeName); + } + else if (typeName.EndsWith("TopicViewModel", StringComparison.OrdinalIgnoreCase)) { + return base.Lookup(typeName.Replace("TopicViewModel", "ViewModel", StringComparison.CurrentCultureIgnoreCase)); + } + return DefaultType; + } + } //Class } //Namespace \ No newline at end of file From 5eb7de96cafa4c457b8f87410815953f94ef46bb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 14:13:19 -0800 Subject: [PATCH 436/778] Established unit tests for new `Lookup()` fallbacks This establishes a unit test for the `AttributeDescriptor` fallback in the `DynamicTopicLookupService` (cd9e540), as well as the `ViewModel` fallback in the `DynamicTopicViewModelLookupService` (902b380). --- OnTopic.Tests/ITypeLookupServiceTest.cs | 35 +++++++++++++++++++ OnTopic.Tests/ViewModels/FallbackViewModel.cs | 23 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 OnTopic.Tests/ViewModels/FallbackViewModel.cs diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 8b9a36a0..60d9fae2 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -6,6 +6,7 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Lookup; +using OnTopic.Metadata; using OnTopic.Tests.TestDoubles; using OnTopic.Tests.ViewModels; using OnTopic.ViewModels; @@ -42,5 +43,39 @@ public void Composite_LookupValidType_ReturnsType() { } + /*========================================================================================================================== + | TEST: DYNAMIC TOPIC LOOKUP SERVICE: LOOKUP MISSING ATTRIBUTE DESCRIPTOR: RETURNS ATTRIBUTE DESCRIPTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new and requests a missing attribute type; confirms it correctly + /// falls back to the expected as a logical default. + /// + [TestMethod] + public void DynamicTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAttributeDescriptor() { + + var lookupService = new DynamicTopicLookupService(); + var attributeType = lookupService.Lookup("ArbitraryAttributeDescriptor"); + + Assert.AreEqual(typeof(AttributeDescriptor), attributeType); + + } + + /*========================================================================================================================== + | TEST: DYNAMIC TOPIC VIEW MODEL LOOKUP SERVICE: LOOKUP TOPIC VIEW MODEL: RETURNS FALLBACK VIEW MODEL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new and requests a type with the TopicViewModel + /// suffix; confirms it correctly falls back to a type with the ViewModel suffix. + /// + [TestMethod] + public void DynamicTopicViewModelLookupService_LookupTopicViewModel_ReturnsFallbackViewModel() { + + var lookupService = new DynamicTopicViewModelLookupService(); + var topicViewModel = lookupService.Lookup("FallbackTopicViewModel"); + + Assert.AreEqual(typeof(FallbackViewModel), topicViewModel); + + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/FallbackViewModel.cs b/OnTopic.Tests/ViewModels/FallbackViewModel.cs new file mode 100644 index 00000000..3af9b753 --- /dev/null +++ b/OnTopic.Tests/ViewModels/FallbackViewModel.cs @@ -0,0 +1,23 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Lookup; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: FALLBACK + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a view model with the ViewModel suffix instead of the TopicViewModel conventions to confirm that + /// the will correctly fall back. + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class FallbackViewModel { + + } //Class +} //Namespace \ No newline at end of file From aac3e971ea6a8a6776b723b70d9dd8d4abd741d2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 14:22:16 -0800 Subject: [PATCH 437/778] Added `AttributeDescriptor` fallback logic for `DefaultTopicLookupService` This corresponds to a similar update for `DynamicTopicLookupService` (cd9e540). --- OnTopic/Lookup/DefaultTopicLookupService.cs | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/OnTopic/Lookup/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs index 7e752fa3..99d37256 100644 --- a/OnTopic/Lookup/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -42,5 +42,30 @@ public DefaultTopicLookupService(IEnumerable? types = null, Type? defaultT } + /*========================================================================================================================== + | METHOD: LOOKUP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The version of will automatically fall back to + /// if the ends with AttributeDescriptor, but a + /// with the specified name cannot be found. This accounts for the fact that strongly + /// typed classes are expected to be in external plugins which are not statically + /// registered with the . In that case, the base + /// class will provide access to the attributes needed by most applications, including the core OnTopic library. + /// + public override Type? Lookup(string typeName) { + if (typeName is null) { + return DefaultType; + } + else if (Contains(typeName)) { + return base.Lookup(typeName); + } + else if (typeName.EndsWith("AttributeDescriptor", StringComparison.OrdinalIgnoreCase)) { + return typeof(AttributeDescriptor); + } + return DefaultType; + } + } //Class } //Namespace \ No newline at end of file From 68a8635a9289ee4e4729a7bbda9a512158ddd8d2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 14:23:00 -0800 Subject: [PATCH 438/778] Added `AttributeDescriptor` fallback for `TopicViewModelLookupService` This corresponds to a similar update for `DynamicTopicViewModelLookupService` (902b380). --- .../TopicViewModelLookupService.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index 80bc2a48..fc8b76a6 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -62,5 +62,32 @@ public TopicViewModelLookupService(IEnumerable? types = null, Type? defaul } + /*========================================================================================================================== + | METHOD: LOOKUP + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The version of will first look for + /// the exact . If that fails, and the ends with TopicViewModel + /// , then it will automatically fall back to look for the with the ViewModel + /// suffix. If that cannot be found, it will return the of . This allows implementors to use the shorter name, if preferred, without breaking compatibility with + /// implementations which default to looking for TopicViewModel, such as the . + /// While this convention is not used by the , this fallback provides support for derived classes + /// which may prefer that convention. + /// + public override Type? Lookup(string typeName) { + if (typeName is null) { + return DefaultType; + } + else if (Contains(typeName)) { + return base.Lookup(typeName); + } + else if (typeName.EndsWith("TopicViewModel", StringComparison.OrdinalIgnoreCase)) { + return base.Lookup(typeName.Replace("TopicViewModel", "ViewModel", StringComparison.CurrentCultureIgnoreCase)); + } + return DefaultType; + } + } //Class } //Namespace \ No newline at end of file From bdfe2f354512a1c0e693fa4c0bb40853109dbe63 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 14:28:22 -0800 Subject: [PATCH 439/778] Established unit tests for new `Lookup()` fallbacks This establishes a unit test for the `AttributeDescriptor` fallback in the `DefaultTopicLookupService` (aac3e97), as well as the `ViewModel` fallback in the `TopicViewModelLookupService` (68a8635). --- OnTopic.Tests/ITypeLookupServiceTest.cs | 34 +++++++++++++++++++ .../TestDoubles/FakeViewModelLookupService.cs | 4 ++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 60d9fae2..bd25bc95 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -77,5 +77,39 @@ public void DynamicTopicViewModelLookupService_LookupTopicViewModel_ReturnsFallb } + /*========================================================================================================================== + | TEST: DEFAULT TOPIC LOOKUP SERVICE: LOOKUP MISSING ATTRIBUTE DESCRIPTOR: RETURNS ATTRIBUTE DESCRIPTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new and requests a missing attribute type; confirms it correctly + /// falls back to the expected as a logical default. + /// + [TestMethod] + public void DefaultTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAttributeDescriptor() { + + var lookupService = new DefaultTopicLookupService(); + var attributeType = lookupService.Lookup("ArbitraryAttributeDescriptor"); + + Assert.AreEqual(typeof(AttributeDescriptor), attributeType); + + } + + /*========================================================================================================================== + | TEST: DEFAULT TOPIC VIEW MODEL LOOKUP SERVICE: LOOKUP TOPIC VIEW MODEL: RETURNS FALLBACK VIEW MODEL + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new and requests a type with the TopicViewModel + /// suffix; confirms it correctly falls back to a type with the ViewModel suffix. + /// + [TestMethod] + public void TopicViewModelLookupService_LookupTopicViewModel_ReturnsFallbackViewModel() { + + var lookupService = new FakeViewModelLookupService(); + var topicViewModel = lookupService.Lookup("FallbackTopicViewModel"); + + Assert.AreEqual(typeof(FallbackViewModel), topicViewModel); + + } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index 4e9fb046..59e961f2 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -6,6 +6,7 @@ using OnTopic.Lookup; using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; +using OnTopic.ViewModels; namespace OnTopic.Tests.TestDoubles { @@ -18,7 +19,7 @@ namespace OnTopic.Tests.TestDoubles { /// /// Allows testing of services that depend on without using expensive reflection. /// - public class FakeViewModelLookupService: StaticTypeLookupService { + public class FakeViewModelLookupService: TopicViewModelLookupService { /*========================================================================================================================== | CONSTRUCTOR @@ -40,6 +41,7 @@ public FakeViewModelLookupService() : base(null, typeof(object)) { Add(typeof(DescendentSpecializedTopicViewModel)); Add(typeof(DescendentTopicViewModel)); Add(typeof(DisableMappingTopicViewModel)); + Add(typeof(FallbackViewModel)); Add(typeof(FilteredTopicViewModel)); Add(typeof(FlattenChildrenTopicViewModel)); Add(typeof(InheritedPropertyTopicViewModel)); From fb663a21c3cb548ef3e6beabc136915685fa50dd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 15:30:24 -0800 Subject: [PATCH 440/778] Established new `ObservableTopicRepository` base class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicRepositoryBase` is a very opinionated base class. That's useful! But there are cases where that level of opinion isn't necessary or even desirable, such as the `CachedTopicRepository`. The `ObservableTopicRepository` separates out the basic event handling logic, which is expected to be shared between all implementations—and isn't usually expected to vary significantly—into its own base class. In a future update, the `TopicRepositoryBase` will be updated to take advantage of this, as will decorators, such as the `CachedTopicRepository`, which don't otherwise need the overhead of `TopicRepositoryBase`. --- .../Repositories/ObservableTopicRepository.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 OnTopic/Repositories/ObservableTopicRepository.cs diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs new file mode 100644 index 00000000..bcdedf0d --- /dev/null +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -0,0 +1,100 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Metadata; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC DATA PROVIDER BASE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Defines a base abstract class for taxonomy data providers. + /// + public abstract class ObservableTopicRepository : ITopicRepository { + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private EventHandler? _deleteEvent; + private EventHandler? _moveEvent; + private EventHandler? _renameEvent; + + /*========================================================================================================================== + | EVENT HANDLERS + \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public virtual event EventHandler? DeleteEvent { + add => _deleteEvent += value; + remove => _deleteEvent -= value; + } + + /// + public virtual event EventHandler? MoveEvent { + add => _moveEvent += value; + remove => _moveEvent -= value; + } + + /// + public virtual event EventHandler? RenameEvent { + add => _renameEvent += value; + remove => _renameEvent -= value; + } + + /*========================================================================================================================== + | GET CONTENT TYPE DESCRIPTORS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract ContentTypeDescriptorCollection GetContentTypeDescriptors(); + + /*========================================================================================================================== + | METHOD: LOAD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true); + + /// + public abstract Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); + + /// + public abstract Topic? Load(Topic topic, DateTime version); + + /// + public abstract Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); + + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Refresh(Topic referenceTopic, DateTime since); + + /*========================================================================================================================== + | METHOD: ROLLBACK + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Rollback(Topic topic, DateTime version); + + /*========================================================================================================================== + | METHOD: SAVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Save(Topic topic, bool isRecursive = false); + + /*========================================================================================================================== + | METHOD: MOVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Move(Topic topic, Topic target, Topic? sibling = null); + + /*========================================================================================================================== + | METHOD: DELETE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public abstract void Delete(Topic topic, bool isRecursive); + + } //Class +} //Namespace \ No newline at end of file From 0e57644df1abd2a386dea4827ae2f8dbb8377a25 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 15:35:26 -0800 Subject: [PATCH 441/778] Introduced `OnTopic{Event}` methods The `On{Event}` methods, such as `OnTopicDeleted`, allow derived classes to easily trigger events on the base class, even though they don't actually have direct access to the private `EventHandler<>` fields (e.g., `_deleteEvent`). As this is `virtual`, these also allows derived classes to override these methods to provide their own custom handling of these events without needing to register a delegate. **Note:** I've named these in the past tense, in anticipation of a future update where we'll be renaming the events to be past tense. --- .../Repositories/ObservableTopicRepository.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index bcdedf0d..2439b21e 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -45,6 +45,75 @@ public virtual event EventHandler? RenameEvent { remove => _renameEvent -= value; } + /*========================================================================================================================== + | ON TOPIC DELETED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Raises the . + /// + /// + /// + /// Raising an event invokes the event handler through a delegate. For more information, see Handling and Raising Events. + /// + /// + /// The method also allows derived classes to handle the event without + /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// + /// + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. + /// + /// + /// An instance of the associated with the event. + protected virtual void OnTopicDeleted(DeleteEventArgs args) => _deleteEvent?.Invoke(this, args); + + /*========================================================================================================================== + | ON TOPIC MOVED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Raises the . + /// + /// + /// + /// Raising an event invokes the event handler through a delegate. For more information, see Handling and Raising Events. + /// + /// + /// The method also allows derived classes to handle the event without + /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// + /// + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. + /// + /// + /// An instance of the associated with the event. + protected virtual void OnTopicMoved(MoveEventArgs args) => _moveEvent?.Invoke(this, args); + + /*========================================================================================================================== + | ON TOPIC RENAMED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Raises the . + /// + /// + /// + /// Raising an event invokes the event handler through a delegate. For more information, see Handling and Raising Events. + /// + /// + /// The method also allows derived classes to handle the event without + /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// + /// + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. + /// + /// + /// An instance of the associated with the event. + protected virtual void OnTopicRenamed(RenameEventArgs args) => _renameEvent?.Invoke(this, args); + /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS \-------------------------------------------------------------------------------------------------------------------------*/ From c8afd136aa8e46d3c8ab1fdb720aaa2a8b89940a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 15:39:23 -0800 Subject: [PATCH 442/778] Updated `TopicRepositoryBase` to implement `ObservableTopicRepository` By deriving from `ObservableTopicRepository` (fb663a2), the `TopicRepositoryBase` no longer needs to implement either a) the event handling logic, nor b) any `abstract` members that it isn't interested in overriding. This simplifies the logic. As part of this, methods that it _does_ want to override must be changed from `virtual` to `override`, since they're overriding the `abstract` implementations from `ObservableTopicRepository`. In addition, raising events must be done using the (new) `OnTopic{Event}()` methods (0e57644), since `TopicRepositoryBase` doesn't have access to the private `EventDelegate` fields on `ObservableTopicRepository`. --- OnTopic/Repositories/TopicRepositoryBase.cs | 61 ++++----------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepositoryBase.cs index 2c301ce4..8e3ed7d3 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepositoryBase.cs @@ -20,43 +20,18 @@ namespace OnTopic.Repositories { /// /// Defines a base abstract class for taxonomy data providers. /// - public abstract class TopicRepositoryBase : ITopicRepository { + public abstract class TopicRepositoryBase : ObservableTopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ private readonly ContentTypeDescriptorCollection _contentTypeDescriptors = new(); - private EventHandler? _deleteEvent; - private EventHandler? _moveEvent; - private EventHandler? _renameEvent; - - /*========================================================================================================================== - | EVENT HANDLERS - \-------------------------------------------------------------------------------------------------------------------------*/ - - /// - public virtual event EventHandler? DeleteEvent { - add => _deleteEvent += value; - remove => _deleteEvent -= value; - } - - /// - public virtual event EventHandler? MoveEvent { - add => _moveEvent += value; - remove => _moveEvent -= value; - } - - /// - public virtual event EventHandler? RenameEvent { - add => _renameEvent += value; - remove => _renameEvent -= value; - } /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual ContentTypeDescriptorCollection GetContentTypeDescriptors() { + public override ContentTypeDescriptorCollection GetContentTypeDescriptors() { /*------------------------------------------------------------------------------------------------------------------------ | Initialize content types @@ -226,31 +201,16 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Cont | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ /// - public abstract Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true); - - /// - public abstract Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); - - /// - public Topic? Load(Topic topic, DateTime version) { + public override Topic? Load(Topic topic, DateTime version) { Contract.Requires(topic, nameof(topic)); return Load(topic.Id, version, topic); } - /// - public abstract Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); - - /*========================================================================================================================== - | METHOD: LOAD - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public abstract void Refresh(Topic referenceTopic, DateTime since); - /*========================================================================================================================== | METHOD: ROLLBACK \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { + public override void Rollback([ValidatedNotNull]Topic topic, DateTime version) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -278,7 +238,7 @@ public virtual void Rollback([ValidatedNotNull]Topic topic, DateTime version) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual void Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { + public override void Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -314,7 +274,7 @@ public virtual void Save([ValidatedNotNull]Topic topic, bool isRecursive = false \-----------------------------------------------------------------------------------------------------------------------*/ if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { var args = new RenameEventArgs(topic); - _renameEvent?.Invoke(this, args); + OnTopicRenamed(args); } /*---------------------------------------------------------------------------------------------------------------------- @@ -372,7 +332,7 @@ _contentTypeDescriptors is not null && | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual void Move([ValidatedNotNull]Topic topic, [ValidatedNotNull]Topic target, Topic? sibling = null) { + public override void Move([ValidatedNotNull]Topic topic, [ValidatedNotNull]Topic target, Topic? sibling = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -398,7 +358,8 @@ topic.Parent is not null && | Perform base logic \-----------------------------------------------------------------------------------------------------------------------*/ var previousParent = topic.Parent; - _moveEvent?.Invoke(this, new MoveEventArgs(topic, target)); + var args = new MoveEventArgs(topic, target); + OnTopicMoved(args); if (sibling is null) { topic.SetParent(target); } @@ -424,7 +385,7 @@ topic.Parent is not null && | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { + public override void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -460,7 +421,7 @@ public virtual void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ var args = new DeleteEventArgs(topic); - _deleteEvent?.Invoke(this, args); + OnTopicDeleted(args); /*------------------------------------------------------------------------------------------------------------------------ | Remove from parent From a3bef5d6e9d192f32b266e5537a4ae40b914fc6f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 15:43:58 -0800 Subject: [PATCH 443/778] Updated documentation for the `ObservableTopicRepository` The header hadn't been updated when establishing the `ObservableTopicRepository`. --- OnTopic/Repositories/ObservableTopicRepository.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 2439b21e..ec30e2fd 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -9,11 +9,17 @@ namespace OnTopic.Repositories { /*============================================================================================================================ - | CLASS: TOPIC DATA PROVIDER BASE + | CLASS: OBSERVABLE TOPIC REPOSITORY \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Defines a base abstract class for taxonomy data providers. + /// Provides an abstract base class for implementations which implements the event handling + /// logic. /// + /// + /// All implementations of are expected to need the following logic at minimum. + /// Concrete implementations that are working directly with an underlying data source should prefer to instead derive from + /// the more opinionated , which provides more built-in business logic. + /// public abstract class ObservableTopicRepository : ITopicRepository { /*========================================================================================================================== From 799e2b661734da0ebe717d5c142c3cb854c304a0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 16:15:26 -0800 Subject: [PATCH 444/778] Established new `TopicRepositoryDecorator` base class The `ITopicRepository` is a prime candidate for decorators. Decorators require very specific handling to register the underlying implementation and wire-up passthroughs for both events and members. Outside of that, decorators don't require much in terms of implementation, and thus don't benefit from deriving from `TopicRepositoryBase`. The `TopicRepositoryDecorator` mitigates this by offering a base class just for decorators. It handls all of the wiring so that implementors don't need to worry about that, and can focus exclusively on the parts they need to override. --- .../Repositories/TopicRepositoryDecorator.cs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 OnTopic/Repositories/TopicRepositoryDecorator.cs diff --git a/OnTopic/Repositories/TopicRepositoryDecorator.cs b/OnTopic/Repositories/TopicRepositoryDecorator.cs new file mode 100644 index 00000000..0a2adebd --- /dev/null +++ b/OnTopic/Repositories/TopicRepositoryDecorator.cs @@ -0,0 +1,126 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; +using OnTopic.Metadata; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC REPOSITORY DECORATOR + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Defines a base abstract class for establishing a decorator of an . + /// + /// + /// Decorators allow implementors to operate off of an underlying version of an , while + /// intercepting calls in order to extend their functionality. They do this by establishing passthrough members to each of + /// the underlying members. The offers a base class for implementing decorators by + /// accepting a via its constructor, and then wiring up each of the members as a passthrough. + /// It also subscribes to the underlying 's events so that they will correctly bubble up + /// through the decorator. This way, derived decorators must only implement the specific methods they wish to override, and + /// can leave everything else as is. + /// + public abstract class TopicRepositoryDecorator : ObservableTopicRepository { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Instantiates a new instance of the with a dependency on an underlying in order to provide necessary data access. + /// + /// + /// A concrete instance of an , which will be used for data access. + /// + /// A new instance of the . + protected TopicRepositoryDecorator(ITopicRepository topicRepository) : base() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate input + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topicRepository, "A concrete implementation of an ITopicRepository is required."); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set values locally + \-----------------------------------------------------------------------------------------------------------------------*/ + TopicRepository = topicRepository; + + /*------------------------------------------------------------------------------------------------------------------------ + | Subscribe to underlying events + \-----------------------------------------------------------------------------------------------------------------------*/ + TopicRepository.DeleteEvent += (object sender, DeleteEventArgs args) => OnTopicDeleted(args); + TopicRepository.MoveEvent += (object sender, MoveEventArgs args) => OnTopicMoved(args); + TopicRepository.RenameEvent += (object sender, RenameEventArgs args) => OnTopicRenamed(args); + + + } + + /*========================================================================================================================== + | DATA PROVIDER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides access to the underlying that this decorates. + /// + protected ITopicRepository TopicRepository { get; set; } + + /*========================================================================================================================== + | GET CONTENT TYPE DESCRIPTORS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override ContentTypeDescriptorCollection GetContentTypeDescriptors() => TopicRepository.GetContentTypeDescriptors(); + + /*========================================================================================================================== + | METHOD: LOAD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override Topic? Load(int topicId, Topic? referenceTopic = null, bool isRecursive = true) => + TopicRepository.Load(topicId, referenceTopic, isRecursive); + + /// + public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) => + TopicRepository.Load(uniqueKey, referenceTopic, isRecursive); + + /// + public override Topic? Load(Topic topic, DateTime version) + => TopicRepository.Load(topic, version); + + /// + public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) => + TopicRepository.Load(topicId, version, referenceTopic); + + /*========================================================================================================================== + | METHOD: REFRESH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Refresh(Topic referenceTopic, DateTime since) => TopicRepository.Refresh(referenceTopic, since); + + /*========================================================================================================================== + | METHOD: ROLLBACK + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Rollback(Topic topic, DateTime version) => TopicRepository.Rollback(topic, version); + + /*========================================================================================================================== + | METHOD: SAVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Save(Topic topic, bool isRecursive = false) => TopicRepository.Save(topic, isRecursive); + + /*========================================================================================================================== + | METHOD: MOVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Move(Topic topic, Topic target, Topic? sibling = null) => TopicRepository.Move(topic, target, sibling); + + /*========================================================================================================================== + | METHOD: DELETE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Delete(Topic topic, bool isRecursive) => TopicRepository.Delete(topic, isRecursive); + + } //Class +} //Namespace \ No newline at end of file From 7c7afbf7822a90d1d6531c352bd0d5c1044f1523 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 16:21:08 -0800 Subject: [PATCH 445/778] Updated `CachedTopicRepository` to implement `TopicRepositoryDecorator` By deriving from `TopicRepositoryDecorator` (799e2b6), the `CachedTopicRepository` no longer needs to implement the passthroughs for events or members that it isn't interested in modifying. This simplifies its logic considerably. --- OnTopic.Data.Caching/CachedTopicRepository.cs | 83 +++---------------- 1 file changed, 10 insertions(+), 73 deletions(-) diff --git a/OnTopic.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs index 4d8f1b7d..77a816de 100644 --- a/OnTopic.Data.Caching/CachedTopicRepository.cs +++ b/OnTopic.Data.Caching/CachedTopicRepository.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using System; using OnTopic.Internal.Diagnostics; -using OnTopic.Metadata; using OnTopic.Querying; using OnTopic.Repositories; @@ -22,39 +21,30 @@ namespace OnTopic.Data.Caching { /// for an actual data access class. /// - public class CachedTopicRepository : TopicRepositoryBase, ITopicRepository { + public class CachedTopicRepository : TopicRepositoryDecorator { /*========================================================================================================================== | VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly ITopicRepository _dataProvider; private readonly Topic _cache; /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Instantiates a new instance of the CachedTopicRepository with a dependency on an underlying ITopicRepository in order - /// to provide necessary data access. + /// Instantiates a new instance of the with a dependency on an underlying in order to provide necessary data access. /// - /// A concrete instance of an ITopicRepository, which will be used for data access. - /// A new instance of the CachedTopicRepository. - public CachedTopicRepository(ITopicRepository dataProvider) : base() { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(dataProvider, "A concrete implementation of an ITopicRepository is required."); - - /*------------------------------------------------------------------------------------------------------------------------ - | Set values locally - \-----------------------------------------------------------------------------------------------------------------------*/ - _dataProvider = dataProvider; + /// + /// A concrete instance of an , which will be used for data access. + /// + /// A new instance of a . + public CachedTopicRepository(ITopicRepository topicRepository) : base(topicRepository) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure topics are loaded \-----------------------------------------------------------------------------------------------------------------------*/ - var rootTopic = _dataProvider.Load(); + var rootTopic = TopicRepository.Load(); Contract.Assume( rootTopic, @@ -69,34 +59,6 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() { } - /*========================================================================================================================== - | EVENT PASSTHROUGHS - \-------------------------------------------------------------------------------------------------------------------------*/ - - /// - public override event EventHandler? DeleteEvent { - add => _dataProvider.DeleteEvent += value; - remove => _dataProvider.DeleteEvent -= value; - } - - /// - public override event EventHandler? MoveEvent { - add => _dataProvider.MoveEvent += value; - remove => _dataProvider.MoveEvent -= value; - } - - /// - public override event EventHandler? RenameEvent { - add => _dataProvider.RenameEvent += value; - remove => _dataProvider.RenameEvent -= value; - } - - /*========================================================================================================================== - | GET CONTENT TYPE DESCRIPTORS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ContentTypeDescriptorCollection GetContentTypeDescriptors() => _dataProvider.GetContentTypeDescriptors(); - /*========================================================================================================================== | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ @@ -149,34 +111,9 @@ public override event EventHandler? RenameEvent { /*------------------------------------------------------------------------------------------------------------------------ | Return appropriate topic \-----------------------------------------------------------------------------------------------------------------------*/ - return _dataProvider.Load(topicId, version, referenceTopic?? _cache); + return TopicRepository.Load(topicId, version, referenceTopic?? _cache); } - /*========================================================================================================================== - | METHOD: REFRESH - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override void Refresh(Topic referenceTopic, DateTime since) => _dataProvider.Refresh(referenceTopic, since); - - /*========================================================================================================================== - | METHOD: SAVE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override void Save(Topic topic, bool isRecursive = false) => - _dataProvider.Save(topic, isRecursive); - - /*========================================================================================================================== - | METHOD: MOVE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override void Move(Topic topic, Topic target, Topic? sibling) => _dataProvider.Move(topic, target, sibling); - - /*========================================================================================================================== - | METHOD: DELETE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override void Delete(Topic topic, bool isRecursive = true) => _dataProvider.Delete(topic, isRecursive); - } //Class } //Namespace \ No newline at end of file From f0d9d449c37ffb7399559b109f6519338cb1007e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 16:34:07 -0800 Subject: [PATCH 446/778] Renamed `TopicRepositoryBase` to `TopicRepository` The .NET Framework Design Guidelines recommend against using the `Base` suffix for publicly available classes. For example, the abstract `Controller` class is named `Controller`, not `ControllerBase`. This should be applied to `TopicRepositoryBase` as well. The fact that it doesn't specify a persistence store (e.g., `SqlTopicRepository`) or any functionality (e.g., `CachedTopicRepository`) should be a clear enough hint that it's not intended for direct usage, since `ITopicRepository` implementations should offer specific implementation logic. While I was at it, I improved the documentation for `TopicRepository` to better articulate when it should be used, and what alternatives are available. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.TestDoubles/DummyTopicRepository.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 12 +++---- OnTopic.Tests/ITopicRepositoryTest.cs | 2 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 36 +++++++++---------- OnTopic/Attributes/AttributeValue.cs | 4 +-- .../Attributes/AttributeValueCollection.cs | 2 +- .../Repositories/ObservableTopicRepository.cs | 2 +- ...icRepositoryBase.cs => TopicRepository.cs} | 22 ++++++++++-- OnTopic/Topic.cs | 2 +- 10 files changed, 51 insertions(+), 35 deletions(-) rename OnTopic/Repositories/{TopicRepositoryBase.cs => TopicRepository.cs} (96%) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 8d745a6d..af1c0a3c 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -28,7 +28,7 @@ namespace OnTopic.Data.Sql { /// /// Concrete implementation of the class. /// - public class SqlTopicRepository : TopicRepositoryBase, ITopicRepository { + public class SqlTopicRepository : TopicRepository, ITopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 524d1014..9aadbc87 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -15,7 +15,7 @@ namespace OnTopic.TestDoubles { /// Provides a basic, non-functional version of a which satisfies the interface requirements, /// but is not intended to be called. /// - public class DummyTopicRepository : TopicRepositoryBase, ITopicRepository { + public class DummyTopicRepository : TopicRepository, ITopicRepository { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 2e2c982f..007db04d 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -25,7 +25,7 @@ namespace OnTopic.TestDoubles { /// database, or working against actual data. This is faster and safer for test methods since it doesn't maintain a /// dependency on a live database or persistent data. /// - public class StubTopicRepository : TopicRepositoryBase, ITopicRepository { + public class StubTopicRepository : TopicRepository, ITopicRepository { /*========================================================================================================================== | VARIABLES @@ -163,7 +163,7 @@ public override void Move(Topic topic, Topic target, Topic? sibling = null) { /*========================================================================================================================== | METHOD: GET ATTRIBUTES (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// public IEnumerable GetAttributesProxy( Topic topic, bool? isExtendedAttribute, @@ -174,13 +174,13 @@ public IEnumerable GetAttributesProxy( /*========================================================================================================================== | METHOD: GET UNMATCHED ATTRIBUTES (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// public IEnumerable GetUnmatchedAttributesProxy(Topic topic) => base.GetUnmatchedAttributes(topic); /*========================================================================================================================== | METHOD: GET CONTENT TYPE DESCRIPTORS (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", true)] public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) => base.SetContentTypeDescriptors(topicGraph); @@ -188,14 +188,14 @@ public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTyp /*========================================================================================================================== | METHOD: SET CONTENT TYPE DESCRIPTORS (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// public ContentTypeDescriptorCollection SetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) => base.SetContentTypeDescriptors(topicGraph); /*========================================================================================================================== | METHOD: GET CONTENT TYPE DESCRIPTOR (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// public ContentTypeDescriptor? GetContentTypeDescriptorProxy(Topic sourceTopic) => base.GetContentTypeDescriptor(sourceTopic); diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index a7ab13aa..72580437 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -21,7 +21,7 @@ namespace OnTopic.Tests { /// /// /// These tests not only validate that the is functioning as expected, but also that the - /// underlying functions are also operating correctly. + /// underlying functions are also operating correctly. /// [TestClass] public class ITopicRepositoryTest { diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index e9566759..0b2d5897 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -19,10 +19,10 @@ namespace OnTopic.Tests { | CLASS: TOPIC REPOSITORY BASE TESTS \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the class. + /// Provides unit tests for the class. /// /// - /// These tests evaluate features that are specific to the class. + /// These tests evaluate features that are specific to the class. /// [TestClass] public class TopicRepositoryBaseTest { @@ -302,7 +302,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() /// Retrieves a list of attributes from a topic, filtering by . Expects the to not be returned even though its /// disagrees with , since it won't match the 's isExtendedAttribute call. + /// cref="TopicRepository.GetAttributes(Topic, Boolean?, Boolean?, Boolean)"/>'s isExtendedAttribute call. /// [TestMethod] public void GetAttributes_ExtendedAttributeMismatch_ReturnsNothing() { @@ -344,7 +344,7 @@ public void GetAttributes_ExcludeLastModified_ReturnsOtherAttributes() { /// /// Sets an arbitrary (unmatched) attribute on a with a value shorter than 255 characters, then /// ensures that it is returned as an an indexed when calling . + /// cref="TopicRepository.GetAttributes(Topic, Boolean?, Boolean?, Boolean)"/>. /// [TestMethod] public void GetAttributes_ArbitraryAttributeWithShortValue_ReturnsAsIndexedAttributes() { @@ -365,7 +365,7 @@ public void GetAttributes_ArbitraryAttributeWithShortValue_ReturnsAsIndexedAttri /// /// Sets an arbitrary (unmatched) attribute on a with a value longer than 255 characters, then /// ensures that it is returned as an an when calling . + /// cref="TopicRepository.GetAttributes(Topic, Boolean?, Boolean?, Boolean)"/>. /// [TestMethod] public void GetAttributes_ArbitraryAttributeWithLongValue_ReturnsAsExtendedAttributes() { @@ -384,7 +384,7 @@ public void GetAttributes_ArbitraryAttributeWithLongValue_ReturnsAsExtendedAttri | TEST: GET UNMATCHED ATTRIBUTES: RETURNS ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Using , ensures that any attributes that exist on the + /// Using , ensures that any attributes that exist on the /// but not the are returned. /// [TestMethod] @@ -405,7 +405,7 @@ public void GetUnmatchedAttributes_ReturnsAttributes() { | TEST: GET UNMATCHED ATTRIBUTES: EMPTY ARBITRARY ATTRIBUTES: RETURNS ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Using , ensures that any attributes that exist on the + /// Using , ensures that any attributes that exist on the /// but not the and are either null or empty are /// returned. This ensures that arbitrary attributes can be deleted programmatically, instead of lingering as orphans in /// the database. @@ -506,9 +506,9 @@ public void GetContentTypeDescriptor_GetInvalidContentType_ReturnsNull() { | TEST: SAVE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Loads the , then saves a new via , and ensures that it is - /// immediately reflected in the cache of s. + /// Loads the , then saves a new via , and ensures that it is + /// immediately reflected in the cache of s. /// [TestMethod] public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { @@ -526,8 +526,8 @@ public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { | TEST: SAVE: CONTENT TYPE DESCRIPTOR: UPDATES PERMITTED CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Loads the , then saves an existing via , and ensures that + /// Loads the , then saves an existing via , and ensures that /// it the cache is updated. /// [TestMethod] @@ -550,9 +550,9 @@ public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { | TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Loads the , then deletes one of the - /// s via , and ensures that it - /// is immediately reflected in the cache of s. + /// Loads the , then deletes one of the + /// s via , and ensures that it + /// is immediately reflected in the cache of s. /// [TestMethod] public void Delete_ContentTypeDescriptor_UpdatesContentTypeCache() { @@ -570,9 +570,9 @@ public void Delete_ContentTypeDescriptor_UpdatesContentTypeCache() { | TEST: MOVE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Loads the , then moves one of the - /// s via , and ensures - /// that it is immediately reflected in the cache of , then moves one of the + /// s via , and ensures + /// that it is immediately reflected in the cache of s. /// [TestMethod] diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index d9786460..78f988d7 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -177,9 +177,9 @@ internal AttributeValue( /// This is important because, otherwise, implementations rely primarily on to determine if a value should be saved. If an attribute's value hasn't changed, but the location /// it should be stored has, that could potentially result in the attribute being deleted, as the attribute won't show - /// up for when is called with isDirty set to true and + /// up for when is called with isDirty set to true and /// isExtendedAttribute is set to either true or false. By introducing , the is able to detect conflicts between the + /// cref="IsExtendedAttribute"/>, the is able to detect conflicts between the /// configuration and the underlying data store, and ensure data is stored appropriately. /// /// diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 91c5758a..ba8cce5a 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -63,7 +63,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar /// As a performance enhancement, implementations will only save topics that are marked as /// . If a is deleted, then it won't be marked as dirty. If no /// other instances were modified, then the topic won't get saved, and that value won't be - /// deleted. Further more, the method has no way of + /// deleted. Further more, the method has no way of /// detecting the deletion of arbitrary attributes�i.e., attributes that were deleted which don't correspond to attributes /// configured on the . By tracking any deleted attributes, we ensure both /// scenarios can be accounted for. diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index ec30e2fd..d8e37367 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -18,7 +18,7 @@ namespace OnTopic.Repositories { /// /// All implementations of are expected to need the following logic at minimum. /// Concrete implementations that are working directly with an underlying data source should prefer to instead derive from - /// the more opinionated , which provides more built-in business logic. + /// the more opinionated , which provides more built-in business logic. /// public abstract class ObservableTopicRepository : ITopicRepository { diff --git a/OnTopic/Repositories/TopicRepositoryBase.cs b/OnTopic/Repositories/TopicRepository.cs similarity index 96% rename from OnTopic/Repositories/TopicRepositoryBase.cs rename to OnTopic/Repositories/TopicRepository.cs index 8e3ed7d3..d3adcb2e 100644 --- a/OnTopic/Repositories/TopicRepositoryBase.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -15,12 +15,28 @@ namespace OnTopic.Repositories { /*============================================================================================================================ - | CLASS: TOPIC DATA PROVIDER BASE + | CLASS: TOPIC DATA PROVIDER \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Defines a base abstract class for taxonomy data providers. + /// Provides a base class for which are responsible for persisting data. /// - public abstract class TopicRepositoryBase : ObservableTopicRepository { + /// + /// + /// The is a highly opinionated base implementation of . + /// In addition to validating parameters and raising events on , , and , it also provides a number of (protected) methods to + /// aid implementors in evaluating and parsing data, such as . It is recommended that all concrete implementations of that are responsible for + /// persisting data to a data store use this as a base class. + /// + /// + /// Implementations of which need to use different business logic, or do not need to + /// implement business logic (such as unit test doubles) may instead opt to derive directly from the , which handles the basic event handling, and nothing else. Implementations of decorators + /// should instead derive from the . + /// + /// + public abstract class TopicRepository : ObservableTopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 64a79c54..ec47705e 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -628,7 +628,7 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// The underlying value of the is stored as the TopicID . /// If the hasn't been saved, then the relationship will be established, but the TopicID /// won't be persisted to the underlying repository upon . That said, - /// when is called, the will be + /// when is called, the will be /// reevaluated and, if it has subsequently been saved, then the TopicID will be updated accordingly. This allows /// in-memory topic graphs to be constructed, while preventing invalid s from being persisted to /// the underlying data storage. As a result, however, a referencing a From 26a74bd8bc14bc73b7b41fafb341652cf4f0a662 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 17:16:04 -0800 Subject: [PATCH 447/778] Updated `Topic` XML Doc to use fully-qualified `Save()` signature --- OnTopic/Topic.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index ec47705e..f856f6b6 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -628,11 +628,11 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// The underlying value of the is stored as the TopicID . /// If the hasn't been saved, then the relationship will be established, but the TopicID /// won't be persisted to the underlying repository upon . That said, - /// when is called, the will be - /// reevaluated and, if it has subsequently been saved, then the TopicID will be updated accordingly. This allows - /// in-memory topic graphs to be constructed, while preventing invalid s from being persisted to - /// the underlying data storage. As a result, however, a referencing a - /// that is unsaved will need to be saved again once the has been saved. + /// when is called, the will + /// be reevaluated and, if it has subsequently been saved, then the TopicID will be updated accordingly. This + /// allows in-memory topic graphs to be constructed, while preventing invalid s from being + /// persisted to the underlying data storage. As a result, however, a referencing a that is unsaved will need to be saved again once the has been saved. /// /// /// The that values should be derived from, if not otherwise available. From 3ce23f628d5eee16e8cbcb2f683339a7dfe76227 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 17:19:01 -0800 Subject: [PATCH 448/778] Derived `DummyTopicRepository` from `ObservableTopicRepository` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As the name suggests, the `DummyTopicRepository` doesn't provide an actual implementation, and thus doesn't need the overhead of the `TopicRepository` base. Instead, it can derive directly from the underlying `ObservableTopicRepository` base. Because the `ObservableTopicRepository` only implements `abstract` members, this necessitates offering overrides for `GetContentTypeDescriptors()`, `Load(Topic, DateTime)`, and `Rollback(Topic, Version)`—all of which were previously implemented via `TopicRepository`. --- OnTopic.TestDoubles/DummyTopicRepository.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs index 9aadbc87..cf73340d 100644 --- a/OnTopic.TestDoubles/DummyTopicRepository.cs +++ b/OnTopic.TestDoubles/DummyTopicRepository.cs @@ -4,6 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Collections; +using OnTopic.Metadata; using OnTopic.Repositories; namespace OnTopic.TestDoubles { @@ -15,7 +17,7 @@ namespace OnTopic.TestDoubles { /// Provides a basic, non-functional version of a which satisfies the interface requirements, /// but is not intended to be called. /// - public class DummyTopicRepository : TopicRepository, ITopicRepository { + public class DummyTopicRepository : ObservableTopicRepository { /*========================================================================================================================== | CONSTRUCTOR @@ -26,6 +28,12 @@ public class DummyTopicRepository : TopicRepository, ITopicRepository { /// A new instance of the . public DummyTopicRepository() : base() { } + /*========================================================================================================================== + | METHOD: GET CONTENT TYPE DESCRIPTORS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override ContentTypeDescriptorCollection GetContentTypeDescriptors() => new(); + /*========================================================================================================================== | METHOD: LOAD \-------------------------------------------------------------------------------------------------------------------------*/ @@ -35,9 +43,18 @@ public DummyTopicRepository() : base() { } /// public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true) => null; + /// + public override Topic? Load(Topic? topic, DateTime version) => throw new NotImplementedException(); + /// public override Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null) => throw new NotImplementedException(); + /*========================================================================================================================== + | METHOD: ROLLBACK + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public override void Rollback(Topic topic, DateTime version) => throw new NotImplementedException(); + /*========================================================================================================================== | METHOD: REFRESH \-------------------------------------------------------------------------------------------------------------------------*/ From fdd00258c5db581a59ca3e3b5ff60aec5b4ab6d8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 17:48:58 -0800 Subject: [PATCH 449/778] Centralized core business logic from `SqlTopicRepository.Save()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `SqlTopicRepository.Save()` implementation had a lot of logic that wasn't specific to SQL Server, and would likely need to be repeated by subsequent implementations. To help avoid that, it has been migrated from `SqlTopicRepository.Save()` and into the underlying `TopicRepository.Save()`. This includes tracking `unresolvedTopics` (i.e., topics whose relationships or references haven't yet been saved) and then resaving them at the end. It also includes the core recursion logic for looping over child topics, if `isRecursive` is set. Finally, it includes establishing a shared `version` to use across all attributes, relationships, and references so all updates that are part of this operation share the same version. While not yet implemented, this will also allow us to extend the unit testing capabilities to evaluate the above features, as the unit tests don't currently cover `SqlTopicRepository.Save()`, but do cover `TopicRepository.Save()`. As part of this, the `Save()` method is marked as `sealed` and the core implementation is moved to the `abstract` `Save(topic, version, persistRelationships)` overload. This prevents the need for derived implementations to call `base.Save()`, and likewise allows the `TopicRepository.Save()` implementation more control over the order of operations—e.g., by placing validation logic above the implementation, while putting topic graph operations after it. This fixes a behavior where an operation would be reflected in the local topic graph even if the database operation failed, which often resulted in confusion since it would appear as though the operation completed based on the cached state of the topic graph. This could also potentially result in bugs in subsequent saves, since those operations would have already been completed. Finally, this includes updates to the `StubTopicRepository` to `override` the updated `Save()` overload, instead of the `ITopicRepository` version. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 117 +++------------------ OnTopic.TestDoubles/StubTopicRepository.cs | 19 +--- OnTopic/Repositories/TopicRepository.cs | 101 +++++++++++++++++- 3 files changed, 114 insertions(+), 123 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index af1c0a3c..ccd178de 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Text; using Microsoft.Data.SqlClient; +using OnTopic.Collections; using OnTopic.Data.Sql.Models; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; @@ -335,79 +336,16 @@ public override void Refresh(Topic referenceTopic, DateTime since) { /*========================================================================================================================== | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override void Save([NotNull]Topic topic, bool isRecursive = false) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Establish dependencies - \-----------------------------------------------------------------------------------------------------------------------*/ - var version = DateTime.UtcNow; - var unresolvedTopics = new List(); - - using var connection = new SqlConnection(_connectionString); - - connection.Open(); - - /*------------------------------------------------------------------------------------------------------------------------ - | Handle first pass - \-----------------------------------------------------------------------------------------------------------------------*/ - Save(topic, isRecursive, connection, unresolvedTopics, version); - - /*------------------------------------------------------------------------------------------------------------------------ - | Attempt to resolve outstanding relationships - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var unresolvedTopic in unresolvedTopics) { - Save(unresolvedTopic, false, connection, unresolvedTopics, version); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Close shared connection - \-----------------------------------------------------------------------------------------------------------------------*/ - connection.Close(); - - } - - /// - /// The private overload of the method provides support for sharing the - /// between multiple requests, and maintaining a list of . - /// - /// - /// - /// When recursively saving a topic graph, it is conceivable that references to other topics—such as or —can't yet be persisted because the target hasn't yet been saved, and thus the is still set to -1. To mitigate - /// this, the allows this private overload to keep track of unresolved - /// relationships. The public overload uses this list to resave any topics - /// that include such references. This adds some overhead due to the duplicate , but helps avoid - /// potential data loss when working with complex topic graphs. - /// - /// - /// The connection sharing probably doesn't provide that much of a gain in that .NET does a good job of connection - /// pooling. Nevertheless, there is some overhead to opening a new connection, so sharing an open connection when we - /// doing a recursive save could potentially provide some performance benefit. - /// - /// - /// The source to save. - /// Determines whether or not to recursively save . - /// The open to use for executing s. - /// A list of s with unresolved topic references. - private void Save( + /// + protected override sealed void Save( [NotNull]Topic topic, - bool isRecursive, - SqlConnection connection, - List unresolvedRelationships, - DateTime version + DateTime version, + bool persistRelationships ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Call base method - will trigger any events associated with the save - \-----------------------------------------------------------------------------------------------------------------------*/ - base.Save(topic, isRecursive); - /*------------------------------------------------------------------------------------------------------------------------ | Define variables \-----------------------------------------------------------------------------------------------------------------------*/ - var areReferencesResolved = true; var isTopicDirty = topic.IsDirty(); var areRelationshipsDirty = topic.Relationships.IsDirty(); var areReferencesDirty = topic.References.IsDirty(); @@ -440,7 +378,6 @@ DateTime version | Bypass is not dirty \-----------------------------------------------------------------------------------------------------------------------*/ if (!isDirty) { - recurse(); return; } @@ -494,29 +431,15 @@ DateTime version /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection \-----------------------------------------------------------------------------------------------------------------------*/ + using var connection = new SqlConnection(_connectionString); var procedureName = topic.IsNew? "CreateTopic" : "UpdateTopic"; + connection.Open(); + using var command = new SqlCommand(procedureName, connection) { CommandType = CommandType.StoredProcedure }; - /*------------------------------------------------------------------------------------------------------------------------ - | Handle unresolved references - >------------------------------------------------------------------------------------------------------------------------- - | If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been - | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The - | same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship. - \-----------------------------------------------------------------------------------------------------------------------*/ - if ( - isRecursive && - ( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || - topic.References.Values.Any(t => t.Id < 0) - ) - ) { - unresolvedRelationships.Add(topic); - areReferencesResolved = false; - } - /*------------------------------------------------------------------------------------------------------------------------ | Establish query parameters \-----------------------------------------------------------------------------------------------------------------------*/ @@ -553,18 +476,14 @@ DateTime version "The call to the CreateTopic stored procedure did not return the expected 'Id' parameter." ); - if (areReferencesResolved && areRelationshipsDirty) { + if (persistRelationships && areRelationshipsDirty) { PersistRelations(topic, version, connection); } - if (areReferencesResolved && areReferencesDirty) { + if (persistRelationships && areReferencesDirty) { PersistReferences(topic, version, connection); } - if (!topic.VersionHistory.Contains(version)) { - topic.VersionHistory.Insert(0, version); - } - topic.Attributes.MarkClean(version); } @@ -580,20 +499,10 @@ DateTime version } /*------------------------------------------------------------------------------------------------------------------------ - | Recuse over any children + | Close connection \-----------------------------------------------------------------------------------------------------------------------*/ - recurse(); - - /*------------------------------------------------------------------------------------------------------------------------ - | Function: Recurse - \-----------------------------------------------------------------------------------------------------------------------*/ - void recurse() { - if (isRecursive) { - foreach (var childTopic in topic.Children) { - childTopic.Attributes.SetValue("ParentID", topic.Id.ToString(CultureInfo.InvariantCulture)); - Save(childTopic, isRecursive, connection, unresolvedRelationships, version); - } - } + finally { + connection.Close(); } } diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 007db04d..7122c7e8 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; @@ -111,29 +112,15 @@ public override void Refresh(Topic referenceTopic, DateTime since) { } | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Save(Topic topic, bool isRecursive = false) { + protected override void Save([NotNull]Topic topic, DateTime version, bool persistRelationships) { /*------------------------------------------------------------------------------------------------------------------------ - | Call base method - will trigger any events associated with the save - \-----------------------------------------------------------------------------------------------------------------------*/ - base.Save(topic, isRecursive); - - /*------------------------------------------------------------------------------------------------------------------------ - | Recurse through children + | Assign faux identity \-----------------------------------------------------------------------------------------------------------------------*/ if (topic.IsNew) { topic.Id = _identity++; } - /*------------------------------------------------------------------------------------------------------------------------ - | Recurse through children - \-----------------------------------------------------------------------------------------------------------------------*/ - if (isRecursive) { - foreach (var childTopic in topic.Children) { - Save(childTopic, isRecursive); - } - } - } /*========================================================================================================================== diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index d3adcb2e..38bec324 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -5,9 +5,11 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft; using OnTopic.Attributes; +using OnTopic.Collections; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Querying; @@ -254,13 +256,57 @@ public override void Rollback([ValidatedNotNull]Topic topic, DateTime version) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Save([ValidatedNotNull]Topic topic, bool isRecursive = false) { + public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursive = false) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Establish dependencies + \-----------------------------------------------------------------------------------------------------------------------*/ + var version = DateTime.UtcNow; + var unresolvedTopics = new TopicCollection(); + + /*------------------------------------------------------------------------------------------------------------------------ + | Handle first pass + \-----------------------------------------------------------------------------------------------------------------------*/ + Save(topic, isRecursive, unresolvedTopics, version); + + /*------------------------------------------------------------------------------------------------------------------------ + | Attempt to resolve outstanding relationships + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var unresolvedTopic in unresolvedTopics.ToList()) { + Save(unresolvedTopic, false, unresolvedTopics, version); + } + + } + + /// + /// Recursive method that validates an individual topic, calls the derived implementation, and then recurses over any + /// child topics. + /// + /// + /// When recursively saving a topic graph, it is conceivable that references to other topics—such as or —can't yet be persisted because the target hasn't yet been saved, and thus the is still set to -1. To mitigate + /// this, the allows this private overload to keep track of unresolved + /// relationships. The public overload uses this list to resave any topics + /// that include such references. This adds some overhead due to the duplicate , but + /// helps avoid potential data loss when working with complex topic graphs. + /// + /// The source to save. + /// Determines whether or not to recursively save . + /// A list of s with unresolved topic references. + /// The version to assign to the updates. + private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unresolvedTopics, DateTime version) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(topic, nameof(topic)); + /*------------------------------------------------------------------------------------------------------------------------ + | Establish variables + \-----------------------------------------------------------------------------------------------------------------------*/ + var isNew = topic.IsNew; + /*------------------------------------------------------------------------------------------------------------------------ | Validate content type \-----------------------------------------------------------------------------------------------------------------------*/ @@ -274,6 +320,20 @@ public override void Save([ValidatedNotNull]Topic topic, bool isRecursive = fals ); } + /*------------------------------------------------------------------------------------------------------------------------ + | Handle unresolved references + >------------------------------------------------------------------------------------------------------------------------- + | If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been + | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The + | same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship. + \-----------------------------------------------------------------------------------------------------------------------*/ + if ( + topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || + topic.References.Values.Any(t => t.Id < 0) + ) { + unresolvedTopics.Add(topic); + } + /*------------------------------------------------------------------------------------------------------------------------ | Ensure derived topic is set >------------------------------------------------------------------------------------------------------------------------- @@ -285,6 +345,18 @@ public override void Save([ValidatedNotNull]Topic topic, bool isRecursive = fals \-----------------------------------------------------------------------------------------------------------------------*/ topic.DerivedTopic = topic.DerivedTopic; + /*------------------------------------------------------------------------------------------------------------------------ + | Execute core implementation + \-----------------------------------------------------------------------------------------------------------------------*/ + Save(topic, version, !isRecursive || !unresolvedTopics.Contains(topic)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Update version history + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!topic.VersionHistory.Contains(version)) { + topic.VersionHistory.Insert(0, version); + } + /*------------------------------------------------------------------------------------------------------------------------ | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ @@ -311,7 +383,7 @@ public override void Save([ValidatedNotNull]Topic topic, bool isRecursive = fals \-----------------------------------------------------------------------------------------------------------------------*/ var asContentType = topic as ContentTypeDescriptor; if ( - topic.IsNew && + isNew && asContentType is not null && _contentTypeDescriptors is not null && !_contentTypeDescriptors.Contains(topic.Key) @@ -333,7 +405,7 @@ _contentTypeDescriptors is not null && | AttributeDescriptors. When a new AttributeDescriptor is added, these collections are reset for the current | ContentTypeDescriptor and all descendents to ensure that they all reflect the new AttributeDescriptor. \-----------------------------------------------------------------------------------------------------------------------*/ - if (topic.IsNew && IsAttributeDescriptor(topic)) { + if (isNew && IsAttributeDescriptor(topic)) { ResetAttributeDescriptors(topic); } @@ -342,8 +414,31 @@ _contentTypeDescriptors is not null && \-----------------------------------------------------------------------------------------------------------------------*/ topic.OriginalKey = null; + /*------------------------------------------------------------------------------------------------------------------------ + | Recurse over children + \-----------------------------------------------------------------------------------------------------------------------*/ + if (isRecursive) { + foreach (var childTopic in topic.Children) { + Save(childTopic, isRecursive, unresolvedTopics, version); + } + } + } + /// + /// The core implementation logic that's executed for every see during a operation. + /// + /// The source to save. + /// The version to assign to the updates. + /// + /// Determines whether or not and should be persisted. + /// This may be set to false if not all references have yet been saved, and the + /// call isRecursive; in that case, will circle back and attempt to save them + /// after the rest of the topic graph has been saved. + /// + protected abstract void Save([NotNull] Topic topic, DateTime version, bool persistRelationships); + /*========================================================================================================================== | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ From 116c8ebd2d325046c2f1185c511b9f54c09ba09a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 18:25:02 -0800 Subject: [PATCH 450/778] Delegated core implementation to `{Operation}Topic()` methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, derived implementations were expected to override the main `ITopicRepository` methods, such as `Save()`, `Move()`, and `Delete()`. That necessitated that they call the `base.{Operation}()` as part of that call, while simultaneously requiring the `TopicRepository` implementation to handle all business logic in one sequence—regardless of made more sense to run it before or after the actual business logic. In practice, implementors were expected to place the `base{Operation}()` call before the implementation logic, but there was no way of strictly enforcing this. These issues are mitigated by delegating the core implementation to new `abstract` `{Operation}Topic()` methods—such as `SaveTopic()`, `MoveTopic()`, and `DeleteTopic()`. These have no need to call the `base.{Operation}()` method. As part of this, the core `{Operation}()` methods are now marked as `sealed` so they can't be overriden directly. That ensures that their business logic is always run. It also allows them to call the `{Operation}Topic()` implementations in the most appropriate place—i.e., after validating the parameters and conditions, but before persisting the values to the topic graph or raising the event. This fixes a behavior where an operation would be reflected in the local topic graph even if the database operation failed, which often resulted in confusion since it would appear as though the operation completed based on the cached state of the topic graph. This could also potentially result in bugs in subsequent saves, since those operations would have already been completed. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 19 +++--- OnTopic.TestDoubles/StubTopicRepository.cs | 11 ++-- OnTopic/Repositories/TopicRepository.cs | 67 ++++++++++++++++++++-- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index ccd178de..3af84e62 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -337,7 +337,7 @@ public override void Refresh(Topic referenceTopic, DateTime since) { | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override sealed void Save( + protected override sealed void SaveTopic( [NotNull]Topic topic, DateTime version, bool persistRelationships @@ -508,15 +508,16 @@ bool persistRelationships } /*========================================================================================================================== - | METHOD: MOVE + | METHOD: MOVE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Move(Topic topic, Topic target, Topic? sibling) { + protected override void MoveTopic(Topic topic, Topic target, Topic? sibling) { /*------------------------------------------------------------------------------------------------------------------------ - | Delete from memory + | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - base.Move(topic, target, sibling); + Contract.Requires(topic, nameof(topic)); + Contract.Requires(target, nameof(target)); /*------------------------------------------------------------------------------------------------------------------------ | Establish database connection @@ -558,15 +559,15 @@ public override void Move(Topic topic, Topic target, Topic? sibling) { } /*========================================================================================================================== - | METHOD: DELETE + | METHOD: DELETE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete(Topic topic, bool isRecursive = false) { + protected override void DeleteTopic(Topic topic) { /*------------------------------------------------------------------------------------------------------------------------ - | Delete from memory + | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - base.Delete(topic, isRecursive); + Contract.Requires(topic, nameof(topic)); /*------------------------------------------------------------------------------------------------------------------------ | Delete from database diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 7122c7e8..57027d8c 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -112,7 +112,7 @@ public override void Refresh(Topic referenceTopic, DateTime since) { } | METHOD: SAVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void Save([NotNull]Topic topic, DateTime version, bool persistRelationships) { + protected override void SaveTopic([NotNull]Topic topic, DateTime version, bool persistRelationships) { /*------------------------------------------------------------------------------------------------------------------------ | Assign faux identity @@ -127,12 +127,13 @@ protected override void Save([NotNull]Topic topic, DateTime version, bool persis | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Move(Topic topic, Topic target, Topic? sibling = null) { + protected override void MoveTopic(Topic topic, Topic target, Topic? sibling = null) { /*------------------------------------------------------------------------------------------------------------------------ - | Delete from memory + | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - base.Move(topic, target, sibling); + Contract.Requires(topic, nameof(topic)); + Contract.Requires(target, nameof(target)); /*------------------------------------------------------------------------------------------------------------------------ | Reset dirty status @@ -145,7 +146,7 @@ public override void Move(Topic topic, Topic target, Topic? sibling = null) { | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete(Topic topic, bool isRecursive = false) => base.Delete(topic, isRecursive); + protected override void DeleteTopic(Topic topic) { } /*========================================================================================================================== | METHOD: GET ATTRIBUTES (PROXY) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 38bec324..d66e0fa2 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -348,7 +348,7 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso /*------------------------------------------------------------------------------------------------------------------------ | Execute core implementation \-----------------------------------------------------------------------------------------------------------------------*/ - Save(topic, version, !isRecursive || !unresolvedTopics.Contains(topic)); + SaveTopic(topic, version, !isRecursive || !unresolvedTopics.Contains(topic)); /*------------------------------------------------------------------------------------------------------------------------ | Update version history @@ -425,10 +425,23 @@ _contentTypeDescriptors is not null && } + /*========================================================================================================================== + | METHOD: SAVE TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// The core implementation logic that's executed for every see during a operation. + /// The core implementation logic for saving an individual during a + /// operation. /// + /// + /// The main implementation handles advanced validation of the parameters, updating the + /// , attempting to pick up any unresolved topics, updating and instances as appropriate, raising the , if needed, and recursing over children. The derived implementation of is then left to focus exclusively on the core logic of persisting the changes + /// to the individual to the underlying data store, and optionally updating its and , assuming is set to + /// true. + /// /// The source to save. /// The version to assign to the updates. /// @@ -437,13 +450,13 @@ _contentTypeDescriptors is not null && /// call isRecursive; in that case, will circle back and attempt to save them /// after the rest of the topic graph has been saved. /// - protected abstract void Save([NotNull] Topic topic, DateTime version, bool persistRelationships); + protected abstract void SaveTopic([NotNull] Topic topic, DateTime version, bool persistRelationships); /*========================================================================================================================== | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Move([ValidatedNotNull]Topic topic, [ValidatedNotNull]Topic target, Topic? sibling = null) { + public override sealed void Move([ValidatedNotNull]Topic topic, [ValidatedNotNull]Topic target, Topic? sibling = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -465,6 +478,11 @@ topic.Parent is not null && return; } + /*------------------------------------------------------------------------------------------------------------------------ + | Execute core implementation + \-----------------------------------------------------------------------------------------------------------------------*/ + MoveTopic(topic, target, sibling); + /*------------------------------------------------------------------------------------------------------------------------ | Perform base logic \-----------------------------------------------------------------------------------------------------------------------*/ @@ -492,11 +510,27 @@ topic.Parent is not null && } + /*========================================================================================================================== + | METHOD: MOVE TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The core implementation logic for moving a . + /// + /// + /// The main implementation handles advanced validation of the parameters, + /// updating the 's location within the topic graph, updating and + /// instances as appropriate, and raising the . + /// The derived implementation of is then left to focus exclusively on the + /// core logic of persisting the change to the underlying data store. + /// + protected abstract void MoveTopic(Topic topic, Topic target, Topic? sibling = null); + /*========================================================================================================================== | METHOD: DELETE \-------------------------------------------------------------------------------------------------------------------------*/ /// - public override void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { + public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursive = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -528,6 +562,11 @@ public override void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { ); } + /*------------------------------------------------------------------------------------------------------------------------ + | Execute core implementation + \-----------------------------------------------------------------------------------------------------------------------*/ + DeleteTopic(topic); + /*------------------------------------------------------------------------------------------------------------------------ | Trigger event \-----------------------------------------------------------------------------------------------------------------------*/ @@ -583,6 +622,22 @@ public override void Delete([ValidatedNotNull]Topic topic, bool isRecursive) { } + /*========================================================================================================================== + | METHOD: DELETE TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// + /// The core implementation logic for deleting a . + /// + /// + /// The main implementation handles advanced validation of the parameters, + /// removing the from the topic graph, updating and instances as appropriate, and raising the . The + /// derived implementation of is then left to focus exclusively on the core logic of + /// persisting the change to the underlying data store. + /// + protected abstract void DeleteTopic(Topic topic); + /*========================================================================================================================== | METHOD: GET ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ From d7c19fd0370f50adc2a7d241651cbeff7f2e0225 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 18:26:47 -0800 Subject: [PATCH 451/778] Use fully qualified `UniqueKey` for `Load()` call Calls to `Load(uniqueKey)` should use the fully qualified, root-relative unique key. Technically, `Root` will be inferred if it's missing, but it's a best practice to explicitly include it. --- OnTopic/Repositories/TopicRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index d66e0fa2..86691ae3 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -62,7 +62,7 @@ public override ContentTypeDescriptorCollection GetContentTypeDescriptors() { var configuration = (Topic?)null; try { - configuration = Load("Configuration"); + configuration = Load("Root:Configuration"); } catch (TopicNotFoundException) { //Swallow missing configuration, as this is an expected condition when working with a new database From d631b3f1a8c3396e3cbdecdcff7279338dedf744 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 18:30:30 -0800 Subject: [PATCH 452/778] Remove legacy `DerivedTopic` hack Previously, the `DerivedTopic` needed to be reset to itself to force the `DerivedTopic` (then `TopicID`) attribute to be resaved to the `Topic.Attributes` collection so that updates to its `Topic.Id` from the `Save()` operation could be updated. Without this, if the `DerivedTopic` hadn't yet been saved, then the original `Save()` operation would have written a `TopicID` of `-1` to the database, and never have been updated with the final post-save `Topic.Id`. As the `DerivedTopic` is now handled as part of the `Topic.References`, this is all handled in a more general fashion, via the `unresolvedTopics` logic, without the need for a specific hack to handle `DerivedTopic`. In fact, based on how `Topic.References` work, this wouldn't do anything even if there was alternate logic, since the `Topic.References` operates off references to the target `Topic`, not a `TopicID` attribute. --- OnTopic/Repositories/TopicRepository.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 86691ae3..34e52797 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -334,17 +334,6 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso unresolvedTopics.Add(topic); } - /*------------------------------------------------------------------------------------------------------------------------ - | Ensure derived topic is set - >------------------------------------------------------------------------------------------------------------------------- - | ### HACK JJC20200523: If a derived topic is linked but hasn't been saved yet, then it should not be persisted to the - | repository, as its topic.Id will be -1. If a derived topic is saved after the relationship has been established, - | however, there isn't currently a way to detect that event and subsequently update the TopicId attribute. To mitigate - | that, we simply set the derived topic to itself before Save(); if it has been saved in the interim, then the topic.Id - | will be set; if not, the topic.Id will remain -1. - \-----------------------------------------------------------------------------------------------------------------------*/ - topic.DerivedTopic = topic.DerivedTopic; - /*------------------------------------------------------------------------------------------------------------------------ | Execute core implementation \-----------------------------------------------------------------------------------------------------------------------*/ From 06bf859a260b07d8efa349237de6db2ea6de5efc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 18:35:55 -0800 Subject: [PATCH 453/778] Disable `IsFullyLoaded` if there's an exception on `Load()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If an exception occurs during `Load(topicId, version, referenceTopic)`, we don't have confidence that the topic's relationships or references were properly loaded. They _might_ have been, but it's not possible to tell for sure. As such, the target `topic` may be left in an incomplete state. To mitigate this from potentially resulting in data loss during a subsequent `Save()` operation, these collections are explicitly marked as `!IsFullyLoaded`. This is especially important since those collections are cleared prior to the operation, since it's otherwise impractical to track relationships and references that were added after the loaded `version`. This is hopefully a rare scenario, but it will help avoid inadvertent data loss in the process—though it may also result in deleted relationships and references not being deleted. (The OnTopic Editor will warn editors about this possibiliy before they save a topic in this state.) --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 3af84e62..169e7cd5 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -254,6 +254,10 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic | Catch exception \-----------------------------------------------------------------------------------------------------------------------*/ catch (SqlException exception) { + if (topic is not null) { + topic.Relationships.IsFullyLoaded = false; + topic.References.IsFullyLoaded = false; + } throw new TopicRepositoryException($"Topics failed to load: '{exception.Message}'", exception); } From 6f55b2ad9dae5c3ea8470a62178a9f5e99362aba Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:29:11 -0800 Subject: [PATCH 454/778] Throw exception during `TopicRepository.Save()` if `unresolvedTopics` Something, unresolved relationships or topic references will occur during a `Save()`. This happens if the topics that are references haven't yet been saved themselves; in that case, they can't be persisted to the database. That's no problem; the `TopicRepository.Save()` keeps track of those, and automatically attempts to resave them after the `Save()` operation has been completed. If, after that operation is complete, there are _still_ `unresolvedTopics`, however, then a `ReferentialIntegrityException` will be thrown. This usually occurs if the relationships or topic references point to newly created topics outside the scope of the `Save()`, and can usually be fixed by expanding the scope of the `Save()` or first saving those referenced topics. --- OnTopic/Repositories/TopicRepository.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 34e52797..f2d9c7bf 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -273,9 +273,22 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv | Attempt to resolve outstanding relationships \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var unresolvedTopic in unresolvedTopics.ToList()) { + unresolvedTopics.Remove(unresolvedTopic); Save(unresolvedTopic, false, unresolvedTopics, version); } + /*------------------------------------------------------------------------------------------------------------------------ + | Throw exception if unresolved topics + \-----------------------------------------------------------------------------------------------------------------------*/ + if (unresolvedTopics.Count > 0) { + throw new ReferentialIntegrityException( + $"The call to ITopicRepository.Save() introduced unresolved references on {unresolvedTopics.Count} topics, " + + $"including '{unresolvedTopics.LastOrDefault().GetUniqueKey()}'. This is usually due to relationships or topic " + + $"references outside the scope of the Save() which, themselves, have not yet been persisted to the data store. If " + + $"this is not resolved, these items will not be correctly persisted." + ); + } + } /// From 316a302394c58fd1d29292426acc32415a05b4c3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:29:41 -0800 Subject: [PATCH 455/778] Introduce unit test for `TopicRepository.Save()`'s `version` handling --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 0b2d5897..8add802a 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -546,6 +546,25 @@ public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { } + /*========================================================================================================================== + | TEST: SAVE: NEW TOPIC: UPDATES VERSION HISTORY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Saves a new and confirms that the is correctly updated with a + /// new version. + /// + [TestMethod] + public void Save_NewTopic_UpdatesVersionHistory() { + + var parent = _topicRepository.Load("Root:Web:Web_3:Web_3_0"); + var topic = TopicFactory.Create("Test", "Page", parent); + + _topicRepository.Save(topic); + + Assert.IsTrue(topic.VersionHistory.Count > 0); + + } + /*========================================================================================================================== | TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ From 8c05a6154781f3b9dd0fc61c8e515ebf5ef57226 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:29:52 -0800 Subject: [PATCH 456/778] Introduce unit test for `TopicRepository.Save()`'s `isRecursive` handling --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 8add802a..da2edff0 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -565,6 +565,26 @@ public void Save_NewTopic_UpdatesVersionHistory() { } + /*========================================================================================================================== + | TEST: SAVE: IS RECURSIVE: SAVES CHILD + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Saves a new with a child and confirms that the of the + /// child is correctly updated. + /// + [TestMethod] + public void Save_IsRecursive_SavesChild() { + + var parent = _topicRepository.Load("Root:Web:Web_3:Web_3_0"); + var topic = TopicFactory.Create("Test", "Page", parent); + var child = TopicFactory.Create("Child", "Page", topic); + + _topicRepository.Save(topic, true); + + Assert.IsFalse(child.IsNew); + + } + /*========================================================================================================================== | TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ From 1616f6db7ccf100b45ab7ae793e8e78532f182b8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:30:19 -0800 Subject: [PATCH 457/778] Introduce unit test for `TopicRepository.Save()`'s `unresolvedTopics` handling --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index da2edff0..962fd151 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -585,6 +585,27 @@ public void Save_IsRecursive_SavesChild() { } + /*========================================================================================================================== + | TEST: SAVE: UNRESOLVED REFERENCE: RESOLVES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Saves a new with an unresolved and confirms that it successfully + /// resolves it by marking the collection as as false. + /// + [TestMethod] + public void Save_UnresolvedReference_Resolves() { + + var parent = _topicRepository.Load("Root:Web:Web_3:Web_3_0"); + var topic = TopicFactory.Create("Test", "Page", parent); + var reference = TopicFactory.Create("Reference", "Page", topic); + + topic.References.SetTopic("Test", reference); + + _topicRepository.Save(topic, true); + + } + /*========================================================================================================================== | TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ From 1ad35417d4ce45ed5f8b0e4c3a2ff1889a083955 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:30:36 -0800 Subject: [PATCH 458/778] Introduce unit test for `TopicRepository.Save()`'s `unresolvedTopics` exception --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 962fd151..4ddcb4cc 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -9,6 +9,7 @@ using OnTopic.Attributes; using OnTopic.Data.Caching; using OnTopic.Metadata; +using OnTopic.References; using OnTopic.Repositories; using OnTopic.TestDoubles; using OnTopic.TestDoubles.Metadata; @@ -606,6 +607,30 @@ public void Save_UnresolvedReference_Resolves() { } + /*========================================================================================================================== + | TEST: SAVE: UNRESOLVED REFERENCE: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Saves a new with an unresolved and confirms that it throws an + /// exception if that reference cannot be resolved. + /// + [TestMethod] + [ExpectedException( + typeof(ReferentialIntegrityException), + "TopicRepository.Save() failed to throw an exception despite an unresolved topic reference." + )] + public void Save_UnresolvedReference_ThrowsException() { + + var parent = _topicRepository.Load("Root:Web:Web_3:Web_3_0"); + var topic = TopicFactory.Create("Test", "Page", parent); + var reference = TopicFactory.Create("Reference", "Page", parent); + + topic.References.SetTopic("Test", reference); + + _topicRepository.Save(topic, true); + + } + /*========================================================================================================================== | TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE \-------------------------------------------------------------------------------------------------------------------------*/ From 934594be817b950469580deb4f135da1e30b27c9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 19:37:31 -0800 Subject: [PATCH 459/778] Fixed error in unit test introduced by new `unresolvedTopics` exception The introduction of the `ReferentialIntegrityException` on `Save()` when there are `unresolvedTopics` (6f55b2a) broke one of the existing unit tests. This is fixed correctly by expanding the scope of the `Save()`. --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 4ddcb4cc..8b6b7bc1 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -535,13 +535,14 @@ public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { var contentTypes = _topicRepository.GetContentTypeDescriptors(); + var contentTypesRoot = contentTypes.GetTopic("ContentTypes"); var pageContentType = contentTypes.GetTopic("Page"); var lookupContentType = contentTypes.GetTopic("Lookup"); var initialCount = pageContentType.PermittedContentTypes.Count; pageContentType.Relationships.SetTopic("ContentTypes", lookupContentType); - _topicRepository.Save(pageContentType); + _topicRepository.Save(contentTypesRoot, true); Assert.AreNotEqual(initialCount, pageContentType.PermittedContentTypes.Count); From 1b8b8f9880df0bc7a8e9c383591b3a9ae3684619 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 20:04:12 -0800 Subject: [PATCH 460/778] Updated new `ITopicRepository` classes to use renamed event arguments Previously, the improvement/ITopicRepository-events branch renamed the `DeleteEvent`, `MoveEvent`, and `RenameEvent` to `TopicEvent`, `TopicMoveEvent`, and `TopicRenameEvent`. In the meanwhile, in the improvement/TopicRepositoryBase-refactoring branch, additional base classes implementing `ITopicRepository` were introduced which referenced these classes using the legacy names. These needed to be updated to use the latest naming conventions. --- .../Repositories/ObservableTopicRepository.cs | 45 ++++++++++--------- OnTopic/Repositories/TopicRepository.cs | 2 +- .../Repositories/TopicRepositoryDecorator.cs | 7 ++- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index d8e37367..3f0c1579 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -25,28 +25,28 @@ public abstract class ObservableTopicRepository : ITopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - private EventHandler? _deleteEvent; - private EventHandler? _moveEvent; - private EventHandler? _renameEvent; + private EventHandler? _deleteEvent; + private EventHandler? _moveEvent; + private EventHandler? _renameEvent; /*========================================================================================================================== | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual event EventHandler? DeleteEvent { + public virtual event EventHandler? DeleteEvent { add => _deleteEvent += value; remove => _deleteEvent -= value; } /// - public virtual event EventHandler? MoveEvent { + public virtual event EventHandler? MoveEvent { add => _moveEvent += value; remove => _moveEvent -= value; } /// - public virtual event EventHandler? RenameEvent { + public virtual event EventHandler? RenameEvent { add => _renameEvent += value; remove => _renameEvent -= value; } @@ -63,16 +63,16 @@ public virtual event EventHandler? RenameEvent { /// https://docs.microsoft.com/en-us/dotnet/standard/events/">Handling and Raising Events. /// /// - /// The method also allows derived classes to handle the event without + /// The method also allows derived classes to handle the event without /// attaching a delegate. This is the preferred technique for handling the event in a derived class. /// /// - /// When overriding the method in a derived class, be sure to call the - /// base class's method so that registered delegates receive the event. + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. /// /// - /// An instance of the associated with the event. - protected virtual void OnTopicDeleted(DeleteEventArgs args) => _deleteEvent?.Invoke(this, args); + /// An instance of the associated with the event. + protected virtual void OnTopicDeleted(TopicEventArgs args) => _deleteEvent?.Invoke(this, args); /*========================================================================================================================== | ON TOPIC MOVED @@ -86,16 +86,16 @@ public virtual event EventHandler? RenameEvent { /// https://docs.microsoft.com/en-us/dotnet/standard/events/">Handling and Raising Events. /// /// - /// The method also allows derived classes to handle the event without + /// The method also allows derived classes to handle the event without /// attaching a delegate. This is the preferred technique for handling the event in a derived class. /// /// - /// When overriding the method in a derived class, be sure to call the - /// base class's method so that registered delegates receive the event. + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. /// /// - /// An instance of the associated with the event. - protected virtual void OnTopicMoved(MoveEventArgs args) => _moveEvent?.Invoke(this, args); + /// An instance of the associated with the event. + protected virtual void OnTopicMoved(TopicMoveEventArgs args) => _moveEvent?.Invoke(this, args); /*========================================================================================================================== | ON TOPIC RENAMED @@ -109,16 +109,17 @@ public virtual event EventHandler? RenameEvent { /// https://docs.microsoft.com/en-us/dotnet/standard/events/">Handling and Raising Events. /// /// - /// The method also allows derived classes to handle the event without - /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// The method also allows derived classes to handle the event + /// without attaching a delegate. This is the preferred technique for handling the event in a derived class. /// /// - /// When overriding the method in a derived class, be sure to call the - /// base class's method so that registered delegates receive the event. + /// When overriding the method in a derived class, be sure to call + /// the base class's method so that registered delegates receive the + /// event. /// /// - /// An instance of the associated with the event. - protected virtual void OnTopicRenamed(RenameEventArgs args) => _renameEvent?.Invoke(this, args); + /// An instance of the associated with the event. + protected virtual void OnTopicRenamed(TopicRenameEventArgs args) => _renameEvent?.Invoke(this, args); /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 8aa5bca0..1c85acef 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -364,7 +364,7 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso \-----------------------------------------------------------------------------------------------------------------------*/ if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { var args = new TopicRenameEventArgs(topic, topic.Key, topic.OriginalKey); - _renameEvent?.Invoke(this, args); + OnTopicRenamed(args); } /*---------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic/Repositories/TopicRepositoryDecorator.cs b/OnTopic/Repositories/TopicRepositoryDecorator.cs index 0a2adebd..7d70ce36 100644 --- a/OnTopic/Repositories/TopicRepositoryDecorator.cs +++ b/OnTopic/Repositories/TopicRepositoryDecorator.cs @@ -52,10 +52,9 @@ protected TopicRepositoryDecorator(ITopicRepository topicRepository) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Subscribe to underlying events \-----------------------------------------------------------------------------------------------------------------------*/ - TopicRepository.DeleteEvent += (object sender, DeleteEventArgs args) => OnTopicDeleted(args); - TopicRepository.MoveEvent += (object sender, MoveEventArgs args) => OnTopicMoved(args); - TopicRepository.RenameEvent += (object sender, RenameEventArgs args) => OnTopicRenamed(args); - + TopicRepository.DeleteEvent += (object sender, TopicEventArgs args) => OnTopicDeleted(args); + TopicRepository.MoveEvent += (object sender, TopicMoveEventArgs args) => OnTopicMoved(args); + TopicRepository.RenameEvent += (object sender, TopicRenameEventArgs args) => OnTopicRenamed(args); } From 8b82a312d858a4428e904d76464b8793f6dec0cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 20:08:59 -0800 Subject: [PATCH 461/778] Raise events after performing all business logic If an exception is thrown, we don't want to raise the event. This is now possible since `TopicRepository` has separated out the implementation logic into e.g. `SaveTopic()`, thus allowing the e.g. `Save()` method to have more control over the timing and order of operations (116c8eb). --- OnTopic/Repositories/TopicRepository.cs | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 1c85acef..66d6b0e6 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -359,14 +359,6 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso topic.VersionHistory.Insert(0, version); } - /*------------------------------------------------------------------------------------------------------------------------ - | Trigger event - \-----------------------------------------------------------------------------------------------------------------------*/ - if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { - var args = new TopicRenameEventArgs(topic, topic.Key, topic.OriginalKey); - OnTopicRenamed(args); - } - /*---------------------------------------------------------------------------------------------------------------------- | Perform reordering and/or move \---------------------------------------------------------------------------------------------------------------------*/ @@ -416,6 +408,13 @@ _contentTypeDescriptors is not null && \-----------------------------------------------------------------------------------------------------------------------*/ topic.OriginalKey = null; + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + if (topic.OriginalKey is not null && topic.OriginalKey != topic.Key) { + OnTopicRenamed(new(topic, topic.Key, topic.OriginalKey)); + } + /*------------------------------------------------------------------------------------------------------------------------ | Recurse over children \-----------------------------------------------------------------------------------------------------------------------*/ @@ -489,8 +488,6 @@ topic.Parent is not null && | Perform base logic \-----------------------------------------------------------------------------------------------------------------------*/ var previousParent = topic.Parent; - var args = new TopicMoveEventArgs(topic, previousParent, target, sibling); - OnTopicMoved(args); if (sibling is null) { topic.SetParent(target); } @@ -510,6 +507,11 @@ topic.Parent is not null && ResetAttributeDescriptors(topic); } + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + OnTopicMoved(new (topic, previousParent, target, sibling)); + } /*========================================================================================================================== @@ -569,12 +571,6 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi \-----------------------------------------------------------------------------------------------------------------------*/ DeleteTopic(topic); - /*------------------------------------------------------------------------------------------------------------------------ - | Trigger event - \-----------------------------------------------------------------------------------------------------------------------*/ - var args = new TopicEventArgs(topic); - OnTopicDeleted(args); - /*------------------------------------------------------------------------------------------------------------------------ | Remove from parent \-----------------------------------------------------------------------------------------------------------------------*/ @@ -622,6 +618,12 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi ResetAttributeDescriptors(topic); } + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + var args = new TopicEventArgs(topic); + OnTopicDeleted(args); + } /*========================================================================================================================== From 2154ddb88c00ae5bc00512cca4723e0e120d6f69 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 26 Jan 2021 20:20:41 -0800 Subject: [PATCH 462/778] Renamed event delegates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The event delegates had ambiguous and non-standard names. They were ambiguous in that they simply said e.g. `Delete` or `Rename` instead of `Deleted` or `Deleting` or `Renamed` or `Renaming`. Since these are intended to run _after_ the operation has been completed they should be _past tense_. They were non-standard because they didn't follow the `{Object}{Verb}` convention, instead using a `{Verb}Event` convention. The delegates shouldn't include `Event` in their identifier, and they should specify what `{Object}` is being operated against—`Topic`, in these cases. These are, obviously, breaking changes, but very necessary changes to bring the events up to .NET design guidelines and best practices. Fortunately, events have almost never been used by implementers—perhaps in part because the naming conventions were so confusing—so we don't anticipate this being a breaking change. While I was at it, I improved the documentation for the events. --- OnTopic.Tests/ITopicRepositoryTest.cs | 4 +-- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 +-- OnTopic/Repositories/ITopicRepository.cs | 15 ++++---- .../Repositories/ObservableTopicRepository.cs | 36 +++++++++---------- OnTopic/Repositories/TopicEventArgs.cs | 4 +-- OnTopic/Repositories/TopicRepository.cs | 6 ++-- .../Repositories/TopicRepositoryDecorator.cs | 6 ++-- 7 files changed, 39 insertions(+), 36 deletions(-) diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index e3425ea4..c4a247de 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -229,7 +229,7 @@ public void Delete_Topic_Removed() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Creates a and then immediately deletes it. Ensures that the is fired, even though the original event is fired from the underlying + /// TopicDeleted"/> is fired, even though the original event is fired from the underlying /// and not the immediate . /// [TestMethod] @@ -239,7 +239,7 @@ public void Delete_DeleteEvent_IsFired() { var hasFired = false; _topicRepository.Save(topic); - _topicRepository.DeleteEvent += eventHandler; + _topicRepository.TopicDeleted += eventHandler; _topicRepository.Delete(topic); Assert.IsTrue(hasFired); diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 4cf2c50b..f661ceb3 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -727,7 +727,7 @@ public void Delete_AttributeDescriptor_UpdatesContentTypeCache() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Creates a and then immediately deletes it. Ensures that the is fired. + /// TopicDeleted"/> is fired. /// [TestMethod] public void Delete_DeleteEvent_IsFired() { @@ -736,7 +736,7 @@ public void Delete_DeleteEvent_IsFired() { var hasFired = false; _topicRepository.Save(topic); - _topicRepository.DeleteEvent += eventHandler; + _topicRepository.TopicDeleted += eventHandler; _topicRepository.Delete(topic); Assert.IsTrue(hasFired); diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index b7109b56..f14e8320 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -20,19 +20,22 @@ public interface ITopicRepository { | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Instantiates the event handler. + /// Raised after a is deleted from the as part of a operation. /// - event EventHandler DeleteEvent; + event EventHandler TopicDeleted; /// - /// Instantiates the event handler. + /// Raised after a is moved within the as part of a operation. /// - event EventHandler MoveEvent; + event EventHandler TopicMoved; /// - /// Instantiates the event handler. + /// Raised after a is renamed as ppart of a + /// operation. /// - event EventHandler RenameEvent; + event EventHandler TopicRenamed; /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 3f0c1579..3be80ffc 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -25,37 +25,37 @@ public abstract class ObservableTopicRepository : ITopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ - private EventHandler? _deleteEvent; - private EventHandler? _moveEvent; - private EventHandler? _renameEvent; + private EventHandler? _topicDeleted; + private EventHandler? _topicMoved; + private EventHandler? _topicRenamed; /*========================================================================================================================== | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual event EventHandler? DeleteEvent { - add => _deleteEvent += value; - remove => _deleteEvent -= value; + public virtual event EventHandler? TopicDeleted { + add => _topicDeleted += value; + remove => _topicDeleted -= value; } /// - public virtual event EventHandler? MoveEvent { - add => _moveEvent += value; - remove => _moveEvent -= value; + public virtual event EventHandler? TopicMoved { + add => _topicMoved += value; + remove => _topicMoved -= value; } /// - public virtual event EventHandler? RenameEvent { - add => _renameEvent += value; - remove => _renameEvent -= value; + public virtual event EventHandler? TopicRenamed { + add => _topicRenamed += value; + remove => _topicRenamed -= value; } /*========================================================================================================================== | ON TOPIC DELETED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the . /// /// /// @@ -72,13 +72,13 @@ public virtual event EventHandler? RenameEvent { /// /// /// An instance of the associated with the event. - protected virtual void OnTopicDeleted(TopicEventArgs args) => _deleteEvent?.Invoke(this, args); + protected virtual void OnTopicDeleted(TopicEventArgs args) => _topicDeleted?.Invoke(this, args); /*========================================================================================================================== | ON TOPIC MOVED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the . /// /// /// @@ -95,13 +95,13 @@ public virtual event EventHandler? RenameEvent { /// /// /// An instance of the associated with the event. - protected virtual void OnTopicMoved(TopicMoveEventArgs args) => _moveEvent?.Invoke(this, args); + protected virtual void OnTopicMoved(TopicMoveEventArgs args) => _topicMoved?.Invoke(this, args); /*========================================================================================================================== | ON TOPIC RENAMED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the . /// /// /// @@ -119,7 +119,7 @@ public virtual event EventHandler? RenameEvent { /// /// /// An instance of the associated with the event. - protected virtual void OnTopicRenamed(TopicRenameEventArgs args) => _renameEvent?.Invoke(this, args); + protected virtual void OnTopicRenamed(TopicRenameEventArgs args) => _topicRenamed?.Invoke(this, args); /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs index f38a358d..b3e6ee46 100644 --- a/OnTopic/Repositories/TopicEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -16,8 +16,8 @@ namespace OnTopic.Repositories { /// /// /// All events share at least one shared element: the being operated - /// against. Some, such as the , only relate to that information. Others, - /// such as , need additional information, and thus offer derived classes, such as + /// against. Some, such as the , only relate to that information. Others, + /// such as , need additional information, and thus offer derived classes, such as /// , to capture additional information. /// public class TopicEventArgs : EventArgs { diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 66d6b0e6..b02dc81d 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -437,7 +437,7 @@ _contentTypeDescriptors is not null && /// The main implementation handles advanced validation of the parameters, updating the /// , attempting to pick up any unresolved topics, updating and instances as appropriate, raising the , if needed, and recursing over children. The derived implementation of , if needed, and recursing over children. The derived implementation of is then left to focus exclusively on the core logic of persisting the changes /// to the individual to the underlying data store, and optionally updating its and , assuming is set to @@ -524,7 +524,7 @@ topic.Parent is not null && /// /// The main implementation handles advanced validation of the parameters, /// updating the 's location within the topic graph, updating and - /// instances as appropriate, and raising the . + /// instances as appropriate, and raising the . /// The derived implementation of is then left to focus exclusively on the /// core logic of persisting the change to the underlying data store. /// @@ -636,7 +636,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi /// /// The main implementation handles advanced validation of the parameters, /// removing the from the topic graph, updating and instances as appropriate, and raising the . The + /// AttributeDescriptor"/> instances as appropriate, and raising the . The /// derived implementation of is then left to focus exclusively on the core logic of /// persisting the change to the underlying data store. /// diff --git a/OnTopic/Repositories/TopicRepositoryDecorator.cs b/OnTopic/Repositories/TopicRepositoryDecorator.cs index 7d70ce36..a98cb18c 100644 --- a/OnTopic/Repositories/TopicRepositoryDecorator.cs +++ b/OnTopic/Repositories/TopicRepositoryDecorator.cs @@ -52,9 +52,9 @@ protected TopicRepositoryDecorator(ITopicRepository topicRepository) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Subscribe to underlying events \-----------------------------------------------------------------------------------------------------------------------*/ - TopicRepository.DeleteEvent += (object sender, TopicEventArgs args) => OnTopicDeleted(args); - TopicRepository.MoveEvent += (object sender, TopicMoveEventArgs args) => OnTopicMoved(args); - TopicRepository.RenameEvent += (object sender, TopicRenameEventArgs args) => OnTopicRenamed(args); + TopicRepository.TopicDeleted += (object sender, TopicEventArgs args) => OnTopicDeleted(args); + TopicRepository.TopicMoved += (object sender, TopicMoveEventArgs args) => OnTopicMoved(args); + TopicRepository.TopicRenamed += (object sender, TopicRenameEventArgs args) => OnTopicRenamed(args); } From fcefffee1eb64747427eacfea2be8f249bce266a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:44:44 -0800 Subject: [PATCH 463/778] Introduced `IsRecursive` property to base `TopicEventArgs` classes Most operations are either implicitly or explicitly recursive. For instance, a `Load()` or `Save()` operation can explicitly choose to include descendents, while a `Move()`, `Rename()`, or `Delete()` implicitly impact all descendents. The `IsRecursive` bit, therefore, defaults to `true`, but can be overwritten by specific events. --- OnTopic/Repositories/TopicEventArgs.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs index b3e6ee46..4c8eb1b9 100644 --- a/OnTopic/Repositories/TopicEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -29,7 +29,8 @@ public class TopicEventArgs : EventArgs { /// Initializes a new instance of the class. /// /// The being operated against. - public TopicEventArgs(Topic topic) : base() { + /// Whether or not descendants of the were also loaded. + public TopicEventArgs(Topic topic, bool isRecursive = true) : base() { Topic = topic; } @@ -41,5 +42,13 @@ public TopicEventArgs(Topic topic) : base() { /// public Topic Topic { get; set; } + /*========================================================================================================================== + | PROPERTY: IS RECURSIVE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the descendents of the are also impacted by this operation. + /// + public bool IsRecursive { get; set; } + } //Class } //Namespace \ No newline at end of file From 722003502565213e57aeb94ee8ea1b8b3d04f09b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:47:28 -0800 Subject: [PATCH 464/778] Explicitly set implicit default for `isRecursive` The default value is `true`, just like the underlying `IsRecursive` property, but by setting it explicitly we're helping reinforce this default by communicating it in the code, while also safeguarding against any potential changes to the default in the future. --- OnTopic/Repositories/TopicMoveEventArgs.cs | 2 +- OnTopic/Repositories/TopicRenameEventArgs.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/TopicMoveEventArgs.cs b/OnTopic/Repositories/TopicMoveEventArgs.cs index 6150714a..8ee2e84d 100644 --- a/OnTopic/Repositories/TopicMoveEventArgs.cs +++ b/OnTopic/Repositories/TopicMoveEventArgs.cs @@ -42,7 +42,7 @@ public class TopicMoveEventArgs : TopicEventArgs { /// /// != /// - public TopicMoveEventArgs(Topic topic, Topic? source, Topic target, Topic? sibling = null): base(topic) { + public TopicMoveEventArgs(Topic topic, Topic? source, Topic target, Topic? sibling = null): base(topic, true) { /*------------------------------------------------------------------------------------------------------------------------ | Vaidate parameters diff --git a/OnTopic/Repositories/TopicRenameEventArgs.cs b/OnTopic/Repositories/TopicRenameEventArgs.cs index 54451c23..e950da88 100644 --- a/OnTopic/Repositories/TopicRenameEventArgs.cs +++ b/OnTopic/Repositories/TopicRenameEventArgs.cs @@ -26,7 +26,7 @@ public class TopicRenameEventArgs : TopicEventArgs { /// The object associated with the rename event. /// The original key of the prior to being renamed. /// The new key of the after being renamed. - public TopicRenameEventArgs(Topic topic, string originalKey, string newKey): base(topic) { + public TopicRenameEventArgs(Topic topic, string originalKey, string newKey): base(topic, true) { /*------------------------------------------------------------------------------------------------------------------------ | Vaidate parameters From d1a07cb163ca51f797ab0c7dc279ccd0cefb524f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:48:26 -0800 Subject: [PATCH 465/778] Handle `Topic` exclusively in base constructor The `topic` is already being set by the base `TopicEventArgs` constructor, so there's no need to validate and set it again in children. --- OnTopic/Repositories/TopicMoveEventArgs.cs | 2 -- OnTopic/Repositories/TopicRenameEventArgs.cs | 1 - 2 files changed, 3 deletions(-) diff --git a/OnTopic/Repositories/TopicMoveEventArgs.cs b/OnTopic/Repositories/TopicMoveEventArgs.cs index 8ee2e84d..e2ac52a6 100644 --- a/OnTopic/Repositories/TopicMoveEventArgs.cs +++ b/OnTopic/Repositories/TopicMoveEventArgs.cs @@ -47,7 +47,6 @@ public TopicMoveEventArgs(Topic topic, Topic? source, Topic target, Topic? sibli /*------------------------------------------------------------------------------------------------------------------------ | Vaidate parameters \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(topic, "topic"); Contract.Requires(target, "target"); Contract.Requires(topic != target, "The topic cannot be its own parent."); Contract.Requires(topic != source, "The topic cannot be its own parent."); @@ -56,7 +55,6 @@ public TopicMoveEventArgs(Topic topic, Topic? source, Topic target, Topic? sibli /*------------------------------------------------------------------------------------------------------------------------ | Initialize properties \-----------------------------------------------------------------------------------------------------------------------*/ - Topic = topic; Source = source; Target = target; Sibling = sibling; diff --git a/OnTopic/Repositories/TopicRenameEventArgs.cs b/OnTopic/Repositories/TopicRenameEventArgs.cs index e950da88..e83959f2 100644 --- a/OnTopic/Repositories/TopicRenameEventArgs.cs +++ b/OnTopic/Repositories/TopicRenameEventArgs.cs @@ -38,7 +38,6 @@ public TopicRenameEventArgs(Topic topic, string originalKey, string newKey): bas /*------------------------------------------------------------------------------------------------------------------------ | Initialize properties \-----------------------------------------------------------------------------------------------------------------------*/ - Topic = topic; OriginalKey = originalKey; NewKey = newKey; From 6e6fb6fe67aa4345afbea81526013bca6930c15d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:49:21 -0800 Subject: [PATCH 466/778] Introduced `TopicLoadEventArgs` for `ITopicRepository.Load()` operations The `TopicLoadEventArgs` introduces a `Version` property for tracking scenarios where a specific version were loaded. --- OnTopic/Repositories/TopicLoadEventArgs.cs | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 OnTopic/Repositories/TopicLoadEventArgs.cs diff --git a/OnTopic/Repositories/TopicLoadEventArgs.cs b/OnTopic/Repositories/TopicLoadEventArgs.cs new file mode 100644 index 00000000..f76afeb4 --- /dev/null +++ b/OnTopic/Repositories/TopicLoadEventArgs.cs @@ -0,0 +1,52 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC LOAD EVENT ARGUMENTS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines an event argument type specific to load events. + /// + public class TopicLoadEventArgs : TopicEventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines the event arguments relevant to a operation and its overloads. + /// + /// The object associated with the rename event. + /// Whether or not descendants of the were also loaded. + /// If a specific version was loaded, specified that version. + public TopicLoadEventArgs(Topic topic, bool isRecursive, DateTime version): base(topic, isRecursive) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Vaidate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(version, nameof(version)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Version = version; + + } + + /*========================================================================================================================== + | PROPERTY: VERSION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the specific version of the that has been loaded. + /// + public DateTime Version { get; set; } + + } //Class +} //Namespace \ No newline at end of file From 97be8ae3905c1175342cb653e2a8407221948561 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:50:08 -0800 Subject: [PATCH 467/778] Introduced `TopicSaveEventArgs` for `ITopicRepository.Save()` operations The `TopicSaveEventArgs` introduces an `IsNew` property for tracking whether or not this is a newly created topic, or an update to an existing one. --- OnTopic/Repositories/TopicSaveEventArgs.cs | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 OnTopic/Repositories/TopicSaveEventArgs.cs diff --git a/OnTopic/Repositories/TopicSaveEventArgs.cs b/OnTopic/Repositories/TopicSaveEventArgs.cs new file mode 100644 index 00000000..24e63409 --- /dev/null +++ b/OnTopic/Repositories/TopicSaveEventArgs.cs @@ -0,0 +1,53 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: TOPIC SAVE EVENT ARGUMENTS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines an event argument type specific to save events. + /// + public class TopicSaveEventArgs : TopicEventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The object defines the event arguments relevant to a operation. + /// + /// The object associated with the rename event. + /// Whether or not descendants of the were also saved. + /// Whether or not this was a newly created . + public TopicSaveEventArgs(Topic topic, bool isRecursive, bool isNew): base(topic, isRecursive) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Vaidate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(isNew, nameof(isNew)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize properties + \-----------------------------------------------------------------------------------------------------------------------*/ + IsNew = isNew; + + } + + /*========================================================================================================================== + | PROPERTY: IS NEW + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets whether the was newly created, or if it was an existing that has been updated. + /// + public bool IsNew { get; set; } + + } //Class +} //Namespace \ No newline at end of file From ea7a0183737b76f03836ba71fd7b928465e7eb77 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:57:43 -0800 Subject: [PATCH 468/778] Introduced `TopicLoaded` and `TopicSaved` events to `ITopicRepository` These correspond to the new `TopicLoadEventArgs` (6e6fb6f) and `TopicSaveEventArgs` (97be8ae). --- OnTopic/Repositories/ITopicRepository.cs | 13 +++++++++++++ OnTopic/Repositories/ObservableTopicRepository.cs | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index f14e8320..71091c72 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -19,6 +19,19 @@ public interface ITopicRepository { /*========================================================================================================================== | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + /// Raised after a is loaded from the as part of a operation, or one of its overloads. + /// + event EventHandler TopicLoaded; + + /// + /// Raised after a is saved in the as part of a operation. + /// + event EventHandler TopicSaved; + /// /// Raised after a is deleted from the as part of a operation. diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 3be80ffc..e64c05fb 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -25,6 +25,8 @@ public abstract class ObservableTopicRepository : ITopicRepository { /*========================================================================================================================== | PRIVATE VARIABLES \-------------------------------------------------------------------------------------------------------------------------*/ + private EventHandler? _topicLoaded; + private EventHandler? _topicSaved; private EventHandler? _topicDeleted; private EventHandler? _topicMoved; private EventHandler? _topicRenamed; @@ -33,6 +35,18 @@ public abstract class ObservableTopicRepository : ITopicRepository { | EVENT HANDLERS \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public virtual event EventHandler? TopicLoaded { + add => _topicLoaded += value; + remove => _topicLoaded -= value; + } + + /// + public virtual event EventHandler? TopicSaved { + add => _topicSaved += value; + remove => _topicSaved -= value; + } + /// public virtual event EventHandler? TopicDeleted { add => _topicDeleted += value; From 603c4d25413023f1b33fef26dedde36e3efa960a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 11:58:43 -0800 Subject: [PATCH 469/778] Introduced `OnTopicLoaded()` and `OnTopicSaved()` raise methods These correspond to the new `TopicLoadEventArgs` (6e6fb6f) and `TopicSaveEventArgs` (97be8ae), and the `TopicLoaded` and `TopicSaved` events (ea7a018). --- .../Repositories/ObservableTopicRepository.cs | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index e64c05fb..318c47b2 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -65,11 +65,57 @@ public virtual event EventHandler? TopicRenamed { remove => _topicRenamed -= value; } + /*========================================================================================================================== + | ON TOPIC LOADED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Raises the event. + /// + /// + /// + /// Raising an event invokes the event handler through a delegate. For more information, see Handling and Raising Events. + /// + /// + /// The method also allows derived classes to handle the event without + /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// + /// + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. + /// + /// + /// An instance of the associated with the event. + protected virtual void OnTopicLoaded(TopicLoadEventArgs args) => _topicLoaded?.Invoke(this, args); + + /*========================================================================================================================== + | ON TOPIC SAVED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Raises the event. + /// + /// + /// + /// Raising an event invokes the event handler through a delegate. For more information, see Handling and Raising Events. + /// + /// + /// The method also allows derived classes to handle the event without + /// attaching a delegate. This is the preferred technique for handling the event in a derived class. + /// + /// + /// When overriding the method in a derived class, be sure to call the + /// base class's method so that registered delegates receive the event. + /// + /// + /// An instance of the associated with the event. + protected virtual void OnTopicSaved(TopicSaveEventArgs args) => _topicSaved?.Invoke(this, args); + /*========================================================================================================================== | ON TOPIC DELETED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the event. /// /// /// @@ -92,7 +138,7 @@ public virtual event EventHandler? TopicRenamed { | ON TOPIC MOVED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the event. /// /// /// @@ -115,7 +161,7 @@ public virtual event EventHandler? TopicRenamed { | ON TOPIC RENAMED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Raises the . + /// Raises the event. /// /// /// From dc150a2ec7757cedc6fc2f42d961822b6908a3e0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:05:03 -0800 Subject: [PATCH 470/778] Updated `TopicLoadEventArgs.Version` to be optional --- OnTopic/Repositories/TopicLoadEventArgs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/TopicLoadEventArgs.cs b/OnTopic/Repositories/TopicLoadEventArgs.cs index f76afeb4..8b0d7f30 100644 --- a/OnTopic/Repositories/TopicLoadEventArgs.cs +++ b/OnTopic/Repositories/TopicLoadEventArgs.cs @@ -26,7 +26,7 @@ public class TopicLoadEventArgs : TopicEventArgs { /// The object associated with the rename event. /// Whether or not descendants of the were also loaded. /// If a specific version was loaded, specified that version. - public TopicLoadEventArgs(Topic topic, bool isRecursive, DateTime version): base(topic, isRecursive) { + public TopicLoadEventArgs(Topic topic, bool isRecursive, DateTime? version = null): base(topic, isRecursive) { /*------------------------------------------------------------------------------------------------------------------------ | Vaidate parameters @@ -46,7 +46,7 @@ public TopicLoadEventArgs(Topic topic, bool isRecursive, DateTime version): base /// /// Gets or sets the specific version of the that has been loaded. /// - public DateTime Version { get; set; } + public DateTime? Version { get; set; } } //Class } //Namespace \ No newline at end of file From 2428ea202451c702d00a5af75fb048cdb408ddec Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:05:38 -0800 Subject: [PATCH 471/778] Raise `TopicSaved` event --- OnTopic/Repositories/TopicRepository.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index b02dc81d..f85494fe 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -263,6 +263,7 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv \-----------------------------------------------------------------------------------------------------------------------*/ var version = DateTime.UtcNow; var unresolvedTopics = new TopicCollection(); + var isNew = topic.IsNew; /*------------------------------------------------------------------------------------------------------------------------ | Handle first pass @@ -289,6 +290,11 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv ); } + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + OnTopicSaved(new(topic, isRecursive, isNew)); + } /// From 7a70d8bf692d20dc58a0dc2e34d1999b7f20298f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:10:37 -0800 Subject: [PATCH 472/778] Raise `TopicLoaded` event in `SqlTopicRepository` Due to the nature of the `TopicLoaded` event, it makes more sense, currently, to raise it in the core implementation, since we don't have a robust default `Load()` implementation in the `TopicRepository` class. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 10 ++++++++++ OnTopic/Repositories/ITopicRepository.cs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 169e7cd5..1fd654d0 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -182,6 +182,11 @@ public override Topic Load(int topicId, Topic? referenceTopic = null, bool isRec \-----------------------------------------------------------------------------------------------------------------------*/ base.SetContentTypeDescriptors(topic); + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + OnTopicLoaded(new(topic, isRecursive)); + /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ @@ -281,6 +286,11 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic topic.Attributes.Remove(attribute.Key); } + /*------------------------------------------------------------------------------------------------------------------------ + | Raise event + \-----------------------------------------------------------------------------------------------------------------------*/ + OnTopicLoaded(new(topic, false, version)); + /*------------------------------------------------------------------------------------------------------------------------ | Return objects \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 71091c72..598be865 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -24,6 +24,12 @@ public interface ITopicRepository { /// Raised after a is loaded from the as part of a operation, or one of its overloads. /// + /// + /// The event should only be raised when a new is loaded from the underlying + /// persistence store. It should not be loaded, for example, if a value is loaded from the cache, or a topicId is + /// queried from the database. Given this, this event will need to be raised in actual implementations, since it is + /// specific to the business logic of each . + /// event EventHandler TopicLoaded; /// From 03523895c01383742dd957bde6c5c019cb159cb7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:12:08 -0800 Subject: [PATCH 473/778] Added event bubbling logic for `TopicLoaded` and `TopicSaved` These correspond to the new `TopicLoadEventArgs` (6e6fb6f) and `TopicSaveEventArgs` (97be8ae), and the `TopicLoaded` and `TopicSaved` events (ea7a018). --- OnTopic/Repositories/TopicRepositoryDecorator.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/TopicRepositoryDecorator.cs b/OnTopic/Repositories/TopicRepositoryDecorator.cs index a98cb18c..6b9355c4 100644 --- a/OnTopic/Repositories/TopicRepositoryDecorator.cs +++ b/OnTopic/Repositories/TopicRepositoryDecorator.cs @@ -52,9 +52,11 @@ protected TopicRepositoryDecorator(ITopicRepository topicRepository) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Subscribe to underlying events \-----------------------------------------------------------------------------------------------------------------------*/ - TopicRepository.TopicDeleted += (object sender, TopicEventArgs args) => OnTopicDeleted(args); - TopicRepository.TopicMoved += (object sender, TopicMoveEventArgs args) => OnTopicMoved(args); - TopicRepository.TopicRenamed += (object sender, TopicRenameEventArgs args) => OnTopicRenamed(args); + TopicRepository.TopicLoaded += (object sender, TopicLoadEventArgs args) => OnTopicLoaded(args); + TopicRepository.TopicSaved += (object sender, TopicSaveEventArgs args) => OnTopicSaved(args); + TopicRepository.TopicDeleted += (object sender, TopicEventArgs args) => OnTopicDeleted(args); + TopicRepository.TopicMoved += (object sender, TopicMoveEventArgs args) => OnTopicMoved(args); + TopicRepository.TopicRenamed += (object sender, TopicRenameEventArgs args) => OnTopicRenamed(args); } From e77c7d5038cc9b4a28daaa140f234e656e8c5b48 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:53:24 -0800 Subject: [PATCH 474/778] Removed `virtual` from the `RedirectController.Redirect()` action This implementation is so straight forward that it's likely anyone with specialized needs would just create their own controller. This is really just a convenience controller for a standard requirement; customers with more sophisticated needs have added their own redirect controllers either to replace or to complement the out-of-the-box `RedirectController`. If we discover a specific extensibility point that we want to customize in the future, we can reevaluate the best approach for that then. --- OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs b/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs index a191cbda..88e1bc93 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/RedirectController.cs @@ -49,7 +49,7 @@ public RedirectController(ITopicRepository topicRepository) : base() { /// Redirect based on . /// /// The to lookup in the . - public virtual ActionResult Redirect(int topicId) { + public ActionResult Redirect(int topicId) { /*------------------------------------------------------------------------------------------------------------------------ | Find the topic with the correct PageID. From da5e81c601ec40366084287c48a57cd807862484 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 12:59:17 -0800 Subject: [PATCH 475/778] Removed `virtual` from the `SitemapController` actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the `SitemapController`, the implementation is so involved that it's unclear how deriving this would satisfy any customization requirements. If there's a need to customize the business logic—and there may well be!—we'd want to revisit what the extensibility points are, and whether there's a better way of accomodating the need. For instance, there may be a good argument for exposing an options object to the constructor, or a delegate which validates which topics or attributes to include in the index. For now, however, having the `Index` and `Extended` actions marked as `virtual` doesn't really facilitate those potential requirements. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index fb4b82c1..7d3e01ff 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -98,7 +98,7 @@ public SitemapController(ITopicRepository topicRepository) { /// Optionally enables indentation of XML elements in output for human readability. /// Optionally enables extended metadata associated with each topic. /// A Sitemap.org sitemap. - public virtual ActionResult Index(bool indent = false, bool includeMetadata = false) { + public ActionResult Index(bool indent = false, bool includeMetadata = false) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure topics are loaded @@ -138,7 +138,7 @@ public virtual ActionResult Index(bool indent = false, bool includeMetadata = fa /// /// Optionally enables indentation of XML elements in output for human readability. /// A Sitemap.org sitemap. - public virtual ActionResult Extended(bool indent = false) => Index(indent, true); + public ActionResult Extended(bool indent = false) => Index(indent, true); /*========================================================================================================================== | METHOD: GENERATE SITEMAP From 2caf2534d579abfbdb4a80a4acfb41b88d39e7d6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:02:41 -0800 Subject: [PATCH 476/778] Removed `virtual` from `TopicView()` helper It's unclear exactly how a derived class might expect to update this. If they needed to customize `TopicViewResult` then they'd likely want a new method, and would likewise expect extensive updates to the underlying infrastructure components that this feeds into down the pipeline. The purpose of this is really just to tie in the contextual `ViewData`, `TempData`, and `ContentType` so it doesn't need to be explicitly defined. --- OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs b/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs index d8e9d172..e16115ef 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs @@ -121,7 +121,7 @@ public async virtual Task IndexAsync(string path) { /// The optional name of the view that is rendered to the response. /// The created object for the response. [NonAction] - public virtual TopicViewResult TopicView(object model, string? viewName = null) => + public TopicViewResult TopicView(object model, string? viewName = null) => new(ViewData, TempData, model, CurrentTopic?.ContentType, viewName); } //Class From f515189fe837ff230b2141ad18fbb49136389d03 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:08:03 -0800 Subject: [PATCH 477/778] Removed `virtual` from the event definitions Originally, these were marked as `virtual` so that they could be overwritten in decorators, such as the `CachedTopicRepository`, in order to handle event bubbling. With the introduction of the raise methods (e.g., `OnTopicDeleted()`), this can be handled in the constructor by simply subscribing to the source `ITopicRepository` events and relaying them to the raise methods. Given that, there's no strict need to keep these marked as virtual. If we rediscover a need to do this, we can revisit that in the future. (Technically, since the only reason we moved away from the field-like event syntax in the first place was to allow them to be `virtual`, we could go back to the symplified event format. But since this is now in its own `ObservableTopicRepository` base class that's not especially critical.) --- OnTopic/Repositories/ObservableTopicRepository.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 318c47b2..7d59a114 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -36,31 +36,31 @@ public abstract class ObservableTopicRepository : ITopicRepository { \-------------------------------------------------------------------------------------------------------------------------*/ /// - public virtual event EventHandler? TopicLoaded { + public event EventHandler? TopicLoaded { add => _topicLoaded += value; remove => _topicLoaded -= value; } /// - public virtual event EventHandler? TopicSaved { + public event EventHandler? TopicSaved { add => _topicSaved += value; remove => _topicSaved -= value; } /// - public virtual event EventHandler? TopicDeleted { + public event EventHandler? TopicDeleted { add => _topicDeleted += value; remove => _topicDeleted -= value; } /// - public virtual event EventHandler? TopicMoved { + public event EventHandler? TopicMoved { add => _topicMoved += value; remove => _topicMoved -= value; } /// - public virtual event EventHandler? TopicRenamed { + public event EventHandler? TopicRenamed { add => _topicRenamed += value; remove => _topicRenamed -= value; } From d802bcdf4038f2509c88fbe3476e7ada18356cfd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:10:31 -0800 Subject: [PATCH 478/778] Removed `virtual` from `SetContentTypeDescriptors()` method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There aren't currently any implementations that override these and it's unclear what specific need they'd be addressing if they did. As always, we can revisit this down the road if there's a customer requirement. But, even if we did, we'd likely want to revisit how this was handled—such as, potentially, a new `protected virtual` overload with no implementation which allows business logic to be _injected_ into the main overload, instead of relying on the derived class to call `base.SetContentTypeDescriptors()`. --- OnTopic/Repositories/TopicRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index f85494fe..d8bee22d 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -118,7 +118,7 @@ protected virtual ContentTypeDescriptorCollection GetContentTypeDescriptors(Cont /// "ContentTypeDescriptor"/> within the graph. /// /// - protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Topic? sourceTopic) => + protected ContentTypeDescriptorCollection SetContentTypeDescriptors(Topic? sourceTopic) => SetContentTypeDescriptors(sourceTopic?.GetByUniqueKey("Root:Configuration:ContentTypes") as ContentTypeDescriptor); /// @@ -139,7 +139,7 @@ protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(Topi /// "ContentTypeDescriptor"/>, but also any descendents. /// /// - protected virtual ContentTypeDescriptorCollection SetContentTypeDescriptors(ContentTypeDescriptor? rootContentType) { + protected ContentTypeDescriptorCollection SetContentTypeDescriptors(ContentTypeDescriptor? rootContentType) { /*------------------------------------------------------------------------------------------------------------------------ | Establish cache From fe609ffa54cda699bbe034a7ac0828ceb0d1dcf7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:11:03 -0800 Subject: [PATCH 479/778] Fixed bug in `isRecursive` parameter handling This wasn't previously wired up to the `IsRecursive` property. Whoops! --- OnTopic/Repositories/TopicEventArgs.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs index 4c8eb1b9..f81a8459 100644 --- a/OnTopic/Repositories/TopicEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -31,7 +31,8 @@ public class TopicEventArgs : EventArgs { /// The being operated against. /// Whether or not descendants of the were also loaded. public TopicEventArgs(Topic topic, bool isRecursive = true) : base() { - Topic = topic; + Topic = topic; + IsRecursive = isRecursive; } /*========================================================================================================================== From 46cc5062c739f34aaf64fccab0e61aaf889c94b5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:36:19 -0800 Subject: [PATCH 480/778] Default to `sealed` for `protected override` members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we override a `virtual` member and provide our own implementation, we should default to `sealing` that member to prevent further derivations from modifying that logic unless we've _explicitly_ reviewed that particular case and determined that it is appropriate for derived classes to override the logic—and, potentially, not call `base`. In most of these cases, we just implemented `protected override` without specific consideration of (further) derived implementations. In many of these cases, such as `SqlTopicRepository`, we don't anticipate derivations. In others, such as `KeyedTopicCollection`, we do, but not of `protected override` members, such as `GetKeyForItem()` or `InsertItem()`. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- .../Collections/TopicViewModelCollection.cs | 2 +- OnTopic/Attributes/AttributeValueCollection.cs | 10 +++++----- OnTopic/Collections/KeyedTopicCollection{T}.cs | 4 ++-- OnTopic/Collections/TopicMultiMap.cs | 2 +- OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs | 4 ++-- .../Internal/Reflection/TypeMemberInfoCollection.cs | 4 ++-- OnTopic/Lookup/TypeCollection.cs | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 1fd654d0..fa5a7143 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -525,7 +525,7 @@ bool persistRelationships | METHOD: MOVE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void MoveTopic(Topic topic, Topic target, Topic? sibling) { + protected override sealed void MoveTopic(Topic topic, Topic target, Topic? sibling) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -576,7 +576,7 @@ protected override void MoveTopic(Topic topic, Topic target, Topic? sibling) { | METHOD: DELETE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void DeleteTopic(Topic topic) { + protected override sealed void DeleteTopic(Topic topic) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters diff --git a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs index a42338e5..54df8b51 100644 --- a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs @@ -68,7 +68,7 @@ public TopicViewModelCollection GetByContentType(string contentType) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(TItem item) { + protected override sealed string GetKeyForItem(TItem item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key?? ""; } diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index ba8cce5a..63043372 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -495,7 +495,7 @@ internal void SetValue( /// An AttributeValue with the Key '{item.Key}' already exists. The Value of the existing item is "{this[item.Key].Value}; /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." /// - protected override void InsertItem(int index, AttributeValue item) { + protected override sealed void InsertItem(int index, AttributeValue item) { Contract.Requires(item, nameof(item)); if (_topicPropertyDispatcher.Enforce(item.Key, item)) { if (!Contains(item.Key)) { @@ -530,7 +530,7 @@ protected override void InsertItem(int index, AttributeValue item) { /// /// The location that the should be set. /// The object which is being inserted. - protected override void SetItem(int index, AttributeValue item) { + protected override sealed void SetItem(int index, AttributeValue item) { Contract.Requires(item, nameof(item)); if (_topicPropertyDispatcher.Enforce(item.Key, item)) { base.SetItem(index, item); @@ -551,7 +551,7 @@ protected override void SetItem(int index, AttributeValue item) { /// When an is removed, will return true—even if no remaining /// s are marked as . /// - protected override void RemoveItem(int index) { + protected override sealed void RemoveItem(int index) { var attribute = this[index]; DeletedAttributes.Add(attribute.Key); base.RemoveItem(index); @@ -568,7 +568,7 @@ protected override void RemoveItem(int index) { /// When an is removed, will return true—even if no remaining /// s are marked as . /// - protected override void ClearItems() { + protected override sealed void ClearItems() { DeletedAttributes.AddRange(Items.Select(a => a.Key)); base.ClearItems(); } @@ -581,7 +581,7 @@ protected override void ClearItems() { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(AttributeValue item) { + protected override sealed string GetKeyForItem(AttributeValue item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key; } diff --git a/OnTopic/Collections/KeyedTopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs index ee61a5f3..cee38824 100644 --- a/OnTopic/Collections/KeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs @@ -70,7 +70,7 @@ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer /// A {typeof(T).Name} with the Key '{item.Key}' already exists. The UniqueKey of the existing {typeof(T).Name} is /// '{GetUniqueKey()}'; the new item's is '{item.GetUniqueKey()}'. /// - protected override void InsertItem(int index, T item) { + protected override sealed void InsertItem(int index, T item) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -115,7 +115,7 @@ protected override void InsertItem(int index, T item) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(T item) { + protected override sealed string GetKeyForItem(T item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key; } diff --git a/OnTopic/Collections/TopicMultiMap.cs b/OnTopic/Collections/TopicMultiMap.cs index 056a23d9..f77b5cba 100644 --- a/OnTopic/Collections/TopicMultiMap.cs +++ b/OnTopic/Collections/TopicMultiMap.cs @@ -147,7 +147,7 @@ public bool Remove(string key, Topic topic) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(KeyValuesPair item) { + protected override sealed string GetKeyForItem(KeyValuesPair item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key; } diff --git a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs index e97ddcfa..4ebf4cc3 100644 --- a/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs +++ b/OnTopic/Internal/Reflection/MemberInfoCollection{T}.cs @@ -75,7 +75,7 @@ internal MemberInfoCollection(Type type, IEnumerable members) : base(StringCo /// The zero-based index at which should be inserted. /// The instance to insert. /// The Type '{Type.Name}' already contains the MemberInfo '{item.Name}' - protected override void InsertItem(int index, T item) { + protected override sealed void InsertItem(int index, T item) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -109,7 +109,7 @@ protected override void InsertItem(int index, T item) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(T item) { + protected override sealed string GetKeyForItem(T item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Name; } diff --git a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs index d8724e91..e9011021 100644 --- a/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs +++ b/OnTopic/Internal/Reflection/TypeMemberInfoCollection.cs @@ -124,7 +124,7 @@ internal MemberInfoCollection GetMembers(Type type) where T : MemberInfo = /// /// The TypeMemberInfoCollection already contains the MemberInfoCollection of the Type '{item.Type}'. /// - protected override void InsertItem(int index, MemberInfoCollection item) { + protected override sealed void InsertItem(int index, MemberInfoCollection item) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -154,7 +154,7 @@ protected override void InsertItem(int index, MemberInfoCollection item) { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override Type GetKeyForItem(MemberInfoCollection item) { + protected override sealed Type GetKeyForItem(MemberInfoCollection item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Type; } diff --git a/OnTopic/Lookup/TypeCollection.cs b/OnTopic/Lookup/TypeCollection.cs index 1ca787ff..c3f91ff7 100644 --- a/OnTopic/Lookup/TypeCollection.cs +++ b/OnTopic/Lookup/TypeCollection.cs @@ -52,7 +52,7 @@ internal TypeCollection(IEnumerable? types = null) : base(StringComparer.I /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override string GetKeyForItem(Type item) { + protected override sealed string GetKeyForItem(Type item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Name; } From 52488e5ad38ac259d5d3cb1408cfcfacc9b2cd7e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:41:42 -0800 Subject: [PATCH 481/778] Mark helper methods as `private` in mapping services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the `TopicMappingService` and `ReverseTopicMappingService`, we had a number of helper methods—such as `SetScalarValue()` or `GetSourceCollection()`—which were marked as `protected`. Presumably, this was so that future derived classes could reuse these, while overriding other aspects of the functionality. In practice, though, none of the members are marked as `virtual`, and so there's no (supported) use case for extensibility. If we want to allow the `TopicMappingService` or `ReverseTopicMappingService` to be extended, we should carefully evaluate what the use cases are, and better address them through e.g. `protected virtual` members, delegates, and possibly an options object. Until then, we should not expose internal helper methods with a more deliberate plan for how they will be used, and care to make sure we're protecting core functionality while exposing room for expansion. Finally, the `BindingModelValidator`, which is related to the `ReverseTopicMappingService`, already had its protected members migrated to `internal` (not `private`, since they're used by `ReverseTopicMappingService`), but the comment headers still communicated that they were `protected`, so that's been resolved while I was at it. --- .../Mapping/Reverse/BindingModelValidator.cs | 6 ++-- .../Reverse/ReverseTopicMappingService.cs | 28 ++++++++-------- OnTopic/Mapping/TopicMappingService.cs | 32 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index df270fda..8590f366 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -51,7 +51,7 @@ static internal class BindingModelValidator { static readonly ConcurrentBag<(Type, string)> _modelsValidated = new(); /*========================================================================================================================== - | PROTECTED: VALIDATE MODEL + | INTERNAL: VALIDATE MODEL \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function that evaluates the binding model against the associated content type to identify any potential @@ -125,7 +125,7 @@ static internal void ValidateModel( } /*========================================================================================================================== - | PROTECTED: VALIDATE PROPERTY + | INTERNAL: VALIDATE PROPERTY \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function that evaluates a property of the binding model against the associated content type to identify any @@ -275,7 +275,7 @@ attributeDescriptor.ModelType is ModelType.Reference && } /*========================================================================================================================== - | PROTECTED: VALIDATE RELATIONSHIP + | INTERNAL: VALIDATE RELATIONSHIP \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function that evaluates a relationship property on the binding model against the associated content type to diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 781812d8..8751136b 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -180,7 +180,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { } /*========================================================================================================================== - | PROTECTED: MAP (TOPIC) + | PRIVATE: MAP (TOPIC) \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a binding model and an existing , will map the properties of the binding model to attributes @@ -194,7 +194,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { /// /// An instance of provided with attributes appropriately mapped. /// - protected async Task MapAsync(object? source, Topic target, string? attributePrefix) { + private async Task MapAsync(object? source, Topic target, string? attributePrefix) { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -232,7 +232,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { } /*========================================================================================================================== - | PROTECTED: SET PROPERTY (ASYNC) + | PRIVATE: SET PROPERTY (ASYNC) \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function that evaluates each property on the source and then attempts to @@ -245,7 +245,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { /// The entity to map the data to. /// Information related to the current property. /// The prefix to apply to the attributes. - protected async Task SetPropertyAsync( + private async Task SetPropertyAsync( object source, Topic target, PropertyInfo property, @@ -324,7 +324,7 @@ await MapAsync( } /*========================================================================================================================== - | PROTECTED: SET SCALAR VALUE + | PRIVATE: SET SCALAR VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets an attribute on the target with a scalar value from the source binding model. @@ -344,7 +344,7 @@ await MapAsync( /// The entity to map the data to. /// The with details about the property's attributes. /// - protected static void SetScalarValue( + private static void SetScalarValue( object source, Topic target, PropertyConfiguration configuration @@ -389,7 +389,7 @@ PropertyConfiguration configuration } /*========================================================================================================================== - | PROTECTED: SET RELATIONSHIPS + | PRIVATE: SET RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a relationship property, identifies the target for each related item, and sets it on the @@ -402,7 +402,7 @@ PropertyConfiguration configuration /// /// The with details about the property's attributes. /// - protected void SetRelationships( + private void SetRelationships( object source, Topic target, PropertyConfiguration configuration @@ -447,7 +447,7 @@ PropertyConfiguration configuration } /*========================================================================================================================== - | PROTECTED: SET NESTED TOPICS + | PRIVATE: SET NESTED TOPICS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a nested topic property, serializes a topic for each property, and sets it on the target 's @@ -460,7 +460,7 @@ PropertyConfiguration configuration /// /// The with details about the property's attributes. /// - protected async Task SetNestedTopicsAsync( + private async Task SetNestedTopicsAsync( object source, Topic target, PropertyConfiguration configuration @@ -495,7 +495,7 @@ PropertyConfiguration configuration } /*========================================================================================================================== - | PROTECTED: SET REFERENCE + | PRIVATE: SET REFERENCE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a reference property, lookup the associated topic and set its on the /// The with details about the property's attributes. /// - protected void SetReference( + private void SetReference( object source, Topic target, PropertyConfiguration configuration @@ -561,14 +561,14 @@ PropertyConfiguration configuration } /*========================================================================================================================== - | PROTECTED: POPULATE TARGET COLLECTION + | PRIVATE: POPULATE TARGET COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a source list, will populate a target list based on the configured behavior of the source property. /// /// The to pull the binding models from. /// The target to add the mapped objects to. - protected async Task PopulateTargetCollectionAsync( + private async Task PopulateTargetCollectionAsync( IList sourceList, KeyedTopicCollection targetList ) { diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index eaecfcf4..e49836b8 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -243,7 +243,7 @@ private async Task MapAsync( } /*========================================================================================================================== - | PROTECTED: SET PROPERTY (ASYNC) + | PRIVATE: SET PROPERTY (ASYNC) \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function that evaluates each property on the target object and attempts to retrieve a value from the source @@ -256,7 +256,7 @@ private async Task MapAsync( /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// Determines if properties not associated with properties should be mapped. - protected async Task SetPropertyAsync( + private async Task SetPropertyAsync( Topic source, object target, Relationships relationships, @@ -346,7 +346,7 @@ await MapAsync( } /*========================================================================================================================== - | PROTECTED: SET SCALAR VALUE + | PRIVATE: SET SCALAR VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets a scalar property on a target DTO. @@ -364,7 +364,7 @@ await MapAsync( /// The target DTO on which to set the property value. /// The with details about the property's attributes. /// - protected static void SetScalarValue(Topic source, object target, PropertyConfiguration configuration) { + private static void SetScalarValue(Topic source, object target, PropertyConfiguration configuration) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -413,7 +413,7 @@ protected static void SetScalarValue(Topic source, object target, PropertyConfig } /*========================================================================================================================== - | PROTECTED: SET COLLECTION VALUE + | PRIVATE: SET COLLECTION VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a collection property, identifies a source collection, maps the values to DTOs, and attempts to add them to the @@ -434,7 +434,7 @@ protected static void SetScalarValue(Topic source, object target, PropertyConfig /// The with details about the property's attributes. /// /// A cache to keep track of already-mapped object instances. - protected async Task SetCollectionValueAsync( + private async Task SetCollectionValueAsync( Topic source, object target, Relationships relationships, @@ -488,7 +488,7 @@ MappedTopicCache cache } /*========================================================================================================================== - | PROTECTED: GET SOURCE COLLECTION + | PRIVATE: GET SOURCE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a source topic and a property configuration, attempts to identify a source collection that maps to the property. @@ -506,7 +506,7 @@ MappedTopicCache cache /// /// The with details about the property's attributes. /// - protected IList GetSourceCollection(Topic source, Relationships relationships, PropertyConfiguration configuration) { + private IList GetSourceCollection(Topic source, Relationships relationships, PropertyConfiguration configuration) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -621,7 +621,7 @@ IList GetRelationship(RelationshipType relationship, Func c } /*========================================================================================================================== - | PROTECTED: POPULATE TARGET COLLECTION + | PRIVATE: POPULATE TARGET COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a source list, will populate a target list based on the configured behavior of the target property. @@ -632,7 +632,7 @@ IList GetRelationship(RelationshipType relationship, Func c /// The with details about the property's attributes. /// /// A cache to keep track of already-mapped object instances. - protected async Task PopulateTargetCollectionAsync( + private async Task PopulateTargetCollectionAsync( IList sourceList, IList targetList, PropertyConfiguration configuration, @@ -729,7 +729,7 @@ void AddToList(object dto) { } /*========================================================================================================================== - | PROTECTED: SET TOPIC REFERENCE + | PRIVATE: SET TOPIC REFERENCE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a reference to an external topic, attempts to match it to a matching property. @@ -740,7 +740,7 @@ void AddToList(object dto) { /// The with details about the property's attributes. /// /// A cache to keep track of already-mapped object instances. - protected async Task SetTopicReferenceAsync( + private async Task SetTopicReferenceAsync( Topic source, object target, PropertyConfiguration configuration, @@ -780,7 +780,7 @@ MappedTopicCache cache } /*========================================================================================================================== - | PROTECTED: FLATTEN TOPIC GRAPH + | PRIVATE: FLATTEN TOPIC GRAPH \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Helper function recursively iterates through children and adds each to a collection. @@ -788,7 +788,7 @@ MappedTopicCache cache /// The entity pull the data from. /// The list of instances to add each child to. /// Optionally enable including nested topics in the list. - protected IList FlattenTopicGraph(Topic source, IList targetList, bool includeNestedTopics = false) { + private IList FlattenTopicGraph(Topic source, IList targetList, bool includeNestedTopics = false) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -812,7 +812,7 @@ protected IList FlattenTopicGraph(Topic source, IList targetList, } /*========================================================================================================================== - | PROTECTED: SET COMPATIBLE PROPERTY + | PRIVATE: SET COMPATIBLE PROPERTY \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets a property on the target view model to a compatible value on the source object. @@ -826,7 +826,7 @@ protected IList FlattenTopicGraph(Topic source, IList targetList, /// The target DTO on which to set the property value. /// The with details about the property's attributes. /// - protected static bool SetCompatibleProperty(Topic source, object target, PropertyConfiguration configuration) { + private static bool SetCompatibleProperty(Topic source, object target, PropertyConfiguration configuration) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters From 0134749b7bf0016d8a3e18814dddf6754f5ca200 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:44:19 -0800 Subject: [PATCH 482/778] Removed validation of boolean types in `TopicEventArgs` derivatives The nullability analysis doesn't necessitate that we validate whether non-nullable value types are null. More importantly, when using `Contract.Requires()` with a Boolean value, the semmantics are confused, as this is checking to determine if a condition is true. As such, `Contract.Requires()` will actually fail if the parameter is set, but set to `false`. That's not what we want. Whoops! --- OnTopic/Repositories/TopicLoadEventArgs.cs | 5 ----- OnTopic/Repositories/TopicSaveEventArgs.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/OnTopic/Repositories/TopicLoadEventArgs.cs b/OnTopic/Repositories/TopicLoadEventArgs.cs index 8b0d7f30..b7725d86 100644 --- a/OnTopic/Repositories/TopicLoadEventArgs.cs +++ b/OnTopic/Repositories/TopicLoadEventArgs.cs @@ -28,11 +28,6 @@ public class TopicLoadEventArgs : TopicEventArgs { /// If a specific version was loaded, specified that version. public TopicLoadEventArgs(Topic topic, bool isRecursive, DateTime? version = null): base(topic, isRecursive) { - /*------------------------------------------------------------------------------------------------------------------------ - | Vaidate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(version, nameof(version)); - /*------------------------------------------------------------------------------------------------------------------------ | Initialize properties \-----------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/TopicSaveEventArgs.cs b/OnTopic/Repositories/TopicSaveEventArgs.cs index 24e63409..daf1e5aa 100644 --- a/OnTopic/Repositories/TopicSaveEventArgs.cs +++ b/OnTopic/Repositories/TopicSaveEventArgs.cs @@ -28,11 +28,6 @@ public class TopicSaveEventArgs : TopicEventArgs { /// Whether or not this was a newly created . public TopicSaveEventArgs(Topic topic, bool isRecursive, bool isNew): base(topic, isRecursive) { - /*------------------------------------------------------------------------------------------------------------------------ - | Vaidate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(isNew, nameof(isNew)); - /*------------------------------------------------------------------------------------------------------------------------ | Initialize properties \-----------------------------------------------------------------------------------------------------------------------*/ From 54501ef99ad6d628136aab19a36a9c4bfbef5d04 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 13:44:27 -0800 Subject: [PATCH 483/778] Validate `topic` parameter for `TopicEventArgs` --- OnTopic/Repositories/TopicEventArgs.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/TopicEventArgs.cs index f81a8459..5ab65afa 100644 --- a/OnTopic/Repositories/TopicEventArgs.cs +++ b/OnTopic/Repositories/TopicEventArgs.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Internal.Diagnostics; namespace OnTopic.Repositories { @@ -31,8 +32,18 @@ public class TopicEventArgs : EventArgs { /// The being operated against. /// Whether or not descendants of the were also loaded. public TopicEventArgs(Topic topic, bool isRecursive = true) : base() { + + /*------------------------------------------------------------------------------------------------------------------------ + | Vaidate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(topic, nameof(topic)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize properties + \-----------------------------------------------------------------------------------------------------------------------*/ Topic = topic; IsRecursive = isRecursive; + } /*========================================================================================================================== From 338d157e4c96bf69e0daec39100eb7f3825a3935 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 14:12:20 -0800 Subject: [PATCH 484/778] Replaced `InvariantCultureIgnoreCase` with preferred `OrdinalIgnoreCase` --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 +- OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs | 4 ++-- OnTopic.ViewModels/NavigationTopicViewModel.cs | 2 +- OnTopic/Attributes/AttributeValueCollection.cs | 4 ++-- OnTopic/Lookup/DynamicTopicViewModelLookupService.cs | 2 +- OnTopic/Lookup/DynamicTypeLookupService.cs | 2 +- OnTopic/Lookup/TypeCollection.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 2 +- OnTopic/Repositories/TopicRepository.cs | 2 +- OnTopic/Topic.cs | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 7d3e01ff..8191fa70 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -240,7 +240,7 @@ from relationship in topic.Relationships from relatedTopic in relationship.Values select new XElement(_pagemapNamespace + "Attribute", new XAttribute("name", "TopicKey"), - new XText(relatedTopic.GetUniqueKey().Replace("Root:", "", StringComparison.InvariantCultureIgnoreCase)) + new XText(relatedTopic.GetUniqueKey().Replace("Root:", "", StringComparison.OrdinalIgnoreCase)) ) ); diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index 2f42e257..cf84fecd 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -118,7 +118,7 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi var splitHeaders = acceptHeaders.Split(new char[] { ',', ';' }); // Validate the content-type after the slash, then validate it against available views for (var i = 0; i < splitHeaders.Length; i++) { - if (splitHeaders[i].Contains("/", StringComparison.InvariantCultureIgnoreCase)) { + if (splitHeaders[i].Contains("/", StringComparison.OrdinalIgnoreCase)) { // Get content-type after the slash and replace '+' characters in the content-type to '-' for view file encoding // purposes var acceptHeader = splitHeaders[i] @@ -146,7 +146,7 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi \-----------------------------------------------------------------------------------------------------------------------*/ if (!view?.Success ?? false) { if (routeData.Values.TryGetValue("action", out var action)) { - var actionName = action?.ToString()?.Replace("Async", "", StringComparison.InvariantCultureIgnoreCase); + var actionName = action?.ToString()?.Replace("Async", "", StringComparison.OrdinalIgnoreCase); view = ViewEngine.FindView(actionContext, actionName, isMainPage: true); searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty()).ToList(); } diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 873c9fee..1448bbd6 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -54,7 +54,7 @@ public sealed record NavigationTopicViewModel : TopicViewModel, INavigationTopic /// typically meaning the user is on the page this object is pointing to. /// public bool IsSelected(string uniqueKey) => - $"{uniqueKey}:".StartsWith($"{UniqueKey}:", StringComparison.InvariantCultureIgnoreCase); + $"{uniqueKey}:".StartsWith($"{UniqueKey}:", StringComparison.OrdinalIgnoreCase); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 63043372..27140f72 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -48,7 +48,7 @@ public class AttributeValueCollection : KeyedCollection /// property. For this reason, the constructor is marked as internal. /// /// A reference to the topic that the current attribute collection is bound to. - internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.InvariantCultureIgnoreCase) { + internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { _associatedTopic = parentTopic; _topicPropertyDispatcher = new(parentTopic); } @@ -91,7 +91,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar public bool IsDirty(bool excludeLastModified = false) => DeletedAttributes.Count > 0 || Items.Any(a => a.IsDirty && - (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.InvariantCultureIgnoreCase)) + (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) ); /// diff --git a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index b4f2cf8b..d4b81cd3 100644 --- a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -24,7 +24,7 @@ public class DynamicTopicViewModelLookupService : DynamicTypeLookupService { /// Establishes a new instance of a . /// public DynamicTopicViewModelLookupService() : base( - t => t.Name.EndsWith("ViewModel", StringComparison.InvariantCultureIgnoreCase), + t => t.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase), typeof(object) ) { } diff --git a/OnTopic/Lookup/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs index 7f873450..db4a39f3 100644 --- a/OnTopic/Lookup/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -36,7 +36,7 @@ public DynamicTypeLookupService(Func predicate, Type? defaultType = .GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => t.IsClass && predicate(t)) - .OrderBy(t => t.Namespace?.StartsWith("OnTopic", StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(t => t.Namespace?.StartsWith("OnTopic", StringComparison.OrdinalIgnoreCase)) .ToList(); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Lookup/TypeCollection.cs b/OnTopic/Lookup/TypeCollection.cs index c3f91ff7..b38ecd3f 100644 --- a/OnTopic/Lookup/TypeCollection.cs +++ b/OnTopic/Lookup/TypeCollection.cs @@ -28,7 +28,7 @@ internal class TypeCollection : KeyedCollection { /// Instantiates a new . Optionally accepts an of instances to prepopulate the collection. /// - internal TypeCollection(IEnumerable? types = null) : base(StringComparer.InvariantCultureIgnoreCase) { + internal TypeCollection(IEnumerable? types = null) : base(StringComparer.OrdinalIgnoreCase) { /*---------------------------------------------------------------------------------------------------------------------- | Populate collection diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 75706964..850b5217 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -187,7 +187,7 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, strin \-----------------------------------------------------------------------------------------------------------------------*/ return topic.FindAll(t => !String.IsNullOrEmpty(t.Attributes.GetValue(name)) && - t.Attributes.GetValue(name).Contains(value, StringComparison.InvariantCultureIgnoreCase) + t.Attributes.GetValue(name).Contains(value, StringComparison.OrdinalIgnoreCase) ); } diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index d8bee22d..d677a30a 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -698,7 +698,7 @@ protected IEnumerable GetAttributes( var attribute = (AttributeDescriptor?)null; //Optionally exclude LastModified attributes - if (excludeLastModified && attributeValue.Key.StartsWith("LastModified", StringComparison.InvariantCultureIgnoreCase)) { + if (excludeLastModified && attributeValue.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) { continue; } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index b9700f46..30cb4ac7 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -462,7 +462,7 @@ public void SetParent(Topic parent, Topic? sibling = null) { /*------------------------------------------------------------------------------------------------------------------------ | Check to ensure that the topic isn't being moved to a descendant (topics cannot be their own grandpa) \-----------------------------------------------------------------------------------------------------------------------*/ - if (parent.GetUniqueKey().StartsWith(GetUniqueKey(), StringComparison.InvariantCultureIgnoreCase)) { + if (parent.GetUniqueKey().StartsWith(GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) { throw new ArgumentOutOfRangeException(nameof(parent), "A descendant cannot be its own parent."); } From f0be3c4d2bc4fcc409f28d71af734f66ad86c539 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:01:33 -0800 Subject: [PATCH 485/778] Replaced `InvariantCulture` with preferred `Ordinal` --- OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs | 4 ++-- OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs | 4 ++-- OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs | 4 ++-- OnTopic.Tests/TopicMappingServiceTest.cs | 6 +++--- OnTopic/Topic.cs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs index e0857ecc..28fb7e71 100644 --- a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs +++ b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs @@ -106,8 +106,8 @@ RouteData routeData \-----------------------------------------------------------------------------------------------------------------------*/ static string? cleanPath(string? path) => path? .Trim(new char[] { '/' }) - .Replace("//", "/", StringComparison.InvariantCulture) - .Replace("/", ":", StringComparison.InvariantCulture); + .Replace("//", "/", StringComparison.Ordinal) + .Replace("/", ":", StringComparison.Ordinal); } diff --git a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs index 7c832456..c48206c8 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs @@ -109,14 +109,14 @@ public IEnumerable ExpandViewLocations(ViewLocationExpanderContext conte | Yield view locations \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var location in ViewLocations) { - yield return location.Replace(@"{3}", (string?)contentType, StringComparison.InvariantCulture); + yield return location.Replace(@"{3}", (string?)contentType, StringComparison.Ordinal); } /*------------------------------------------------------------------------------------------------------------------------ | Yield area view locations \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var location in AreaViewLocations) { - yield return location.Replace(@"{3}", (string?)contentType, StringComparison.InvariantCulture); + yield return location.Replace(@"{3}", (string?)contentType, StringComparison.Ordinal); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index cf84fecd..b6916b0f 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -122,8 +122,8 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi // Get content-type after the slash and replace '+' characters in the content-type to '-' for view file encoding // purposes var acceptHeader = splitHeaders[i] - [(splitHeaders[i].IndexOf("/", StringComparison.InvariantCulture) + 1)..] - .Replace("+", "-", StringComparison.InvariantCulture); + [(splitHeaders[i].IndexOf("/", StringComparison.Ordinal) + 1)..] + .Replace("+", "-", StringComparison.Ordinal); // Validate against available views; if content-type represents a valid view, stop validation if (acceptHeader is not null) { view = viewEngine.FindView(actionContext, acceptHeader, isMainPage: true); diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 808a9a41..d1bcb88d 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -736,7 +736,7 @@ public async Task Map_TopicEntities_ReturnsTopics() { Assert.AreEqual(relatedTopic3.Key, relatedTopic3copy.Key); Topic? getRelatedTopic(RelatedEntityTopicViewModel topic, string key) - => topic.RelatedTopics.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); + => topic.RelatedTopics.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.Ordinal)); } @@ -1044,10 +1044,10 @@ public async Task Map_CachedTopic_ReturnsCachedModel() { /// A helper function which retrieves a child topic based on the key. /// public static KeyOnlyTopicViewModel? GetChildTopic(IEnumerable topicCollection, string key) - => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); + => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.Ordinal)); public static TopicViewModel? GetChildTopic(IEnumerable topicCollection, string key) - => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.InvariantCulture)); + => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.Ordinal)); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 30cb4ac7..bb2dba74 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -545,7 +545,7 @@ public string GetWebPath() { var uniqueKey = GetUniqueKey() .Replace("Root:", "/", StringComparison.Ordinal) .Replace(":", "/", StringComparison.Ordinal) + "/"; - if (!uniqueKey.StartsWith("/", StringComparison.InvariantCulture)) { + if (!uniqueKey.StartsWith("/", StringComparison.Ordinal)) { uniqueKey = $"/{uniqueKey}"; } return uniqueKey; From 8a08e534aba413cb31a9c44a89b429f401b2994a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:03:19 -0800 Subject: [PATCH 486/778] Prefer `Ordinal` over `OrdinalIgnoreCase` where appropriate In these cases, we never expect for the values to vary by case, and therefore doing an `OrdinalIgnoreCase` adds a small (but unnecessary) amount of overhead. This applies to cases where we're comparing a symbol (e.g., `:`, `/`) or a namespace (e.g., `OnTopic`). --- OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs | 2 +- OnTopic/Lookup/DynamicTypeLookupService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index b6916b0f..2a155b98 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -118,7 +118,7 @@ public ViewEngineResult FindView(ActionContext actionContext, TopicViewResult vi var splitHeaders = acceptHeaders.Split(new char[] { ',', ';' }); // Validate the content-type after the slash, then validate it against available views for (var i = 0; i < splitHeaders.Length; i++) { - if (splitHeaders[i].Contains("/", StringComparison.OrdinalIgnoreCase)) { + if (splitHeaders[i].Contains("/", StringComparison.Ordinal)) { // Get content-type after the slash and replace '+' characters in the content-type to '-' for view file encoding // purposes var acceptHeader = splitHeaders[i] diff --git a/OnTopic/Lookup/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs index db4a39f3..7f05fbeb 100644 --- a/OnTopic/Lookup/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -36,7 +36,7 @@ public DynamicTypeLookupService(Func predicate, Type? defaultType = .GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => t.IsClass && predicate(t)) - .OrderBy(t => t.Namespace?.StartsWith("OnTopic", StringComparison.OrdinalIgnoreCase)) + .OrderBy(t => t.Namespace?.StartsWith("OnTopic", StringComparison.Ordinal)) .ToList(); /*------------------------------------------------------------------------------------------------------------------------ From bcd37af98a2968cefa65b6e51208199ed44bed40 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:05:28 -0800 Subject: [PATCH 487/778] Renamed `FindAllByAttribute()` parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed `FindAllByAttribute()` parameters from `name` and `value` to `attributeKey` and `attributeValue` to better align with broader naming conventions. In particular, using `name` where we usually use `key` is confusing. But, more specifically, we try to always use `AttributeKey` and `AttributeValue` when discussing attributes—with the one exception of the `AttributeValue` record, where that would be redundant. --- OnTopic/Querying/TopicExtensions.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 850b5217..3e0129fa 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -161,8 +161,8 @@ public static ReadOnlyTopicCollection FindAll(this Topic topic, Func /// The instance of the to operate against; populated automatically by .NET. - /// The string identifier for the against which to be searched. - /// The text value for the against which to be searched. + /// The string identifier for the against which to be searched. + /// The text value for the against which to be searched. /// A collection of topics matching the input parameters. /// /// !String.IsNullOrWhiteSpace(name) @@ -172,22 +172,22 @@ public static ReadOnlyTopicCollection FindAll(this Topic topic, Func /// !name.Contains(" ") /// - public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, string name, string value) { + public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, string attributeKey, string attributeValue) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(topic, nameof(topic)); - Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - Contract.Requires(!String.IsNullOrWhiteSpace(value), nameof(value)); - TopicFactory.ValidateKey(name); + Contract.Requires(!String.IsNullOrWhiteSpace(attributeKey), nameof(attributeKey)); + Contract.Requires(!String.IsNullOrWhiteSpace(attributeValue), nameof(attributeValue)); + TopicFactory.ValidateKey(attributeKey); /*------------------------------------------------------------------------------------------------------------------------ | Return results \-----------------------------------------------------------------------------------------------------------------------*/ return topic.FindAll(t => - !String.IsNullOrEmpty(t.Attributes.GetValue(name)) && - t.Attributes.GetValue(name).Contains(value, StringComparison.OrdinalIgnoreCase) + !String.IsNullOrEmpty(t.Attributes.GetValue(attributeKey)) && + t.Attributes.GetValue(attributeKey).Contains(attributeValue, StringComparison.OrdinalIgnoreCase) ); } From 494ceb57c6ea82c1edecb8c1ca13e67ec1158f31 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:24:07 -0800 Subject: [PATCH 488/778] Removed legacy binding to `TopicID` The `DerivedTopic` is now stored with the key `DerivedTopic` (though under `Topic.References`, not `Topic.Attributes`) and, thus, no longer needs an `[AttributeKey()]`. --- .../BindingModels/InvalidReferenceTypeTopicBindingModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs index b78b1354..4003cea8 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs @@ -24,7 +24,6 @@ public class InvalidReferenceTypeTopicBindingModel : BasicTopicBindingModel { public InvalidReferenceTypeTopicBindingModel(string? key = null) : base(key, "Page") { } - [AttributeKey("TopicId")] public TopicViewModel DerivedTopic { get; } = new(); } //Class From ab44d23c3430d1ba7659e84d835c59bc1d136d75 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:25:59 -0800 Subject: [PATCH 489/778] Removed legacy test for `DerivedTopic` This test was originally intended to ensure that the `DerivedTopic` didn't set the `Topic.Id` in the `Topic.Attributes` collection if the `DerivedTopic` was unresolved (i.e., not saved). The `DerivedTopic` is now stored in the `Topic.References` collection, however, and so this test will _always_ succeed. --- OnTopic.Tests/TopicTest.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 97d1bc6f..6fb8a927 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -294,26 +294,6 @@ public void DerivedTopic_UpdateValue_ReturnsExpectedValue() { } - /*========================================================================================================================== - | TEST: DERIVED TOPIC: UNSAVED VALUE: RETURNS EXPECTED VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Sets a derived topic to an unsaved topic entity. Ensures that the derived topic is correctly set, but that the is not persisted as an underlying . - /// - [TestMethod] - public void DerivedTopic_UnsavedValue_ReturnsExpectedValue() { - - var topic = TopicFactory.Create("Topic", "Page"); - var derivedTopic = TopicFactory.Create("DerivedTopic", "Page"); - - topic.DerivedTopic = derivedTopic; - - Assert.ReferenceEquals(topic.DerivedTopic, derivedTopic); - Assert.AreEqual(-2, topic.Attributes.GetInteger("TopicID", -2)); - - } - /*========================================================================================================================== | TEST: DERIVED TOPIC: RESAVED VALUE: RETURNS EXPECTED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ From 0ede031284f54b0915e5f898db9612938781bcac Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:27:45 -0800 Subject: [PATCH 490/778] Removed check for `TopicId` in `GetValue()` fallback It's not entirely clear why this condition was here, as it's redundant with the next condition, but a check for the legacy `TopicId` attribute will no longer work going forward, as that value is now represented by the topic reference with the key `DerivedTopic`. Regardless, the correct way to evaluate that remains to check if `DerivedTopic` is `null`. --- OnTopic/Attributes/AttributeValueCollection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 27140f72..9569734a 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -246,7 +246,6 @@ public void MarkClean(string name, DateTime? version = null) { \-----------------------------------------------------------------------------------------------------------------------*/ if ( String.IsNullOrEmpty(value) && - !name.Equals("TopicId", StringComparison.OrdinalIgnoreCase) && _associatedTopic.DerivedTopic is not null && maxHops > 0 ) { From dff6cddbd442aaf869f9afe8c15a62d446ce7749 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:30:09 -0800 Subject: [PATCH 491/778] Removed manual setting of `ParentID` attribute in `StubTopicRepository` This mimicked legacy business logic from when core topics were still stored as attributes. This is no longer necessary or of value. --- OnTopic.TestDoubles/StubTopicRepository.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 57027d8c..ffd0f787 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -127,20 +127,7 @@ protected override void SaveTopic([NotNull]Topic topic, DateTime version, bool p | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void MoveTopic(Topic topic, Topic target, Topic? sibling = null) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(topic, nameof(topic)); - Contract.Requires(target, nameof(target)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Reset dirty status - \-----------------------------------------------------------------------------------------------------------------------*/ - topic.Attributes.SetValue("ParentId", target.Id.ToString(CultureInfo.InvariantCulture), false); - - } + protected override void MoveTopic(Topic topic, Topic target, Topic? sibling = null) { } /*========================================================================================================================== | METHOD: DELETE From f8ba689dc0137502bfae1176171bb5b482c3e85d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 15:31:56 -0800 Subject: [PATCH 492/778] Clarified that core attributes are legacy Maintained the exception in `GetUnmatchedAttributes()` for core attributes, but explicitly noted that they are "legacy" so that it's clear that this is an intentional piece of code. This can be removed later, but for now it helps safeguard against any legacy values slipping through. --- OnTopic/Repositories/TopicRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index d677a30a..54159fa9 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -781,7 +781,7 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { continue; } - // Ignore system attributes + // Ignore (now legacy) core attributes if (attribute.Key is "Key" or "ContentType" or "ParentID") { continue; } From d2ea8d3f5c03f58ec40ba8cd1fd0d3915bd23139 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 16:38:38 -0800 Subject: [PATCH 493/778] Added `CompressHierarchy` as part of post-test cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Normally, we don't need to run `CompressHierarchy` on the topics hierarchy. The unit tests, however, take a few shortcuts when bulk deleting topics—such as deleting all records with a key starting with the test name—instead of calling `DeleteTopic`, which properly closes any gaps. Given that, we need to run `CompressHierarchy` at the end to ensure that there aren't large gaps left over after running the unit tests. (These gaps don't hurt anything, though they make it harder to debug the hierarchy if there's a need—and, over a long period, could hypothetically overflow the maximum range of the `INT` field if left unchecked.) --- OnTopic.Data.Sql.Database.Tests/Functions.cs | 138 +++++++-------- .../Functions.resx | 5 + .../StoredProcedures.cs | 163 +++++++++--------- .../StoredProcedures.resx | 6 + 4 files changed, 165 insertions(+), 147 deletions(-) diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs index 662dce8c..d2a2f756 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.cs +++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs @@ -33,15 +33,20 @@ public void TestCleanup() { private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_TestAction; System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Functions)); + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getExtendedAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetParentIDTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicIDTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getIDTopicValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetUniqueKeyTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getUniqueKeyValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition findTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_TestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getAttributeValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetChildTopicIDsTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetAttributesTest_PosttestAction; @@ -52,13 +57,8 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postFunctionTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_FindTopicIDsTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preFindAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getChildTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getParentIDValue; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getIDTopicValue; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getUniqueKeyValue; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition getExtendedAttributeValue; this.dbo_GetExtendedAttributeTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetParentIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicIDTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -67,15 +67,20 @@ private void InitializeComponent() { this.dbo_GetAttributesTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetChildTopicIDsTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); dbo_GetExtendedAttributeTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetParentIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetTopicIDTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getIDTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetUniqueKeyTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getUniqueKeyValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_FindTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); findTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); getAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); getAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetChildTopicIDsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preGetAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetAttributesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); @@ -86,34 +91,69 @@ private void InitializeComponent() { postFunctionTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_FindTopicIDsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getChildTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getParentIDValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); - getIDTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); - getUniqueKeyValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); dbo_GetExtendedAttributeTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preGetExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getExtendedAttributeValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition(); // // dbo_GetExtendedAttributeTest_TestAction // dbo_GetExtendedAttributeTest_TestAction.Conditions.Add(getExtendedAttributeValue); resources.ApplyResources(dbo_GetExtendedAttributeTest_TestAction, "dbo_GetExtendedAttributeTest_TestAction"); // + // getExtendedAttributeValue + // + getExtendedAttributeValue.ColumnNumber = 1; + getExtendedAttributeValue.Enabled = true; + getExtendedAttributeValue.ExpectedValue = "Value2"; + getExtendedAttributeValue.Name = "getExtendedAttributeValue"; + getExtendedAttributeValue.NullExpected = false; + getExtendedAttributeValue.ResultSet = 1; + getExtendedAttributeValue.RowNumber = 1; + // // dbo_GetParentIDTest_TestAction // dbo_GetParentIDTest_TestAction.Conditions.Add(getParentIDValue); resources.ApplyResources(dbo_GetParentIDTest_TestAction, "dbo_GetParentIDTest_TestAction"); // + // getParentIDValue + // + getParentIDValue.ColumnNumber = 1; + getParentIDValue.Enabled = true; + getParentIDValue.ExpectedValue = "0"; + getParentIDValue.Name = "getParentIDValue"; + getParentIDValue.NullExpected = false; + getParentIDValue.ResultSet = 1; + getParentIDValue.RowNumber = 1; + // // dbo_GetTopicIDTest_TestAction // dbo_GetTopicIDTest_TestAction.Conditions.Add(getIDTopicValue); resources.ApplyResources(dbo_GetTopicIDTest_TestAction, "dbo_GetTopicIDTest_TestAction"); // + // getIDTopicValue + // + getIDTopicValue.ColumnNumber = 1; + getIDTopicValue.Enabled = true; + getIDTopicValue.ExpectedValue = "0"; + getIDTopicValue.Name = "getIDTopicValue"; + getIDTopicValue.NullExpected = false; + getIDTopicValue.ResultSet = 1; + getIDTopicValue.RowNumber = 1; + // // dbo_GetUniqueKeyTest_TestAction // dbo_GetUniqueKeyTest_TestAction.Conditions.Add(getUniqueKeyValue); resources.ApplyResources(dbo_GetUniqueKeyTest_TestAction, "dbo_GetUniqueKeyTest_TestAction"); // + // getUniqueKeyValue + // + getUniqueKeyValue.ColumnNumber = 1; + getUniqueKeyValue.Enabled = true; + getUniqueKeyValue.ExpectedValue = "Root:FunctionTests:Topic_1:Topic_1_1:Topic_1_1_1:Topic_1_1_1_2"; + getUniqueKeyValue.Name = "getUniqueKeyValue"; + getUniqueKeyValue.NullExpected = false; + getUniqueKeyValue.ResultSet = 1; + getUniqueKeyValue.RowNumber = 1; + // // dbo_FindTopicIDsTest_TestAction // dbo_FindTopicIDsTest_TestAction.Conditions.Add(findTopicCount); @@ -154,6 +194,13 @@ private void InitializeComponent() { dbo_GetChildTopicIDsTest_TestAction.Conditions.Add(getChildTopicCount); resources.ApplyResources(dbo_GetChildTopicIDsTest_TestAction, "dbo_GetChildTopicIDsTest_TestAction"); // + // getChildTopicCount + // + getChildTopicCount.Enabled = true; + getChildTopicCount.Name = "getChildTopicCount"; + getChildTopicCount.ResultSet = 1; + getChildTopicCount.RowCount = 3; + // // dbo_GetAttributesTest_PretestAction // dbo_GetAttributesTest_PretestAction.Conditions.Add(preGetAttributeCount); @@ -214,6 +261,18 @@ private void InitializeComponent() { preFindAttributeCount.ResultSet = 1; preFindAttributeCount.RowCount = 3; // + // dbo_GetExtendedAttributeTest_PretestAction + // + dbo_GetExtendedAttributeTest_PretestAction.Conditions.Add(preGetExtendedAttributeCount); + resources.ApplyResources(dbo_GetExtendedAttributeTest_PretestAction, "dbo_GetExtendedAttributeTest_PretestAction"); + // + // preGetExtendedAttributeCount + // + preGetExtendedAttributeCount.Enabled = true; + preGetExtendedAttributeCount.Name = "preGetExtendedAttributeCount"; + preGetExtendedAttributeCount.ResultSet = 1; + preGetExtendedAttributeCount.RowCount = 1; + // // dbo_GetExtendedAttributeTestData // this.dbo_GetExtendedAttributeTestData.PosttestAction = null; @@ -256,65 +315,6 @@ private void InitializeComponent() { this.dbo_GetChildTopicIDsTestData.PretestAction = null; this.dbo_GetChildTopicIDsTestData.TestAction = dbo_GetChildTopicIDsTest_TestAction; // - // getChildTopicCount - // - getChildTopicCount.Enabled = true; - getChildTopicCount.Name = "getChildTopicCount"; - getChildTopicCount.ResultSet = 1; - getChildTopicCount.RowCount = 3; - // - // getParentIDValue - // - getParentIDValue.ColumnNumber = 1; - getParentIDValue.Enabled = true; - getParentIDValue.ExpectedValue = "0"; - getParentIDValue.Name = "getParentIDValue"; - getParentIDValue.NullExpected = false; - getParentIDValue.ResultSet = 1; - getParentIDValue.RowNumber = 1; - // - // getIDTopicValue - // - getIDTopicValue.ColumnNumber = 1; - getIDTopicValue.Enabled = true; - getIDTopicValue.ExpectedValue = "0"; - getIDTopicValue.Name = "getIDTopicValue"; - getIDTopicValue.NullExpected = false; - getIDTopicValue.ResultSet = 1; - getIDTopicValue.RowNumber = 1; - // - // getUniqueKeyValue - // - getUniqueKeyValue.ColumnNumber = 1; - getUniqueKeyValue.Enabled = true; - getUniqueKeyValue.ExpectedValue = "Root:FunctionTests:Topic_1:Topic_1_1:Topic_1_1_1:Topic_1_1_1_2"; - getUniqueKeyValue.Name = "getUniqueKeyValue"; - getUniqueKeyValue.NullExpected = false; - getUniqueKeyValue.ResultSet = 1; - getUniqueKeyValue.RowNumber = 1; - // - // dbo_GetExtendedAttributeTest_PretestAction - // - dbo_GetExtendedAttributeTest_PretestAction.Conditions.Add(preGetExtendedAttributeCount); - resources.ApplyResources(dbo_GetExtendedAttributeTest_PretestAction, "dbo_GetExtendedAttributeTest_PretestAction"); - // - // preGetExtendedAttributeCount - // - preGetExtendedAttributeCount.Enabled = true; - preGetExtendedAttributeCount.Name = "preGetExtendedAttributeCount"; - preGetExtendedAttributeCount.ResultSet = 1; - preGetExtendedAttributeCount.RowCount = 1; - // - // getExtendedAttributeValue - // - getExtendedAttributeValue.ColumnNumber = 1; - getExtendedAttributeValue.Enabled = true; - getExtendedAttributeValue.ExpectedValue = "Value2"; - getExtendedAttributeValue.Name = "getExtendedAttributeValue"; - getExtendedAttributeValue.NullExpected = false; - getExtendedAttributeValue.ResultSet = 1; - getExtendedAttributeValue.RowNumber = 1; - // // Functions // this.TestCleanupAction = testCleanupAction; diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.resx b/OnTopic.Data.Sql.Database.Tests/Functions.resx index a32a37d1..335a6812 100644 --- a/OnTopic.Data.Sql.Database.Tests/Functions.resx +++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx @@ -549,6 +549,11 @@ IF @RootTopicID IS NOT NULL EXECUTE [dbo].[DeleteTopic] @RootTopicID; END +-------------------------------------------------------------------------------------------------------------------------------- +-- COMPRESS HIERARCHY +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [Utilities].[CompressHierarchy] + -------------------------------------------------------------------------------------------------------------------------------- -- VERIFY TEST DATA -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs index bb2d20ba..9ccfa814 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs @@ -117,19 +117,20 @@ private void InitializeComponent() { Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postUpdateExtendedAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testInitializeAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_TestAction; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesTopicCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesExtendedAttributeCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesRelationshipCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesReferenceCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesVersionHistoryCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_PretestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesTopicCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesAttributeCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetUpdatesReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesTopicCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesExtendedAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesRelationshipCount; Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_PosttestAction; Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postGetUpdatesAttributeCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesReferenceCount; - Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition getUpdatesVersionHistoryCount; + Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction testCleanupAction; this.dbo_CreateTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_DeleteTopicTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); this.dbo_GetTopicVersionTestData = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestActions(); @@ -226,19 +227,20 @@ private void InitializeComponent() { postUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); testInitializeAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); dbo_GetTopicUpdatesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); + getUpdatesTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + getUpdatesVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicUpdatesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); preGetUpdatesTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetUpdatesRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); preGetUpdatesReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesRelationshipCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); dbo_GetTopicUpdatesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); postGetUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); - getUpdatesVersionHistoryCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition(); + testCleanupAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction(); // // dbo_CreateTopicTest_TestAction // @@ -833,6 +835,48 @@ private void InitializeComponent() { dbo_GetTopicUpdatesTest_TestAction.Conditions.Add(getUpdatesVersionHistoryCount); resources.ApplyResources(dbo_GetTopicUpdatesTest_TestAction, "dbo_GetTopicUpdatesTest_TestAction"); // + // getUpdatesTopicCount + // + getUpdatesTopicCount.Enabled = true; + getUpdatesTopicCount.Name = "getUpdatesTopicCount"; + getUpdatesTopicCount.ResultSet = 5; + getUpdatesTopicCount.RowCount = 1; + // + // getUpdatesAttributeCount + // + getUpdatesAttributeCount.Enabled = true; + getUpdatesAttributeCount.Name = "getUpdatesAttributeCount"; + getUpdatesAttributeCount.ResultSet = 2; + getUpdatesAttributeCount.RowCount = 4; + // + // getUpdatesExtendedAttributeCount + // + getUpdatesExtendedAttributeCount.Enabled = true; + getUpdatesExtendedAttributeCount.Name = "getUpdatesExtendedAttributeCount"; + getUpdatesExtendedAttributeCount.ResultSet = 3; + getUpdatesExtendedAttributeCount.RowCount = 2; + // + // getUpdatesRelationshipCount + // + getUpdatesRelationshipCount.Enabled = true; + getUpdatesRelationshipCount.Name = "getUpdatesRelationshipCount"; + getUpdatesRelationshipCount.ResultSet = 4; + getUpdatesRelationshipCount.RowCount = 1; + // + // getUpdatesReferenceCount + // + getUpdatesReferenceCount.Enabled = true; + getUpdatesReferenceCount.Name = "getUpdatesReferenceCount"; + getUpdatesReferenceCount.ResultSet = 5; + getUpdatesReferenceCount.RowCount = 1; + // + // getUpdatesVersionHistoryCount + // + getUpdatesVersionHistoryCount.Enabled = true; + getUpdatesVersionHistoryCount.Name = "getUpdatesVersionHistoryCount"; + getUpdatesVersionHistoryCount.ResultSet = 6; + getUpdatesVersionHistoryCount.RowCount = 2; + // // dbo_GetTopicUpdatesTest_PretestAction // dbo_GetTopicUpdatesTest_PretestAction.Conditions.Add(preGetUpdatesTopicCount); @@ -855,6 +899,32 @@ private void InitializeComponent() { preGetUpdatesAttributeCount.ResultSet = 2; preGetUpdatesAttributeCount.RowCount = 8; // + // preGetUpdatesRelationshipCount + // + preGetUpdatesRelationshipCount.Enabled = true; + preGetUpdatesRelationshipCount.Name = "preGetUpdatesRelationshipCount"; + preGetUpdatesRelationshipCount.ResultSet = 3; + preGetUpdatesRelationshipCount.RowCount = 2; + // + // preGetUpdatesReferenceCount + // + preGetUpdatesReferenceCount.Enabled = true; + preGetUpdatesReferenceCount.Name = "preGetUpdatesReferenceCount"; + preGetUpdatesReferenceCount.ResultSet = 3; + preGetUpdatesReferenceCount.RowCount = 2; + // + // dbo_GetTopicUpdatesTest_PosttestAction + // + dbo_GetTopicUpdatesTest_PosttestAction.Conditions.Add(postGetUpdatesAttributeCount); + resources.ApplyResources(dbo_GetTopicUpdatesTest_PosttestAction, "dbo_GetTopicUpdatesTest_PosttestAction"); + // + // postGetUpdatesAttributeCount + // + postGetUpdatesAttributeCount.Enabled = true; + postGetUpdatesAttributeCount.Name = "postGetUpdatesAttributeCount"; + postGetUpdatesAttributeCount.ResultSet = 1; + postGetUpdatesAttributeCount.RowCount = 0; + // // dbo_CreateTopicTestData // this.dbo_CreateTopicTestData.PosttestAction = dbo_CreateTopicTest_PosttestAction; @@ -921,76 +991,13 @@ private void InitializeComponent() { this.dbo_GetTopicUpdatesTestData.PretestAction = dbo_GetTopicUpdatesTest_PretestAction; this.dbo_GetTopicUpdatesTestData.TestAction = dbo_GetTopicUpdatesTest_TestAction; // - // preGetUpdatesRelationshipCount - // - preGetUpdatesRelationshipCount.Enabled = true; - preGetUpdatesRelationshipCount.Name = "preGetUpdatesRelationshipCount"; - preGetUpdatesRelationshipCount.ResultSet = 3; - preGetUpdatesRelationshipCount.RowCount = 2; - // - // preGetUpdatesReferenceCount - // - preGetUpdatesReferenceCount.Enabled = true; - preGetUpdatesReferenceCount.Name = "preGetUpdatesReferenceCount"; - preGetUpdatesReferenceCount.ResultSet = 3; - preGetUpdatesReferenceCount.RowCount = 2; - // - // getUpdatesTopicCount - // - getUpdatesTopicCount.Enabled = true; - getUpdatesTopicCount.Name = "getUpdatesTopicCount"; - getUpdatesTopicCount.ResultSet = 5; - getUpdatesTopicCount.RowCount = 1; - // - // getUpdatesAttributeCount - // - getUpdatesAttributeCount.Enabled = true; - getUpdatesAttributeCount.Name = "getUpdatesAttributeCount"; - getUpdatesAttributeCount.ResultSet = 2; - getUpdatesAttributeCount.RowCount = 4; + // testCleanupAction // - // getUpdatesExtendedAttributeCount - // - getUpdatesExtendedAttributeCount.Enabled = true; - getUpdatesExtendedAttributeCount.Name = "getUpdatesExtendedAttributeCount"; - getUpdatesExtendedAttributeCount.ResultSet = 3; - getUpdatesExtendedAttributeCount.RowCount = 2; - // - // getUpdatesRelationshipCount - // - getUpdatesRelationshipCount.Enabled = true; - getUpdatesRelationshipCount.Name = "getUpdatesRelationshipCount"; - getUpdatesRelationshipCount.ResultSet = 4; - getUpdatesRelationshipCount.RowCount = 1; - // - // dbo_GetTopicUpdatesTest_PosttestAction - // - dbo_GetTopicUpdatesTest_PosttestAction.Conditions.Add(postGetUpdatesAttributeCount); - resources.ApplyResources(dbo_GetTopicUpdatesTest_PosttestAction, "dbo_GetTopicUpdatesTest_PosttestAction"); - // - // postGetUpdatesAttributeCount - // - postGetUpdatesAttributeCount.Enabled = true; - postGetUpdatesAttributeCount.Name = "postGetUpdatesAttributeCount"; - postGetUpdatesAttributeCount.ResultSet = 1; - postGetUpdatesAttributeCount.RowCount = 0; - // - // getUpdatesReferenceCount - // - getUpdatesReferenceCount.Enabled = true; - getUpdatesReferenceCount.Name = "getUpdatesReferenceCount"; - getUpdatesReferenceCount.ResultSet = 5; - getUpdatesReferenceCount.RowCount = 1; - // - // getUpdatesVersionHistoryCount - // - getUpdatesVersionHistoryCount.Enabled = true; - getUpdatesVersionHistoryCount.Name = "getUpdatesVersionHistoryCount"; - getUpdatesVersionHistoryCount.ResultSet = 6; - getUpdatesVersionHistoryCount.RowCount = 2; + resources.ApplyResources(testCleanupAction, "testCleanupAction"); // // StoredProcedures // + this.TestCleanupAction = testCleanupAction; this.TestInitializeAction = testInitializeAction; } diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx index c21e279e..b3f95e0f 100644 --- a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx +++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx @@ -1693,6 +1693,12 @@ EXECUTE [dbo].[DeleteTopic] SELECT * FROM Attributes WHERE AttributeKey LIKE 'GetTopicUpdatesTest%' + + + -------------------------------------------------------------------------------------------------------------------------------- +-- COMPRESS HIERARCHY +-------------------------------------------------------------------------------------------------------------------------------- +EXECUTE [Utilities].[CompressHierarchy] True From bfdec1cc5c5c8a8bc097c62b0b770168613f6c45 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 16:39:28 -0800 Subject: [PATCH 494/778] Fixed `RAISERROR` format The `RAISERROR()` calls in `MoveTopic` used the incorrect `%n` token; they meant to use the `%d` token, which is used for numbers. --- OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index 40286bac..2e268e1c 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -95,7 +95,7 @@ ELSE IF @TopicID IS NULL OR @OriginalLeft IS NULL OR @OriginalRight IS NULL BEGIN RAISERROR ( - N'The topic ("%n") could not be found.', + N'The topic ("%d") could not be found.', 15, -- Severity, 1, -- State, @TopicID @@ -107,7 +107,7 @@ IF @TopicID IS NULL OR @OriginalLeft IS NULL OR @OriginalRight IS NULL IF @ParentID IS NULL OR @InsertionPoint IS NULL BEGIN RAISERROR ( - N'The parent ("%n") could not be found.', + N'The parent ("%d") could not be found.', 15, -- Severity, 1, -- State, @ParentID @@ -119,7 +119,7 @@ IF @ParentID IS NULL OR @InsertionPoint IS NULL IF @InsertionPoint >= @OriginalLeft AND @InsertionPoint <= @OriginalRight BEGIN RAISERROR ( - N'A topic ("%n") cannot be moved within a child of itself ("%n").', + N'A topic ("%d") cannot be moved within a child of itself ("%d").', 10, -- Severity, 1, -- State, @TopicID, From c8af390eeea364d2bb512836e829b0137694c0a3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 16:41:19 -0800 Subject: [PATCH 495/778] Allow `ParentID` to be `NULL` for `CreateTopic`, `MoveTopic` This will optionally allow new topics to be created in the root. This isn't normally recommended, but currently it results in a malformed hierarchy, so we want to make sure that doesn't happen. (We do create topics in the root as part of the unit tests, in order to minimize variables.) --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 4 ++-- OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 80437f0c..0d6adbed 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -7,7 +7,7 @@ CREATE PROCEDURE [dbo].[CreateTopic] @Key VARCHAR(128) , @ContentType VARCHAR(128) , - @ParentID INT = -1, + @ParentID INT = NULL, @Attributes AttributeValues READONLY, @ExtendedAttributes XML = NULL, @References TopicReferences READONLY, @@ -29,7 +29,7 @@ SET @RangeRight = 0 -------------------------------------------------------------------------------------------------------------------------------- -- RESERVE SPACE FOR NEW CHILD. -------------------------------------------------------------------------------------------------------------------------------- -IF (@ParentID > -1) +IF (@ParentID IS NOT NULL) BEGIN SELECT @RangeRight = RangeRight FROM Topics diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index 2e268e1c..d6043ef4 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -6,7 +6,7 @@ CREATE PROCEDURE [dbo].[MoveTopic] @TopicID INT , - @ParentID INT , + @ParentID INT = NULL , @SiblingID INT = -1 AS @@ -104,7 +104,7 @@ IF @TopicID IS NULL OR @OriginalLeft IS NULL OR @OriginalRight IS NULL RETURN END -IF @ParentID IS NULL OR @InsertionPoint IS NULL +IF @InsertionPoint IS NULL BEGIN RAISERROR ( N'The parent ("%d") could not be found.', From 51997f8902f3790888ff109f08db7c9cb46dd530 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 16:42:52 -0800 Subject: [PATCH 496/778] Correctly set `@RangeRight` in `CreateTopic` if `@ParentID` is `NULL` Previously, if the `@ParentID` was `NULL` (or `-1`), the `@RangeRight` wasn't defined, and thus would end up getting inserted in an overlapping range with the root node. This addresses that, by placing it at the end of the root node(s). --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 0d6adbed..99b9f407 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -50,6 +50,11 @@ IF (@ParentID IS NOT NULL) END WHERE RangeRight >= @RangeRight END +ELSE + BEGIN + SELECT @RangeRight = MAX(RangeRight) + 1 + FROM Topics + END -------------------------------------------------------------------------------------------------------------------------------- -- CREATE NEW TOPIC From 3e9f5c719f57c135ee4df5b37a76ac92e991b4b1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 27 Jan 2021 16:44:33 -0800 Subject: [PATCH 497/778] Correctly set `@InsertionPoint` in `MoveTopic` if `@ParentID` is `NULL` Previously, if the `@ParentID` was `NULL` (or `-1`), the `@InsertionPoint` wasn't defined, and thus would end up getting inserted in an overlapping range with the root node. This addresses that, by placing it at the end of the root node(s). (This change looks a bit more invasive than it is because I rearranged the order of the logic to keep the conditions simple.) --- .../Stored Procedures/MoveTopic.sql | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql index d6043ef4..e60bd99b 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql @@ -74,16 +74,20 @@ WHERE TopicID = @TopicID -- EXAMPLE: If a sibling (@SiblingID) lives between 12 and 24, then the insertion point (@InsertionPoint) will be 25; if there -- is no sibling, but a parent lives between 6 and 26, then the insertion point (@InsertionPoint) will be 7. -------------------------------------------------------------------------------------------------------------------------------- -IF @SiblingID < 0 +IF @SiblingID >= 0 + -- Place immediately to the right of a sibling, if specified + SELECT @InsertionPoint = RangeRight + 1 + FROM Topics + WHERE TopicID = @SiblingID +ELSE IF ISNULL(@ParentID, -1) >= 0 -- Place as the first sibling if a sibling isn't specified SELECT @InsertionPoint = RangeLeft + 1 FROM Topics WHERE TopicID = @ParentID ELSE - -- Place immediately to the right of a sibling, if specified - SELECT @InsertionPoint = RangeRight + 1 + -- Place after the last node + SELECT @InsertionPoint = MAX(RangeRight) + 1 FROM Topics - WHERE TopicID = @SiblingID -------------------------------------------------------------------------------------------------------------------------------- -- VALIDATE REQUEST From dcd1b4c920d57c3c5a1b9b2527c1c1c6529bb8c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 12:33:44 -0800 Subject: [PATCH 498/778] Introduce `References` into extended sitemap If there are more than one `Topic.References`, then these will be exposed in a new `DataObject` of type `References`, with each key corresponding to the `referenceKey`. --- .../Controllers/SitemapController.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 8191fa70..758a5676 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -199,7 +199,8 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false new XAttribute("type", topic.ContentType?? "Page"), getAttributes() ), - getRelationships() + getRelationships(), + getReferences() ) : null ); if ( @@ -244,6 +245,21 @@ from relatedTopic in relationship.Values ) ); + /*------------------------------------------------------------------------------------------------------------------------ + | Get references + \-----------------------------------------------------------------------------------------------------------------------*/ + XElement? getReferences() => + topic.References.Count is 0? + null : + new XElement(_pagemapNamespace + "DataObject", + new XAttribute("type", "References"), + from reference in topic.References + select new XElement(_pagemapNamespace + "Attribute", + new XAttribute("name", reference.Key), + new XText(reference.Value.GetUniqueKey().Replace("Root:", "", StringComparison.OrdinalIgnoreCase)) + ) + ); + } } //Class From f79f1fff9e7207c4d0e30a13b7204b02adf683f1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 12:37:09 -0800 Subject: [PATCH 499/778] Refactored `getAttributes()` logic to return container `DataObject` Updated the `getAttributes()` function so that the root `DataObject` is created by the function itself. This makes it more consistent with the other local functions, and cleans up the calling code. --- .../Controllers/SitemapController.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 758a5676..e72f63a8 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -195,10 +195,7 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false new XElement(_sitemapNamespace + "lastmod", lastModified.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)), new XElement(_sitemapNamespace + "priority", 1), includeMetadata? new XElement(_pagemapNamespace + "PageMap", - new XElement(_pagemapNamespace + "DataObject", - new XAttribute("type", topic.ContentType?? "Page"), - getAttributes() - ), + getAttributes(), getRelationships(), getReferences() ) : null @@ -222,14 +219,17 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false /*------------------------------------------------------------------------------------------------------------------------ | Get attributes \-----------------------------------------------------------------------------------------------------------------------*/ - IEnumerable getAttributes() => - from attribute in topic.Attributes - where !ExcludeAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) - where topic.Attributes.GetValue(attribute.Key)?.Length < 256 - select new XElement(_pagemapNamespace + "Attribute", - new XAttribute("name", attribute.Key), - new XText(topic.Attributes.GetValue(attribute.Key)) - ); + XElement getAttributes() => + new XElement(_pagemapNamespace + "DataObject", + new XAttribute("type", "Attributes"), + from attribute in topic.Attributes + where !ExcludeAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) + where topic.Attributes.GetValue(attribute.Key)?.Length < 256 + select new XElement(_pagemapNamespace + "Attribute", + new XAttribute("name", attribute.Key), + new XText(topic.Attributes.GetValue(attribute.Key)) + ) + ); /*------------------------------------------------------------------------------------------------------------------------ | Get relationships From 0061843de9cbbc1ad5cc2a6471bdc6bc68d7e0f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 12:38:25 -0800 Subject: [PATCH 500/778] Explicitly added `ContentType` as an `Attribute` Since `ContentType` is no longer stored in `Topic.Attributes`, it wasn't showing up in the `DataObject` of type `Attributes`. Given that this is a core piece of metadata, and every topic has one, I've hard-coded a reference to it as the first attribute in the `DataObject`. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index e72f63a8..c78453d1 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -222,6 +222,10 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false XElement getAttributes() => new XElement(_pagemapNamespace + "DataObject", new XAttribute("type", "Attributes"), + new XElement(_pagemapNamespace + "Attribute", + new XAttribute("name", "ContentType"), + new XText(topic.ContentType?? "Page") + ), from attribute in topic.Attributes where !ExcludeAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) where topic.Attributes.GetValue(attribute.Key)?.Length < 256 From a9f04ab3c8d9b1a03e0aa8da4c0af8b7ef004567 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 13:06:20 -0800 Subject: [PATCH 501/778] Allow configuration by exposing excluded and skipped collections Exposed the static `ExcludedContentTypes`, `SkippedContentTypes`, and `ExcludedAttributes` collections as `public` so that implementors can optionally adjust these. This isn't the most elegant approach, but it's quick and easy, and doesn't necessitate any `virtual` members, as the previous approach took, while covering the most common use cases for customization. While I was at it, I renamed the collections to consistently use the past-tense (e.g., `ExcludedContentTypes`, instead of `ExcludeContentTypes`) and set them to return `Collection` instead of a `List`, since `public` properties shouldn't expose `List`s. --- .../Controllers/SitemapController.cs | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index c78453d1..c38d8e61 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml; @@ -23,6 +24,22 @@ namespace OnTopic.AspNetCore.Mvc.Controllers { /// Responds to requests for a sitemap according to sitemap.org's schema. The view is expected to recursively loop over /// child topics to generate the appropriate markup. /// + /// + /// + /// By default, some s are excluded based on their content types—which includes not only the + /// , but also all of its descendents. Other s are skipped, also based on + /// their content types; in this case, the is excluded, but its descendents are not. What content + /// types are excluded or skipped can be configured, respectively, by modifying the static and collections. + /// . + /// + /// + /// The action enables an extended sitemap with Google's custom PageMap schema for + /// exposing , , and . By + /// default, some content attributes, such as Body, IsDisabled, and NoIndex, are hidden. This list + /// can be modified by updating the static collection. + /// + /// public class SitemapController : Controller { /*========================================================================================================================== @@ -37,12 +54,14 @@ public class SitemapController : Controller { private static readonly XNamespace _pagemapNamespace = "http://www.google.com/schemas/sitemap-pagemap/1.0"; /*========================================================================================================================== - | EXCLUDE CONTENT TYPES + | EXCLUDED CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Specifies what content types should not be listed in the sitemap, including any descendents. /// - private static string[] ExcludeContentTypes { get; } = { "List" }; + public static Collection ExcludedContentTypes { get; } = new() { + "List" + }; /*========================================================================================================================== | SKIPPED CONTENT TYPES @@ -50,19 +69,22 @@ public class SitemapController : Controller { /// /// Specifies what content types should not be listed in the sitemap—but whose descendents should still be evaluated. /// - private static string[] SkippedContentTypes { get; } = { "PageGroup", "Container" }; + public static Collection SkippedContentTypes { get; } = new() { + "PageGroup", + "Container" + }; /*========================================================================================================================== - | EXCLUDE ATTRIBUTES + | EXCLUDED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Specifies what attributes should not be listed in the sitemap. /// - private static string[] ExcludeAttributes { get; } = { + public static Collection ExcludedAttributes { get; } = new() { "Body", "IsDisabled", - "ParentID", - "TopicID", + "ParentID", //Legacy, but exposed for avoid leacking legacy data + "TopicID", //Legacy, but exposed for avoid leacking legacy data "IsHidden", "NoIndex", "SortOrder" @@ -178,7 +200,7 @@ private IEnumerable AddTopic(Topic topic, bool includeMetadata = false if (topic is null) return topics; if (topic.Attributes.GetBoolean("NoIndex")) return topics; if (topic.Attributes.GetBoolean("IsDisabled")) return topics; - if (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics; + if (ExcludedContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics; /*------------------------------------------------------------------------------------------------------------------------ | Establish variables @@ -227,7 +249,7 @@ XElement getAttributes() => new XText(topic.ContentType?? "Page") ), from attribute in topic.Attributes - where !ExcludeAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) + where !ExcludedAttributes.Contains(attribute.Key, StringComparer.OrdinalIgnoreCase) where topic.Attributes.GetValue(attribute.Key)?.Length < 256 select new XElement(_pagemapNamespace + "Attribute", new XAttribute("name", attribute.Key), From 4183bb4b50de4115a6af2f08a23cf2a0b49bb369 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 13:13:17 -0800 Subject: [PATCH 502/778] Fixed unit test to account for `DataObject` rename This accounts for a breaking change introduced in a previous commit (0061843). --- OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index ba0ae272..2fbf4741 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -182,7 +182,7 @@ public void SitemapController_Index_ExcludesContentTypes() { controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model.Contains("")); + Assert.IsTrue(model.Contains("")); Assert.IsFalse(model.Contains("")); Assert.IsFalse(model.Contains("")); Assert.IsFalse(model.Contains("")); From d16696b3c9b75a2becd6b10ce21c241ab1612ab8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 13:27:19 -0800 Subject: [PATCH 503/778] Added missing `Slideshow` view Slideshow is one of the built-in content types exposed by one of the out-of-the-box view models. There should be a corresponding view in the `Host` test site. Note: Technically a slideshow is a type of `ContentList` and could just share that. I've duplicated the code, however, since `Slideshow` also exposes a custom `TransitionEffect` attribute. --- .../Views/ContentTypes/Slideshow.cshtml | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Slideshow.cshtml diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Slideshow.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Slideshow.cshtml new file mode 100644 index 00000000..191a3c87 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentTypes/Slideshow.cshtml @@ -0,0 +1,27 @@ +@model SlideshowTopicViewModel + + + +

Attributes

+
    +
  • TransitionEffect: @Model.TransitionEffect
  • +
+ +

Collections

+ +

Slides

+@foreach (var contentItem in Model.ContentItems) { +

@contentItem.Key

+
    +
  • Title: @contentItem.Title
  • +
  • Category: @contentItem.Category
  • +
  • Learn More
  • +
  • +
  • Description: @contentItem.Description
  • +
+} + + \ No newline at end of file From db94d35128328bcce4ce71a65c01778ddc619a72 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 14:24:15 -0800 Subject: [PATCH 504/778] Moved specialized collections to a new `Specialized` namespace These collections will not typically be instantiated directly by customers, but will rather by used as base classes or return types, if at all. They are primarily intended to support other specialized collections in e.g. the `OnTopic.Attributes` and `OnTopic.References` namespaces. Moving them to `OnTopic.Collections.Specialized` helps keep the `OnTopic.Collections` namespace focused on general-purpose, customer-oriented collections. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 1 + OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 2 +- OnTopic/Collections/{ => Specialized}/KeyValuesPair.cs | 2 +- OnTopic/Collections/{ => Specialized}/ReadOnlyTopicMultiMap.cs | 2 +- OnTopic/Collections/{ => Specialized}/TopicIndex.cs | 2 +- OnTopic/Collections/{ => Specialized}/TopicMultiMap.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 1 + OnTopic/References/TopicReferenceDictionary.cs | 1 + OnTopic/References/TopicRelationshipMultiMap.cs | 1 + 9 files changed, 9 insertions(+), 5 deletions(-) rename OnTopic/Collections/{ => Specialized}/KeyValuesPair.cs (98%) rename OnTopic/Collections/{ => Specialized}/ReadOnlyTopicMultiMap.cs (99%) rename OnTopic/Collections/{ => Specialized}/TopicIndex.cs (97%) rename OnTopic/Collections/{ => Specialized}/TopicMultiMap.cs (99%) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 4f5bd1cf..31b1f42a 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -11,6 +11,7 @@ using System.Net; using Microsoft.Data.SqlClient; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 10078c7b..9493f62c 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -6,7 +6,7 @@ using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.References; namespace OnTopic.Tests { diff --git a/OnTopic/Collections/KeyValuesPair.cs b/OnTopic/Collections/Specialized/KeyValuesPair.cs similarity index 98% rename from OnTopic/Collections/KeyValuesPair.cs rename to OnTopic/Collections/Specialized/KeyValuesPair.cs index 9c60d9c7..f3dda410 100644 --- a/OnTopic/Collections/KeyValuesPair.cs +++ b/OnTopic/Collections/Specialized/KeyValuesPair.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Collections { +namespace OnTopic.Collections.Specialized { /*============================================================================================================================ | CLASS: KEY VALUES PAIR diff --git a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs similarity index 99% rename from OnTopic/Collections/ReadOnlyTopicMultiMap.cs rename to OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index d0edbdef..a6179de9 100644 --- a/OnTopic/Collections/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -11,7 +11,7 @@ using System.Linq; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Collections { +namespace OnTopic.Collections.Specialized { /*============================================================================================================================ | CLASS: READ-ONLY TOPIC MULTIMAP diff --git a/OnTopic/Collections/TopicIndex.cs b/OnTopic/Collections/Specialized/TopicIndex.cs similarity index 97% rename from OnTopic/Collections/TopicIndex.cs rename to OnTopic/Collections/Specialized/TopicIndex.cs index 56cffc2d..5c2ea8b5 100644 --- a/OnTopic/Collections/TopicIndex.cs +++ b/OnTopic/Collections/Specialized/TopicIndex.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System.Collections.Generic; -namespace OnTopic.Collections { +namespace OnTopic.Collections.Specialized { /*============================================================================================================================ | CLASS: TOPIC INDEX diff --git a/OnTopic/Collections/TopicMultiMap.cs b/OnTopic/Collections/Specialized/TopicMultiMap.cs similarity index 99% rename from OnTopic/Collections/TopicMultiMap.cs rename to OnTopic/Collections/Specialized/TopicMultiMap.cs index f77b5cba..260aaa55 100644 --- a/OnTopic/Collections/TopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/TopicMultiMap.cs @@ -7,7 +7,7 @@ using System.Collections.ObjectModel; using OnTopic.Internal.Diagnostics; -namespace OnTopic.Collections { +namespace OnTopic.Collections.Specialized { /*============================================================================================================================ | CLASS: TOPIC MULTIMAP diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 3e0129fa..ec0364b2 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using OnTopic.Attributes; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index 2a40f4bb..82c40843 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Repositories; diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index ad41f997..6447e15b 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; From 4f72f2a18402a1e42933044cd55b4c13b13bb4d7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 14:42:01 -0800 Subject: [PATCH 505/778] Introduced new `ITrackDirtyKeys` interface A number of collections track whether the collection or a specific key is dirty through a set of methods such as `IsDirty()`, `IsDirty(key)`, `MarkClean(key)`, &c. By establishing an interface, we help formalize and standardize that interface, while also centralizing the documentation. --- .../Specialized/ITrackDirtyKeys.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 OnTopic/Collections/Specialized/ITrackDirtyKeys.cs diff --git a/OnTopic/Collections/Specialized/ITrackDirtyKeys.cs b/OnTopic/Collections/Specialized/ITrackDirtyKeys.cs new file mode 100644 index 00000000..f9ca2a33 --- /dev/null +++ b/OnTopic/Collections/Specialized/ITrackDirtyKeys.cs @@ -0,0 +1,44 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace OnTopic.Collections.Specialized { + + /*============================================================================================================================ + | INTERFACE: TRACK DIRTY KEYS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Defines an interface for tracking dirty keys. + /// + public interface ITrackDirtyKeys { + + /*========================================================================================================================== + | METHOD: IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether the collection is dirty. + /// + bool IsDirty(); + + /// + /// Determines whether the provided in the collection is dirty. + /// + bool IsDirty(string key); + + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Marks the collection as clean. + /// + void MarkClean(); + + /// + /// Marks the specified in the collection as clean. + /// + void MarkClean(string key); + + } //Interface +} //Namespace \ No newline at end of file From 80e2215240b48eb3dcf095853d7f33a1011c42cb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 14:46:52 -0800 Subject: [PATCH 506/778] Applied `ITrackDirtyKeys` to `AttributeValueCollection` This required refactoring the collection slightly to account for its specific overloads, while still honoring the less specialized methods of `ITrackDirtyKeys`. For instance, `AttributeValueCollection.MarkClean()` accepts an optional `version` parameter. Since interfaces don't recognize optional parameter, this required that parameter being marked as required, and introducing a `MarkClean()` method without any parameters. --- .../Attributes/AttributeValueCollection.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 9569734a..baa99be6 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -8,6 +8,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Repositories; @@ -25,7 +26,7 @@ namespace OnTopic.Attributes { /// The class tracks these through its property, which is an instance of /// the class. /// - public class AttributeValueCollection : KeyedCollection { + public class AttributeValueCollection : KeyedCollection, ITrackDirtyKeys { /*========================================================================================================================== | DISPATCHER @@ -73,6 +74,10 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Ordin /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public bool IsDirty() => IsDirty(false); + /// /// Determine if any attributes in the are dirty. /// @@ -88,7 +93,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Ordin /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. /// /// True if the attribute value is marked as dirty; otherwise false. - public bool IsDirty(bool excludeLastModified = false) + public bool IsDirty(bool excludeLastModified) => DeletedAttributes.Count > 0 || Items.Any(a => a.IsDirty && (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) @@ -103,18 +108,22 @@ public bool IsDirty(bool excludeLastModified = false) /// is a state of the current , it does not support inheritFromParent or /// inheritFromDerived (which otherwise default to true). /// - /// The string identifier for the . + /// The string identifier for the . /// True if the attribute value is marked as dirty; otherwise false. - public bool IsDirty(string name) { - if (!Contains(name)) { + public bool IsDirty(string key) { + if (!Contains(key)) { return false; } - return this[name].IsDirty; + return this[key].IsDirty; } /*========================================================================================================================== | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public void MarkClean() => MarkClean((DateTime?)null); + /// /// Marks the collection—including all items—as clean, meaning they have been persisted to /// the underlying . @@ -128,7 +137,7 @@ public bool IsDirty(string name) { /// The value that the attributes were last saved. This corresponds to the . /// - public void MarkClean(DateTime? version = null) { + public void MarkClean(DateTime? version) { foreach (var attribute in Items.Where(a => a.IsDirty).ToArray()) { SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); } @@ -138,6 +147,10 @@ public void MarkClean(DateTime? version = null) { /*========================================================================================================================== | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public void MarkClean(string key) => MarkClean(key, null); + /// /// Marks an individual as clean. /// @@ -146,14 +159,14 @@ public void MarkClean(DateTime? version = null) { /// mark an as clean. After this, will return false for /// that item until it is modified. /// - /// The string identifier for the . + /// The string identifier for the . /// /// The value that the attribute was last modified. This denotes the associated with the specific attribute. /// - public void MarkClean(string name, DateTime? version = null) { - if (Contains(name)) { - var attribute = this[name]; + public void MarkClean(string key, DateTime? version) { + if (Contains(key)) { + var attribute = this[key]; if (attribute.IsDirty) { SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); } From 9705c15162ae541e67a904b4826a6abd88cf38bb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 14:49:38 -0800 Subject: [PATCH 507/778] Introduced specialized `DirtyKeyCollection` The `DirtyKeyCollection` is an `internal` collection meant to help standardize how dirty keys are tracked, by moving that logic out of disparate collections and instead into a single utility collection. It implements the new `ITrackDirtyKeys` interface, but also introduces a couple of more specialized methods, such as `MarkDirty()` and `MarkAs()`. --- .../Specialized/DirtyKeyCollection.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 OnTopic/Collections/Specialized/DirtyKeyCollection.cs diff --git a/OnTopic/Collections/Specialized/DirtyKeyCollection.cs b/OnTopic/Collections/Specialized/DirtyKeyCollection.cs new file mode 100644 index 00000000..6cdf6e7b --- /dev/null +++ b/OnTopic/Collections/Specialized/DirtyKeyCollection.cs @@ -0,0 +1,80 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.ObjectModel; + +namespace OnTopic.Collections.Specialized { + + /*============================================================================================================================ + | CLASS: DIRTY KEY COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of dirty keys. + /// + /// + /// This collection does not track the values of those keys or attempt to determine if a value is dirty. It simply provides + /// a convenient way for other collections to track dirty keys based on their own internal logic. + /// + internal class DirtyKeyCollection : Collection, ITrackDirtyKeys { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the . + /// + public DirtyKeyCollection() : base() {} + + /*========================================================================================================================== + | METHOD: IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public bool IsDirty() => Count > 0; + + /// + public bool IsDirty(string key) => Contains(key); + + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public void MarkClean() => Clear(); + + /// + public void MarkClean(string key) { + if (Contains(key)) { + Remove(key); + } + } + + /*========================================================================================================================== + | METHOD: MARK DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Marks a specific as dirty, if it isn't already. + /// + public void MarkDirty(string key) { + if (!Contains(key)) { + Add(key); + } + } + + /*========================================================================================================================== + | METHOD: MARK AS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Marks a specific as clean or dirty based on the parameter. + /// + public void MarkAs(string key, bool markDirty) { + if (markDirty) { + MarkDirty(key); + } + else { + MarkClean(key); + } + } + + } //Class +} //Namespace \ No newline at end of file From b3dd1941ee7df1bbc03f0d1cdaaaac9399737cf9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:15:00 -0800 Subject: [PATCH 508/778] Implemented `DirtyKeyCollection` in `TopicReferenceDictionary` This (slightly) simplifies the (admittedly simple) logic in `DirtyKeyCollection` by centralizing the implementation of these methods. As the `TopicReferenceDictionary` didn't previously track individual keys, this actually extends the functionality slightly, and makes it marginally more intelligent. For instance, if an item is removed from an unsaved topic, then it marks the key as clean, not dirty, since it knows it cannot have been previously saved. --- .../References/TopicReferenceDictionary.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index 82c40843..f90bb10e 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -6,7 +6,6 @@ using System; using System.Collections; using System.Collections.Generic; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; @@ -32,7 +31,7 @@ public class TopicReferenceDictionary : IDictionary { \-------------------------------------------------------------------------------------------------------------------------*/ readonly Topic _parent; readonly IDictionary _storage; - private bool _isDirty; + readonly DirtyKeyCollection _dirtyKeys; /*========================================================================================================================== | CONSTRUCTOR @@ -53,6 +52,7 @@ public TopicReferenceDictionary(Topic parent) { _parent = parent; _storage = new Dictionary(); _topicPropertyDispatcher = new(parent); + _dirtyKeys = new(); } @@ -120,7 +120,7 @@ public Topic this[string referenceKey] { | Set dirty state \---------------------------------------------------------------------------------------------------------------------*/ if (!_storage.TryGetValue(referenceKey, out var existing) || existing != value) { - _isDirty = true; + _dirtyKeys.MarkDirty(referenceKey); } /*---------------------------------------------------------------------------------------------------------------------- @@ -175,7 +175,7 @@ void ICollection>.Add(KeyValuePair it | Mark dirty \-----------------------------------------------------------------------------------------------------------------------*/ if (!_storage.TryGetValue(item.Key, out var existing) || existing != item.Value) { - _isDirty = true; + _dirtyKeys.MarkDirty(item.Key); } /*------------------------------------------------------------------------------------------------------------------------ @@ -225,7 +225,7 @@ internal void SetTopic(string key, Topic? value, bool? markDirty, bool enforceBu /*------------------------------------------------------------------------------------------------------------------------ | Establish state \-----------------------------------------------------------------------------------------------------------------------*/ - var wasDirty = _isDirty; + var wasDirty = _dirtyKeys.IsDirty(key); /*------------------------------------------------------------------------------------------------------------------------ | Register that business logic has already been enforced @@ -257,7 +257,7 @@ internal void SetTopic(string key, Topic? value, bool? markDirty, bool enforceBu | Set dirty state \-----------------------------------------------------------------------------------------------------------------------*/ if (wasDirty is false && markDirty is false) { - _isDirty = false; + _dirtyKeys.MarkClean(key); } } @@ -269,10 +269,10 @@ internal void SetTopic(string key, Topic? value, bool? markDirty, bool enforceBu public void Clear() { /*------------------------------------------------------------------------------------------------------------------------ - | Mark dirty + | Mark keys as dirty \-----------------------------------------------------------------------------------------------------------------------*/ - if (Count > 0) { - _isDirty = true; + foreach (var item in _storage) { + _dirtyKeys.MarkAs(item.Key, markDirty: !_parent.IsNew); } /*------------------------------------------------------------------------------------------------------------------------ @@ -336,7 +336,7 @@ public bool Remove(string key) { \-----------------------------------------------------------------------------------------------------------------------*/ if (TryGetValue(key, out var existing)) { existing.IncomingRelationships.RemoveTopic(key, _parent, true); - _isDirty = true; + _dirtyKeys.MarkAs(key, markDirty: !_parent.IsNew); } /*------------------------------------------------------------------------------------------------------------------------ @@ -375,7 +375,7 @@ public bool Remove(string key) { /// Determines if the dictionary has been modified. This value is set to true any time a new item is inserted or /// removed from the dictionary. /// - public bool IsDirty() => _isDirty; + public bool IsDirty() => _dirtyKeys.IsDirty(); /*========================================================================================================================== | MARK CLEAN @@ -383,7 +383,7 @@ public bool Remove(string key) { /// /// Resets the status of the . /// - public void MarkClean() => _isDirty = false; + public void MarkClean() => _dirtyKeys.MarkClean(); } //Class } //Namespace \ No newline at end of file From 6f828446203dd6d815dabd2deca84c1d91b5e289 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:25:41 -0800 Subject: [PATCH 509/778] Implemented `DirtyKeyCollection` in `TopicRelationshipMultiMap` This (slightly) simplifies the (admittedly simple) logic in `DirtyKeyCollection` by centralizing the implementation of these methods. This also improves the `IsDirty()` handling of `TopicRelationshipsMultiMap` by allowing it to mark a key as clean when it's being removed if it points to an unsaved `Topic`; in that case, we know that it hasn't previously been saved, and thus should not mark the collection as `IsDirty()`. --- .../References/TopicRelationshipMultiMap.cs | 33 +++++-------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 6447e15b..5b368c33 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -4,8 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; -using OnTopic.Collections; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; @@ -31,7 +29,7 @@ public class TopicRelationshipMultiMap : ReadOnlyTopicMultiMap { \-------------------------------------------------------------------------------------------------------------------------*/ readonly Topic _parent; readonly bool _isIncoming; - readonly List _isDirty = new(); + readonly DirtyKeyCollection _dirtyKeys = new(); readonly TopicMultiMap _storage = new(); /*========================================================================================================================== @@ -69,7 +67,7 @@ public void ClearTopics(string relationshipKey) { if (_storage.Contains(relationshipKey)) { var relationship = _storage.GetTopics(relationshipKey); if (relationship.Count > 0) { - MarkDirty(relationshipKey); + _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew); } _storage.Clear(relationshipKey); } @@ -132,7 +130,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) /*------------------------------------------------------------------------------------------------------------------------ | Remove relationship \-----------------------------------------------------------------------------------------------------------------------*/ - MarkDirty(relationshipKey); + _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew && !topic.IsNew); _storage.Remove(relationshipKey, topic); /*------------------------------------------------------------------------------------------------------------------------ @@ -186,14 +184,14 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo | Add relationship \-----------------------------------------------------------------------------------------------------------------------*/ var topics = _storage.GetTopics(relationshipKey); - var wasDirty = _isDirty.Contains(relationshipKey); + var wasDirty = _dirtyKeys.IsDirty(relationshipKey); if (!topics.Contains(topic)) { _storage.Add(relationshipKey, topic); if (markDirty.HasValue && !markDirty.Value && !wasDirty) { MarkClean(relationshipKey); } else { - MarkDirty(relationshipKey); + _dirtyKeys.MarkDirty(relationshipKey); } } @@ -241,19 +239,8 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo /// Determines if any of the relationships have been modified; if they have, returns true. ///
public bool IsDirty() => _isDirty.Count > 0; + public bool IsDirty() => _dirtyKeys.IsDirty(); - /*========================================================================================================================== - | METHOD: MARK DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Evaluates each of the relationships to determine if any of them are set to . If they are, - /// returns true. - /// - private void MarkDirty(string relationshipKey) { - if (!_isDirty.Contains(relationshipKey)) { - _isDirty.Add(relationshipKey); - } - } /*========================================================================================================================== | METHOD: MARK CLEAN @@ -261,16 +248,12 @@ private void MarkDirty(string relationshipKey) { /// /// Marks the relationships collections as clean. /// - public void MarkClean() => _isDirty.Clear(); + public void MarkClean() => _dirtyKeys.MarkClean(); /// /// Removes the from the collection, if it exists. /// - public void MarkClean(string relationshipKey) { - if (_isDirty.Contains(relationshipKey)) { - _isDirty.Remove(relationshipKey); - } - } + public void MarkClean(string key) => _dirtyKeys.MarkClean(key); } //Class } //Namespace \ No newline at end of file From 401eefcb5a63506975fb7024c87c259c0373dd62 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:28:18 -0800 Subject: [PATCH 510/778] Applied `ITrackDirtyKeys` to `TopicReferenceDictionary` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This required introducing some overloads that hadn't previously been accounted for—namely `IsDirty(key)` and `MarkClean(key)`—since `TopicReferenceDictionary` hadn't previously track individual keys before the introduction of `DirtyKeyCollection` (9705c15). As a result, these overloads introduced ambiguity in some of the XML Doc references in teh `TopicReferenceDictionaryTest`, so they needed to be updated to refer to the specific overload. --- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 11 ++++++----- OnTopic/References/TopicReferenceDictionary.cs | 17 +++++++++-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index 58a3d2c7..87408b74 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -15,7 +15,7 @@ namespace OnTopic.Tests { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the , with a particular emphasis on the custom features - /// such as , , , , and the cross-referencing of reciprocal /// values in the property. /// @@ -27,7 +27,7 @@ public class TopicReferenceDictionaryTest { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// is correctly set. + /// is correctly set. /// [TestMethod] public void Add_NewReference_IsDirty() { @@ -48,7 +48,7 @@ public void Add_NewReference_IsDirty() { /// /// Assembles a new , adds a new reference using , and confirms that is not set. + /// TopicReferenceDictionary.IsDirty()"/> is not set. /// [TestMethod] public void SetTopic_NewReference_NotDirty() { @@ -68,7 +68,8 @@ public void SetTopic_NewReference_NotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a topic reference, removes that reference using , and confirms that is set. + /// "TopicReferenceDictionary.Remove(String)"/> , and confirms that is + /// set. /// [TestMethod] public void Remove_ExistingReference_IsDirty() { @@ -90,7 +91,7 @@ public void Remove_ExistingReference_IsDirty() { /// /// Assembles a new , adds a new reference using , calls and confirms that is set. + /// .Clear()"/> and confirms that is set. /// [TestMethod] public void Clear_ExistingReferences_IsDirty() { diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index f90bb10e..ee18feb1 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -19,7 +19,7 @@ namespace OnTopic.References { /// /// Represents a collection of objects associated with particular reference keys. /// - public class TopicReferenceDictionary : IDictionary { + public class TopicReferenceDictionary : IDictionary, ITrackDirtyKeys { /*========================================================================================================================== | DISPATCHER @@ -371,19 +371,20 @@ public bool Remove(string key) { /*========================================================================================================================== | IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Determines if the dictionary has been modified. This value is set to true any time a new item is inserted or - /// removed from the dictionary. - /// + /// public bool IsDirty() => _dirtyKeys.IsDirty(); + /// + public bool IsDirty(string key) => _dirtyKeys.IsDirty(key); + /*========================================================================================================================== | MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Resets the status of the . - /// + /// public void MarkClean() => _dirtyKeys.MarkClean(); + /// + public void MarkClean(string key) => _dirtyKeys.MarkClean(key); + } //Class } //Namespace \ No newline at end of file From 6a99577c6cfc70a391b308b7c0224fd02ea94720 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:30:17 -0800 Subject: [PATCH 511/778] Applied `ITrackDirtyKeys` to `TopicRelationshipMultiMap` This required introducing a new `IsDirty(key)` overload. This should have been present, for consistency, but wasn't. As a result, this overload introduced ambiguity in some of the XML Doc references in the `TopicRelationshipMultiMapTest`, so they needed to be updated to refer to the specific overload. --- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 18 +++++++++--------- .../References/TopicRelationshipMultiMap.cs | 17 ++++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 9493f62c..2aaf21b6 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -166,8 +166,8 @@ public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { | TEST: SET TOPIC: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a topic to a and confirms that is - /// set. + /// Adds a topic to a and confirms that is set. /// [TestMethod] public void SetTopic_IsDirty() { @@ -187,7 +187,7 @@ public void SetTopic_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a duplicate topic to a and confirms that value of is false. + /// cref="TopicRelationshipMultiMap.IsDirty()"/> is false. /// [TestMethod] public void SetTopic_IsDuplicate_IsNotDirty() { @@ -210,7 +210,7 @@ public void SetTopic_IsDuplicate_IsNotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a duplicate topic to a and confirms that value of is false. + /// cref="TopicRelationshipMultiMap.IsDirty()"/> is false. /// [TestMethod] public void SetTopic_IsDuplicate_StaysDirty() { @@ -233,7 +233,7 @@ public void SetTopic_IsDuplicate_StaysDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Removes an existing from a and conirms that the value for returns true. + /// cref="TopicRelationshipMultiMap.IsDirty()"/> returns true. /// [TestMethod] public void RemoveTopic_IsDirty() { @@ -255,7 +255,7 @@ public void RemoveTopic_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Removes a non-existent from a and conirms that the value for - /// returns false. + /// returns false. /// [TestMethod] public void RemoveTopic_MissingTopic_IsNotDirty() { @@ -276,7 +276,7 @@ public void RemoveTopic_MissingTopic_IsNotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Removes a non-existent from a and conirms that the value for - /// stays true. + /// stays true. /// [TestMethod] public void RemoveTopic_MissingTopic_StaysDirty() { @@ -300,7 +300,7 @@ public void RemoveTopic_MissingTopic_StaysDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Call and confirms that value of is true. + /// cref="TopicRelationshipMultiMap.IsDirty()"/> is true. /// [TestMethod] public void ClearTopics_ExistingTopics_IsDirty() { @@ -322,7 +322,7 @@ public void ClearTopics_ExistingTopics_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Call with no existing s and confirms that - /// the value of is set to false. + /// the value of is set to false. /// [TestMethod] public void ClearTopics_NoTopics_IsNotDirty() { diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 5b368c33..91958781 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -22,7 +22,7 @@ namespace OnTopic.References { /// simplifying access to the , but also ensuring that business logic is enforced, such as local /// state tracking and handling of reciprocal relationships. /// - public class TopicRelationshipMultiMap : ReadOnlyTopicMultiMap { + public class TopicRelationshipMultiMap : ReadOnlyTopicMultiMap, ITrackDirtyKeys { /*========================================================================================================================== | PRIVATE VARIABLES @@ -235,24 +235,19 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo /*========================================================================================================================== | METHOD: IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Determines if any of the relationships have been modified; if they have, returns true. - /// - public bool IsDirty() => _isDirty.Count > 0; + /// public bool IsDirty() => _dirtyKeys.IsDirty(); + /// + public bool IsDirty(string key) => _dirtyKeys.IsDirty(key); /*========================================================================================================================== | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Marks the relationships collections as clean. - /// + /// public void MarkClean() => _dirtyKeys.MarkClean(); - /// - /// Removes the from the collection, if it exists. - /// + /// public void MarkClean(string key) => _dirtyKeys.MarkClean(key); } //Class From 3603d2b6a656003a1e46213e04f23b1496c5b685 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:46:38 -0800 Subject: [PATCH 512/778] Updated unit tests to account for new `Remove()`, `Clear()` logic With the introduction of the new `DirtyKeyCollection`, the `TopicReferenceDictionary` and `TopicRelationshipMultiMap` now use more intelligent logic on `Remove()` and `Clear()`, only marking the key as dirty if the current (source) topic hasn't been saved, and otherwise marking it as clean. To account for this, the `TopicReferenceDictionary` and `TopicRelationshipMultiMap` tests needed to be updated to assign an `Id` to the test topics so that the `IsDirty()` state can correctly be set. --- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 4 ++-- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index 87408b74..d5292a3e 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -74,7 +74,7 @@ public void SetTopic_NewReference_NotDirty() { [TestMethod] public void Remove_ExistingReference_IsDirty() { - var topic = TopicFactory.Create("Topic", "Page"); + var topic = TopicFactory.Create("Topic", "Page", 1); var reference = TopicFactory.Create("Reference", "Page"); topic.References.SetTopic("Reference", reference, false); @@ -96,7 +96,7 @@ public void Remove_ExistingReference_IsDirty() { [TestMethod] public void Clear_ExistingReferences_IsDirty() { - var topic = TopicFactory.Create("Topic", "Page"); + var topic = TopicFactory.Create("Topic", "Page", 1); var reference = TopicFactory.Create("Reference", "Page"); topic.References.SetTopic("Reference", reference, false); diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 2aaf21b6..49271292 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -238,7 +238,7 @@ public void SetTopic_IsDuplicate_StaysDirty() { [TestMethod] public void RemoveTopic_IsDirty() { - var topic = TopicFactory.Create("Test", "Page"); + var topic = TopicFactory.Create("Test", "Page", 1); var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); @@ -305,7 +305,7 @@ public void RemoveTopic_MissingTopic_StaysDirty() { [TestMethod] public void ClearTopics_ExistingTopics_IsDirty() { - var topic = TopicFactory.Create("Test", "Page"); + var topic = TopicFactory.Create("Test", "Page", 1); var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); From 816a9f01008d2e7da42552f509cc93941357ee2d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 15:48:31 -0800 Subject: [PATCH 513/778] Fixed bug in `RemoveTopic()`'s `MarkDirty()` handling In a previous update, I had introduced an optimization to mark the `relationshipKey` as clean if the target `topic` was not saved. That makes sense in that we know that a `topic` that's `IsNew` cannot have been saved previously, and thus shouldn't affect the `IsDirty()` state. But that doesn't account for _other_ topics in the relationship. Removing a new topic from the collection shouldn't mark the collection as clean if there are _other_ topics in the collection that _may_ have been marked as dirty. This could be accounted for by adding a check to see if _all_ items in the collection are `IsNew`, in which case this logic would be appropriate. But this is such a corner case that the additional complexity and overhead of a more sophisticated check doesn't really make sense here. --- OnTopic/References/TopicRelationshipMultiMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/References/TopicRelationshipMultiMap.cs index 91958781..54db4e7f 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/References/TopicRelationshipMultiMap.cs @@ -130,7 +130,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) /*------------------------------------------------------------------------------------------------------------------------ | Remove relationship \-----------------------------------------------------------------------------------------------------------------------*/ - _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew && !topic.IsNew); + _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew); _storage.Remove(relationshipKey, topic); /*------------------------------------------------------------------------------------------------------------------------ From 4bf55a5bc9de182ff3e02613f8fd3452f7c763a0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 16:16:44 -0800 Subject: [PATCH 514/778] Removed unused overloads for `ReferentialIntegrityException` The overload that accepts a `sourceTopic` is hard-coded for handling missing or unsaved `DerivedTopic` references. In practice, referential integrity exceptions can be thrown in a variety of circumstances, and the `DerivedTopic` case is more generally handled by validating the new `Topic.References` when saving a topic. Given that, these overloads are not used. Not only that, but while it might be tempted to attempt to generalize the messaging so that a `sourceTopic` and a `targetTopic` can be passed in, even that introduces challenges since a referential integrity exception _could_ potentially mean that one of those values cannot be found. We may revisit this later if we identify a use case for it, but for now this scenario is being removed. --- .../ReferentialIntegrityException.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/OnTopic/Repositories/ReferentialIntegrityException.cs b/OnTopic/Repositories/ReferentialIntegrityException.cs index eadf323b..3046cc47 100644 --- a/OnTopic/Repositories/ReferentialIntegrityException.cs +++ b/OnTopic/Repositories/ReferentialIntegrityException.cs @@ -32,30 +32,6 @@ public class ReferentialIntegrityException: TopicRepositoryException { ///
public ReferentialIntegrityException() : base() { } - /// - /// Initializes a new instance based on the source . - /// - /// The source which triggered the exception. - public ReferentialIntegrityException(Topic sourceTopic): - base( - $"The operation on the topic '{sourceTopic?.DerivedTopic?.GetUniqueKey()}' would introduce a referential integrity " + - $"violation in the underlying persistence layer; the topic '{sourceTopic?.GetUniqueKey()}' depends upon it." - ) { } - - /// - /// Initializes a new instance based on the source . - /// - /// The source which triggered the exception. - /// The reference to the original, underlying exception. - public ReferentialIntegrityException(Topic sourceTopic, Exception innerException): - base( - $"The operation on the topic '{sourceTopic?.DerivedTopic?.GetUniqueKey()}' would introduce a referential integrity " + - $"violation in the underlying persistence layer; the topic '{sourceTopic?.GetUniqueKey()}' depends upon it.", - innerException - ) { } - /// /// Initializes a new instance based on a . From 41d7d3b48ca7c3dbcb3038a9b0dd0ddcc5104bd2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 16:32:01 -0800 Subject: [PATCH 515/778] Migrated from `GitVersionTask` to `GitVersion.MsBuild` The `GitVersionTask` package is deprecated; `GitVersion.MsBuild` effectively replaces it. Confirmed that the semantic version is correctly determined using the new package. --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 6b267fde..b1fc5269 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 7fafb2c7..85bc01b8 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 626add6f..fa2d8e10 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -36,7 +36,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 6c0a7309..63e4820f 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -38,7 +38,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 608c14d4..0de3fa47 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 2d10ab8d413791d19bd2b86a8a81bd27b69c25ab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 16:33:29 -0800 Subject: [PATCH 516/778] Updated to latest version of `Microsoft.NET.Test.Sdk` Confirmed that all unit tests continue to pass. --- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index c1fbcb27..c1b60105 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 13037233..a580804e 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -33,7 +33,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 62374e061f985ab9ba210971f9aac8fa661c499a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Thu, 28 Jan 2021 16:38:44 -0800 Subject: [PATCH 517/778] Updated to 2.0.0 release of `Microsoft.Data.SqlClient` This is one of the few production dependencies of the OnTopic Library which is relayed to implementers; the rest are mostly build dependencies. Since this is a public library, we try to keep production dependencies on their lowest possible version in order to maximize backward compatibility. That said, as this is a major release that is over six months old, and this is a major update to OnTopic, we think it's reasonable to expect implementors to upgrade to the latest major version of `SqlClient` while they're upgrading to OnTopic 5.0.0. Still, we're only requiring `SqlClient` 2.0.0. While we expect customers will likely want to use the latest version, we're allowing flexibility in which version they adopt, in case there are e.g. particular bugs or limitations introduced by minor updates or patches, and which affect other operations outside of OnTopic. --- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index fa2d8e10..14acd865 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -44,7 +44,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 99d6d5134da7305c0f322ba84339b5725dad13f5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 14:45:01 -0800 Subject: [PATCH 518/778] Consolidated overloads of `TopicFactory.Create()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There were two overloads of `TopicFactory.Create()`, which can be satisfied with one overload with optional parameters assuming a different parameter ordering. That paramter ordering is actually consistent with the respective parameter ordering of the `Topic` constructor itself, and thus makes more sense for consistency anyway. As such, I've consolidated these two overloads into one, and flipped the order of the (optional) `parent` and `id` parameters. Technically, this is a breaking change. But, generally, the only codes that call the full `TopicFactory.Create()` overload are the `ITopicRepository` implementations—namely `SqlTopicRepository`—and the unit tests. Outside of that, most callers of `TopicFactory.Create()` will only be passing the `key`, `contentType`, and `parent` parameters, which will remain compatible with this ordering. --- OnTopic/TopicFactory.cs | 70 +++-------------------------------------- 1 file changed, 5 insertions(+), 65 deletions(-) diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 8e1f9682..8a85fe26 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -31,66 +31,6 @@ public static class TopicFactory { /*========================================================================================================================== | METHOD: CREATE \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Factory method for creating new strongly-typed instances of the topics class, assuming a strongly-typed subclass is - /// available. - /// - /// - /// When creating new attributes, the s for both and will be set to , which is required in order to - /// correctly save new topics to the database. - /// - /// A string representing the key for the new topic instance. - /// A string representing the key of the target content type. - /// Optional topic to set as the new topic's parent. - /// - /// Thrown when the class representing the content type is found, but doesn't derive from . - /// - /// A strongly-typed instance of the class based on the target content type. - /// - /// !String.IsNullOrWhiteSpace(key) - /// - /// - /// !key.Contains(" ") - /// - /// - /// !String.IsNullOrWhiteSpace(contentType) - /// - /// - /// !contentType.Contains(" ") - /// - public static Topic Create(string key, string contentType, Topic? parent = null) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate contracts - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); - Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); - TopicFactory.ValidateKey(key); - TopicFactory.ValidateKey(contentType); - - /*------------------------------------------------------------------------------------------------------------------------ - | Determine target type - \-----------------------------------------------------------------------------------------------------------------------*/ - var targetType = TypeLookupService.Lookup(contentType); - - Contract.Assume( - targetType, - $"The content type {contentType} could not be located in the ITypeLookupService, and no fallback could be " + - $"identified." - ); - - /*------------------------------------------------------------------------------------------------------------------------ - | Identify the appropriate topic - \-----------------------------------------------------------------------------------------------------------------------*/ - return (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1)!; - - } - /// /// Factory method for creating new strongly-typed instances of the topics class, assuming a strongly-typed subclass is /// available. Used for cases where a is being deserialized from an existing instance, as indicated @@ -103,22 +43,22 @@ public static Topic Create(string key, string contentType, Topic? parent = null) /// /// A string representing the key for the new topic instance. /// A string representing the key of the target content type. - /// The unique identifier assigned by the data store for an existing topic. /// Optional topic to set as the new topic's parent. + /// The unique identifier assigned by the data store for an existing topic. /// /// Thrown when the class representing the content type is found, but doesn't derive from . /// /// A strongly-typed instance of the class based on the target content type. - public static Topic Create(string key, string contentType, int id, Topic? parent = null) { + public static Topic Create(string key, string contentType, Topic? parent = null, int id = -1) { /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType)); - Contract.Requires(id > 0, nameof(id)); - TopicFactory.ValidateKey(key); - TopicFactory.ValidateKey(contentType); + + ValidateKey(key); + ValidateKey(contentType); /*------------------------------------------------------------------------------------------------------------------------ | Determine target type From 0fcd8b234e40226c099074458237dbe1aad1c4eb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 14:48:18 -0800 Subject: [PATCH 519/778] Introduced `TopicFactory.Create()` overload with `id`, but not `parent` It's really rare that production code will need to create a `Topic` with an `id` but not a `parent`. But this use case happens frequently in test cases, where we want to simulate a saved entity (by assigning it an `Id`) but don't want to make it part of the broader topic graph. Technically, this could probably be marked `internal` since it is expected to primarily be used by unit tests. But we'll maintain it as it helps provide backward compatibility with the previous overloads, prior to merging the two main `Create()` implementations (99d6d51). --- OnTopic/TopicFactory.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 8a85fe26..cf3a6922 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -78,6 +78,10 @@ public static Topic Create(string key, string contentType, Topic? parent = null, } + /// + public static Topic Create(string key, string contentType, int id) => + Create(key, contentType, null, id); + /*========================================================================================================================== | METHOD: VALIDATE KEY \-------------------------------------------------------------------------------------------------------------------------*/ From 48a2eccc8adb588f3ecfbd61592971872b2fee26 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:01:59 -0800 Subject: [PATCH 520/778] Updated unit tests to account for `TopicFactory.Create()` parameter order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a previous update, I consolidated two overloads of `TopicFactory.Create()` (99d6d51), which resulted in changing the parameter order of the last two optional parameters (`parent`, and `id`). This order is actually consistent with the `Topic` constructor and, thus, preferred. It's really rare for code to provide all of these parameters and, thus, it normally doesn't matter. The main use case for this is when loading data from persistence stores, and so the only production code we'd expect to call it is the `ITopicRepository` implementations—and, specifically, the `SqlTopicRepository` (as the rest are all test doubles, base classes, or decorators). That said, the one _other_ place where we use all four parameter are the unit tests, as they're needed when a) defining a topic graph, and b) assigning each entity a fake identifier (to simulate a saved state). Given that, the unit tests—and the `StubTopicRepository` they rely on—need to be updated to reflect the new order. --- OnTopic.TestDoubles/StubTopicRepository.cs | 6 ++-- OnTopic.Tests/ITopicRepositoryTest.cs | 2 +- OnTopic.Tests/SqlTopicRepositoryTest.cs | 4 +-- OnTopic.Tests/TopicMappingServiceTest.cs | 2 +- OnTopic.Tests/TopicQueryingTest.cs | 36 +++++++++++----------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index ffd0f787..8df76359 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -252,7 +252,7 @@ AttributeDescriptor addAttribute( container = TopicFactory.Create("Attributes", "List", contentType); container.Attributes.SetBoolean("IsHidden", true); } - var attribute = (AttributeDescriptor)TopicFactory.Create(attributeKey, editorType, currentAttributeId++, container); + var attribute = (AttributeDescriptor)TopicFactory.Create(attributeKey, editorType, container, currentAttributeId++); attribute.IsRequired = isRequired; attribute.IsExtendedAttribute = isExtended; return attribute; @@ -272,7 +272,7 @@ AttributeDescriptor addAttribute( /*------------------------------------------------------------------------------------------------------------------------ | Establish content \-----------------------------------------------------------------------------------------------------------------------*/ - var web = TopicFactory.Create("Web", "Page", 10000, rootTopic); + var web = TopicFactory.Create("Web", "Page", rootTopic, 10000); CreateFakeData(web, 2, 3); @@ -296,7 +296,7 @@ AttributeDescriptor addAttribute( /// private void CreateFakeData(Topic parent, int count = 3, int depth = 3) { for (var i = 0; i < count; i++) { - var topic = TopicFactory.Create(parent.Key + "_" + i, "Page", parent.Id + (int)Math.Pow(10, depth) * i, parent); + var topic = TopicFactory.Create(parent.Key + "_" + i, "Page", parent, parent.Id + (int)Math.Pow(10, depth) * i); topic.Attributes.SetValue("ParentKey", parent.Key); topic.Attributes.SetValue("DepthCount", (depth+i).ToString(CultureInfo.InvariantCulture)); if (depth > 0) { diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index c4a247de..ee6f6dea 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -74,7 +74,7 @@ public void Load_Default_ReturnsTopicTopic() { public void Load_ValidUniqueKey_ReturnsCorrectTopic() { var topic = _topicRepository.Load("Root:Configuration:ContentTypes:Page"); - var child = TopicFactory.Create("Child", "ContentType", Int32.MaxValue, topic); + var child = TopicFactory.Create("Child", "ContentType", topic, Int32.MaxValue); Assert.AreEqual("Page", topic.Key); diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index b8679dd4..b2c6373b 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -200,8 +200,8 @@ public void LoadTopicGraph_WithMissingReference_NotFullyLoaded() { public void LoadTopicGraph_WithDeletedRelationship_RemovesRelationship() { var topic = TopicFactory.Create("Test", "Container", 1); - var child = TopicFactory.Create("Child", "Container", 2, topic); - var related = TopicFactory.Create("Related", "Container", 3, topic); + var child = TopicFactory.Create("Child", "Container", topic, 2); + var related = TopicFactory.Create("Related", "Container", topic, 3); child.Relationships.SetTopic("Test", related); diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index d1bcb88d..9efd1465 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -770,7 +770,7 @@ public async Task Map_MetadataLookup_ReturnsLookupItems() { public async Task Map_CircularReference_ReturnsCachedParent() { var topic = TopicFactory.Create("Test", "Circular", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Circular", 2, topic); + var childTopic = TopicFactory.Create("ChildTopic", "Circular", topic, 2); var mappedTopic = await _mappingService.MapAsync(topic).ConfigureAwait(false); diff --git a/OnTopic.Tests/TopicQueryingTest.cs b/OnTopic.Tests/TopicQueryingTest.cs index 9a969109..d72dbd56 100644 --- a/OnTopic.Tests/TopicQueryingTest.cs +++ b/OnTopic.Tests/TopicQueryingTest.cs @@ -54,10 +54,10 @@ public TopicQueryingTest() { public void FindAllByAttribute_ReturnsCorrectTopics() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic); - var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic); - var grandNieceTopic = TopicFactory.Create("GrandNieceTopic", "Page", 3, childTopic); - var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic); + var childTopic = TopicFactory.Create("ChildTopic", "Page", parentTopic, 5); + var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", childTopic, 20); + var grandNieceTopic = TopicFactory.Create("GrandNieceTopic", "Page", childTopic, 3); + var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", grandChildTopic, 7); grandChildTopic.Attributes.SetValue("Foo", "Baz"); greatGrandChildTopic.Attributes.SetValue("Foo", "Bar"); @@ -79,9 +79,9 @@ public void FindAllByAttribute_ReturnsCorrectTopics() { public void FindFirstParent_ReturnsCorrectTopic() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic); - var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic); - var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic); + var childTopic = TopicFactory.Create("ChildTopic", "Page", parentTopic, 5); + var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", childTopic, 20); + var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", grandChildTopic, 7); var foundTopic = greatGrandChildTopic.FindFirstParent(t => t.Id is 5); @@ -99,9 +99,9 @@ public void FindFirstParent_ReturnsCorrectTopic() { public void GetRootTopic_ReturnsRootTopic() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic); - var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic); - var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic); + var childTopic = TopicFactory.Create("ChildTopic", "Page", parentTopic, 5); + var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", childTopic, 20); + var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", grandChildTopic, 7); var rootTopic = greatGrandChildTopic.GetRootTopic(); @@ -119,7 +119,7 @@ public void GetRootTopic_ReturnsRootTopic() { public void GetByUniqueKey_RootKey_ReturnsRootTopic() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - _ = TopicFactory.Create("ChildTopic", "Page", 2, parentTopic); + _ = TopicFactory.Create("ChildTopic", "Page", parentTopic, 2); var foundTopic = parentTopic.GetByUniqueKey("ParentTopic"); @@ -138,10 +138,10 @@ public void GetByUniqueKey_RootKey_ReturnsRootTopic() { public void GetByUniqueKey_ValidKey_ReturnsTopic() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic); - var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic); - var greatGrandChildTopic1 = TopicFactory.Create("GreatGrandChildTopic1", "Page", 7, grandChildTopic); - var greatGrandChildTopic2 = TopicFactory.Create("GreatGrandChildTopic2", "Page", 7, grandChildTopic); + var childTopic = TopicFactory.Create("ChildTopic", "Page", parentTopic, 5); + var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", childTopic, 20); + var greatGrandChildTopic1 = TopicFactory.Create("GreatGrandChildTopic1", "Page", grandChildTopic, 7); + var greatGrandChildTopic2 = TopicFactory.Create("GreatGrandChildTopic2", "Page", grandChildTopic, 7); var foundTopic = greatGrandChildTopic1.GetByUniqueKey("ParentTopic:ChildTopic:GrandChildTopic:GreatGrandChildTopic2"); @@ -160,9 +160,9 @@ public void GetByUniqueKey_ValidKey_ReturnsTopic() { public void GetByUniqueKey_InvalidKey_ReturnsNull() { var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1); - var childTopic = TopicFactory.Create("ChildTopic", "Page", 5, parentTopic); - var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", 20, childTopic); - var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", 7, grandChildTopic); + var childTopic = TopicFactory.Create("ChildTopic", "Page", parentTopic, 5); + var grandChildTopic = TopicFactory.Create("GrandChildTopic", "Page", childTopic, 20); + var greatGrandChildTopic = TopicFactory.Create("GreatGrandChildTopic", "Page", grandChildTopic, 7); var foundTopic = greatGrandChildTopic.GetByUniqueKey("ParentTopic:ChildTopic:GrandChildTopic:GreatGrandChildTopic2"); From dc110bd1eb1b172e92148b4ab6b6c68ee9a738ab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:34:31 -0800 Subject: [PATCH 521/778] Updated `ITypeLookupService.Lookup()` to accept a `params string[]` Previously, the `ITypeLookupService`'s `Lookup()` method required a single `string typeName` parameter. Now, that has been updated to a `params string[] typeNames` parameter. This allows a prioritize list of fallbacks to be supplied in order to support multiple acepted naming conventions and/or acceptable defaults. The first match will be returned. This will break all `ITypeLookupServices`. The following commits will resolve these issues in the OnTopic library. External libraries, such as the OnTopic Editor, as well as third-party implementations will need to be updated to reflect this change. --- OnTopic/Lookup/ITypeLookupService.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/OnTopic/Lookup/ITypeLookupService.cs b/OnTopic/Lookup/ITypeLookupService.cs index 5f991612..c9db5035 100644 --- a/OnTopic/Lookup/ITypeLookupService.cs +++ b/OnTopic/Lookup/ITypeLookupService.cs @@ -27,10 +27,21 @@ public interface ITypeLookupService { | METHOD: LOOKUP \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets the requested . + /// Attempts to retrieve a based on one or more supplied . /// - /// The name of the to retrieve. - Type? Lookup(string typeName); + /// + /// No matter how many are entered, at most one will be returned. Each + /// subsequent is treated as a fallback in case the previous one cannot be located. As such, + /// the offers a way for callers to provide a prioritized list of fallbacks. This is useful + /// for scenarios where there are multiple accepted naming conventions, or there's a global default that can be accepted. + /// + /// + /// The name of the to retrieve. If multiple names are supplied, then the first match is returned. + /// + /// + /// A corresponding to one of the specified , if available. + /// + Type? Lookup(params string[] typeNames); } //Class } //Namespace \ No newline at end of file From 7cd0ee3da02a2ab1764d064b925f0d4fdfa84f4c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:37:32 -0800 Subject: [PATCH 522/778] Updated `StaticTypeLookupService` to loop through `typeNames` With the update of the `ITypeLookupService.Lookup()` to expect `params string[] typeNames` instead of `string typeName`, the base implementation of the `StaticTypeLookupService`'s `Lookup()` method has been updated to loop through the `typeNames` collection and return the first match, if available. Since most `ITypeLookupService` implementations derive from `StaticTypeLookupService` and rely on its base implementation of `Lookup()`, this provides a base implementation for most type lookup services. --- OnTopic/Lookup/StaticTypeLookupService.cs | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index 8c09fb5c..780b7a37 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -71,20 +71,17 @@ public StaticTypeLookupService( /*========================================================================================================================== | METHOD: LOOKUP \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Retrieves a from the class based on its string representation. - /// - /// A string representing the type. - /// A class type corresponding to the specified string. - /// - /// !String.IsNullOrWhiteSpace(contentType) - /// - /// - /// !contentType.Contains(" ") - /// - public virtual Type? Lookup(string typeName) => Contains(typeName) ? _typeCollection[typeName] : DefaultType; + /// + public virtual Type? Lookup(params string[] typeNames) { + if (typeNames is not null) { + foreach (var typeName in typeNames) { + if (Contains(typeName)) { + return _typeCollection[typeName]; + } + } + } + return null; + } /*========================================================================================================================== | METHOD: ADD From 2ca3f1a0fd8835f55dd987da460cd2eceaf472a4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:43:11 -0800 Subject: [PATCH 523/778] Removed the `StaticTypeLookupService.DefaultType` Previously, the `StaticTypeLookupService` offered a `DefaultType` property which allowed each implementation to have a default fallback established that would be returned if the supplied `typeName` isn't found. In practice, this often led to confusion, since implementations didn't know what that `DefaultType` was, and had no way to access it off the `ITypeLookupService` abstraction and, therefore, had to evaluate the result against their input to determine if a concrete match was returned or not. Since we can now specify alternative or fallback types via the `Lookup()` method, there's no need for the `DefaultType` anymore. Instead, the preferene is for callers to specify a default type that's acceptable to them as the last argument in their `typeNames`. This provides more transparency, flexibility, and control to the callers, and helps ensure that there aren't unexpected consequences when swapping out `ITypeLookupService` implementations. This breaks all derived classes. Those that are part of the OnTopic library will be updated in subsequent commits. External libraries, such as the OnTopic Editor, and third-party implementations may need to update any `ITypeLookupService` implementations they have to reflect this change in the constructor. --- OnTopic/Lookup/StaticTypeLookupService.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index 780b7a37..3d93bc4a 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -36,17 +36,10 @@ public class StaticTypeLookupService: ITypeLookupService { /// cref="MemberInfo.Name"/>; if they are not, they will be removed. /// /// The list of instances to expose as part of this service. - /// The default type to return if no match can be found. Defaults to object. public StaticTypeLookupService( - IEnumerable? types = null, - Type? defaultType = null + IEnumerable? types = null ) { - /*------------------------------------------------------------------------------------------------------------------------ - | Set default type - \-----------------------------------------------------------------------------------------------------------------------*/ - DefaultType = defaultType; - /*------------------------------------------------------------------------------------------------------------------------ | Populate collection \-----------------------------------------------------------------------------------------------------------------------*/ @@ -60,14 +53,6 @@ public StaticTypeLookupService( } - /*========================================================================================================================== - | PROPERTY: DEFAULT TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// The default type to return in case cannot find a match. - /// - public Type? DefaultType { get; } - /*========================================================================================================================== | METHOD: LOOKUP \-------------------------------------------------------------------------------------------------------------------------*/ From 8a9a3bcf4d8986f733deed3cc597709c5cb2a9c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:46:49 -0800 Subject: [PATCH 524/778] Updated `StaticTypeLookupService` derivatives `base()` constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the change of the `StaticTypeLookupService`'s constructor to no longer accept an optional `defaultType` parameter, each derivative implementation must be updated to remove this `defaultType`. If fallbacks are necessary, they will need to be accounted for in calling code—which will be implemented for the OnTopic Library in subsequent updates. --- OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs | 2 +- OnTopic.ViewModels/TopicViewModelLookupService.cs | 4 +--- OnTopic/Lookup/DefaultTopicLookupService.cs | 4 +--- OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs | 5 +---- OnTopic/Lookup/DynamicTopicLookupService.cs | 3 +-- OnTopic/Lookup/DynamicTopicViewModelLookupService.cs | 3 +-- OnTopic/Lookup/DynamicTypeLookupService.cs | 3 +-- 7 files changed, 7 insertions(+), 17 deletions(-) diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs index 59e961f2..190a26b5 100644 --- a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs +++ b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs @@ -28,7 +28,7 @@ public class FakeViewModelLookupService: TopicViewModelLookupService { /// Instantiates a new instance of the . /// /// A new instance of the . - public FakeViewModelLookupService() : base(null, typeof(object)) { + public FakeViewModelLookupService() : base() { /*------------------------------------------------------------------------------------------------------------------------ | Add test specific view models diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index fc8b76a6..d4c6969b 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -30,9 +30,7 @@ public class TopicViewModelLookupService : StaticTypeLookupService { /// cref="MemberInfo.Name"/>; if they are not, they will be removed. /// /// The list of instances to expose as part of this service. - /// The default type to return if no match can be found. Defaults to object. - public TopicViewModelLookupService(IEnumerable? types = null, Type? defaultType = null) : - base(types, defaultType?? typeof(TopicViewModel)) { + public TopicViewModelLookupService(IEnumerable? types = null) : base(types) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure local view models are accounted for diff --git a/OnTopic/Lookup/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs index 99d37256..d81e1499 100644 --- a/OnTopic/Lookup/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -30,9 +30,7 @@ public class DefaultTopicLookupService: StaticTypeLookupService { /// cref="MemberInfo.Name"/>; if they are not, they will be removed. /// /// The list of instances to expose as part of this service. - /// The default type to return if no match can be found. Defaults to object. - public DefaultTopicLookupService(IEnumerable? types = null, Type? defaultType = null) : - base(types, defaultType?? typeof(Topic)) { + public DefaultTopicLookupService(IEnumerable? types = null) : base(types) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure editor types are accounted for diff --git a/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs index 548153d9..a723b177 100644 --- a/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicBindingModelLookupService.cs @@ -23,10 +23,7 @@ public class DynamicTopicBindingModelLookupService : DynamicTypeLookupService { /// /// Establishes a new instance of a . /// - public DynamicTopicBindingModelLookupService() : base( - t => typeof(ITopicBindingModel).IsAssignableFrom(t), - typeof(object) - ) { } + public DynamicTopicBindingModelLookupService() : base(t => typeof(ITopicBindingModel).IsAssignableFrom(t)) { } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Lookup/DynamicTopicLookupService.cs b/OnTopic/Lookup/DynamicTopicLookupService.cs index a5d975c6..313632cc 100644 --- a/OnTopic/Lookup/DynamicTopicLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicLookupService.cs @@ -24,8 +24,7 @@ public class DynamicTopicLookupService : DynamicTypeLookupService { /// Establishes a new instance of a . ///
public DynamicTopicLookupService() : base( - t => typeof(Topic).IsAssignableFrom(t), - typeof(Topic) + t => typeof(Topic).IsAssignableFrom(t) ) { } /*========================================================================================================================== diff --git a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index d4b81cd3..c134b652 100644 --- a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -24,8 +24,7 @@ public class DynamicTopicViewModelLookupService : DynamicTypeLookupService { /// Establishes a new instance of a . /// public DynamicTopicViewModelLookupService() : base( - t => t.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase), - typeof(object) + t => t.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) ) { } /*========================================================================================================================== diff --git a/OnTopic/Lookup/DynamicTypeLookupService.cs b/OnTopic/Lookup/DynamicTypeLookupService.cs index 7f05fbeb..59e4e8cd 100644 --- a/OnTopic/Lookup/DynamicTypeLookupService.cs +++ b/OnTopic/Lookup/DynamicTypeLookupService.cs @@ -25,8 +25,7 @@ public class DynamicTypeLookupService : StaticTypeLookupService { /// optionally, a default object to return if none is specified. /// /// The search condition to use to identify target classes. - /// The default type to return if no match can be found. Defaults to object. - public DynamicTypeLookupService(Func predicate, Type? defaultType = null) : base(null, defaultType) { + public DynamicTypeLookupService(Func predicate) : base() { /*------------------------------------------------------------------------------------------------------------------------ | Find target classes From fd8ef0f9ac02dfdce830db6ea8d295e0744161a7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:53:27 -0800 Subject: [PATCH 525/778] Moved fallback logic for `TopicLookupService`s to `TopicFactory` With the preference against relying on the `ITypeLookupService` implementations to handle fallback and defaults, the `TopicFactory` has been updated to handle the `AttributeDescriptor` alternative and `Topic` default, instead of relying on the `DefaultTopicLookupService` or the `DynamicTopicLookupService` to handle this. Positively, this centralizes the `AttributeDescriptor` fallback logic, which was previously repeated in the `DefaultTopicLookupService` as well as the `DynamicTopicLookupService`, since they didn't have a common topic-specific base class. Technically, this implementation _could_ have utilized the new `Lookup(params)` argument by specifying these defaults in the arguments list. Since they're well-known types, however, and are never expected to be replaced, the code is a bit more readable in this format, which consolidates the fallback with the necessary null check. --- OnTopic/Lookup/DefaultTopicLookupService.cs | 25 -------------------- OnTopic/Lookup/DynamicTopicLookupService.cs | 26 --------------------- OnTopic/TopicFactory.cs | 16 ++++++++----- 3 files changed, 10 insertions(+), 57 deletions(-) diff --git a/OnTopic/Lookup/DefaultTopicLookupService.cs b/OnTopic/Lookup/DefaultTopicLookupService.cs index d81e1499..fdb9f061 100644 --- a/OnTopic/Lookup/DefaultTopicLookupService.cs +++ b/OnTopic/Lookup/DefaultTopicLookupService.cs @@ -40,30 +40,5 @@ public DefaultTopicLookupService(IEnumerable? types = null) : base(types) } - /*========================================================================================================================== - | METHOD: LOOKUP - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// - /// The version of will automatically fall back to - /// if the ends with AttributeDescriptor, but a - /// with the specified name cannot be found. This accounts for the fact that strongly - /// typed classes are expected to be in external plugins which are not statically - /// registered with the . In that case, the base - /// class will provide access to the attributes needed by most applications, including the core OnTopic library. - /// - public override Type? Lookup(string typeName) { - if (typeName is null) { - return DefaultType; - } - else if (Contains(typeName)) { - return base.Lookup(typeName); - } - else if (typeName.EndsWith("AttributeDescriptor", StringComparison.OrdinalIgnoreCase)) { - return typeof(AttributeDescriptor); - } - return DefaultType; - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Lookup/DynamicTopicLookupService.cs b/OnTopic/Lookup/DynamicTopicLookupService.cs index 313632cc..4870926c 100644 --- a/OnTopic/Lookup/DynamicTopicLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicLookupService.cs @@ -27,31 +27,5 @@ public DynamicTopicLookupService() : base( t => typeof(Topic).IsAssignableFrom(t) ) { } - /*========================================================================================================================== - | METHOD: LOOKUP - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// - /// The version of will automatically fall back to - /// if the ends with AttributeDescriptor, but a - /// with the specified name cannot be found. This accounts for the fact that strongly - /// typed classes are expected to be in external plugins which may not be available - /// unless the current application is configured to use the OnTopic Editor. In that case, the base class will provide access to the attributes needed by most applications, including the core - /// OnTopic library. - /// - public override Type? Lookup(string typeName) { - if (typeName is null) { - return DefaultType; - } - else if (Contains(typeName)) { - return base.Lookup(typeName); - } - else if (typeName.EndsWith("AttributeDescriptor", StringComparison.OrdinalIgnoreCase)) { - return typeof(AttributeDescriptor); - } - return DefaultType; - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index cf3a6922..ac637d2e 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -8,6 +8,7 @@ using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; +using OnTopic.Metadata; namespace OnTopic { @@ -63,13 +64,16 @@ public static Topic Create(string key, string contentType, Topic? parent = null, /*------------------------------------------------------------------------------------------------------------------------ | Determine target type \-----------------------------------------------------------------------------------------------------------------------*/ - var targetType = TypeLookupService.Lookup(contentType); + var targetType = TypeLookupService.Lookup(contentType); - Contract.Assume( - targetType, - $"The content type {contentType} could not be located in the ITypeLookupService, and no fallback could be " + - $"identified." - ); + //Fallback to generic AttributeDescriptor if the specific attribute descriptor cannot be found + if (targetType is null && contentType.EndsWith("AttributeDescriptor", StringComparison.OrdinalIgnoreCase)) { + targetType = typeof(AttributeDescriptor); + } + + if (targetType is null) { + targetType = typeof(Topic); + } /*------------------------------------------------------------------------------------------------------------------------ | Identify the appropriate topic From 789228a566bb245ec2bac8dd3245431cd688e1de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 15:56:57 -0800 Subject: [PATCH 526/778] Moved fallback logic for `TopicViewModel`s to `TopicMappingService` With the preference against relying on the `ITypeLookupService` implementations to handle fallback and defaults, the `TopicMappingService` has been updated to handle the `ViewModel` alternative, instead of relying on the `DynamicTopicViewModelLookupService` or the `TopicViewModelLookupService` to handle this. Positively, this centralizes the `ViewModel` fallback logic, which was previously repeated in the two implementations, since they didn't have a common view model specific base class. This also allows us to simplify the detection of a miss, since we don't need to contend with the expectation of a default value being returned. (Technically, an `ITypeLookupService` could still opt to do this, but that is recommended against.) --- .../TopicViewModelLookupService.cs | 27 ------------------- .../DynamicTopicViewModelLookupService.cs | 26 ------------------ OnTopic/Mapping/TopicMappingService.cs | 4 +-- 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index d4c6969b..b7a8cf0f 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -60,32 +60,5 @@ public TopicViewModelLookupService(IEnumerable? types = null) : base(types } - /*========================================================================================================================== - | METHOD: LOOKUP - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// - /// The version of will first look for - /// the exact . If that fails, and the ends with TopicViewModel - /// , then it will automatically fall back to look for the with the ViewModel - /// suffix. If that cannot be found, it will return the of . This allows implementors to use the shorter name, if preferred, without breaking compatibility with - /// implementations which default to looking for TopicViewModel, such as the . - /// While this convention is not used by the , this fallback provides support for derived classes - /// which may prefer that convention. - /// - public override Type? Lookup(string typeName) { - if (typeName is null) { - return DefaultType; - } - else if (Contains(typeName)) { - return base.Lookup(typeName); - } - else if (typeName.EndsWith("TopicViewModel", StringComparison.OrdinalIgnoreCase)) { - return base.Lookup(typeName.Replace("TopicViewModel", "ViewModel", StringComparison.CurrentCultureIgnoreCase)); - } - return DefaultType; - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs index c134b652..dc64b9e1 100644 --- a/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs +++ b/OnTopic/Lookup/DynamicTopicViewModelLookupService.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Mapping; namespace OnTopic.Lookup { @@ -27,30 +26,5 @@ public DynamicTopicViewModelLookupService() : base( t => t.Name.EndsWith("ViewModel", StringComparison.OrdinalIgnoreCase) ) { } - /*========================================================================================================================== - | METHOD: LOOKUP - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// - /// The version of will first look for - /// the exact . If that fails, and the ends with TopicViewModel - /// , then it will automatically fall back to look for the with the ViewModel - /// suffix. If that cannot be found, it will return the of . This allows implementors to use the shorter name, if preferred, without breaking compatibility with - /// implementations which default to looking for TopicViewModel, such as the . - /// - public override Type? Lookup(string typeName) { - if (typeName is null) { - return DefaultType; - } - else if (Contains(typeName)) { - return base.Lookup(typeName); - } - else if (typeName.EndsWith("TopicViewModel", StringComparison.OrdinalIgnoreCase)) { - return base.Lookup(typeName.Replace("TopicViewModel", "ViewModel", StringComparison.CurrentCultureIgnoreCase)); - } - return DefaultType; - } - } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index e49836b8..5c23290c 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -118,9 +118,9 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService \-----------------------------------------------------------------------------------------------------------------------*/ else { - var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel"); + var viewModelType = _typeLookupService.Lookup($"{topic.ContentType}TopicViewModel", $"{topic.ContentType}ViewModel"); - if (viewModelType is null || !viewModelType.Name.EndsWith("TopicViewModel", StringComparison.CurrentCultureIgnoreCase)) { + if (viewModelType is null) { throw new InvalidTypeException( $"No class named '{topic.ContentType}TopicViewModel' could be located in any loaded assemblies. This is required " + $"to map the topic '{topic.GetUniqueKey()}'." From e5879982fff54076f2b5cede0447cf34c7d4602a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 16:00:32 -0800 Subject: [PATCH 527/778] Updated `CompositeTypeLookupService` to loop through `typeNames` The `CompositeTypeLookupService` needs to use care when working with the `params string[] typeNames` array. A naive implementation might simply relay the `typeNames` to the `Lookup()` method of each underlying `ITypeLookupService` implementation. That would result in prioritizing the fallbacks, however, in the case that the first implementation couldn't find a prioritized match. Instead, it must loop through each `typeName` and pass it to each `Lookup()` method, thus ensuring that the priority of both the `typeNames` as well as the `ITypeLookupService` implementations is honored. --- OnTopic/Lookup/CompositeTypeLookupService.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/OnTopic/Lookup/CompositeTypeLookupService.cs b/OnTopic/Lookup/CompositeTypeLookupService.cs index 18ed8e8d..518d14be 100644 --- a/OnTopic/Lookup/CompositeTypeLookupService.cs +++ b/OnTopic/Lookup/CompositeTypeLookupService.cs @@ -52,12 +52,16 @@ public CompositeTypeLookupService(params ITypeLookupService[] typeLookupServices | METHOD: LOOKUP \-------------------------------------------------------------------------------------------------------------------------*/ /// - public Type? Lookup(string typeName) { - var type = typeof(Object); - foreach (var typeLookupService in _typeLookupServices) { - type = typeLookupService.Lookup(typeName); - if (type is not null && type.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)) { - return type; + public Type? Lookup(params string[] typeNames) { + var type = typeof(object); + if (typeNames is not null) { + foreach (var typeName in typeNames) { + foreach (var typeLookupService in _typeLookupServices) { + type = typeLookupService.Lookup(typeName); + if (type is not null && type.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)) { + return type; + } + } } } //Default to default return type of last query From c12e2d76c4fa91df8ed6726e100d47dd63272533 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 16:01:08 -0800 Subject: [PATCH 528/778] Updated XML doc to reflect new `Lookup(params)` signature --- OnTopic.Tests/ITypeLookupServiceTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index bd25bc95..8da4511d 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -28,7 +28,8 @@ public class ITypeLookupServiceTest { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with two instances of a and - /// confirms that it returns the expected for a query. + /// confirms that it returns the expected for a + /// query. /// [TestMethod] public void Composite_LookupValidType_ReturnsType() { From 7f8f61634abd06d9a815c731f411438ce2c39ca9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 16:05:11 -0800 Subject: [PATCH 529/778] Removed check for `DefaultType` The `StaticTypeLookupService` no longer has a `DefaultType` (2ca3f1a), and is no longer expected to return a default of `object` if the supplied `typeName(s)` cannot be found. Given that, the `Assert()` that checks this now looks for a default of `null`. --- OnTopic.Tests/ITypeLookupServiceTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 8da4511d..a6d20922 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -40,7 +40,7 @@ public void Composite_LookupValidType_ReturnsType() { Assert.AreEqual(typeof(SlideshowTopicViewModel), compositeLookup.Lookup(nameof(SlideshowTopicViewModel))); Assert.AreEqual(typeof(MapToParentTopicViewModel), compositeLookup.Lookup(nameof(MapToParentTopicViewModel))); - Assert.AreEqual(typeof(Object), compositeLookup.Lookup(nameof(Topic))); + Assert.AreEqual(null, compositeLookup.Lookup(nameof(Topic))); } From 27e1ec071484beedd57a63c7c65f0b4a5f3c7c47 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 16:09:29 -0800 Subject: [PATCH 530/778] Evaluate fallbacks using new `params string[] typeNames` Previously, fallbacks were evaluated using built-in fallback logic in the concrete `ITypeLookupService` implementation. This is a confusing approach and prone to introducing not only repetitive logic (between implementations) but also errors when swapping out implementations. To mitigate that, this is now controlled specifically by the caller when doing a `Lookup()`, by specifying any acceptable alternatives or defaults as part of the `params` list. These unit tests are updated to reflect that. --- OnTopic.Tests/ITypeLookupServiceTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index a6d20922..482e8adb 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -72,7 +72,7 @@ public void DynamicTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAt public void DynamicTopicViewModelLookupService_LookupTopicViewModel_ReturnsFallbackViewModel() { var lookupService = new DynamicTopicViewModelLookupService(); - var topicViewModel = lookupService.Lookup("FallbackTopicViewModel"); + var topicViewModel = lookupService.Lookup("FallbackTopicViewModel", "FallbackViewModel"); Assert.AreEqual(typeof(FallbackViewModel), topicViewModel); @@ -106,7 +106,7 @@ public void DefaultTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAt public void TopicViewModelLookupService_LookupTopicViewModel_ReturnsFallbackViewModel() { var lookupService = new FakeViewModelLookupService(); - var topicViewModel = lookupService.Lookup("FallbackTopicViewModel"); + var topicViewModel = lookupService.Lookup("FallbackTopicViewModel", "FallbackViewModel"); Assert.AreEqual(typeof(FallbackViewModel), topicViewModel); From 33132b968f1c5310052c1b321826c60572b55af8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 29 Jan 2021 16:09:47 -0800 Subject: [PATCH 531/778] Remove unit tests evaluating `AttributeDescriptor` fallback This fallback is now handled by explicitly passing the alternative into the `Lookup()` service, which is effectively tested by other tests (27e1ec0). --- OnTopic.Tests/ITypeLookupServiceTest.cs | 34 ------------------------- 1 file changed, 34 deletions(-) diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index 482e8adb..d21bc291 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -44,23 +44,6 @@ public void Composite_LookupValidType_ReturnsType() { } - /*========================================================================================================================== - | TEST: DYNAMIC TOPIC LOOKUP SERVICE: LOOKUP MISSING ATTRIBUTE DESCRIPTOR: RETURNS ATTRIBUTE DESCRIPTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Assembles a new and requests a missing attribute type; confirms it correctly - /// falls back to the expected as a logical default. - /// - [TestMethod] - public void DynamicTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAttributeDescriptor() { - - var lookupService = new DynamicTopicLookupService(); - var attributeType = lookupService.Lookup("ArbitraryAttributeDescriptor"); - - Assert.AreEqual(typeof(AttributeDescriptor), attributeType); - - } - /*========================================================================================================================== | TEST: DYNAMIC TOPIC VIEW MODEL LOOKUP SERVICE: LOOKUP TOPIC VIEW MODEL: RETURNS FALLBACK VIEW MODEL \-------------------------------------------------------------------------------------------------------------------------*/ @@ -78,23 +61,6 @@ public void DynamicTopicViewModelLookupService_LookupTopicViewModel_ReturnsFallb } - /*========================================================================================================================== - | TEST: DEFAULT TOPIC LOOKUP SERVICE: LOOKUP MISSING ATTRIBUTE DESCRIPTOR: RETURNS ATTRIBUTE DESCRIPTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Assembles a new and requests a missing attribute type; confirms it correctly - /// falls back to the expected as a logical default. - /// - [TestMethod] - public void DefaultTopicLookupService_LookupMissingAttributeDescriptor_ReturnsAttributeDescriptor() { - - var lookupService = new DefaultTopicLookupService(); - var attributeType = lookupService.Lookup("ArbitraryAttributeDescriptor"); - - Assert.AreEqual(typeof(AttributeDescriptor), attributeType); - - } - /*========================================================================================================================== | TEST: DEFAULT TOPIC VIEW MODEL LOOKUP SERVICE: LOOKUP TOPIC VIEW MODEL: RETURNS FALLBACK VIEW MODEL \-------------------------------------------------------------------------------------------------------------------------*/ From 1006a2baa88430f0e514daa20120207b5479f943 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 11:35:35 -0800 Subject: [PATCH 532/778] Renamed `DerivedTopic` to `BaseTopic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The name `DerivedTopic` doesn't make semantic sense. It points to the `Topic` that the current `Topic` is derived _from_, but the name suggests the opposite—that it refers to a `Topic` that that derives from the _current_ `Topic`. This was previously addressed in the OnTopic Editor by labeling this attribute as "Inherited Topic". But even that is a bit ambiguous, and used sometimes to refer to the base class and other times to refer to the derived class. In the meanwhile, the code has continued to use `TopicDescriptor` for backward compatibility. With so many breaking changes queued up for the OnTopic 5.0.0 release, this is a good opportunity to finally resolve this nomenclature. While, technically, `InheritedTopic` should be accurate, the inconsistency of its use doesn't make it a marked improvement over `DerivedTopic`. Given that, we're going with `BaseTopic`—which isn't ideal, but is semantically unambiguous. (**Note:** Microsoft often uses a property called "Base" for these circumstances. That itself is a bit confusing since the `base` keyword refers to the superclass, but the `Base` class refers to an instance whose values are inherited, not a class whose members are inherited. The purpose of `BaseTopic` is to help clarify this.) This will almost certainly break downstream libraries and implementations. This is particularly true of the OnTopic Editor, which has special handling for the inherited topic. This also breaks the actual database implementation, and necessitates that existing topic references have their names updated; that will be handled via the database migration script in a subsequent commit. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic.Tests/AttributeValueCollectionTest.cs | 10 ++-- .../InvalidReferenceTypeTopicBindingModel.cs | 2 +- .../ReferenceTopicBindingModel.cs | 2 +- .../ReverseTopicMappingServiceTest.cs | 6 +-- OnTopic.Tests/TopicReferenceDictionaryTest.cs | 44 ++++++++--------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 10 ++-- OnTopic.Tests/TopicTest.cs | 45 +++++++++--------- .../Attributes/AttributeValueCollection.cs | 8 ++-- .../AttributeValueCollectionExtensions.cs | 12 ++--- .../Reflection/TopicPropertyDispatcher.cs | 2 +- OnTopic/Mapping/Annotations/Relationships.cs | 2 +- OnTopic/Metadata/ModelType.cs | 2 +- .../References/ReferenceSetterAttribute.cs | 6 +-- .../References/TopicReferenceDictionary.cs | 2 +- .../ReferentialIntegrityException.cs | 2 +- OnTopic/Repositories/TopicRepository.cs | 12 ++--- OnTopic/Topic.cs | 47 ++++++++++--------- 19 files changed, 111 insertions(+), 107 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 31b1f42a..64805cee 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -54,7 +54,7 @@ internal static class SqlDataReaderExtensions { /// /// /// Optionally disables populating external references such as and . This is useful for cases where it's known that a shallow copy is being retrieved, and + /// cref="Topic.BaseTopic"/>. This is useful for cases where it's known that a shallow copy is being retrieved, and /// thus external references aren't likely to be available. /// internal static Topic? LoadTopicGraph( diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 8df76359..1797d200 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -197,7 +197,7 @@ private Topic CreateFakeData() { addAttribute(contentTypes, "Key", "TextAttributeDescriptor", false, true); addAttribute(contentTypes, "ContentType", "TextAttributeDescriptor", false, true); addAttribute(contentTypes, "Title", "TextAttributeDescriptor", true, true); - addAttribute(contentTypes, "DerivedTopic", "TopicReferenceAttributeDescriptor", false); + addAttribute(contentTypes, "BaseTopic", "TopicReferenceAttributeDescriptor", false); var contentTypeDescriptor = TopicFactory.Create("ContentTypeDescriptor", "ContentTypeDescriptor", contentTypes); diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 5fe524b6..10374eb0 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -712,19 +712,19 @@ public void GetValue_InheritFromParent_ReturnsParentValue() { } /*========================================================================================================================== - | TEST: GET VALUE: INHERIT FROM DERIVED: RETURNS DERIVED VALUE + | TEST: GET VALUE: INHERIT FROM BASE: RETURNS INHERITED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a long tree of derives topics, and ensures that the derived value is returned. + /// Establishes a long tree of derived topics, and ensures that the inherited value is returned. /// [TestMethod] - public void GetValue_InheritFromDerived_ReturnsDerivedValue() { + public void GetValue_InheritFromBase_ReturnsInheritedValue() { var topics = new Topic[5]; for (var i = 0; i <= 4; i++) { var topic = TopicFactory.Create("Topic" + i, "Container"); - if (i > 0) topics[i - 1].DerivedTopic = topic; + if (i > 0) topics[i - 1].BaseTopic = topic; topics[i] = topic; } @@ -747,7 +747,7 @@ public void GetValue_ExceedsMaxHops_ReturnsDefault() { for (var i = 0; i <= 7; i++) { var topic = TopicFactory.Create("Topic" + i, "Container"); - if (i > 0) topics[i - 1].DerivedTopic = topic; + if (i > 0) topics[i - 1].BaseTopic = topic; topics[i] = topic; } diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs index 4003cea8..c8f0e658 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs @@ -24,7 +24,7 @@ public class InvalidReferenceTypeTopicBindingModel : BasicTopicBindingModel { public InvalidReferenceTypeTopicBindingModel(string? key = null) : base(key, "Page") { } - public TopicViewModel DerivedTopic { get; } = new(); + public TopicViewModel BaseTopic { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index 60f186fd..64f79c40 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -22,7 +22,7 @@ public class ReferenceTopicBindingModel : BasicTopicBindingModel { public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttributeDescriptor") { } - public RelatedTopicBindingModel? DerivedTopic { get; set; } + public RelatedTopicBindingModel? BaseTopic { get; set; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index e0a55c67..dbdc8b6d 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -290,15 +290,15 @@ public async Task Map_TopicReferences_ReturnsMappedTopic() { var mappingService = new ReverseTopicMappingService(_topicRepository); var bindingModel = new ReferenceTopicBindingModel("Test") { - DerivedTopic = new() { + BaseTopic = new() { UniqueKey = _topicRepository.Load("Root:Configuration:ContentTypes:Attributes:Title").GetUniqueKey() } }; var target = (TopicReferenceAttributeDescriptor?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false); - Assert.IsNotNull(target.DerivedTopic); - Assert.AreEqual("Title", target.DerivedTopic.Key); + Assert.IsNotNull(target.BaseTopic); + Assert.AreEqual("Title", target.BaseTopic.Key); Assert.AreEqual("TopicReference", target.EditorType); } diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceDictionaryTest.cs index d5292a3e..ef54a69a 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceDictionaryTest.cs @@ -253,68 +253,68 @@ public void GetTopic_MissingReference_ReturnsNull() { } /*========================================================================================================================== - | TEST: GET TOPIC: DERIVED REFERENCE: RETURNS TOPIC + | TEST: GET TOPIC: INHERITED REFERENCE: RETURNS TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that with a , adds a new reference to the , and confirms that correctly returns the related topic reference. /// [TestMethod] - public void GetTopic_DerivedReference_ReturnsTopic() { + public void GetTopic_InheritedReference_ReturnsTopic() { var topic = TopicFactory.Create("Topic", "Page"); - var derived = TopicFactory.Create("Derived", "Page"); + var baseTopic = TopicFactory.Create("Base", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.DerivedTopic = derived; - derived.References.Add("Reference", reference); + topic.BaseTopic = baseTopic; + baseTopic.References.Add("Reference", reference); Assert.AreEqual(reference, topic.References.GetTopic("Reference")); } /*========================================================================================================================== - | TEST: GET TOPIC: DERIVED REFERENCE: RETURNS NULL + | TEST: GET TOPIC: INHERITED REFERENCE: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that with a , adds a new reference to the , and confirms that correctly returns null if an incorrect referencedKey /// is entered. /// [TestMethod] - public void GetTopic_DerivedReference_ReturnsNull() { + public void GetTopic_InheritedReference_ReturnsNull() { var topic = TopicFactory.Create("Topic", "Page"); - var derived = TopicFactory.Create("Derived", "Page"); + var baseTopic = TopicFactory.Create("Base", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.DerivedTopic = derived; - derived.References.Add("Reference", reference); + topic.BaseTopic = baseTopic; + baseTopic.References.Add("Reference", reference); Assert.IsNull(topic.References.GetTopic("MissingReference")); } /*========================================================================================================================== - | TEST: GET TOPIC: DERIVED REFERENCE WITHOUT INHERIT: RETURNS NULL + | TEST: GET TOPIC: INHERITED REFERENCE WITHOUT INHERITANCE: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromDerived is + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromBase is /// set to false. /// [TestMethod] - public void GetTopic_DerivedReferenceWithoutInherit_ReturnsNull() { + public void GetTopic_InheritedReferenceWithoutInheritance_ReturnsNull() { var topic = TopicFactory.Create("Topic", "Page"); - var derived = TopicFactory.Create("Derived", "Page"); + var baseTopic = TopicFactory.Create("Base", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.DerivedTopic = derived; - derived.References.Add("Reference", reference); + topic.BaseTopic = baseTopic; + baseTopic.References.Add("Reference", reference); Assert.IsNull(topic.References.GetTopic("Reference", false)); diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index f661ceb3..abb29f78 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -50,21 +50,21 @@ public TopicRepositoryBaseTest() { } /*========================================================================================================================== - | TEST: DELETE: DERIVED TOPIC: THROWS EXCEPTION + | TEST: DELETE: BASE TOPIC: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Deletes a topic which other topics, outside of the graph, derive from. Expects exception. /// [TestMethod] [ExpectedException(typeof(ReferentialIntegrityException))] - public void Delete_DerivedTopic_ThrowsException() { + public void Delete_BaseTopic_ThrowsException() { var root = TopicFactory.Create("Root", "Page"); var topic = TopicFactory.Create("Topic", "Page", root); var child = TopicFactory.Create("Child", "Page", topic); - var derived = TopicFactory.Create("Derived", "Page", root); + var derivedTopic = TopicFactory.Create("Derived", "Page", root); - derived.DerivedTopic = child; + derivedTopic.BaseTopic = child; _topicRepository.Delete(topic, true); @@ -84,7 +84,7 @@ public void Delete_InternallyDerivedTopic_Succeeds() { var child = TopicFactory.Create("Child", "Page", topic); var derived = TopicFactory.Create("Derived", "Page", topic); - derived.DerivedTopic = child; + derived.BaseTopic = child; _topicRepository.Delete(topic, true); diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 6fb8a927..1e0f54b2 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -271,49 +271,48 @@ public void LastModified_UpdateValue_ReturnsExpectedValue() { } /*========================================================================================================================== - | TEST: DERIVED TOPIC: UPDATE VALUE: RETURNS EXPECTED VALUE + | TEST: BASE TOPIC: UPDATE VALUE: RETURNS EXPECTED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a derived topic to a topic entity, then replaces the references with a new topic entity. Ensures that both the - /// derived topic as well as the underlying correctly reference the new value. + /// Sets a base topic to a topic entity, then replaces the references with a new topic entity. Ensures that both the + /// base topic as well as the underlying correctly reference the new value. /// [TestMethod] - public void DerivedTopic_UpdateValue_ReturnsExpectedValue() { + public void BaseTopic_UpdateValue_ReturnsExpectedValue() { var topic = TopicFactory.Create("Topic", "Page"); - var firstDerivedTopic = TopicFactory.Create("DerivedTopic", "Page"); - var secondDerivedTopic = TopicFactory.Create("DerivedTopic", "Page", 1); - var finalDerivedTopic = TopicFactory.Create("DerivedTopic", "Page", 2); + var firstBaseTopic = TopicFactory.Create("BaseTopic", "Page"); + var secondBaseTopic = TopicFactory.Create("BaseTopic", "Page", 1); + var finalBaseTopic = TopicFactory.Create("BaseTopic", "Page", 2); - topic.DerivedTopic = firstDerivedTopic; - topic.DerivedTopic = secondDerivedTopic; - topic.DerivedTopic = finalDerivedTopic; + topic.BaseTopic = firstBaseTopic; + topic.BaseTopic = secondBaseTopic; + topic.BaseTopic = finalBaseTopic; - Assert.ReferenceEquals(topic.DerivedTopic, finalDerivedTopic); - Assert.AreEqual(2, topic.References.GetTopic("DerivedTopic").Id); + Assert.ReferenceEquals(topic.BaseTopic, finalBaseTopic); + Assert.AreEqual(2, topic.References.GetTopic("BaseTopic").Id); } /*========================================================================================================================== - | TEST: DERIVED TOPIC: RESAVED VALUE: RETURNS EXPECTED VALUE + | TEST: BASE TOPIC: RESAVED VALUE: RETURNS EXPECTED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a derived topic to an unsaved topic entity, then saves the entity and reestablishes the relationship. Ensures - /// that the derived topic is correctly set, including the as an underlying . + /// Sets a base topic to an unsaved topic entity, then saves the entity and reestablishes the relationship. Ensures + /// that the base topic is correctly set as a entry. /// [TestMethod] - public void DerivedTopic_ResavedValue_ReturnsExpectedValue() { + public void BaseTopic_ResavedValue_ReturnsExpectedValue() { var topic = TopicFactory.Create("Topic", "Page"); - var derivedTopic = TopicFactory.Create("DerivedTopic", "Page"); + var baseTopic = TopicFactory.Create("BaseTopic", "Page"); - topic.DerivedTopic = derivedTopic; - derivedTopic.Id = 5; - topic.DerivedTopic = derivedTopic; + topic.BaseTopic = baseTopic; + baseTopic.Id = 5; + topic.BaseTopic = baseTopic; - Assert.ReferenceEquals(topic.DerivedTopic, derivedTopic); - Assert.AreEqual(5, topic.References.GetTopic("DerivedTopic").Id); + Assert.ReferenceEquals(topic.BaseTopic, baseTopic); + Assert.AreEqual(5, topic.References.GetTopic("BaseTopic").Id); } diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index baa99be6..cc8e0277 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -197,7 +197,7 @@ public void MarkClean(string key, DateTime? version) { /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// /// - /// Boolean indicator nothing whether to search through any of the topic's topics in + /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. /// /// The string value for the Attribute. @@ -209,7 +209,7 @@ public void MarkClean(string key, DateTime? version) { /// /// Gets a named attribute from the Attributes dictionary with a specified default value and an optional number of - /// s through whom to crawl to retrieve an inherited value. + /// s through whom to crawl to retrieve an inherited value. /// /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. @@ -259,10 +259,10 @@ public void MarkClean(string key, DateTime? version) { \-----------------------------------------------------------------------------------------------------------------------*/ if ( String.IsNullOrEmpty(value) && - _associatedTopic.DerivedTopic is not null && + _associatedTopic.BaseTopic is not null && maxHops > 0 ) { - value = _associatedTopic.DerivedTopic.Attributes.GetValue(name, null, false, maxHops - 1); + value = _associatedTopic.BaseTopic.Attributes.GetValue(name, null, false, maxHops - 1); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index bddae70d..7eaac1ad 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -33,8 +33,8 @@ public static class AttributeValueCollectionExtensions { /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// /// - /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. + /// Boolean indicator nothing whether to search through any of the topic's s in order to get /// /// The value for the attribute as a boolean. public static bool GetBoolean( @@ -71,8 +71,8 @@ out var result /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// /// - /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. + /// Boolean indicator nothing whether to search through any of the topic's s in order to get /// /// The value for the attribute as an integer. public static int GetInteger( @@ -109,8 +109,8 @@ out var result /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// /// - /// Boolean indicator nothing whether to search through any of the topic's topics in - /// order to get the value. + /// Boolean indicator nothing whether to search through any of the topic's s in order to get + /// the value. /// /// The value for the attribute as a double. public static double GetDouble( @@ -147,8 +147,8 @@ out var result /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// /// - /// Boolean indicator nothing whether to search through any of the topic's topics in - /// order to get the value. + /// Boolean indicator nothing whether to search through any of the topic's s in order to get + /// the value. /// /// The value for the attribute as a DateTime object. public static DateTime GetDateTime( diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 71779630..27440b41 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -26,7 +26,7 @@ namespace OnTopic.Internal.Reflection { /// Collections on , such as and , aren't /// well-positioned to enforce attribute-specific business logic when adding or setting items in the collection. Instead, /// this logic is typically handled by property setters on , such as or . This introduces a potential backdoor, as updates made directly to the collection can + /// cref="Topic.BaseTopic"/>. This introduces a potential backdoor, as updates made directly to the collection can /// bypass any business logic—such as data validation or local state management—handled by those property setters. The /// class addresses this by allowing those collections /// to route requests through appropriately decorated properties on prior to adding or setting a diff --git a/OnTopic/Mapping/Annotations/Relationships.cs b/OnTopic/Mapping/Annotations/Relationships.cs index 1a2d5edc..628214a3 100644 --- a/OnTopic/Mapping/Annotations/Relationships.cs +++ b/OnTopic/Mapping/Annotations/Relationships.cs @@ -86,7 +86,7 @@ public enum Relationships { | REFERENCES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Map topic pointer references, such as . + /// Map topic pointer references, such as . /// /// /// By convention, types refer to a , /// A reference to a separate . References are ideally exposed and populated as strongly-typed - /// properties on —or a derived class—as done with e.g. . + /// properties on —or a derived class—as done with e.g. . /// Reference = 3, diff --git a/OnTopic/References/ReferenceSetterAttribute.cs b/OnTopic/References/ReferenceSetterAttribute.cs index 0abab0bc..322f9ee4 100644 --- a/OnTopic/References/ReferenceSetterAttribute.cs +++ b/OnTopic/References/ReferenceSetterAttribute.cs @@ -23,9 +23,9 @@ namespace OnTopic.References { /// business logic to be potentially bypassed by writing directly to the collection. /// /// - /// As an example, the property is adorned with the . As a result, if a client calls topic.References.SetTopic("DerivedTopic", topic), then that update - /// will be routed through , thus enforcing any validation. + /// As an example, the property is adorned with the . As a result, if a client calls topic.References.SetTopic("BaseTopic", topic), then that update + /// will be routed through , thus enforcing any validation. /// /// /// To ensure this logic, it is critical that implementers of ensure that the diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index ee18feb1..16c1c8f4 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -363,7 +363,7 @@ public bool Remove(string key) { return existing; } else if (inheritFromDerived) { - return _parent.DerivedTopic?.References.GetTopic(key); + return _parent.BaseTopic?.References.GetTopic(key); } return null; } diff --git a/OnTopic/Repositories/ReferentialIntegrityException.cs b/OnTopic/Repositories/ReferentialIntegrityException.cs index 3046cc47..817b3dc0 100644 --- a/OnTopic/Repositories/ReferentialIntegrityException.cs +++ b/OnTopic/Repositories/ReferentialIntegrityException.cs @@ -18,7 +18,7 @@ namespace OnTopic.Repositories { /// /// /// Generally, the will occur when a is being - /// deleted, but the topic or one of its descendents is the of another . + /// deleted, but the topic or one of its descendents is the of another . /// In that case, deleting the topic will violate the referential integrity of the target topic. /// [Serializable] diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 54159fa9..a7eb9d27 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -303,7 +303,7 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv /// /// /// When recursively saving a topic graph, it is conceivable that references to other topics—such as or —can't yet be persisted because the target or —can't yet be persisted because the target hasn't yet been saved, and thus the is still set to -1. To mitigate /// this, the allows this private overload to keep track of unresolved /// relationships. The public overload uses this list to resave any topics @@ -558,16 +558,16 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi } /*------------------------------------------------------------------------------------------------------------------------ - | Validate derived topics + | Validate base topics \-----------------------------------------------------------------------------------------------------------------------*/ var childTopics = topic.FindAll(); var allTopics = topic.GetRootTopic().FindAll().Except(childTopics); - var derivedTopic = allTopics.FirstOrDefault(t => t.DerivedTopic is not null && childTopics.Contains(t.DerivedTopic)); + var baseTopic = allTopics.FirstOrDefault(t => t.BaseTopic is not null && childTopics.Contains(t.BaseTopic)); - if (derivedTopic is not null) { + if (baseTopic is not null) { throw new ReferentialIntegrityException( - $"The topic '{topic.GetUniqueKey()}' cannot be deleted. The topic '{derivedTopic.GetUniqueKey()}' derives from the " + - $"topic '{derivedTopic.DerivedTopic!.GetUniqueKey()}'. Deleting this would cause violate the integrity of the " + + $"The topic '{topic.GetUniqueKey()}' cannot be deleted. The topic '{baseTopic.GetUniqueKey()}' derives from the " + + $"topic '{baseTopic.BaseTopic!.GetUniqueKey()}'. Deleting this would cause violate the integrity of the " + $"persistence store." ); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index bb2dba74..cdd77dce 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -10,6 +10,7 @@ using System.Linq; using OnTopic.Attributes; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.References; @@ -610,45 +611,49 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) #region Relationship and Collection Properties /*========================================================================================================================== - | PROPERTY: DERIVED TOPIC + | PROPERTY: BASE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Reference to the topic that this topic is derived from, if available. + /// Reference to the topic that this topic inherits from, if available. /// /// /// - /// Derived topics allow attribute values to be inherited from another topic. When a derived topic is configured via the - /// TopicId attribute key, values from that topic are used when the method unable to find a local value for the attribute. + /// Base topics allow attribute values to be inherited from another topic. When a is configured + /// as a BaseTopic , values from that are used when the method is unable to find a local value for the + /// attribute. /// /// - /// Be aware that while multiple levels of derived topics can be configured, the method defaults to a maximum level of five "hops". + /// Be aware that while multiple levels of s can be configured, the method defaults to a maximum level of five "hops" in + /// order to help avoid an infinite loop. /// /// - /// The underlying value of the is stored as the TopicID . - /// If the hasn't been saved, then the relationship will be established, but the TopicID - /// won't be persisted to the underlying repository upon . That said, - /// when is called, the will - /// be reevaluated and, if it has subsequently been saved, then the TopicID will be updated accordingly. This - /// allows in-memory topic graphs to be constructed, while preventing invalid s from being - /// persisted to the underlying data storage. As a result, however, a referencing a that is unsaved will need to be saved again once the has been saved. + /// The underlying value of the is stored as a topic reference with the of BaseTopic in . If the hasn't been saved, then the relationship will be established, but the BaseTopic won't be persisted + /// to the underlying repository upon . That said, when is called, the will be reevaluated + /// and, if it has subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory + /// topic graphs to be constructed, while preventing invalid s from being persisted to the + /// underlying data storage. As a result, however, a referencing an that is + /// unsaved will need to be saved again once the has been saved, assuming it's otherwise outside + /// the scope of the original call. /// /// - /// The that values should be derived from, if not otherwise available. + /// The that values should be inherited from, if not otherwise available. /// /// value != this /// [ReferenceSetter] - public Topic? DerivedTopic { - get => References.GetTopic("DerivedTopic", false); + public Topic? BaseTopic { + get => References.GetTopic("BaseTopic", false); set { Contract.Requires( value != this, "A topic may not derive from itself." ); - References.SetTopic("DerivedTopic", value); + References.SetTopic("BaseTopic", value); } } @@ -686,11 +691,11 @@ public Topic? DerivedTopic { | PROPERTY: REFERENCES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// A façade for accessing references topics based on a reference key; can be used for derived topics, etc. + /// A façade for accessing referenced topics based on a reference key; can be used for base topics, etc. /// /// /// The references property exposes a with child topics representing named references (e.g., - /// "DerivedTopic" for a derived topic). + /// BaseTopic for a ). /// /// The current 's relationships. public TopicReferenceDictionary References { get; } From 18a75035edecd6f0e21dc5762faf9f2d738a7db9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 11:40:05 -0800 Subject: [PATCH 533/778] Updated documentation referring to `BaseTopic` In the previous commit, I renamed `Topic.DerivedTopic` to `Topic.BaseTopic` to better clarify the relationship (1006a2b). As part of that, I updated the documentation for the impacted members and any method parameters. In addition, however, there are a number of cases where the documentation alludes to this relationship, even though the immediate member may not make a direct call to `Topic.BaseTopic`. These needed to be gone through carefully, as the word "derived" is often used for cases where it is still the most appropriate term (e.g., in terms of _derived classes_ or _derived types_ or _derived methods_, as used with regard to class inheritance and polymorphism). This commit picks up those references. --- .../Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql | 4 ++-- OnTopic.Tests/TopicTest.cs | 4 ++-- OnTopic/Attributes/AttributeValueCollectionExtensions.cs | 8 ++++---- OnTopic/Repositories/TopicRepository.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql index 480617d0..e1552f33 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 3 to OnTopic 4.sql @@ -22,8 +22,8 @@ COLUMN AttributeID -- INHERIT TYPES -------------------------------------------------------------------------------------------------------------------------------- -- Attribute Descriptors will be converted to more specific content types based on their legacy attribute type. Attribute types --- may be inherited from derived topics, however. This script identifies any cases where the attribute type is inherited, and --- updates the target topic with the derived value. This may need to be run multiple times if there are multiple layers of +-- may be inherited from base topics, however. This script identifies any cases where the attribute type is inherited, and +-- updates the target topic with the inherited value. This may need to be run multiple times if there are multiple layers of -- inheritance (e.g., an attribute derives from an attribute which derives from another attribute). -------------------------------------------------------------------------------------------------------------------------------- diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 1e0f54b2..98f53683 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -39,8 +39,8 @@ public void Create_ReturnsTopic() { | TEST: CREATE: CONTENT TYPE: RETURNS DERIVED TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Creates a topic of a content type which has been derived, and ensures the derived version of is - /// returned. + /// Creates a topic of a content type which maps to a class derived from , and ensures the derived + /// version of the class is returned. /// [TestMethod] public void Create_ContentType_ReturnsDerivedTopic() { diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 7eaac1ad..4398a9e9 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -24,7 +24,7 @@ public static class AttributeValueCollectionExtensions { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through derived topics for values. Return as a boolean. + /// of inheritance, and an optional setting for searching through base topics for values. Return as a boolean. /// /// The instance of the this extension is bound to. /// The string identifier for the . @@ -62,7 +62,7 @@ out var result \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through derived topics for values. Return as a integer. + /// of inheritance, and an optional setting for searching through base topics for values. Return as a integer. /// /// The instance of the this extension is bound to. /// The string identifier for the . @@ -100,7 +100,7 @@ out var result \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through derived topics for values. Return as a double. + /// of inheritance, and an optional setting for searching through base topics for values. Return as a double. /// /// The instance of the this extension is bound to. /// The string identifier for the . @@ -138,7 +138,7 @@ out var result \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through derived topics for values. Return as a DateTime. + /// of inheritance, and an optional setting for searching through base topics for values. Return as a DateTime. /// /// The instance of the this extension is bound to. /// The string identifier for the . diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index a7eb9d27..1e97a737 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -344,7 +344,7 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso >------------------------------------------------------------------------------------------------------------------------- | If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The - | same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship. + | same can be done for Base Topic references, which are effectively establish a 1:1 relationship. \-----------------------------------------------------------------------------------------------------------------------*/ if ( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || From 482ee0fa0f435265417be18919251b7b5e9a3069 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 11:41:26 -0800 Subject: [PATCH 534/778] Renamed `inheritFromDerived` to `inheritFromBase` This is intended to remain consistent with the rename from `Topic.DerivedTopic` to `Topic.BaseTopic` (1006a2b). --- .../Attributes/AttributeValueCollection.cs | 10 +++---- .../AttributeValueCollectionExtensions.cs | 28 +++++++++---------- .../References/TopicReferenceDictionary.cs | 4 +-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index cc8e0277..df137677 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -106,7 +106,7 @@ public bool IsDirty(bool excludeLastModified) /// This method is intended primarily for data storage providers, such as , which may need /// to determine if a specific attribute key is dirty prior to saving it to the data storage medium. Because IsDirty /// is a state of the current , it does not support inheritFromParent or - /// inheritFromDerived (which otherwise default to true). + /// inheritFromBase (which otherwise default to true). /// /// The string identifier for the . /// True if the attribute value is marked as dirty; otherwise false. @@ -189,22 +189,22 @@ public void MarkClean(string key, DateTime? version) { /// /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through derived topics for values. + /// of inheritance, and an optional setting for searching through base topics for values. /// /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// - /// + /// /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. /// /// The string value for the Attribute. [return: NotNullIfNotNull("defaultValue")] - public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromDerived = true) { + public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - return GetValue(name, defaultValue, inheritFromParent, (inheritFromDerived? 5 : 0)); + return GetValue(name, defaultValue, inheritFromParent, (inheritFromBase? 5 : 0)); } /// diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 4398a9e9..97de09b0 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -32,9 +32,9 @@ public static class AttributeValueCollectionExtensions { /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// - /// - /// order to get the value. + /// /// Boolean indicator nothing whether to search through any of the topic's s in order to get + /// the value. /// /// The value for the attribute as a boolean. public static bool GetBoolean( @@ -42,7 +42,7 @@ public static bool GetBoolean( string name, bool defaultValue = default, bool inheritFromParent = false, - bool inheritFromDerived = true + bool inheritFromBase = true ) { Contract.Requires(attributes); Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); @@ -51,7 +51,7 @@ public static bool GetBoolean( name, defaultValue ? "1" : "0", inheritFromParent, - inheritFromDerived ? 5 : 0 + inheritFromBase ? 5 : 0 ), out var result ) ? result is 1 : defaultValue; @@ -70,9 +70,9 @@ out var result /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// - /// - /// order to get the value. + /// /// Boolean indicator nothing whether to search through any of the topic's s in order to get + /// the value. /// /// The value for the attribute as an integer. public static int GetInteger( @@ -80,7 +80,7 @@ public static int GetInteger( string name, int defaultValue = default, bool inheritFromParent = false, - bool inheritFromDerived = true + bool inheritFromBase = true ) { Contract.Requires(attributes); Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); @@ -89,7 +89,7 @@ public static int GetInteger( name, defaultValue.ToString(CultureInfo.InvariantCulture), inheritFromParent, - inheritFromDerived? 5 : 0 + inheritFromBase? 5 : 0 ), out var result ) ? result : defaultValue; @@ -108,7 +108,7 @@ out var result /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// - /// + /// /// Boolean indicator nothing whether to search through any of the topic's s in order to get /// the value. /// @@ -118,7 +118,7 @@ public static double GetDouble( string name, double defaultValue = default, bool inheritFromParent = false, - bool inheritFromDerived = true + bool inheritFromBase = true ) { Contract.Requires(attributes); Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); @@ -127,7 +127,7 @@ public static double GetDouble( name, defaultValue.ToString(CultureInfo.InvariantCulture), inheritFromParent, - inheritFromDerived? 5 : 0 + inheritFromBase? 5 : 0 ), out var result ) ? result : defaultValue; @@ -146,7 +146,7 @@ out var result /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. /// - /// + /// /// Boolean indicator nothing whether to search through any of the topic's s in order to get /// the value. /// @@ -156,7 +156,7 @@ public static DateTime GetDateTime( string name, DateTime defaultValue = default, bool inheritFromParent = false, - bool inheritFromDerived = true + bool inheritFromBase = true ) { Contract.Requires(attributes); Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); @@ -165,7 +165,7 @@ public static DateTime GetDateTime( name, defaultValue.ToString(CultureInfo.InvariantCulture), inheritFromParent, - inheritFromDerived ? 5 : 0 + inheritFromBase ? 5 : 0 ), out var result ) ? result : defaultValue; diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs index 16c1c8f4..288c9426 100644 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ b/OnTopic/References/TopicReferenceDictionary.cs @@ -358,11 +358,11 @@ public bool Remove(string key) { /// /// Attempts to retrieve a topic reference based on its ; if it doesn't exist, returns null. /// - public Topic? GetTopic(string key, bool inheritFromDerived = true) { + public Topic? GetTopic(string key, bool inheritFromBase = true) { if (TryGetValue(key, out var existing)) { return existing; } - else if (inheritFromDerived) { + else if (inheritFromBase) { return _parent.BaseTopic?.References.GetTopic(key); } return null; From 3a9fd847710f670ce9e5dd72ea4a7d0810c69aae Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 11:42:52 -0800 Subject: [PATCH 535/778] Renamed `TopicReferences` from `DerivedTopic` to `BaseTopic` The `DerivedTopic` nomenclature didn't just relate to the .NET data model, but also the actual data in the SQL database. As part of that rename, we need to update all references in the `TopicReferences` table to use the `BaseTopic` key instead of the `DerivedTopic` key. --- .../Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index e48fa802..05ee48de 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -93,7 +93,7 @@ IN ( 'Key', -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE TOPIC REFERENCES -------------------------------------------------------------------------------------------------------------------------------- --- In OnTopic 5, references to other topics—such as `DerivedTopic`—have been moved from the Attributes table to a new +-- In OnTopic 5, references to other topics—such as `BaseTopic`—have been moved from the Attributes table to a new -- TopicReferences table, where they act more like relationships. This allows referential integrity to be enforced through -- foreign key constraints, and formalizes the relationship so we don't need to rely on hacks in e.g. the Topic Data Transer -- service to infer which attributes represent relationships in order to translate their values from `TopicID` to `UniqueKey`. @@ -119,7 +119,7 @@ WHERE AttributeKey LIKE '%ID' AND Topics.TopicID IS NOT NULL UPDATE TopicReferences -SET ReferenceKey = 'DerivedTopic' +SET ReferenceKey = 'BaseTopic' WHERE ReferenceKey = 'Topic' -------------------------------------------------------------------------------------------------------------------------------- From 29cce45840c8135aba623f0c4a738df96b9cd8d2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 11:53:00 -0800 Subject: [PATCH 536/778] Renamed `DerivedTopic` attribute descriptor to `BaseTopic` During the database migration, rename the actual `TopicReferenceAttributeDescriptor` used for `DerivedTopic` to reflect the new name. In OnTopic 4.x, the name of the attribute was incidental to how it was stored, due to a quirk of the implementation, as well as historical conventions specific to `DerivedTopic`. In OnTopic 5.x, that will be made consistent with other attributes, where the descriptor's key denotes the storage key (in this case, `ReferenceKey`). As part of this, I updated the inline comments to better document what's going on, and to separate it from the topic references update that the original query was part of. --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 05ee48de..88095fd6 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -118,10 +118,24 @@ WHERE AttributeKey LIKE '%ID' AND ISNUMERIC(AttributeValue) = 1 AND Topics.TopicID IS NOT NULL +-------------------------------------------------------------------------------------------------------------------------------- +-- MIGRATE DERIVED TOPICS +-------------------------------------------------------------------------------------------------------------------------------- +-- The above migration to topic references includes the DerivedTopic. To better clarify the purpose and intent of that +-- relationship, we're renaming the attribute from 'DerivedTopic' to 'BaseTopic', and the actual storage field from 'Topic(ID)' +-- to 'BaseTopic'. This is not only a more accurate identifier, but also unifies the label between the attribute descriptor +-- and how its 'ReferenceKey'. +-------------------------------------------------------------------------------------------------------------------------------- + UPDATE TopicReferences SET ReferenceKey = 'BaseTopic' WHERE ReferenceKey = 'Topic' +UPDATE Topics +SET TopicKey = 'BaseTopic' +WHERE TopicKey = 'DerivedTopic' +AND ContentType = 'TopicReferenceAttributeDescriptor' + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE ATTRIBUTE KEYS -------------------------------------------------------------------------------------------------------------------------------- From 4ed8ef18833e96736dcd0f62c36559d312848a36 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 13:08:23 -0800 Subject: [PATCH 537/778] Introduced `TrackedItem` as an abstraction for `AttributeValue` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idea of the `AttributeValue` is to provide a key/value pair with metadata for tracking whether a record which is intended to be saved in the data store `IsDirty` and when it was `LastModified`. This concept, however, isn't specific to attributes. It also pertains to topic references—or, at least, should. Given that, the new `TrackedItem` provides an `abstract` class for handling both `AttributeValue` and a forthcoming `TopicReference` record. Note: The names of derived records may change, but the concepts remain. --- .../Collections/Specialized/TrackedItem{T}.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 OnTopic/Collections/Specialized/TrackedItem{T}.cs diff --git a/OnTopic/Collections/Specialized/TrackedItem{T}.cs b/OnTopic/Collections/Specialized/TrackedItem{T}.cs new file mode 100644 index 00000000..01f9d9ff --- /dev/null +++ b/OnTopic/Collections/Specialized/TrackedItem{T}.cs @@ -0,0 +1,107 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.Generic; +using OnTopic.Attributes; +using OnTopic.Internal.Diagnostics; +using OnTopic.Repositories; + +namespace OnTopic.Collections.Specialized { + + /*============================================================================================================================ + | CLASS: TRACKED ITEM + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a base class for tracking versioned records, such as . + /// + /// + /// The class is comparable to the , in that it tracks the and for an item, but it additionally provides metadata related to the record, including the + /// and whether or not it . This makes it easier for e.g. implementations to make more informed decisions about whether a record needs to be saved or + /// overwritten during a or . + /// + public abstract record TrackedItem { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Constructs a new instance of a class. + /// + /// The for the instance. + /// The for the instance. + /// The optional state for the instance. + /// The optional for the instance. + protected TrackedItem(string key, T value, bool isDirty = true, DateTime? lastModified = null) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + TopicFactory.ValidateKey(key, false); + Contract.Requires(value, nameof(value)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set properties + \-----------------------------------------------------------------------------------------------------------------------*/ + Key = key; + Value = value; + IsDirty = isDirty; + LastModified = lastModified?? DateTime.UtcNow; + + } + + /*========================================================================================================================== + | PROPERTY: KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the for a given item. + /// + /// + /// !value.Contains(" ") + /// + public string Key { get; init; } + + /*========================================================================================================================== + | PROPERTY: VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the for the given item. + /// + public T? Value { get; init; } + + /*========================================================================================================================== + | PROPERTY: IS DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if the item has been modified relative to its persisted state. + /// + /// + /// The property is used by the to determine whether or + /// not the value has been persisted to the data store. If it is set to true, the item's value is sent to the + /// data store when is called. Otherwise, it is ignored, + /// thus preventing the need to update records (or create new versions of records) whose values haven't changed. + /// + public bool IsDirty { get; init; } + + /*========================================================================================================================== + | PROPERTY: LAST MODIFIED + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the for the given item. + /// + /// + /// If loaded from a data store from e.g. , the should be set to the Version. If the is novel, however, then it should be + /// set to the current date. That won't be the same date established by for the Version, however, which is why this property is labeled . + /// + public DateTime LastModified { get; init; } + + } //Class +} //Namespace \ No newline at end of file From 3afca3226c1c38972b89ab6d365f7d5279bcc12b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 13:12:36 -0800 Subject: [PATCH 538/778] Updated `AttributeValue` to derive from `TrackedItem` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TrackedItem` class provides a base abstraction for centralizing common elements from `AttributeValue` so they can be shared with a forthcoming e.g. `TopicReference` record (4ed8ef1). As the first step of implementing this, the `AttributeValue` record is being updated to derive from `TrackedItem`—and, specifically, `TrackItem`, since the value of an `AttributeValue` is a string. --- OnTopic/Attributes/AttributeValue.cs | 119 +++++---------------------- 1 file changed, 21 insertions(+), 98 deletions(-) diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index 78f988d7..0df53c51 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Collections.Specialized; using OnTopic.Metadata; using OnTopic.Repositories; @@ -17,8 +18,9 @@ namespace OnTopic.Attributes { /// /// /// - /// Provides values and metadata specific to individual attribute values, such as state (e.g., the - /// property signifies whether the attribute value has changed) and its date. + /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. /// /// /// Typically, the will be exposed as part of a via @@ -37,44 +39,11 @@ namespace OnTopic.Attributes { /// method. /// /// - public record AttributeValue { + public record AttributeValue: TrackedItem { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the class, using the specified key/value pair. - /// - /// - /// The string identifier for the collection item key/value pair. - /// - /// - /// The string value text for the collection item key/value pair. - /// - /// - /// An optional boolean indicator noting whether the collection item is a new value, and - /// should thus be saved to the database when is next called. - /// - /// - /// !String.IsNullOrWhiteSpace(key) - /// - public AttributeValue(string key, string? value, bool isDirty = true) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - TopicFactory.ValidateKey(key, false); - - /*------------------------------------------------------------------------------------------------------------------------ - | Set local values - \-----------------------------------------------------------------------------------------------------------------------*/ - Key = key; - Value = value; - IsDirty = isDirty; - - } - /// /// Initializes a new instance of the class, using the specified key/value pair. /// @@ -89,7 +58,7 @@ public AttributeValue(string key, string? value, bool isDirty = true) { /// should thus be saved to the database when is next called. /// /// - /// The value that the attribute was last modified. This is intended exclusively for use when + /// The value that the attribute was last modified. This is intended primarily for use when /// populating the topic graph from a persistent data store as a means of indicating the current version for each /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. /// @@ -98,66 +67,20 @@ public AttributeValue(string key, string? value, bool isDirty = true) { /// description="The key must be specified for the key/value pair." exception="T:System.ArgumentNullException"> /// !String.IsNullOrWhiteSpace(key) /// - internal AttributeValue( + public AttributeValue( string key, - string? value, - bool isDirty, + string value, + bool isDirty = true, DateTime? lastModified = null, bool? isExtendedAttribute = null - ): this( - key, - value, - isDirty - ) { - LastModified = lastModified?? DateTime.Now; - IsExtendedAttribute = isExtendedAttribute; - } + ): base(key, value, isDirty, lastModified) { - /*========================================================================================================================== - | PROPERTY: KEY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the key of the attribute. - /// - /// - /// !String.IsNullOrWhiteSpace(value) - /// - /// - /// !value.Contains(" ") - /// - public string Key { get; init; } - - /*========================================================================================================================== - | PROPERTY: VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets the current value of the attribute. - /// - public string? Value { get; init; } - - /*========================================================================================================================== - | PROPERTY: IS DIRTY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Boolean setting which is set automatically when an attribute's is set to a new value. - /// - /// - /// The IsDirty property is used by the to determine whether or not - /// the value has been persisted to the database. If it is set to true, the attribute's value is sent to the database - /// when is called. Otherwise, it is ignored, thus - /// preventing the need to update attributes (or create new versions of attributes) whose values haven't changed. - /// - public bool IsDirty { get; init; } + /*------------------------------------------------------------------------------------------------------------------------ + | Set local values + \-----------------------------------------------------------------------------------------------------------------------*/ + IsExtendedAttribute = isExtendedAttribute; - /*========================================================================================================================== - | PROPERTY: LAST MODIFIED - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Read-only reference to the last DateTime the instance was updated. - /// - public DateTime LastModified { get; init; } = DateTime.Now; + } /*========================================================================================================================== | PROPERTY: IS EXTENDED ATTRIBUTE @@ -175,12 +98,12 @@ internal AttributeValue( /// /// /// This is important because, otherwise, implementations rely primarily on to determine if a value should be saved. If an attribute's value hasn't changed, but the location - /// it should be stored has, that could potentially result in the attribute being deleted, as the attribute won't show - /// up for when is called with isDirty set to true and - /// isExtendedAttribute is set to either true or false. By introducing , the is able to detect conflicts between the - /// configuration and the underlying data store, and ensure data is stored appropriately. + /// cref="TrackedItem{T}.IsDirty"/> to determine if a value should be saved. If an attribute's value hasn't changed, but + /// the location it should be stored has, that could potentially result in the attribute being deleted, as the attribute + /// won't show up for when is called with isDirty set to true + /// and isExtendedAttribute is set to either true or false. By introducing , the is able to detect conflicts between the configuration and + /// the underlying data store, and ensure data is stored appropriately. /// /// /// The property maps to the From c4e66a38ba3879351b65bff4ff917127c6c1489f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 13:14:29 -0800 Subject: [PATCH 539/778] Updated XML Docs to reflect inheritance from `TrackedItem` By deriving `AttributeValue` (3afca32) from the new `TrackedItem{T}` record (4ed8ef1), I broke the XML Doc references pointing to e.g. `AttributeValue.IsDirty`, since those members no longer exists _directly_ on `AttributeValue`, but are instead inherited from `TrackedItem`. As such, all XML Doc references pointing to common properties of `TrackedItem` needed to be updated to point to that base class. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 15 ++++++++------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 7 ++++--- OnTopic/Attributes/AttributeValueCollection.cs | 14 +++++++------- .../AttributeValueCollectionExtensions.cs | 9 +++++---- .../Reflection/TopicPropertyDispatcher.cs | 3 ++- OnTopic/Metadata/AttributeDescriptor.cs | 5 +++-- OnTopic/Metadata/ContentTypeDescriptor.cs | 5 +++-- OnTopic/Repositories/TopicRepository.cs | 9 +++++---- OnTopic/Topic.cs | 10 +++++----- OnTopic/TopicFactory.cs | 3 ++- 10 files changed, 44 insertions(+), 36 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 10374eb0..2a357541 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -8,6 +8,7 @@ using System.Globalization; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Tests.Entities; namespace OnTopic.Tests { @@ -317,7 +318,7 @@ public void Clear_ExistingValues_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets the value of a custom to the existing value and ensures it is not marked as - /// . + /// . /// [TestMethod] public void SetValue_ValueUnchanged_IsNotDirty() { @@ -336,7 +337,7 @@ public void SetValue_ValueUnchanged_IsNotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is marked as . Confirms that returns + /// cref="TrackedItem{T}.IsDirty"/>. Confirms that returns /// true. /// [TestMethod] @@ -374,7 +375,7 @@ public void IsDirty_DeletedValues_ReturnsTrue() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is not marked as - /// . Confirms that returns + /// . Confirms that returns /// false/ /// [TestMethod] @@ -393,7 +394,7 @@ public void IsDirty_NoDirtyValues_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is not marked as - /// as well as a LastModified that is. Confirms + /// as well as a LastModified that is. Confirms /// that returns false. /// [TestMethod] @@ -414,7 +415,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a and then deletes it. Confirms - /// that the returns the new version after calling returns the new version after calling . /// [TestMethod] @@ -623,8 +624,8 @@ public void Add_InvalidAttributeValue_ThrowsException() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new which maps to directly to a and confirms that the original is replaced if the - /// changes. + /// "AttributeValueCollection"/> and confirms that the original is replaced if the + /// changes. /// [TestMethod] public void Add_WithBusinessLogic_MaintainsIsDirty() { diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index abb29f78..a03a3974 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Data.Caching; using OnTopic.Metadata; using OnTopic.References; @@ -278,8 +279,8 @@ public void GetAttributes_ExtendedAttributes_ReturnsExtendedAttributes() { | TEST: GET ATTRIBUTES: EXTENDED ATTRIBUTE MISMATCH: RETURNS EXTENDED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a list of attributes from a topic, filtering by . Expects an to be returned even if it's not but its + /// Retrieves a list of attributes from a topic, filtering by . Expects an to be returned even if it's not but its /// disagrees with . /// [TestMethod] @@ -300,7 +301,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() | TEST: GET ATTRIBUTES: EXTENDED ATTRIBUTE MISMATCH: RETURNS NOTHING \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a list of attributes from a topic, filtering by . Expects the . Expects the to not be returned even though its /// disagrees with , since it won't match the 's isExtendedAttribute call. diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index df137677..b5c4e89a 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -299,7 +299,7 @@ _associatedTopic.BaseTopic is not null && /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . @@ -346,7 +346,7 @@ public void SetValue( /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . @@ -490,14 +490,14 @@ internal void SetValue( /// /// /// - /// If a settable property is available corresponding to the , the call should be routed + /// If a settable property is available corresponding to the , the call should be routed /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is /// set by the 's /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. /// /// /// Compared to the base implementation, will throw a specific error if a duplicate key - /// is inserted. This conveniently provides the name of the so it's clear what key is + /// is inserted. This conveniently provides the name of the so it's clear what key is /// being duplicated. /// /// @@ -535,7 +535,7 @@ protected override sealed void InsertItem(int index, AttributeValue item) { /// logic is enforced. /// /// - /// If a settable property is available corresponding to the , the call should be routed + /// If a settable property is available corresponding to the , the call should be routed /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is /// set by the 's /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. @@ -561,7 +561,7 @@ protected override sealed void SetItem(int index, AttributeValue item) { /// /// /// When an is removed, will return true—even if no remaining - /// s are marked as . + /// s are marked as . /// protected override sealed void RemoveItem(int index) { var attribute = this[index]; @@ -578,7 +578,7 @@ protected override sealed void RemoveItem(int index) { /// /// /// When an is removed, will return true—even if no remaining - /// s are marked as . + /// s are marked as . /// protected override sealed void ClearItems() { DeletedAttributes.AddRange(Items.Select(a => a.Key)); diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 97de09b0..cfc8d1dd 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Globalization; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; @@ -182,7 +183,7 @@ out var result /// The string identifier for the . /// The boolean value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . @@ -220,7 +221,7 @@ public static void SetBoolean( /// The string identifier for the . /// The integer value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . @@ -262,7 +263,7 @@ public static void SetInteger( /// The string identifier for the . /// The double value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . @@ -304,7 +305,7 @@ public static void SetDouble( /// The string identifier for the . /// The value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as + /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 27440b41..544c5f1b 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Runtime.ExceptionServices; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.References; namespace OnTopic.Internal.Reflection { @@ -51,7 +52,7 @@ namespace OnTopic.Internal.Reflection { /// and can optionally be retrieved via . This is useful in case there /// is data from the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as - /// , , and , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we /// allow the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 355f89a8..cf6af478 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; namespace OnTopic.Metadata { @@ -48,9 +49,9 @@ public class AttributeDescriptor : Topic { /// /// /// By default, when creating new attributes, the s for both and will be set to , which is required in order to + /// cref="Topic.ContentType"/> will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the /// persistence store. /// diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index bb6320f9..1ceebc0a 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -7,6 +7,7 @@ using System.Linq; using OnTopic.Attributes; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.References; @@ -50,9 +51,9 @@ public class ContentTypeDescriptor : Topic { /// /// /// By default, when creating new attributes, the s for both and will be set to , which is required in order to + /// cref="Topic.ContentType"/> will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the /// persistence store. /// diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 1e97a737..e336cf71 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -10,6 +10,7 @@ using Microsoft; using OnTopic.Attributes; using OnTopic.Collections; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; using OnTopic.Querying; @@ -653,7 +654,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a , returns a list of , optionally filtering based on and . + /// cref="AttributeDescriptor.IsExtendedAttribute"/> and . /// /// The from which to pull the attributes. /// @@ -661,7 +662,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi /// cref="AttributeValue"/>s are returned. /// /// - /// Whether or not to filter by . If null, all s + /// Whether or not to filter by . If null, all s /// are returned. /// /// Exclude any attributes that start with LastModified. @@ -853,13 +854,13 @@ private static bool IsAttributeDescriptor(Topic topic) => /// determines where an attribute was stored. If these two /// values are in conflict, that suggests the coniguration for has /// changed since the attribute value was last saved. In that case, it should be treated as even though its value hasn't changed to ensure that its storage location is + /// cref="TrackedItem{T}.IsDirty"/> even though its value hasn't changed to ensure that its storage location is /// updated. /// /// /// If cannot be found then the is arbitrary attribute /// not mapped to the schema. In that case, its storage location is dynamically determined based on its length, and thus - /// it should only change locations when it . Otherwise, its length will remain the + /// it should only change locations when it . Otherwise, its length will remain the /// same, and thus the storage location should remain unchanged. /// /// diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index cdd77dce..31e5e190 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -45,9 +45,9 @@ public class Topic { /// /// /// By default, when creating new attributes, the s for both and will be set to , which is required in order to correctly save + /// cref="ContentType"/> will be set to , which is required in order to correctly save /// new topics to the database. When the parameter is set, however, the property is set to falseon and , as + /// cref="TrackedItem{T}.IsDirty"/> property is set to falseon and , as /// it is assumed these are being set to the same values currently used in the persistence store. /// /// A string representing the key for the new topic instance. @@ -666,8 +666,8 @@ public Topic? BaseTopic { /// /// /// Attributes are stored via an class which, in addition to the Attribute Key and Value, - /// also track other metadata for the attribute, such as the version (via the - /// property) and whether it has been persisted to the database or not (via the + /// also track other metadata for the attribute, such as the version (via the + /// property) and whether it has been persisted to the database or not (via the /// property). /// /// The current 's attributes. @@ -750,7 +750,7 @@ public Topic? BaseTopic { /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. /// - /// Specified whether the value should be marked as . By default, it will be marked + /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index ac637d2e..217a76ea 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -6,6 +6,7 @@ using System; using System.Text.RegularExpressions; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; using OnTopic.Metadata; @@ -38,7 +39,7 @@ public static class TopicFactory { /// by the parameter. /// /// - /// When the parameter is set the property is set to + /// When the parameter is set the property is set to /// false on as well as on , since it is assumed these are /// being set to the same values currently used in the persistence store. /// From 1c2f8ca5f8450d9b912d11b54b3ff38d407bdd74 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 15:00:39 -0800 Subject: [PATCH 540/778] Derived `TrackedCollection` from `AttributeValueCollection` The `TrackedCollection` will provide the same basic functionality of `AttributeValueCollection` and, in fact, act as a base class for `AttributeValueCollection`. Since much of the code from each will derive from the current implementation of `AttributeValueCollection`, both classes should share the same git history. --- .../TrackedCollection{TTrackedItem,TValue,TAttribute}.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename OnTopic/{Attributes/AttributeValueCollection.cs => Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs} (100%) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs similarity index 100% rename from OnTopic/Attributes/AttributeValueCollection.cs rename to OnTopic/Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs From 57565b93c45cb739a9cf7fd764091a3cd0c3d3ed Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 15:03:35 -0800 Subject: [PATCH 541/778] Restored `AttributeValueCollection` To complete the derivation of `TrackedCollection` from `AttributeValueCollection` (1c2f8ca), we must restore the original `AttributeValueCollection`. --- .../Attributes/AttributeValueCollection.cs | 602 ++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 OnTopic/Attributes/AttributeValueCollection.cs diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs new file mode 100644 index 00000000..b5c4e89a --- /dev/null +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -0,0 +1,602 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using OnTopic.Collections.Specialized; +using OnTopic.Internal.Diagnostics; +using OnTopic.Internal.Reflection; +using OnTopic.Repositories; + +namespace OnTopic.Attributes { + + /*============================================================================================================================ + | CLASS: ATTRIBUTE VALUE COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects. + /// + /// + /// objects represent individual instances of attributes associated with particular topics. + /// The class tracks these through its property, which is an instance of + /// the class. + /// + public class AttributeValueCollection : KeyedCollection, ITrackDirtyKeys { + + /*========================================================================================================================== + | DISPATCHER + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly TopicPropertyDispatcher _topicPropertyDispatcher; + + /*========================================================================================================================== + | PRIVATE VARIABLES + \-------------------------------------------------------------------------------------------------------------------------*/ + private readonly Topic _associatedTopic; + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class. + /// + /// + /// The is intended exclusively for providing access to attributes via the + /// property. For this reason, the constructor is marked as internal. + /// + /// A reference to the topic that the current attribute collection is bound to. + internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { + _associatedTopic = parentTopic; + _topicPropertyDispatcher = new(parentTopic); + } + + /*========================================================================================================================== + | PROPERTY: DELETED ATTRIBUTES + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// When an attribute is deleted, keep track of it so that it can be marked for deletion when the topic is saved. + /// + /// + /// As a performance enhancement, implementations will only save topics that are marked as + /// . If a is deleted, then it won't be marked as dirty. If no + /// other instances were modified, then the topic won't get saved, and that value won't be + /// deleted. Further more, the method has no way of + /// detecting the deletion of arbitrary attributes�i.e., attributes that were deleted which don't correspond to attributes + /// configured on the . By tracking any deleted attributes, we ensure both + /// scenarios can be accounted for. + /// + internal List DeletedAttributes { get; } = new(); + + /*========================================================================================================================== + | METHOD: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public bool IsDirty() => IsDirty(false); + + /// + /// Determine if any attributes in the are dirty. + /// + /// + /// This method is intended primarily for data storage providers, such as , which may need + /// to determine if any attributes are dirty prior to saving them to the data storage medium. Be aware that this does + /// not track whether any have been modified; as such, it may still be necessary + /// to persist changes to the storage medium. + /// + /// + /// Optionally excludes s whose keys start with LastModified. This is useful for + /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically + /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. + /// + /// True if the attribute value is marked as dirty; otherwise false. + public bool IsDirty(bool excludeLastModified) + => DeletedAttributes.Count > 0 || Items.Any(a => + a.IsDirty && + (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) + ); + + /// + /// Determine if a given attribute is marked as dirty. Will return false if the attribute key cannot be found. + /// + /// + /// This method is intended primarily for data storage providers, such as , which may need + /// to determine if a specific attribute key is dirty prior to saving it to the data storage medium. Because IsDirty + /// is a state of the current , it does not support inheritFromParent or + /// inheritFromBase (which otherwise default to true). + /// + /// The string identifier for the . + /// True if the attribute value is marked as dirty; otherwise false. + public bool IsDirty(string key) { + if (!Contains(key)) { + return false; + } + return this[key].IsDirty; + } + + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public void MarkClean() => MarkClean((DateTime?)null); + + /// + /// Marks the collection—including all items—as clean, meaning they have been persisted to + /// the underlying . + /// + /// + /// This method is intended primarily for data storage providers, such as , so that they can + /// mark the collection, and all items it contains, as clean. After this, will return false until any items are modified or removed. + /// + /// + /// The value that the attributes were last saved. This corresponds to the . + /// + public void MarkClean(DateTime? version) { + foreach (var attribute in Items.Where(a => a.IsDirty).ToArray()) { + SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); + } + DeletedAttributes.Clear(); + } + + /*========================================================================================================================== + | METHOD: MARK CLEAN + \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public void MarkClean(string key) => MarkClean(key, null); + + /// + /// Marks an individual as clean. + /// + /// + /// This method is intended primarily for data storage providers, such as , so that they can + /// mark an as clean. After this, will return false for + /// that item until it is modified. + /// + /// The string identifier for the . + /// + /// The value that the attribute was last modified. This denotes the associated with the specific attribute. + /// + public void MarkClean(string key, DateTime? version) { + if (Contains(key)) { + var attribute = this[key]; + if (attribute.IsDirty) { + SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); + } + } + } + + /*========================================================================================================================== + | METHOD: GET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets a named attribute from the Attributes dictionary. + /// + /// The string identifier for the . + /// + /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// + /// The string value for the Attribute. + [return: NotNull] + public string GetValue(string name, bool inheritFromParent = false) => GetValue(name, "", inheritFromParent); + + /// + /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling + /// of inheritance, and an optional setting for searching through base topics for values. + /// + /// The string identifier for the . + /// A string value to which to fall back in the case the value is not found. + /// + /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// + /// + /// Boolean indicator nothing whether to search through any of the topic's topics in + /// order to get the value. + /// + /// The string value for the Attribute. + [return: NotNullIfNotNull("defaultValue")] + public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); + return GetValue(name, defaultValue, inheritFromParent, (inheritFromBase? 5 : 0)); + } + + /// + /// Gets a named attribute from the Attributes dictionary with a specified default value and an optional number of + /// s through whom to crawl to retrieve an inherited value. + /// + /// The string identifier for the . + /// A string value to which to fall back in the case the value is not found. + /// + /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// + /// The number of recursions to perform when attempting to get the value. + /// The string value for the Attribute. + /// + /// !String.IsNullOrWhiteSpace(name) + /// + /// + /// !name.Contains(" ") + /// + /// + /// maxHops >= 0 + /// + /// + /// maxHops <= 100 + /// + [return: NotNullIfNotNull("defaultValue")] + internal string? GetValue(string name, string? defaultValue, bool inheritFromParent, int maxHops) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate contracts + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); + Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); + Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); + TopicFactory.ValidateKey(name); + + string? value = null; + + /*------------------------------------------------------------------------------------------------------------------------ + | Look up value from Attributes + \-----------------------------------------------------------------------------------------------------------------------*/ + if (Contains(name)) { + value = this[name]?.Value; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Look up value from topic pointer + \-----------------------------------------------------------------------------------------------------------------------*/ + if ( + String.IsNullOrEmpty(value) && + _associatedTopic.BaseTopic is not null && + maxHops > 0 + ) { + value = _associatedTopic.BaseTopic.Attributes.GetValue(name, null, false, maxHops - 1); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Look up value from parent + \-----------------------------------------------------------------------------------------------------------------------*/ + if (String.IsNullOrEmpty(value) && inheritFromParent && _associatedTopic.Parent is not null) { + value = _associatedTopic.Parent.Attributes.GetValue(name, defaultValue, inheritFromParent); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Return value, if found + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!String.IsNullOrEmpty(value)) { + return value; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Finally, return default + \-----------------------------------------------------------------------------------------------------------------------*/ + return defaultValue; + + } + + /*========================================================================================================================== + | METHOD: SET VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Helper method that either adds a new object or updates the value of an existing one, + /// depending on whether that value already exists. + /// + /// + /// Minimizes the need for defensive conditions throughout the library. + /// + /// The string identifier for the AttributeValue. + /// The text value for the AttributeValue. + /// + /// Specified whether the value should be marked as . By default, it will be marked as + /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being + /// persisted to the data store on . + /// + /// + /// The value that the attribute was last modified. This is intended exclusively for use when + /// populating the topic graph from a persistent data store as a means of indicating the current version for each + /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. + /// + /// Determines if the attribute originated from an extended attributes data store. + /// + /// !String.IsNullOrWhiteSpace(key) + /// + /// + /// !String.IsNullOrWhiteSpace(value) + /// + /// + /// !value.Contains(" ") + /// + public void SetValue( + string key, + string? value, + bool? markDirty = null, + DateTime? version = null, + bool? isExtendedAttribute = null + ) + => SetValue(key, value, markDirty, true, version, isExtendedAttribute); + + /// + /// Protected helper method that either adds a new object or updates the value of an existing + /// one, depending on whether that value already exists. + /// + /// + /// When this overload is called, no attempt will be made to route the call through corresponding properties, if + /// available. As such, this is intended specifically to be called by internal properties as a means of avoiding a + /// feedback loop. + /// + /// The string identifier for the AttributeValue. + /// The text value for the AttributeValue. + /// + /// Specified whether the value should be marked as . By default, it will be marked as + /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being + /// persisted to the data store on . + /// + /// + /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. + /// This should be set to false if setting attributes from internal properties in order to avoid an infinite loop. + /// + /// + /// The value that the attribute was last modified. This is intended exclusively for use when + /// populating the topic graph from a persistent data store as a means of indicating the current version for each + /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. + /// + /// Determines if the attribute originated from an extended attributes data store. + /// + /// !String.IsNullOrWhiteSpace(key) + /// + /// + /// !String.IsNullOrWhiteSpace(value) + /// + /// + /// !value.Contains(" ") + /// + internal void SetValue( + string key, + string? value, + bool? markDirty, + bool enforceBusinessLogic, + DateTime? version = null, + bool? isExtendedAttribute = null + ) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate input + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + TopicFactory.ValidateKey(key); + + /*------------------------------------------------------------------------------------------------------------------------ + | Retrieve original attribute + \-----------------------------------------------------------------------------------------------------------------------*/ + AttributeValue? originalAttributeValue = null; + + if (Contains(key)) { + originalAttributeValue = this[key]; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Update from business logic + >-----------------------------------------------------------------------------------------------------------------------— + | If the original values have already been applied, and SetValue() is being triggered a second time after enforcing + | business logic, then use the original values, while applying any change in the value triggered by the business logic. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (_topicPropertyDispatcher.IsRegistered(key, out var updatedAttributeValue)) { + if (updatedAttributeValue.Value != value) { + updatedAttributeValue = updatedAttributeValue with { + Value = value + }; + } + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Update existing attribute value + >-----------------------------------------------------------------------------------------------------------------------— + | Because AttributeValue is immutable, a new instance must be constructed to replace the previous version. + \-----------------------------------------------------------------------------------------------------------------------*/ + else if (originalAttributeValue is not null) { + var markAsDirty = originalAttributeValue.IsDirty; + if (markDirty.HasValue) { + markAsDirty = markDirty.Value; + } + else if (originalAttributeValue.Value != value) { + markAsDirty = true; + } + updatedAttributeValue = originalAttributeValue with { + Value = value, + IsDirty = markAsDirty, + LastModified = version?? originalAttributeValue.LastModified, + IsExtendedAttribute = isExtendedAttribute?? originalAttributeValue.IsExtendedAttribute + }; + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Ignore if null + >------------------------------------------------------------------------------------------------------------------------- + | ###NOTE JJC20200501: Null or empty values are treated as deletions, and are not persisted to the data store. With + | existing values, these are written to ensure that the collection is marked as IsDirty, thus allowing previous values to + | be overwritten. Non-existent values, however, should simply be ignored. + \-----------------------------------------------------------------------------------------------------------------------*/ + else if (String.IsNullOrEmpty(value)) { + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Create new attribute value + \-----------------------------------------------------------------------------------------------------------------------*/ + else { + updatedAttributeValue = new AttributeValue(key, value, markDirty ?? true, version, isExtendedAttribute); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Register that business logic has already been enforced + >------------------------------------------------------------------------------------------------------------------------- + | We want to ensure that any attempt to set references that have corresponding (writable) properties use those properties, + | thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by ICollection, and not + | just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) call the Enforce() method. If it returns false, + | they assume the property set the value (e.g., by calling the internal SetValue method with enforceBusinessLogic set to + | false). Otherwise, the corresponding property will be called. The Register() method thus avoids a redirect loop in this + | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. + \-----------------------------------------------------------------------------------------------------------------------*/ + if (!enforceBusinessLogic) { + _topicPropertyDispatcher.Register(key, updatedAttributeValue); + } + + /*------------------------------------------------------------------------------------------------------------------------ + | Persist attribute value + \-----------------------------------------------------------------------------------------------------------------------*/ + if (updatedAttributeValue is null) { + return; + } + else if (originalAttributeValue is not null) { + this[IndexOf(originalAttributeValue)] = updatedAttributeValue; + } + else { + Add(updatedAttributeValue); + } + + } + + /*========================================================================================================================== + | OVERRIDE: INSERT ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Intercepts all attempts to insert a new into the collection, to ensure that local + /// business logic is enforced. + /// + /// + /// + /// If a settable property is available corresponding to the , the call should be routed + /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is + /// set by the 's + /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. + /// + /// + /// Compared to the base implementation, will throw a specific error if a duplicate key + /// is inserted. This conveniently provides the name of the so it's clear what key is + /// being duplicated. + /// + /// + /// The location that the should be set. + /// The object which is being inserted. + /// + /// An AttributeValue with the Key '{item.Key}' already exists. The Value of the existing item is "{this[item.Key].Value}; + /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." + /// + protected override sealed void InsertItem(int index, AttributeValue item) { + Contract.Requires(item, nameof(item)); + if (_topicPropertyDispatcher.Enforce(item.Key, item)) { + if (!Contains(item.Key)) { + base.InsertItem(index, item); + if (DeletedAttributes.Contains(item.Key)) { + DeletedAttributes.Remove(item.Key); + } + } + else { + throw new ArgumentException( + $"An {nameof(AttributeValue)} with the Key '{item.Key}' already exists. The Value of the existing item is " + + $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(AttributeValue)}s are associated " + + $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'.", + nameof(item) + ); + } + } + } + + /*========================================================================================================================== + | OVERRIDE: SET ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Intercepts all attempts to update an in the collection, to ensure that local business + /// logic is enforced. + /// + /// + /// If a settable property is available corresponding to the , the call should be routed + /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is + /// set by the 's + /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. + /// + /// The location that the should be set. + /// The object which is being inserted. + protected override sealed void SetItem(int index, AttributeValue item) { + Contract.Requires(item, nameof(item)); + if (_topicPropertyDispatcher.Enforce(item.Key, item)) { + base.SetItem(index, item); + if (DeletedAttributes.Contains(item.Key)) { + DeletedAttributes.Remove(item.Key); + } + } + } + + /*========================================================================================================================== + | OVERRIDE: REMOVE ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Intercepts all attempts to remove an from the collection, to ensure that it is + /// appropriately marked as . + /// + /// + /// When an is removed, will return true—even if no remaining + /// s are marked as . + /// + protected override sealed void RemoveItem(int index) { + var attribute = this[index]; + DeletedAttributes.Add(attribute.Key); + base.RemoveItem(index); + } + + /*========================================================================================================================== + | OVERRIDE: CLEAR ITEMS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Intercepts all attempts to clear the , to ensure that it is appropriately marked + /// as . + /// + /// + /// When an is removed, will return true—even if no remaining + /// s are marked as . + /// + protected override sealed void ClearItems() { + DeletedAttributes.AddRange(Items.Select(a => a.Key)); + base.ClearItems(); + } + + /*========================================================================================================================== + | OVERRIDE: GET KEY FOR ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Method must be overridden for the EntityCollection to extract the keys from the items. + /// + /// The object from which to extract the key. + /// The key for the specified collection item. + protected override sealed string GetKeyForItem(AttributeValue item) { + Contract.Requires(item, "The item must be available in order to derive its key."); + return item.Key; + } + + } //Class +} //Namespace \ No newline at end of file From 69bde1a35a8ea307c8ed0b5d47f2cc6dd0310eed Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Feb 2021 16:00:12 -0800 Subject: [PATCH 542/778] Introduced empty constructor for use with generics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a subsequent update, we'll be generalizing the format of `AttributeValueCollection`, which will necessitate creating `TrackedItem`s without knowing anything about the _specific_ type. Unfortunately, a generic constraint can require an empty constructor, but not a constructor with a specific signature. To support this, an empty constructor is introduced—and, by necessity, the otherwise required `Key` property is moved to support nulls, while being annotated with `[NotNull]`. That suggests that while the property _can_ be null after _construction_, it should not be null after _initialization_. --- OnTopic/Collections/Specialized/TrackedItem{T}.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/OnTopic/Collections/Specialized/TrackedItem{T}.cs b/OnTopic/Collections/Specialized/TrackedItem{T}.cs index 01f9d9ff..a510bdf3 100644 --- a/OnTopic/Collections/Specialized/TrackedItem{T}.cs +++ b/OnTopic/Collections/Specialized/TrackedItem{T}.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; @@ -29,6 +30,14 @@ public abstract record TrackedItem { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + /// Constructs a new instance of a class. + /// + protected TrackedItem() { + LastModified = DateTime.UtcNow; + } + /// /// Constructs a new instance of a class. /// @@ -65,7 +74,8 @@ protected TrackedItem(string key, T value, bool isDirty = true, DateTime? lastMo /// exception="T:System.ArgumentException"> /// !value.Contains(" ") /// - public string Key { get; init; } + [NotNull] + public string? Key { get; init; } /*========================================================================================================================== | PROPERTY: VALUE From 7b1fdf1f5b5618c2f2a587bd624a3e859ac64e03 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 01:12:49 -0800 Subject: [PATCH 543/778] Introduced empty constructor for `AttributeValue` This builds off of the empty constructor added to `TrackedItem` (69bde1a), thus allowing the `AttributeValue` to be used as a `new()`able generic argument to the `TrackedCollection`. --- OnTopic/Attributes/AttributeValue.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index 0df53c51..f6cf9f86 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -44,6 +44,9 @@ public record AttributeValue: TrackedItem { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public AttributeValue(): base() { } + /// /// Initializes a new instance of the class, using the specified key/value pair. /// From 995b6fe3d720b423f11ad56bf425d37c67b68d6f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:23:13 -0800 Subject: [PATCH 544/778] Generalized `AttributeValueCollection` as generic `TrackedCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new `TrackedCollection<…>` serves the same basic function as `AttributeValueCollection` without being specific to `AttributeValue`. Namely, it provides keyed access to items which contain metadata for tracking their `IsDirty` and `Version` state, supports inheritance from both `Parent` and `BaseTopic`, and will attempt to enforce business logic via property settings if they're decorated with the appropriate attribute. By abstracting this out one level, this base class can centralize the functionality between both `AttributeValueCollection` (obviously) as well as `TopicReferenceDictionary`. This not only centralizes code, but also unifies the experience of working with these similar data types. In fact, previously, `TopicReferenceDictionary` was modeled more off of relationships, since the results are `Topic` instances, but in reality it operates more like `Topic`-valued attributes, so having consistent semantics and syntax makes more sense here. Finally, this will also allow us to extend the functionality of the `TopicReferenceDictionary` by supporting e.g. default values and inheritance from `Parent` when calling `GetValue()`. This isn't yet implemented; it will be updated to be a base class of `AttributeValueCollection` and `TopicReferenceDictionary` in subsequent commits. --- ...kedCollection{TItem,TValue,TAttribute}.cs} | 461 +++++++++--------- 1 file changed, 242 insertions(+), 219 deletions(-) rename OnTopic/Collections/Specialized/{TrackedCollection{TTrackedItem,TValue,TAttribute}.cs => TrackedCollection{TItem,TValue,TAttribute}.cs} (55%) diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs similarity index 55% rename from OnTopic/Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs rename to OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index b5c4e89a..0fae85a8 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TTrackedItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -8,108 +8,115 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; -using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Repositories; -namespace OnTopic.Attributes { +namespace OnTopic.Collections.Specialized { /*============================================================================================================================ - | CLASS: ATTRIBUTE VALUE COLLECTION + | CLASS: TRACKED COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Represents a collection of objects. + /// Represents a collection of records, along with methods "updating" those records and working + /// with their state. /// /// - /// objects represent individual instances of attributes associated with particular topics. - /// The class tracks these through its property, which is an instance of - /// the class. + /// records represent individual instances of values associated with a particular . The class tracks these through e.g. its property. The class provides a base class with methods for working with these + /// records, such as , for determining if a given record has been modified, or for creating or "updating" a record. (Records are + /// immutable, so updates actually involve cloning the record with updated values.) /// - public class AttributeValueCollection : KeyedCollection, ITrackDirtyKeys { + public abstract class TrackedCollection : + KeyedCollection, ITrackDirtyKeys + where TItem: TrackedItem, new() + where TAttribute: Attribute + where TValue : class + { /*========================================================================================================================== | DISPATCHER \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly TopicPropertyDispatcher _topicPropertyDispatcher; + private readonly TopicPropertyDispatcher _topicPropertyDispatcher; /*========================================================================================================================== - | PRIVATE VARIABLES + | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly Topic _associatedTopic; + /// + /// Initializes a new instance of the class. + /// + /// A reference to the topic that the current collection is bound to. + internal TrackedCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(parentTopic, nameof(parentTopic)); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set properties + \-----------------------------------------------------------------------------------------------------------------------*/ + AssociatedTopic = parentTopic; + _topicPropertyDispatcher = new(parentTopic); + + } /*========================================================================================================================== - | CONSTRUCTOR + | ASSOCIATED TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// The that the current collection is associated with. /// /// - /// The is intended exclusively for providing access to attributes via the - /// property. For this reason, the constructor is marked as internal. + /// This is used for operations that require inheritance or pass-through of properies in order to e.g. + /// enforce business logic. /// - /// A reference to the topic that the current attribute collection is bound to. - internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { - _associatedTopic = parentTopic; - _topicPropertyDispatcher = new(parentTopic); - } + protected Topic AssociatedTopic { get; init; } /*========================================================================================================================== - | PROPERTY: DELETED ATTRIBUTES + | PROPERTY: DELETED ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// When an attribute is deleted, keep track of it so that it can be marked for deletion when the topic is saved. + /// When a is deleted, keep track of it so that it can be marked for deletion when the is saved. /// /// /// As a performance enhancement, implementations will only save topics that are marked as - /// . If a is deleted, then it won't be marked as dirty. If no - /// other instances were modified, then the topic won't get saved, and that value won't be - /// deleted. Further more, the method has no way of - /// detecting the deletion of arbitrary attributes�i.e., attributes that were deleted which don't correspond to attributes - /// configured on the . By tracking any deleted attributes, we ensure both - /// scenarios can be accounted for. + /// . If a is deleted, then it won't be marked as . If no other instances were modified, then the won't get saved, and that won't be deleted. Further more, methods like the method have no way of detecting the deletion of arbitrary + /// values—i.e., attributes that were deleted which don't correspond to attributes configured on the . By tracking any deleted instances, we ensure both scenarios can + /// be accounted for. /// - internal List DeletedAttributes { get; } = new(); + internal List DeletedItems { get; } = new(); /*========================================================================================================================== | METHOD: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - public bool IsDirty() => IsDirty(false); - - /// - /// Determine if any attributes in the are dirty. - /// - /// - /// This method is intended primarily for data storage providers, such as , which may need - /// to determine if any attributes are dirty prior to saving them to the data storage medium. Be aware that this does - /// not track whether any have been modified; as such, it may still be necessary - /// to persist changes to the storage medium. - /// - /// - /// Optionally excludes s whose keys start with LastModified. This is useful for - /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically - /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. - /// - /// True if the attribute value is marked as dirty; otherwise false. - public bool IsDirty(bool excludeLastModified) - => DeletedAttributes.Count > 0 || Items.Any(a => - a.IsDirty && - (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) - ); + public virtual bool IsDirty() => DeletedItems.Count > 0 || Items.Any(a => a.IsDirty); /// - /// Determine if a given attribute is marked as dirty. Will return false if the attribute key cannot be found. + /// Determine if a given is marked as . Will return + /// false if the cannot be found in the collection. /// /// /// This method is intended primarily for data storage providers, such as , which may need - /// to determine if a specific attribute key is dirty prior to saving it to the data storage medium. Because IsDirty - /// is a state of the current , it does not support inheritFromParent or - /// inheritFromBase (which otherwise default to true). + /// to determine the state of a prior to saving it to + /// the data storage medium. Because is a state of the current , it does not support inheritFromParent or inheritFromBase (which otherwise default to + /// true). /// - /// The string identifier for the . - /// True if the attribute value is marked as dirty; otherwise false. + /// The string identifier for the . + /// + /// Returns true if the is marked as ; otherwise + /// false. + /// public bool IsDirty(string key) { if (!Contains(key)) { return false; @@ -125,50 +132,47 @@ public bool IsDirty(string key) { public void MarkClean() => MarkClean((DateTime?)null); /// - /// Marks the collection—including all items—as clean, meaning they have been persisted to - /// the underlying . + /// Marks the collection—including all instances—as clean, meaning they have been persisted + /// to the underlying . /// /// /// This method is intended primarily for data storage providers, such as , so that they can - /// mark the collection, and all items it contains, as clean. After this, will return false until any items are modified or removed. + /// mark the collection, and all instances it contains, as clean. After this, method will return false until any instances are added, modified, or + /// removed. /// /// - /// The value that the attributes were last saved. This corresponds to the . + /// The value that the was last saved. This corresponds to the . /// public void MarkClean(DateTime? version) { - foreach (var attribute in Items.Where(a => a.IsDirty).ToArray()) { - SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); + foreach (var trackedItem in Items.Where(a => a.IsDirty).ToArray()) { + SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); } - DeletedAttributes.Clear(); + DeletedItems.Clear(); } - /*========================================================================================================================== - | METHOD: MARK CLEAN - \-------------------------------------------------------------------------------------------------------------------------*/ - /// public void MarkClean(string key) => MarkClean(key, null); /// - /// Marks an individual as clean. + /// Marks an individual as clean. /// /// /// This method is intended primarily for data storage providers, such as , so that they can - /// mark an as clean. After this, will return false for + /// mark an as clean. After this, will return false for /// that item until it is modified. /// - /// The string identifier for the . + /// The string identifier for the . /// - /// The value that the attribute was last modified. This denotes the associated with the specific attribute. + /// The value that the was last saved. This corresponds to the . /// public void MarkClean(string key, DateTime? version) { if (Contains(key)) { - var attribute = this[key]; - if (attribute.IsDirty) { - SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); + var trackedItem = this[key]; + if (trackedItem.IsDirty) { + SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); } } } @@ -176,55 +180,59 @@ public void MarkClean(string key, DateTime? version) { /*========================================================================================================================== | METHOD: GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ + /// - /// Gets a named attribute from the Attributes dictionary. + /// Gets a from the collection based on the . /// - /// The string identifier for the . + /// The string identifier for the . /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// Boolean indicator nothing whether to recusrively search through s in order to get the value. /// - /// The string value for the Attribute. - [return: NotNull] - public string GetValue(string name, bool inheritFromParent = false) => GetValue(name, "", inheritFromParent); + /// The for the . + public TValue? GetValue(string key, bool inheritFromParent = false) => GetValue(key, null, inheritFromParent); /// - /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through base topics for values. + /// Gets a from the collection based on the with a specified + /// , an optional setting to enable , and an optional + /// setting for . /// - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// Boolean indicator nothing whether to recusrively search through s in order to get the value. /// /// /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. /// - /// The string value for the Attribute. + /// The for the . [return: NotNullIfNotNull("defaultValue")] - public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { - Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - return GetValue(name, defaultValue, inheritFromParent, (inheritFromBase? 5 : 0)); + public TValue? GetValue(string key, TValue? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { + Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); + return GetValue(key, defaultValue, inheritFromParent, inheritFromBase ? 5 : 0); } /// - /// Gets a named attribute from the Attributes dictionary with a specified default value and an optional number of - /// s through whom to crawl to retrieve an inherited value. + /// Gets a from the collection based on the with a specified + /// and an optional number of s through whom to crawl to + /// retrieve an inherited value. /// - /// The string identifier for the . - /// A string value to which to fall back in the case the value is not found. + /// The string identifier for the . + /// + /// A to which to fall back in the case the value is not found. + /// /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. + /// Boolean indicator nothing whether to recusrively search through s in order to get the value. /// /// The number of recursions to perform when attempting to get the value. - /// The string value for the Attribute. - /// - /// !String.IsNullOrWhiteSpace(name) + /// The value for the . + /// + /// !String.IsNullOrWhiteSpace(key) /// /// - /// !name.Contains(" ") + /// !key.Contains(" ") /// /// @@ -235,47 +243,54 @@ public void MarkClean(string key, DateTime? version) { /// maxHops <= 100 /// [return: NotNullIfNotNull("defaultValue")] - internal string? GetValue(string name, string? defaultValue, bool inheritFromParent, int maxHops) { + internal virtual TValue? GetValue(string key, TValue? defaultValue, bool inheritFromParent, int maxHops) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); + TopicFactory.ValidateKey(key, true); Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); - TopicFactory.ValidateKey(name); - string? value = null; + TValue? value = null; /*------------------------------------------------------------------------------------------------------------------------ - | Look up value from Attributes + | Look up value from collection \-----------------------------------------------------------------------------------------------------------------------*/ - if (Contains(name)) { - value = this[name]?.Value; + if (Contains(key)) { + value = this[key].Value; + } + + if (value is not null && value.ToString().Length == 0) { + value = null; } /*------------------------------------------------------------------------------------------------------------------------ | Look up value from topic pointer \-----------------------------------------------------------------------------------------------------------------------*/ if ( - String.IsNullOrEmpty(value) && - _associatedTopic.BaseTopic is not null && - maxHops > 0 + value is null && + maxHops > 0 && + BaseCollection is not null ) { - value = _associatedTopic.BaseTopic.Attributes.GetValue(name, null, false, maxHops - 1); + value = BaseCollection.GetValue(key, null, false, maxHops - 1); } /*------------------------------------------------------------------------------------------------------------------------ | Look up value from parent \-----------------------------------------------------------------------------------------------------------------------*/ - if (String.IsNullOrEmpty(value) && inheritFromParent && _associatedTopic.Parent is not null) { - value = _associatedTopic.Parent.Attributes.GetValue(name, defaultValue, inheritFromParent); + if ( + value is null && + inheritFromParent && + ParentCollection is not null + ) { + value = ParentCollection.GetValue(key, defaultValue, inheritFromParent); } /*------------------------------------------------------------------------------------------------------------------------ | Return value, if found \-----------------------------------------------------------------------------------------------------------------------*/ - if (!String.IsNullOrEmpty(value)) { + if (value is not null) { return value; } @@ -286,18 +301,40 @@ _associatedTopic.BaseTopic is not null && } + /*========================================================================================================================== + | METHOD: PARENT COLLECTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the corresponding on the , if available. + /// + protected abstract TrackedCollection? ParentCollection { get; } + + /*========================================================================================================================== + | METHOD: BASE COLLECTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a reference to the corresponding on the , if available. + /// + protected abstract TrackedCollection? BaseCollection { get; } + /*========================================================================================================================== | METHOD: SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// - /// Minimizes the need for defensive conditions throughout the library. + /// Working with records can be a bit cumbersome, and especially in determining if a value should be marked as , since that's based on a comparison with the previous value. The method handles this logic for implementers, while simultaneously allowing callers to + /// explicitly set whether the instances should be marked as dirty—via the parameter—and, optionally, what the should be. /// - /// The string identifier for the AttributeValue. - /// The text value for the AttributeValue. + /// The string identifier for the . + /// The text value for the . /// /// Specified whether the value should be marked as . By default, it will be marked as /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -305,99 +342,85 @@ _associatedTopic.BaseTopic is not null && /// persisted to the data store on . /// /// - /// The value that the attribute was last modified. This is intended exclusively for use when - /// populating the topic graph from a persistent data store as a means of indicating the current version for each - /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. + /// The value that the was last modified. This is intended exclusively + /// for use when populating the topic graph from a persistent data store as a means of indicating the current version for + /// each . This is used when e.g. importing values to determine if the existing value is newer + /// than the source value. /// - /// Determines if the attribute originated from an extended attributes data store. /// /// !String.IsNullOrWhiteSpace(key) /// /// - /// !String.IsNullOrWhiteSpace(value) - /// - /// /// !value.Contains(" ") /// - public void SetValue( + public virtual void SetValue( string key, - string? value, + TValue? value, bool? markDirty = null, - DateTime? version = null, - bool? isExtendedAttribute = null + DateTime? version = null ) - => SetValue(key, value, markDirty, true, version, isExtendedAttribute); + => SetValue(key, value, true, markDirty, version); /// - /// Protected helper method that either adds a new object or updates the value of an existing + /// Internal helper method that either adds a new object or updates the value of an existing /// one, depending on whether that value already exists. /// /// - /// When this overload is called, no attempt will be made to route the call through corresponding properties, if - /// available. As such, this is intended specifically to be called by internal properties as a means of avoiding a - /// feedback loop. + /// When the parameter is called, no attempt will be made to route the call + /// through the corresponding properties, if available. As such, this is intended specifically to be called by internal + /// properties as a means of avoiding the property being called again when a caller uses the property's setter directly. /// - /// The string identifier for the AttributeValue. - /// The text value for the AttributeValue. + /// The string identifier for the . + /// The text value for the . + /// + /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. + /// This should be set to false if setting items from internal properties in order to avoid an infinite loop. + /// /// /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// true if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// - /// - /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. - /// This should be set to false if setting attributes from internal properties in order to avoid an infinite loop. - /// /// /// The value that the attribute was last modified. This is intended exclusively for use when /// populating the topic graph from a persistent data store as a means of indicating the current version for each /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. /// - /// Determines if the attribute originated from an extended attributes data store. /// /// !String.IsNullOrWhiteSpace(key) /// /// - /// !String.IsNullOrWhiteSpace(value) - /// - /// /// !value.Contains(" ") /// - internal void SetValue( + public void SetValue( string key, - string? value, - bool? markDirty, + TValue? value, bool enforceBusinessLogic, - DateTime? version = null, - bool? isExtendedAttribute = null + bool? markDirty, + DateTime? version = null ) { /*------------------------------------------------------------------------------------------------------------------------ | Validate input \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); - TopicFactory.ValidateKey(key); + TopicFactory.ValidateKey(key, true); /*------------------------------------------------------------------------------------------------------------------------ - | Retrieve original attribute + | Retrieve original item \-----------------------------------------------------------------------------------------------------------------------*/ - AttributeValue? originalAttributeValue = null; + TItem? originalItem = null; if (Contains(key)) { - originalAttributeValue = this[key]; + originalItem = this[key]; } /*------------------------------------------------------------------------------------------------------------------------ @@ -406,32 +429,31 @@ internal void SetValue( | If the original values have already been applied, and SetValue() is being triggered a second time after enforcing | business logic, then use the original values, while applying any change in the value triggered by the business logic. \-----------------------------------------------------------------------------------------------------------------------*/ - if (_topicPropertyDispatcher.IsRegistered(key, out var updatedAttributeValue)) { - if (updatedAttributeValue.Value != value) { - updatedAttributeValue = updatedAttributeValue with { + if (_topicPropertyDispatcher.IsRegistered(key, out var updatedItem)) { + if (updatedItem.Value != value) { + updatedItem = updatedItem with { Value = value }; } } /*------------------------------------------------------------------------------------------------------------------------ - | Update existing attribute value + | Update existing item >-----------------------------------------------------------------------------------------------------------------------— - | Because AttributeValue is immutable, a new instance must be constructed to replace the previous version. + | Because TrackedItem is immutable, a new instance must be constructed to replace the previous version. \-----------------------------------------------------------------------------------------------------------------------*/ - else if (originalAttributeValue is not null) { - var markAsDirty = originalAttributeValue.IsDirty; + else if (originalItem is not null) { + var markAsDirty = originalItem.IsDirty; if (markDirty.HasValue) { markAsDirty = markDirty.Value; } - else if (originalAttributeValue.Value != value) { + else if (originalItem.Value != value) { markAsDirty = true; } - updatedAttributeValue = originalAttributeValue with { + updatedItem = originalItem with { Value = value, IsDirty = markAsDirty, - LastModified = version?? originalAttributeValue.LastModified, - IsExtendedAttribute = isExtendedAttribute?? originalAttributeValue.IsExtendedAttribute + LastModified = version?? originalItem.LastModified }; } @@ -442,14 +464,19 @@ internal void SetValue( | existing values, these are written to ensure that the collection is marked as IsDirty, thus allowing previous values to | be overwritten. Non-existent values, however, should simply be ignored. \-----------------------------------------------------------------------------------------------------------------------*/ - else if (String.IsNullOrEmpty(value)) { + else if (value is null || String.IsNullOrEmpty(value.ToString())) { } /*------------------------------------------------------------------------------------------------------------------------ - | Create new attribute value + | Create new item \-----------------------------------------------------------------------------------------------------------------------*/ else { - updatedAttributeValue = new AttributeValue(key, value, markDirty ?? true, version, isExtendedAttribute); + updatedItem = new TItem() { + Key = key, + Value = value, + IsDirty = markDirty ?? true, + LastModified = version?? DateTime.UtcNow + }; } /*------------------------------------------------------------------------------------------------------------------------ @@ -463,20 +490,20 @@ internal void SetValue( | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. \-----------------------------------------------------------------------------------------------------------------------*/ if (!enforceBusinessLogic) { - _topicPropertyDispatcher.Register(key, updatedAttributeValue); + _topicPropertyDispatcher.Register(key, updatedItem); } /*------------------------------------------------------------------------------------------------------------------------ - | Persist attribute value + | Persist item to collection \-----------------------------------------------------------------------------------------------------------------------*/ - if (updatedAttributeValue is null) { + if (updatedItem is null) { return; } - else if (originalAttributeValue is not null) { - this[IndexOf(originalAttributeValue)] = updatedAttributeValue; + else if (originalItem is not null) { + this[IndexOf(originalItem)] = updatedItem; } else { - Add(updatedAttributeValue); + Add(updatedItem); } } @@ -485,15 +512,13 @@ internal void SetValue( | OVERRIDE: INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to insert a new into the collection, to ensure that local + /// Intercepts all attempts to insert a new into the collection, to ensure that local /// business logic is enforced. /// /// /// /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is - /// set by the 's - /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. + /// through that to ensure local business logic is enforced, if it hasn't already been enforced. /// /// /// Compared to the base implementation, will throw a specific error if a duplicate key @@ -501,26 +526,26 @@ internal void SetValue( /// being duplicated. /// /// - /// The location that the should be set. - /// The object which is being inserted. + /// The location that the should be set. + /// The object which is being inserted. /// - /// An AttributeValue with the Key '{item.Key}' already exists. The Value of the existing item is "{this[item.Key].Value}; - /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." + /// An is thrown if an with the same as the already exists. /// - protected override sealed void InsertItem(int index, AttributeValue item) { + protected override void InsertItem(int index, TItem item) { Contract.Requires(item, nameof(item)); if (_topicPropertyDispatcher.Enforce(item.Key, item)) { if (!Contains(item.Key)) { base.InsertItem(index, item); - if (DeletedAttributes.Contains(item.Key)) { - DeletedAttributes.Remove(item.Key); + if (DeletedItems.Contains(item.Key)) { + DeletedItems.Remove(item.Key); } } else { throw new ArgumentException( - $"An {nameof(AttributeValue)} with the Key '{item.Key}' already exists. The Value of the existing item is " + - $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(AttributeValue)}s are associated " + - $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'.", + $"An {nameof(TItem)} with the Key '{item.Key}' already exists. The Value of the existing item is " + + $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(TItem)}s are associated " + + $"with the {nameof(Topic)} '{AssociatedTopic.GetUniqueKey()}'.", nameof(item) ); } @@ -531,23 +556,21 @@ protected override sealed void InsertItem(int index, AttributeValue item) { | OVERRIDE: SET ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to update an in the collection, to ensure that local business + /// Intercepts all attempts to update an in the collection, to ensure that local business /// logic is enforced. /// /// /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is - /// set by the 's - /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. + /// through that to ensure local business logic is enforced, if it hasn't already been enforced. /// - /// The location that the should be set. - /// The object which is being inserted. - protected override sealed void SetItem(int index, AttributeValue item) { + /// The location that the should be set. + /// The object which is being inserted. + protected override void SetItem(int index, TItem item) { Contract.Requires(item, nameof(item)); if (_topicPropertyDispatcher.Enforce(item.Key, item)) { base.SetItem(index, item); - if (DeletedAttributes.Contains(item.Key)) { - DeletedAttributes.Remove(item.Key); + if (DeletedItems.Contains(item.Key)) { + DeletedItems.Remove(item.Key); } } } @@ -556,16 +579,16 @@ protected override sealed void SetItem(int index, AttributeValue item) { | OVERRIDE: REMOVE ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to remove an from the collection, to ensure that it is - /// appropriately marked as . + /// Intercepts all attempts to remove an from the collection, to ensure that it is + /// appropriately marked as . /// /// - /// When an is removed, will return true—even if no remaining - /// s are marked as . + /// When an is removed, will return true—even if no remaining s are marked as . /// - protected override sealed void RemoveItem(int index) { - var attribute = this[index]; - DeletedAttributes.Add(attribute.Key); + protected override void RemoveItem(int index) { + var trackedItem = this[index]; + DeletedItems.Add(trackedItem.Key); base.RemoveItem(index); } @@ -573,15 +596,15 @@ protected override sealed void RemoveItem(int index) { | OVERRIDE: CLEAR ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to clear the , to ensure that it is appropriately marked - /// as . + /// Intercepts all attempts to clear the , to ensure that it is + /// appropriately marked as . /// /// - /// When an is removed, will return true—even if no remaining - /// s are marked as . + /// When an is removed, will return true—even if no remaining s are marked as . /// - protected override sealed void ClearItems() { - DeletedAttributes.AddRange(Items.Select(a => a.Key)); + protected override void ClearItems() { + DeletedItems.AddRange(Items.Select(a => a.Key)); base.ClearItems(); } @@ -593,7 +616,7 @@ protected override sealed void ClearItems() { /// /// The object from which to extract the key. /// The key for the specified collection item. - protected override sealed string GetKeyForItem(AttributeValue item) { + protected override sealed string GetKeyForItem(TItem item) { Contract.Requires(item, "The item must be available in order to derive its key."); return item.Key; } From beb3d1fa754dfa4c5dae8418dae783af25192f44 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:37:06 -0800 Subject: [PATCH 545/778] Derived `AttributeValueCollection` from the new `TrackedCollection` The `TrackedCollection` offers a generic foundation for `AttributeValueCollection` (995b6fe). As it centralizes much of the functionality, that allows us to remove most of this functionality from the `AttributeValueCollection`. The two pieces that remain needed are a) an overload to `SetValue()` that accepts `isExtendedAttribute`, and b) an overload of `IsDirty` that allows the exclusion of `LastModified` attributes (which are expected to change every update, and thus may not be an accurate reflection of actual changes to the data). There are a couple of breaking changes due to how the overloads work. Most notably, the overload of `SetValue()` that allows disabling `enforceBusinessLogic` no longer supports the `isExtendedAttribute`. That's because the `enforceBusinessLogic` overload is only needed when making a call from a property setter (via `[AttributeSetter]`), and won't have any need or awareness of `isExtendedAttribute`; it should instead simply maintain whatever preexisting value exists. In practice, this should only affects unit tests and the `AttributeValueCollection` extension methods. In addition, the internal `DeletedAttributes` property was renamed to `DeletedItems` to help generalize the functionality. Finally, the default value for `GetValue()` was changed from empty (`""`) to `null`. This isn't a preference, but there's no easy way to guarantee not null with with the generic version, unfortunately, so it's better to be explicit by requiring the "" as a default value if that's important. --- .../Attributes/AttributeValueCollection.cs | 505 +----------------- 1 file changed, 21 insertions(+), 484 deletions(-) diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index b5c4e89a..6338922b 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -4,13 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; -using OnTopic.Internal.Reflection; using OnTopic.Repositories; namespace OnTopic.Attributes { @@ -26,17 +21,7 @@ namespace OnTopic.Attributes { /// The class tracks these through its property, which is an instance of /// the class. /// - public class AttributeValueCollection : KeyedCollection, ITrackDirtyKeys { - - /*========================================================================================================================== - | DISPATCHER - \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly TopicPropertyDispatcher _topicPropertyDispatcher; - - /*========================================================================================================================== - | PRIVATE VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly Topic _associatedTopic; + public class AttributeValueCollection : TrackedCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -49,34 +34,26 @@ public class AttributeValueCollection : KeyedCollection, /// property. For this reason, the constructor is marked as internal. /// /// A reference to the topic that the current attribute collection is bound to. - internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { - _associatedTopic = parentTopic; - _topicPropertyDispatcher = new(parentTopic); + internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { } /*========================================================================================================================== - | PROPERTY: DELETED ATTRIBUTES + | PROPERTY: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// When an attribute is deleted, keep track of it so that it can be marked for deletion when the topic is saved. - /// - /// - /// As a performance enhancement, implementations will only save topics that are marked as - /// . If a is deleted, then it won't be marked as dirty. If no - /// other instances were modified, then the topic won't get saved, and that value won't be - /// deleted. Further more, the method has no way of - /// detecting the deletion of arbitrary attributes�i.e., attributes that were deleted which don't correspond to attributes - /// configured on the . By tracking any deleted attributes, we ensure both - /// scenarios can be accounted for. - /// - internal List DeletedAttributes { get; } = new(); + /// + protected override TrackedCollection? ParentCollection => + AssociatedTopic.Parent?.Attributes; /*========================================================================================================================== - | METHOD: IS DIRTY + | PROPERTY: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool IsDirty() => IsDirty(false); + protected override TrackedCollection? BaseCollection => + AssociatedTopic.BaseTopic?.Attributes; + + /*========================================================================================================================== + | METHOD: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Determine if any attributes in the are dirty. @@ -94,198 +71,11 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Ordin /// /// True if the attribute value is marked as dirty; otherwise false. public bool IsDirty(bool excludeLastModified) - => DeletedAttributes.Count > 0 || Items.Any(a => + => DeletedItems.Count > 0 || Items.Any(a => a.IsDirty && (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase)) ); - /// - /// Determine if a given attribute is marked as dirty. Will return false if the attribute key cannot be found. - /// - /// - /// This method is intended primarily for data storage providers, such as , which may need - /// to determine if a specific attribute key is dirty prior to saving it to the data storage medium. Because IsDirty - /// is a state of the current , it does not support inheritFromParent or - /// inheritFromBase (which otherwise default to true). - /// - /// The string identifier for the . - /// True if the attribute value is marked as dirty; otherwise false. - public bool IsDirty(string key) { - if (!Contains(key)) { - return false; - } - return this[key].IsDirty; - } - - /*========================================================================================================================== - | METHOD: MARK CLEAN - \-------------------------------------------------------------------------------------------------------------------------*/ - - /// - public void MarkClean() => MarkClean((DateTime?)null); - - /// - /// Marks the collection—including all items—as clean, meaning they have been persisted to - /// the underlying . - /// - /// - /// This method is intended primarily for data storage providers, such as , so that they can - /// mark the collection, and all items it contains, as clean. After this, will return false until any items are modified or removed. - /// - /// - /// The value that the attributes were last saved. This corresponds to the . - /// - public void MarkClean(DateTime? version) { - foreach (var attribute in Items.Where(a => a.IsDirty).ToArray()) { - SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); - } - DeletedAttributes.Clear(); - } - - /*========================================================================================================================== - | METHOD: MARK CLEAN - \-------------------------------------------------------------------------------------------------------------------------*/ - - /// - public void MarkClean(string key) => MarkClean(key, null); - - /// - /// Marks an individual as clean. - /// - /// - /// This method is intended primarily for data storage providers, such as , so that they can - /// mark an as clean. After this, will return false for - /// that item until it is modified. - /// - /// The string identifier for the . - /// - /// The value that the attribute was last modified. This denotes the associated with the specific attribute. - /// - public void MarkClean(string key, DateTime? version) { - if (Contains(key)) { - var attribute = this[key]; - if (attribute.IsDirty) { - SetValue(attribute.Key, attribute.Value, false, false, version?? DateTime.UtcNow); - } - } - } - - /*========================================================================================================================== - | METHOD: GET VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets a named attribute from the Attributes dictionary. - /// - /// The string identifier for the . - /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. - /// - /// The string value for the Attribute. - [return: NotNull] - public string GetValue(string name, bool inheritFromParent = false) => GetValue(name, "", inheritFromParent); - - /// - /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling - /// of inheritance, and an optional setting for searching through base topics for values. - /// - /// The string identifier for the . - /// A string value to which to fall back in the case the value is not found. - /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. - /// - /// - /// Boolean indicator nothing whether to search through any of the topic's topics in - /// order to get the value. - /// - /// The string value for the Attribute. - [return: NotNullIfNotNull("defaultValue")] - public string? GetValue(string name, string? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { - Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - return GetValue(name, defaultValue, inheritFromParent, (inheritFromBase? 5 : 0)); - } - - /// - /// Gets a named attribute from the Attributes dictionary with a specified default value and an optional number of - /// s through whom to crawl to retrieve an inherited value. - /// - /// The string identifier for the . - /// A string value to which to fall back in the case the value is not found. - /// - /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. - /// - /// The number of recursions to perform when attempting to get the value. - /// The string value for the Attribute. - /// - /// !String.IsNullOrWhiteSpace(name) - /// - /// - /// !name.Contains(" ") - /// - /// - /// maxHops >= 0 - /// - /// - /// maxHops <= 100 - /// - [return: NotNullIfNotNull("defaultValue")] - internal string? GetValue(string name, string? defaultValue, bool inheritFromParent, int maxHops) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate contracts - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name)); - Contract.Requires(maxHops >= 0, "The maximum number of hops should be a positive number."); - Contract.Requires(maxHops <= 100, "The maximum number of hops should not exceed 100."); - TopicFactory.ValidateKey(name); - - string? value = null; - - /*------------------------------------------------------------------------------------------------------------------------ - | Look up value from Attributes - \-----------------------------------------------------------------------------------------------------------------------*/ - if (Contains(name)) { - value = this[name]?.Value; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Look up value from topic pointer - \-----------------------------------------------------------------------------------------------------------------------*/ - if ( - String.IsNullOrEmpty(value) && - _associatedTopic.BaseTopic is not null && - maxHops > 0 - ) { - value = _associatedTopic.BaseTopic.Attributes.GetValue(name, null, false, maxHops - 1); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Look up value from parent - \-----------------------------------------------------------------------------------------------------------------------*/ - if (String.IsNullOrEmpty(value) && inheritFromParent && _associatedTopic.Parent is not null) { - value = _associatedTopic.Parent.Attributes.GetValue(name, defaultValue, inheritFromParent); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Return value, if found - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!String.IsNullOrEmpty(value)) { - return value; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Finally, return default - \-----------------------------------------------------------------------------------------------------------------------*/ - return defaultValue; - - } - /*========================================================================================================================== | METHOD: SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ @@ -331,271 +121,18 @@ public void SetValue( bool? markDirty = null, DateTime? version = null, bool? isExtendedAttribute = null - ) - => SetValue(key, value, markDirty, true, version, isExtendedAttribute); - - /// - /// Protected helper method that either adds a new object or updates the value of an existing - /// one, depending on whether that value already exists. - /// - /// - /// When this overload is called, no attempt will be made to route the call through corresponding properties, if - /// available. As such, this is intended specifically to be called by internal properties as a means of avoiding a - /// feedback loop. - /// - /// The string identifier for the AttributeValue. - /// The text value for the AttributeValue. - /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is - /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being - /// persisted to the data store on . - /// - /// - /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. - /// This should be set to false if setting attributes from internal properties in order to avoid an infinite loop. - /// - /// - /// The value that the attribute was last modified. This is intended exclusively for use when - /// populating the topic graph from a persistent data store as a means of indicating the current version for each - /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. - /// - /// Determines if the attribute originated from an extended attributes data store. - /// - /// !String.IsNullOrWhiteSpace(key) - /// - /// - /// !String.IsNullOrWhiteSpace(value) - /// - /// - /// !value.Contains(" ") - /// - internal void SetValue( - string key, - string? value, - bool? markDirty, - bool enforceBusinessLogic, - DateTime? version = null, - bool? isExtendedAttribute = null ) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate input - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); - TopicFactory.ValidateKey(key); - - /*------------------------------------------------------------------------------------------------------------------------ - | Retrieve original attribute - \-----------------------------------------------------------------------------------------------------------------------*/ - AttributeValue? originalAttributeValue = null; - + base.SetValue(key, value, markDirty, version); if (Contains(key)) { - originalAttributeValue = this[key]; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Update from business logic - >-----------------------------------------------------------------------------------------------------------------------— - | If the original values have already been applied, and SetValue() is being triggered a second time after enforcing - | business logic, then use the original values, while applying any change in the value triggered by the business logic. - \-----------------------------------------------------------------------------------------------------------------------*/ - if (_topicPropertyDispatcher.IsRegistered(key, out var updatedAttributeValue)) { - if (updatedAttributeValue.Value != value) { - updatedAttributeValue = updatedAttributeValue with { - Value = value + var attributeValue = this[key]; + var attributeIndex = IndexOf(attributeValue); + if (isExtendedAttribute is not null && isExtendedAttribute != attributeValue.IsExtendedAttribute) { + attributeValue = attributeValue with { + IsExtendedAttribute = isExtendedAttribute }; + base[attributeIndex] = attributeValue; } } - - /*------------------------------------------------------------------------------------------------------------------------ - | Update existing attribute value - >-----------------------------------------------------------------------------------------------------------------------— - | Because AttributeValue is immutable, a new instance must be constructed to replace the previous version. - \-----------------------------------------------------------------------------------------------------------------------*/ - else if (originalAttributeValue is not null) { - var markAsDirty = originalAttributeValue.IsDirty; - if (markDirty.HasValue) { - markAsDirty = markDirty.Value; - } - else if (originalAttributeValue.Value != value) { - markAsDirty = true; - } - updatedAttributeValue = originalAttributeValue with { - Value = value, - IsDirty = markAsDirty, - LastModified = version?? originalAttributeValue.LastModified, - IsExtendedAttribute = isExtendedAttribute?? originalAttributeValue.IsExtendedAttribute - }; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Ignore if null - >------------------------------------------------------------------------------------------------------------------------- - | ###NOTE JJC20200501: Null or empty values are treated as deletions, and are not persisted to the data store. With - | existing values, these are written to ensure that the collection is marked as IsDirty, thus allowing previous values to - | be overwritten. Non-existent values, however, should simply be ignored. - \-----------------------------------------------------------------------------------------------------------------------*/ - else if (String.IsNullOrEmpty(value)) { - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Create new attribute value - \-----------------------------------------------------------------------------------------------------------------------*/ - else { - updatedAttributeValue = new AttributeValue(key, value, markDirty ?? true, version, isExtendedAttribute); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Register that business logic has already been enforced - >------------------------------------------------------------------------------------------------------------------------- - | We want to ensure that any attempt to set references that have corresponding (writable) properties use those properties, - | thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by ICollection, and not - | just SetValue, the underlying interceptors (e.g., InsertItem, SetItem) call the Enforce() method. If it returns false, - | they assume the property set the value (e.g., by calling the internal SetValue method with enforceBusinessLogic set to - | false). Otherwise, the corresponding property will be called. The Register() method thus avoids a redirect loop in this - | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!enforceBusinessLogic) { - _topicPropertyDispatcher.Register(key, updatedAttributeValue); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Persist attribute value - \-----------------------------------------------------------------------------------------------------------------------*/ - if (updatedAttributeValue is null) { - return; - } - else if (originalAttributeValue is not null) { - this[IndexOf(originalAttributeValue)] = updatedAttributeValue; - } - else { - Add(updatedAttributeValue); - } - - } - - /*========================================================================================================================== - | OVERRIDE: INSERT ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Intercepts all attempts to insert a new into the collection, to ensure that local - /// business logic is enforced. - /// - /// - /// - /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is - /// set by the 's - /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. - /// - /// - /// Compared to the base implementation, will throw a specific error if a duplicate key - /// is inserted. This conveniently provides the name of the so it's clear what key is - /// being duplicated. - /// - /// - /// The location that the should be set. - /// The object which is being inserted. - /// - /// An AttributeValue with the Key '{item.Key}' already exists. The Value of the existing item is "{this[item.Key].Value}; - /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'." - /// - protected override sealed void InsertItem(int index, AttributeValue item) { - Contract.Requires(item, nameof(item)); - if (_topicPropertyDispatcher.Enforce(item.Key, item)) { - if (!Contains(item.Key)) { - base.InsertItem(index, item); - if (DeletedAttributes.Contains(item.Key)) { - DeletedAttributes.Remove(item.Key); - } - } - else { - throw new ArgumentException( - $"An {nameof(AttributeValue)} with the Key '{item.Key}' already exists. The Value of the existing item is " + - $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(AttributeValue)}s are associated " + - $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'.", - nameof(item) - ); - } - } - } - - /*========================================================================================================================== - | OVERRIDE: SET ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Intercepts all attempts to update an in the collection, to ensure that local business - /// logic is enforced. - /// - /// - /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is - /// set by the 's - /// enforceBusinessLogic parameter. To avoid an infinite loop, internal setters must call this overload. - /// - /// The location that the should be set. - /// The object which is being inserted. - protected override sealed void SetItem(int index, AttributeValue item) { - Contract.Requires(item, nameof(item)); - if (_topicPropertyDispatcher.Enforce(item.Key, item)) { - base.SetItem(index, item); - if (DeletedAttributes.Contains(item.Key)) { - DeletedAttributes.Remove(item.Key); - } - } - } - - /*========================================================================================================================== - | OVERRIDE: REMOVE ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Intercepts all attempts to remove an from the collection, to ensure that it is - /// appropriately marked as . - /// - /// - /// When an is removed, will return true—even if no remaining - /// s are marked as . - /// - protected override sealed void RemoveItem(int index) { - var attribute = this[index]; - DeletedAttributes.Add(attribute.Key); - base.RemoveItem(index); - } - - /*========================================================================================================================== - | OVERRIDE: CLEAR ITEMS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Intercepts all attempts to clear the , to ensure that it is appropriately marked - /// as . - /// - /// - /// When an is removed, will return true—even if no remaining - /// s are marked as . - /// - protected override sealed void ClearItems() { - DeletedAttributes.AddRange(Items.Select(a => a.Key)); - base.ClearItems(); - } - - /*========================================================================================================================== - | OVERRIDE: GET KEY FOR ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Method must be overridden for the EntityCollection to extract the keys from the items. - /// - /// The object from which to extract the key. - /// The key for the specified collection item. - protected override sealed string GetKeyForItem(AttributeValue item) { - Contract.Requires(item, "The item must be available in order to derive its key."); - return item.Key; } } //Class From b5a0a0b9956e1653b981090edda9f060359f3bbf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:39:48 -0800 Subject: [PATCH 546/778] Changed reference to `DeletedAttributes` to `DeletedItems` In generalizing the base logic from `AttributeValueCollection` into `TrackedCollection`, the internal property `DeletedAttributes` was renamed to the more general `DeletedItems` (995b6fe). As this is now the base class for `AttributeValueCollection`, references to this need to be updated to reflect that change. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 2 +- OnTopic/Repositories/TopicRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 2a357541..c07e35ed 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -309,7 +309,7 @@ public void Clear_ExistingValues_IsDirty() { topic.Attributes.Clear(); Assert.IsTrue(topic.Attributes.IsDirty()); - Assert.IsTrue(topic.Attributes.DeletedAttributes.Contains("Foo")); + Assert.IsTrue(topic.Attributes.DeletedItems.Contains("Foo")); } diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index e336cf71..28b3f9fd 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -816,7 +816,7 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { var attributeKeys = topic.Attributes .Where(a => String.IsNullOrEmpty(a.Value)) .Select(a => a.Key) - .Union(topic.Attributes.DeletedAttributes); + .Union(topic.Attributes.DeletedItems); foreach (var attributeKey in attributeKeys) { if (!attributes.Contains(attributeKey)) { attributes.Add((AttributeDescriptor)TopicFactory.Create(attributeKey, "TextAttributeDescriptor")); From cf60d15eec03b64430b795c0da722cc90f4ec6b6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:45:59 -0800 Subject: [PATCH 547/778] Updated calls to `GetValue()` to accept `null` default Previously, `AttributeValueCollection.GetValue()` returned an empty string as the default. This is difficult to maintain with any guarantee of `[NotNull]` while generalizing the base `TrackedCollection`. As a result, if `[NotNull]` is needed, callers should now explicitly pass "" as the `defaultValue`, or otherwise handle potential `null` returns. This update corrects callers to make one of these two adjustments. --- OnTopic.Tests/Entities/CustomTopic.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 8a92653b..41cbf6f6 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -34,7 +34,7 @@ public CustomTopic(string key, string contentType, Topic? parent = null, int id /// Provides a text property which is intended to be mapped to a text attribute. /// [AttributeSetter] - public string TextAttribute { + public string? TextAttribute { get => Attributes.GetValue("TextAttribute"); set => SetAttributeValue("TextAttribute", value); } diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index ec0364b2..3738d18d 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -188,7 +188,7 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, strin \-----------------------------------------------------------------------------------------------------------------------*/ return topic.FindAll(t => !String.IsNullOrEmpty(t.Attributes.GetValue(attributeKey)) && - t.Attributes.GetValue(attributeKey).Contains(attributeValue, StringComparison.OrdinalIgnoreCase) + t.Attributes.GetValue(attributeKey, "").Contains(attributeValue, StringComparison.OrdinalIgnoreCase) ); } From d72f875f27f5cca9dd38d5dec2ef38f83a6e141d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:54:59 -0800 Subject: [PATCH 548/778] Removed explicit setting of `enforceBusinessLogic` Previously, the `enforceBusinessLogic` parameter was explicitly set. The parameter order of the internal `SetValue()` overload has changed such that the order of the last two parameters would need to be flipped. But, there's no reason to call the overload here; the public overloads make more sense. In that case, simply removing the `enforceBusinessLogic` parameter ensures the internal overload is called, and that the `enforceBusinessLogic` continues to default to `true`. --- OnTopic/Attributes/AttributeValueCollectionExtensions.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index cfc8d1dd..e4490904 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -208,7 +208,7 @@ public static void SetBoolean( string key, bool value, bool? isDirty = null - ) => attributes?.SetValue(key, value ? "1" : "0", isDirty, true); + ) => attributes?.SetValue(key, value ? "1" : "0", isDirty); /*========================================================================================================================== | METHOD: SET INTEGER @@ -249,7 +249,7 @@ public static void SetInteger( ) => attributes?.SetValue( key, value.ToString(CultureInfo.InvariantCulture), - isDirty, true + isDirty ); /*========================================================================================================================== @@ -291,7 +291,7 @@ public static void SetDouble( ) => attributes?.SetValue( key, value.ToString(CultureInfo.InvariantCulture), - isDirty, true + isDirty ); /*========================================================================================================================== @@ -333,8 +333,7 @@ public static void SetDateTime( ) => attributes?.SetValue( key, value.ToString(CultureInfo.InvariantCulture), - isDirty, - true + isDirty ); } //Class From 16031757956941c110bb32e68ab911e97ba00366 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 12:57:55 -0800 Subject: [PATCH 549/778] Reverted `SetValue()` with `enforceBusinessLogic` to `internal` This was originally set to `internal`, but had been temporarily flipped to `public` while troubleshooting an overload resolution issue. It was inadvertantly committed as such (995b6fe). Whoops! We don't want this to be publicly accessible. --- .../Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index 0fae85a8..9fa02a64 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -401,7 +401,7 @@ public virtual void SetValue( /// exception="T:System.ArgumentException"> /// !value.Contains(" ") /// - public void SetValue( + internal void SetValue( string key, TValue? value, bool enforceBusinessLogic, From f5ce8372f1a4b5f6bec977b816da3ee58fdf3e99 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 13:07:27 -0800 Subject: [PATCH 550/778] Reverted the parameter order of `enforceBusinessLogic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are reasonable arguments for changing the parameter order of the internal `SetValue()` overload—as I did during the generalization of the `AttributeValueCollection` into `TrackedCollection` (995b6fe)—but, ultimately, the gains are marginal while the confusion is more significant, since a) the difference might not be registered by the compiler due to the two parameters both being Booleans (assuming the `isDirty` value isn't nullable), and b) this has long been the current order, and is thus familiar to internal developers. Ultimately, it doesn't matter _too_ much, since it's an `internal` method, but to avoid potential confusion I'd rather keep it as is. --- .../Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index 9fa02a64..d6473784 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -363,7 +363,7 @@ public virtual void SetValue( bool? markDirty = null, DateTime? version = null ) - => SetValue(key, value, true, markDirty, version); + => SetValue(key, value, markDirty, true, version); /// /// Internal helper method that either adds a new object or updates the value of an existing @@ -404,8 +404,8 @@ public virtual void SetValue( internal void SetValue( string key, TValue? value, - bool enforceBusinessLogic, bool? markDirty, + bool enforceBusinessLogic, DateTime? version = null ) { From 82b903fc3808bfc764cc33fdf7a33c976ad54501 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 13:31:39 -0800 Subject: [PATCH 551/778] Introduce concrete `TrackedItem` class for topic references This will allow us to use the `TrackedCollection<>` as a base class for `TopicReferenceDictionary`. --- OnTopic/References/TopicReference.cs | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 OnTopic/References/TopicReference.cs diff --git a/OnTopic/References/TopicReference.cs b/OnTopic/References/TopicReference.cs new file mode 100644 index 00000000..71650d4e --- /dev/null +++ b/OnTopic/References/TopicReference.cs @@ -0,0 +1,80 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Collections.Specialized; +using OnTopic.Metadata; +using OnTopic.Repositories; + +namespace OnTopic.References { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCE + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents the immutable value of a particular topic reference on a . + /// + /// + /// + /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. + /// + /// + /// Typically, the will be exposed as part of a via + /// the collection. + /// + /// + /// Be aware that while represents the value of a specific topic reference, the metadata for + /// describing the purpose, constraints, and usage of that particular attribute is described by the class. + /// + /// + /// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either + /// create a new instance of the class or, preferably, call the 's method. + /// + /// + public record TopicReference: TrackedItem { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public TopicReference(): base() { } + + /// + /// Initializes a new instance of the class, using the specified key/value pair. + /// + /// + /// The string identifier for the collection item key/value pair. + /// + /// + /// The string value text for the collection item key/value pair. + /// + /// + /// An optional boolean indicator noting whether the collection item is a new value, and + /// should thus be saved to the database when is next called. + /// + /// + /// The value that the attribute was last modified. This is intended primarily for use when + /// populating the topic graph from a persistent data store as a means of indicating the current version for each + /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. + /// + /// + /// !String.IsNullOrWhiteSpace(key) + /// + public TopicReference( + string key, + Topic value, + bool isDirty = true, + DateTime? lastModified = null + ): base(key, value, isDirty, lastModified) { + + } + + } //Class +} //Namespace \ No newline at end of file From 400786d9cfd5fc415b5e6bbc1d826a9113d57e20 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 13:36:16 -0800 Subject: [PATCH 552/778] Updated XML Docs to point to base `TrackedCollection` as appropriate XML Documents don't honor class inheritance. As a result, references to members must point to the class where they're derived from. Since most of `AttributeValueCollection`'s members have been moved to the new `TrackedCollection<>` abstraction, these XML docs must be updated to references that collection. This makes them quite a bit longer and, unfortunately, requires a lot more (re)wrapping. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 8 ++++---- OnTopic/Attributes/AttributeSetterAttribute.cs | 13 +++++++------ .../Reflection/TopicPropertyDispatcher.cs | 16 ++++++++-------- .../Mapping/Annotations/AttributeKeyAttribute.cs | 4 ++-- OnTopic/Mapping/Annotations/InheritAttribute.cs | 14 +++++++------- .../Mapping/Internal/PropertyConfiguration.cs | 5 +++-- OnTopic/Mapping/TopicMappingService.cs | 7 ++++--- OnTopic/Topic.cs | 15 ++++++++------- 8 files changed, 43 insertions(+), 39 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index c07e35ed..560535c0 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -416,7 +416,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { /// /// Populates the with a and then deletes it. Confirms /// that the returns the new version after calling . + /// TrackedCollection{TItem, TValue, TAttribute}.MarkClean(DateTime?)"/>. /// [TestMethod] public void IsDirty_MarkClean_UpdatesLastModified() { @@ -438,7 +438,7 @@ public void IsDirty_MarkClean_UpdatesLastModified() { /// /// Populates the with a and then deletes it. Confirms /// that returns false after calling . + /// TrackedCollection{TItem, TValue, TAttribute}.MarkClean(DateTime?)"/>. /// [TestMethod] public void IsDirty_MarkClean_ReturnsFalse() { @@ -461,8 +461,8 @@ public void IsDirty_MarkClean_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a and then confirms that returns false for that attribute after calling . + /// cref="TrackedCollection{TItem, TValue, TAttribute}.IsDirty(String)"/> returns false for that attribute after + /// calling . /// [TestMethod] public void IsDirty_MarkAttributeClean_ReturnsFalse() { diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs index 973a4337..4c6bce50 100644 --- a/OnTopic/Attributes/AttributeSetterAttribute.cs +++ b/OnTopic/Attributes/AttributeSetterAttribute.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using OnTopic.Collections; +using OnTopic.Collections.Specialized; namespace OnTopic.Attributes { @@ -33,12 +34,12 @@ namespace OnTopic.Attributes { /// /// /// To ensure this logic, it is critical that implementers of ensure that the - /// property setters call overload with the final parameter set to false to disable the enforcement of business logic. Otherwise, - /// an infinite loop will occur. Calling that overload tells that the business - /// logic has already been enforced by the caller. As this is an internal overload, implementers should use the local - /// proxy at , which ensures that final parameter is set to - /// false. + /// property setters call overload with the final parameter set to false to disable the enforcement of business + /// logic. Otherwise, an infinite loop will occur. Calling that overload tells that + /// the business logic has already been enforced by the caller. As this is an internal overload, implementers should use + /// the local proxy at , which ensures that final parameter + /// is set to false. /// /// [AttributeUsage(AttributeTargets.Property)] diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 544c5f1b..0a5f3160 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -78,11 +78,11 @@ namespace OnTopic.Internal.Reflection { /// cref="Register(String, TValueType?)"/> method will not have been called. To mitigate the property setter getting /// called twice, collection implementors are advised to offer an internal overload that allows an item to be added to the /// collection while bypassing the business logic. For instance, this can be done using or ; in each case, the internally accessible - /// enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this is done by - /// calling , thus assuring that the - /// business logic has already occurred. + /// TrackedCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, Boolean, DateTime?)"/> or ; in each case, + /// the internally accessible enforceBusinessLogic parameter allows a property setter to disable business logic. + /// Internally, this is done by calling , thus assuring that the business logic has already occurred. /// /// internal class TopicPropertyDispatcher @@ -165,9 +165,9 @@ internal TopicPropertyDispatcher(Topic associatedTopic) { /// themselves to bypass business logic, thus preventing them from being called twice. These methods should be marked /// internal to prevent external actors from bypassing the business logic; the purpose is to confirm that the business /// logic has already been enforced, not to make the business logic optional. Two examples of this are the internal - /// enforceBusinessLogic parameters on and . + /// enforceBusinessLogic parameters on and . /// /// /// It's worth noting that any calls to are invalidated the next time /// Flags that a property should be mapped to a specific attributeKey in when calling . + /// cref="TrackedCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)"/>. /// /// /// By default, implementations will attempt to map the property of the target data diff --git a/OnTopic/Mapping/Annotations/InheritAttribute.cs b/OnTopic/Mapping/Annotations/InheritAttribute.cs index 96909951..787415a5 100644 --- a/OnTopic/Mapping/Annotations/InheritAttribute.cs +++ b/OnTopic/Mapping/Annotations/InheritAttribute.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Attributes; +using OnTopic.Collections.Specialized; namespace OnTopic.Mapping.Annotations { @@ -13,14 +13,14 @@ namespace OnTopic.Mapping.Annotations { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Flags that a property should be inherit its value from when calling . + /// cref="TrackedCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)"/>. /// /// - /// By default, implementations will call with the inheritFromParent parameter set to its default - /// of false. This attribute instructs it to instead set that parameter to true, which in turn causes the - /// to crawl up the tree until a value is found. This is - /// useful when an attribute is expected to be inherited by all child topics. + /// By default, implementations will call with the inheritFromParent parameter set to its default of false. + /// This attribute instructs it to instead set that parameter to true, which in turn causes the to crawl up the tree until a value is found. + /// This is useful when an attribute is expected to be inherited by all child topics. /// [System.AttributeUsage(System.AttributeTargets.Property)] public sealed class InheritAttribute : System.Attribute { diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 6332cdeb..b322a7bd 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Reflection; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Annotations; @@ -221,8 +222,8 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// The configuration is only applicable if the value is pulled from the collection. This is the equivalent to calling the method with an InheritFromParent parameter set to - /// True. + /// cref="TrackedCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)"/> method with an InheritFromParent + /// parameter set to True. /// /// /// The property corresponds to the being set on a given diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 5c23290c..db56ab3a 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading.Tasks; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Internal.Reflection; using OnTopic.Lookup; @@ -356,9 +357,9 @@ await MapAsync( /// cref="String"/>, , , or , the method will attempt to set the property on the based on, in order, the 's Get{Property}() method, {Property} - /// property, and, finally, its collection (using ). If the property is not of a settable type, - /// or the source value cannot be identified on the , then the property is not set. + /// property, and, finally, its collection (using ). If the property is not of a settable type, or the source value + /// cannot be identified on the , then the property is not set. /// /// The source from which to pull the value. /// The target DTO on which to set the property value. diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 31e5e190..577461a0 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -620,13 +620,13 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// /// Base topics allow attribute values to be inherited from another topic. When a is configured /// as a BaseTopic , values from that are used when the method is unable to find a local value for the - /// attribute. + /// cref="TrackedCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)" /> method is unable to find a local + /// value for the attribute. /// /// - /// Be aware that while multiple levels of s can be configured, the method defaults to a maximum level of five "hops" in - /// order to help avoid an infinite loop. + /// Be aware that while multiple levels of s can be configured, the method defaults to a maximum level of five "hops" in order + /// to help avoid an infinite loop. /// /// /// The underlying value of the is stored as a topic reference with the . This is intended to enforce local business logic, and prevent /// callers from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the /// that the business logic has already been enforced. To do that, they must either - /// call with the - /// enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this overload. + /// call + /// with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this + /// overload. /// /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. From 74d5907a6da5424b4603c731f898b64b9cd7e326 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 13:51:26 -0800 Subject: [PATCH 553/778] Derived `TopicReferenceCollection` from the new `TrackedCollection` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TrackedCollection` offers a generic foundation for `TopicReferenceCollection` (995b6fe). As it centralizes much of the functionality, that allows us to remove much of this functionality from the `TopicReferenceCollection`. The only pieces that remain needed are a) the `IsFullyLoaded` property for tracking whether references were able to be merged into the local topic graph during load, and b) the protect overrides (i.e.., `InsertItem()`, `SetItem()`, `RemoveItem()`, and `ClearItems()`) so that we can handle the recipricol relationships (via `Topic.IncomingRelationships`) that's specific to the topic references, but not other tracked collections. There are a couple of breaking changes due to generalizing the nomenclature. Most notably, `GetTopic()` and `SetTopic()` have been renamed to `GetValue()` and `SetValue()`. As this refers specifically to the `TrackedItem.Value` field, that isn't unreasonable—though it does remove the hint that this is setting or getting a `Topic` specifically. Finally, as this now derives from `KeyedCollection<>` instead of implementing `IDictionary<>`, a number of dictionary-specific methods have been removed, and the semantics are updated slightly. Most notably, the indexer no longer supports setting the value. As part of this, I also renamed it from `TopicReferenceDictionary` to `TopicReferenceCollection` to better articulate this change in semantics. As the `TopicReferenceCollection` is new to OnTopic 5.x, these changes aren't expected to have any impact on downstream implementations—outside of OnTopic Editor, which is working off a preview release of OnTopic 5.x—and thus shouldn't have any impact on customers. The next few commits will resolve the breaking changes within the OnTopic library itself. --- ...est.cs => TopicReferenceCollectionTest.cs} | 5 +- .../References/TopicReferenceCollection.cs | 149 +++++++ .../References/TopicReferenceDictionary.cs | 390 ------------------ OnTopic/Topic.cs | 2 +- 4 files changed, 153 insertions(+), 393 deletions(-) rename OnTopic.Tests/{TopicReferenceDictionaryTest.cs => TopicReferenceCollectionTest.cs} (99%) create mode 100644 OnTopic/References/TopicReferenceCollection.cs delete mode 100644 OnTopic/References/TopicReferenceDictionary.cs diff --git a/OnTopic.Tests/TopicReferenceDictionaryTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs similarity index 99% rename from OnTopic.Tests/TopicReferenceDictionaryTest.cs rename to OnTopic.Tests/TopicReferenceCollectionTest.cs index ef54a69a..8e3af372 100644 --- a/OnTopic.Tests/TopicReferenceDictionaryTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -7,11 +7,12 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.References; using OnTopic.Tests.Entities; +using OnTopic.Collections.Specialized; namespace OnTopic.Tests { /*============================================================================================================================ - | CLASS: TOPIC REFERENCE DICTIONARY TEST + | CLASS: TOPIC REFERENCE COLLECTION TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the , with a particular emphasis on the custom features @@ -20,7 +21,7 @@ namespace OnTopic.Tests { /// values in the property. /// [TestClass] - public class TopicReferenceDictionaryTest { + public class TopicReferenceCollectionTest { /*========================================================================================================================== | TEST: ADD: NEW REFERENCE: IS DIRTY diff --git a/OnTopic/References/TopicReferenceCollection.cs b/OnTopic/References/TopicReferenceCollection.cs new file mode 100644 index 00000000..3114d699 --- /dev/null +++ b/OnTopic/References/TopicReferenceCollection.cs @@ -0,0 +1,149 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Collections.Specialized; +using OnTopic.Repositories; + +namespace OnTopic.References { + + /*============================================================================================================================ + | CLASS: TOPIC REFERENCE COLLECTION + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Represents a collection of objects associated with particular reference keys. + /// + public class TopicReferenceCollection : TrackedCollection { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the . + /// + /// A reference to the topic that the current collection is bound to. + public TopicReferenceCollection(Topic parentTopic) : base(parentTopic) { } + + /*========================================================================================================================== + | PROPERTY: PARENT COLLECTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override TrackedCollection? ParentCollection => + AssociatedTopic.Parent?.References; + + /*========================================================================================================================== + | PROPERTY: BASE COLLECTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override TrackedCollection? BaseCollection => + AssociatedTopic.BaseTopic?.References; + + /*========================================================================================================================== + | IS FULLY LOADED? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether or not the collection was fully loaded from the persistence store. + /// + /// + /// + /// When loading an individual or branch from the persistence store, it is possible that topic + /// references may not be fully available. In this scenario, updating topic references while e.g. deleting unmatched + /// relationships can result in unintended data loss. To account for this, the property ' + /// tracks whether a collection was fully loaded from the persistence store; if it wasn't, the should not deleted unmatched topic references. + /// + /// + /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to + /// a valid reference in memory. + /// + /// + public bool IsFullyLoaded { get; set; } = true; + + /*========================================================================================================================== + | INSERT ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override void InsertItem(int index, TopicReference item) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Provide base logic + \-----------------------------------------------------------------------------------------------------------------------*/ + base.InsertItem(index, item); + + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + item?.Value?.IncomingRelationships.SetTopic(item.Key, AssociatedTopic, null, true); + + } + + /*========================================================================================================================== + | OVERRIDE: SET ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override void SetItem(int index, TopicReference item) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Get existing reference + \-----------------------------------------------------------------------------------------------------------------------*/ + var existingItem = this[index]; + + /*------------------------------------------------------------------------------------------------------------------------ + | Provide base logic + \-----------------------------------------------------------------------------------------------------------------------*/ + base.SetItem(index, item); + + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + item?.Value?.IncomingRelationships.SetTopic(item.Key, AssociatedTopic, null, true); + existingItem?.Value?.IncomingRelationships.SetTopic(existingItem.Key, AssociatedTopic, null, true); + + } + + /*========================================================================================================================== + | OVERRIDE: REMOVE ITEM + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override sealed void RemoveItem(int index) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + var existing = this[index]; + + existing.Value?.IncomingRelationships.RemoveTopic(existing.Key, AssociatedTopic, true); + + /*------------------------------------------------------------------------------------------------------------------------ + | Provide base logic + \-----------------------------------------------------------------------------------------------------------------------*/ + base.RemoveItem(index); + + } + + /*========================================================================================================================== + | OVERRIDE: CLEAR ITEMS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + protected override sealed void ClearItems() { + + + /*------------------------------------------------------------------------------------------------------------------------ + | Provide base logic + \-----------------------------------------------------------------------------------------------------------------------*/ + base.ClearItems(); + + /*------------------------------------------------------------------------------------------------------------------------ + | Handle recipricol references + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var item in Items) { + item.Value?.IncomingRelationships.RemoveTopic(item.Key, AssociatedTopic, true); + } + + } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/References/TopicReferenceDictionary.cs b/OnTopic/References/TopicReferenceDictionary.cs deleted file mode 100644 index 288c9426..00000000 --- a/OnTopic/References/TopicReferenceDictionary.cs +++ /dev/null @@ -1,390 +0,0 @@ -/*============================================================================================================================== -| Author Ignia, LLC -| Client Ignia, LLC -| Project Topics Library -\=============================================================================================================================*/ -using System; -using System.Collections; -using System.Collections.Generic; -using OnTopic.Collections.Specialized; -using OnTopic.Internal.Diagnostics; -using OnTopic.Internal.Reflection; -using OnTopic.Repositories; - -namespace OnTopic.References { - - /*============================================================================================================================ - | CLASS: TOPIC REFERENCE DICTIONARY - \---------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Represents a collection of objects associated with particular reference keys. - /// - public class TopicReferenceDictionary : IDictionary, ITrackDirtyKeys { - - /*========================================================================================================================== - | DISPATCHER - \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly TopicPropertyDispatcher _topicPropertyDispatcher; - - /*========================================================================================================================== - | PRIVATE VARIABLES - \-------------------------------------------------------------------------------------------------------------------------*/ - readonly Topic _parent; - readonly IDictionary _storage; - readonly DirtyKeyCollection _dirtyKeys; - - /*========================================================================================================================== - | CONSTRUCTOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Initializes a new instance of the . - /// - public TopicReferenceDictionary(Topic parent) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(parent, nameof(parent)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Initialize backing fields - \-----------------------------------------------------------------------------------------------------------------------*/ - _parent = parent; - _storage = new Dictionary(); - _topicPropertyDispatcher = new(parent); - _dirtyKeys = new(); - - } - - /*========================================================================================================================== - | COUNT - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public int Count => _storage.Count; - - /*========================================================================================================================== - | IS READ ONLY? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool IsReadOnly => false; - - /*========================================================================================================================== - | IS FULLY LOADED? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Determines whether or not the collection was fully loaded from the persistence store. - /// - /// - /// - /// When loading an individual or branch from the persistence store, it is possible that topic - /// references may not be fully available. In this scenario, updating topic references while e.g. deleting unmatched - /// relationships can result in unintended data loss. To account for this, the property ' - /// tracks whether a collection was fully loaded from the persistence store; if it wasn't, the should not deleted unmatched topic references. - /// - /// - /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to - /// a valid reference in memory. - /// - /// - public bool IsFullyLoaded { get; set; } = true; - - /*========================================================================================================================== - | ITEM - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public Topic this[string referenceKey] { - get => _storage[referenceKey]; - set { - - /*---------------------------------------------------------------------------------------------------------------------- - | Validate parameters - \---------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires( - value != _parent, - "A topic reference may not point to itself." - ); - - /*---------------------------------------------------------------------------------------------------------------------- - | Enforce business logic - >----------------------------------------------------------------------------------------------------------------------- - | If the reference is eligible for business logic enforcement, but the business logic hasn't yet been enforce, skip - | further processing and instead route the request through the associated property setter. - \---------------------------------------------------------------------------------------------------------------------*/ - if (!_topicPropertyDispatcher.Enforce(referenceKey, value)) { - return; - } - - /*---------------------------------------------------------------------------------------------------------------------- - | Set dirty state - \---------------------------------------------------------------------------------------------------------------------*/ - if (!_storage.TryGetValue(referenceKey, out var existing) || existing != value) { - _dirtyKeys.MarkDirty(referenceKey); - } - - /*---------------------------------------------------------------------------------------------------------------------- - | Set topic reference - \---------------------------------------------------------------------------------------------------------------------*/ - _storage[referenceKey] = value; - - } - } - - /*========================================================================================================================== - | KEYS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public ICollection Keys => _storage.Keys; - - /*========================================================================================================================== - | VALUES - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public ICollection Values => _storage.Values; - - /*========================================================================================================================== - | ADD - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - void ICollection>.Add(KeyValuePair item) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(item, nameof(item)); - - TopicFactory.ValidateKey(item.Key); - - Contract.Requires( - item.Value != _parent, - "A topic reference may not point to itself." - ); - - /*------------------------------------------------------------------------------------------------------------------------ - | Enforce business logic - >------------------------------------------------------------------------------------------------------------------------- - | If the reference is eligible for business logic enforcement, but the business logic hasn't yet been enforce, skip - | further processing and instead route the request through the associated property setter. - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!_topicPropertyDispatcher.Enforce(item.Key, item.Value)) { - return; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Mark dirty - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!_storage.TryGetValue(item.Key, out var existing) || existing != item.Value) { - _dirtyKeys.MarkDirty(item.Key); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Handle recipricol references - \-----------------------------------------------------------------------------------------------------------------------*/ - item.Value.IncomingRelationships.SetTopic(item.Key, _parent, null, true); - - /*------------------------------------------------------------------------------------------------------------------------ - | Add item - \-----------------------------------------------------------------------------------------------------------------------*/ - _storage.Add(item); - - } - - /// - public void Add(string key, Topic value) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(key, nameof(key)); - Contract.Requires(value, nameof(value)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Add item - \-----------------------------------------------------------------------------------------------------------------------*/ - var self = this as ICollection>; - self.Add(new(key, value)); - - } - - /*========================================================================================================================== - | SET TOPIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is - /// removed. - /// - public void SetTopic(string key, Topic? value, bool? markDirty = null) => SetTopic(key, value, markDirty, true); - - /// - /// Adds a new topic reference—or updates one, if it already exists. If the value is null, and a value exits, it is - /// removed. - /// - internal void SetTopic(string key, Topic? value, bool? markDirty, bool enforceBusinessLogic) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Establish state - \-----------------------------------------------------------------------------------------------------------------------*/ - var wasDirty = _dirtyKeys.IsDirty(key); - - /*------------------------------------------------------------------------------------------------------------------------ - | Register that business logic has already been enforced - >------------------------------------------------------------------------------------------------------------------------- - | We want to ensure that any attempt to set references that have corresponding (writable) properties use those properties, - | thus enforcing business logic. In order to ensure this is enforced on all entry points exposed by IDictionary, and not - | just SetTopic, the underlying interceptors (e.g., Add, Item) call the Enforce() method. If it returns false, they assume - | the property set the value (e.g., by calling the internal SetTopic method with enforceBusinessLogic set to false). - | Otherwise, the corresponding property will be called. The Register() method thus avoids a redirect loop in this - | scenario. This, of course, assumes that properties are correctly written to call the enforceBusinessLogic parameter. - \-----------------------------------------------------------------------------------------------------------------------*/ - if (!enforceBusinessLogic) { - _topicPropertyDispatcher.Register(key, value); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Set value - \-----------------------------------------------------------------------------------------------------------------------*/ - if (value is null) { - if (ContainsKey(key)) { - Remove(key); - } - } - else { - this[key] = value; - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Set dirty state - \-----------------------------------------------------------------------------------------------------------------------*/ - if (wasDirty is false && markDirty is false) { - _dirtyKeys.MarkClean(key); - } - - } - - /*========================================================================================================================== - | CLEAR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public void Clear() { - - /*------------------------------------------------------------------------------------------------------------------------ - | Mark keys as dirty - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var item in _storage) { - _dirtyKeys.MarkAs(item.Key, markDirty: !_parent.IsNew); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Handle recipricol references - \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (var item in _storage) { - item.Value.IncomingRelationships.RemoveTopic(item.Key, _parent, true); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Call base method - \-----------------------------------------------------------------------------------------------------------------------*/ - _storage.Clear(); - - } - - /*========================================================================================================================== - | CONTAINS - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool Contains(KeyValuePair item) => _storage.Contains(item); - - /*========================================================================================================================== - | CONTAINS KEY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool ContainsKey(string key) => _storage.ContainsKey(key); - - /*========================================================================================================================== - | COPY TO - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public void CopyTo(KeyValuePair[] array, int arrayIndex) => _storage.CopyTo(array, arrayIndex); - - /*========================================================================================================================== - | GET ENUMERATOR - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - IEnumerator IEnumerable.GetEnumerator() => _storage.GetEnumerator(); - - /// - public IEnumerator> GetEnumerator() => _storage.GetEnumerator(); - - /*========================================================================================================================== - | REMOVE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - bool ICollection>.Remove(KeyValuePair item) => - Contains(item) && Remove(item.Key); - - /// - public bool Remove(string key) { - - /*------------------------------------------------------------------------------------------------------------------------ - | Validate parameters - \-----------------------------------------------------------------------------------------------------------------------*/ - Contract.Requires(key, nameof(key)); - - /*------------------------------------------------------------------------------------------------------------------------ - | Handle existing - \-----------------------------------------------------------------------------------------------------------------------*/ - if (TryGetValue(key, out var existing)) { - existing.IncomingRelationships.RemoveTopic(key, _parent, true); - _dirtyKeys.MarkAs(key, markDirty: !_parent.IsNew); - } - - /*------------------------------------------------------------------------------------------------------------------------ - | Call base method - \-----------------------------------------------------------------------------------------------------------------------*/ - return _storage.Remove(key); - - } - - /*========================================================================================================================== - | TRY/GET VALUE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool TryGetValue(string key, out Topic value) => _storage.TryGetValue(key, out value!); - - /*========================================================================================================================== - | GET TOPIC - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Attempts to retrieve a topic reference based on its ; if it doesn't exist, returns null. - /// - public Topic? GetTopic(string key, bool inheritFromBase = true) { - if (TryGetValue(key, out var existing)) { - return existing; - } - else if (inheritFromBase) { - return _parent.BaseTopic?.References.GetTopic(key); - } - return null; - } - - /*========================================================================================================================== - | IS DIRTY? - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public bool IsDirty() => _dirtyKeys.IsDirty(); - - /// - public bool IsDirty(string key) => _dirtyKeys.IsDirty(key); - - /*========================================================================================================================== - | MARK CLEAN - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public void MarkClean() => _dirtyKeys.MarkClean(); - - /// - public void MarkClean(string key) => _dirtyKeys.MarkClean(key); - - } //Class -} //Namespace \ No newline at end of file diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 577461a0..fbf345c4 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -698,7 +698,7 @@ public Topic? BaseTopic { /// BaseTopic for a ). /// /// The current 's relationships. - public TopicReferenceDictionary References { get; } + public TopicReferenceCollection References { get; } /*========================================================================================================================== | PROPERTY: INCOMING RELATIONSHIPS From 0544353e28c11f560ee15ca995715d8b1e4f0146 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 13:57:43 -0800 Subject: [PATCH 554/778] Updated `GetTopic()`, `SetTopic()` to `GetValue()`, `SetValue()` With the adoption of `TrackedCollection<>` as a base class for `TopicReferenceCollection`, the `GetTopic()` and `SetTopic()` methods have been renamed to the more general `GetValue()` and `SetValue()` (74d5907). This requires updating all references within the code base. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.Tests/Entities/CustomTopic.cs | 4 +-- OnTopic.Tests/SqlTopicRepositoryTest.cs | 4 +-- OnTopic.Tests/TopicMappingServiceTest.cs | 4 +-- OnTopic.Tests/TopicReferenceCollectionTest.cs | 25 ++++++++++--------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 +-- OnTopic.Tests/TopicTest.cs | 8 +++--- .../Reverse/ReverseTopicMappingService.cs | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 64805cee..358e4236 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -434,7 +434,7 @@ private static void SetReferences(this IDataReader reader, TopicIndex topics, bo /*------------------------------------------------------------------------------------------------------------------------ | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ - current.References.SetTopic(relationshipKey, referenced, markDirty); + current.References.SetValue(relationshipKey, referenced, markDirty); } diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 41cbf6f6..8679bc8f 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -95,13 +95,13 @@ public DateTime DateTimeAttribute { /// [ReferenceSetter] public Topic? TopicReference { - get => References.GetTopic("TopicReference"); + get => References.GetValue("TopicReference"); set { Contract.Requires( value.ContentType == ContentType, $"{nameof(TopicReference)} expects a topic with the same content type as the parent: {ContentType}." ); - References.SetTopic("TopicReference", value); + References.SetValue("TopicReference", value); } } diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index b2c6373b..90515352 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -124,7 +124,7 @@ public void LoadTopicGraph_WithReference_ReturnsReference() { Assert.IsNotNull(topic); Assert.AreEqual(1, topic.Id); - Assert.AreEqual(2, topic.References.GetTopic("Test")?.Id); + Assert.AreEqual(2, topic.References.GetValue("Test")?.Id); Assert.IsTrue(topic.References.IsDirty()); } @@ -154,7 +154,7 @@ public void LoadTopicGraph_WithExternalReference_ReturnsReference() { Assert.IsNotNull(topic); Assert.AreEqual(1, topic.Id); - Assert.AreEqual(2, topic.References.GetTopic("Test")?.Id); + Assert.AreEqual(2, topic.References.GetValue("Test")?.Id); Assert.IsTrue(topic.References.IsFullyLoaded); Assert.IsFalse(topic.References.IsDirty()); diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 9efd1465..348e4064 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -593,7 +593,7 @@ public async Task Map_TopicReferences_ReturnsMappedModel() { var topic = TopicFactory.Create("Test", "TopicReference"); - topic.References.SetTopic("TopicReference", topicReference); + topic.References.SetValue("TopicReference", topicReference); var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); @@ -618,7 +618,7 @@ public async Task Map_TopicReferences_SkipsDisabled() { topicReference.IsDisabled = true; - topic.References.SetTopic("TopicReference", topicReference); + topic.References.SetValue("TopicReference", topicReference); var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false); diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 8e3af372..ec86d102 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -57,7 +57,7 @@ public void SetTopic_NewReference_NotDirty() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.SetTopic("Reference", reference, false); + topic.References.SetValue("Reference", reference, false); Assert.AreEqual(1, topic.References.Count); Assert.IsFalse(topic.References.IsDirty()); @@ -78,7 +78,7 @@ public void Remove_ExistingReference_IsDirty() { var topic = TopicFactory.Create("Topic", "Page", 1); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.SetTopic("Reference", reference, false); + topic.References.SetValue("Reference", reference, false); topic.References.Remove("Reference"); Assert.AreEqual(0, topic.References.Count); @@ -100,7 +100,7 @@ public void Clear_ExistingReferences_IsDirty() { var topic = TopicFactory.Create("Topic", "Page", 1); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.SetTopic("Reference", reference, false); + topic.References.SetValue("Reference", reference, false); topic.References.Clear(); Assert.AreEqual(0, topic.References.Count); @@ -165,10 +165,10 @@ public void SetTopic_ExistingReference_TopicUpdated() { var reference = TopicFactory.Create("Reference", "Page"); var newReference = TopicFactory.Create("NewReference", "Page"); - topic.References.Add("Reference", reference); - topic.References.SetTopic("Reference", newReference); + topic.References.SetValue("Reference", reference); + topic.References.SetValue("Reference", newReference); - Assert.AreEqual(newReference, topic.References.GetTopic("Reference")); + Assert.AreEqual(newReference, topic.References.GetValue("Reference")); } @@ -188,9 +188,10 @@ public void SetTopic_NullReference_TopicRemoved() { var reference = TopicFactory.Create("Reference", "Page"); topic.References.Add("Reference", reference); - topic.References.SetTopic("Reference", null); + topic.References.SetValue("Reference", null); Assert.AreEqual(0, topic.References.Count); + Assert.IsNull(topic.References.GetValue("Reference")); } @@ -229,7 +230,7 @@ public void GetTopic_ExistingReference_ReturnsTopic() { topic.References.Add("Reference", reference); - Assert.AreEqual(reference, topic.References.GetTopic("Reference")); + Assert.AreEqual(reference, topic.References.GetValue("Reference")); } @@ -249,7 +250,7 @@ public void GetTopic_MissingReference_ReturnsNull() { topic.References.Add("Reference", reference); - Assert.IsNull(topic.References.GetTopic("MissingReference")); + Assert.IsNull(topic.References.GetValue("MissingReference")); } @@ -271,7 +272,7 @@ public void GetTopic_InheritedReference_ReturnsTopic() { topic.BaseTopic = baseTopic; baseTopic.References.Add("Reference", reference); - Assert.AreEqual(reference, topic.References.GetTopic("Reference")); + Assert.AreEqual(reference, topic.References.GetValue("Reference")); } @@ -294,7 +295,7 @@ public void GetTopic_InheritedReference_ReturnsNull() { topic.BaseTopic = baseTopic; baseTopic.References.Add("Reference", reference); - Assert.IsNull(topic.References.GetTopic("MissingReference")); + Assert.IsNull(topic.References.GetValue("MissingReference")); } @@ -317,7 +318,7 @@ public void GetTopic_InheritedReferenceWithoutInheritance_ReturnsNull() { topic.BaseTopic = baseTopic; baseTopic.References.Add("Reference", reference); - Assert.IsNull(topic.References.GetTopic("Reference", false)); + Assert.IsNull(topic.References.GetValue("Reference", null, false, false)); } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index a03a3974..52168856 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -603,7 +603,7 @@ public void Save_UnresolvedReference_Resolves() { var topic = TopicFactory.Create("Test", "Page", parent); var reference = TopicFactory.Create("Reference", "Page", topic); - topic.References.SetTopic("Test", reference); + topic.References.SetValue("Test", reference); _topicRepository.Save(topic, true); @@ -627,7 +627,7 @@ public void Save_UnresolvedReference_ThrowsException() { var topic = TopicFactory.Create("Test", "Page", parent); var reference = TopicFactory.Create("Reference", "Page", parent); - topic.References.SetTopic("Test", reference); + topic.References.SetValue("Test", reference); _topicRepository.Save(topic, true); diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 98f53683..b281449b 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -290,7 +290,7 @@ public void BaseTopic_UpdateValue_ReturnsExpectedValue() { topic.BaseTopic = finalBaseTopic; Assert.ReferenceEquals(topic.BaseTopic, finalBaseTopic); - Assert.AreEqual(2, topic.References.GetTopic("BaseTopic").Id); + Assert.AreEqual(2, topic.References.GetValue("BaseTopic").Id); } @@ -312,7 +312,7 @@ public void BaseTopic_ResavedValue_ReturnsExpectedValue() { topic.BaseTopic = baseTopic; Assert.ReferenceEquals(topic.BaseTopic, baseTopic); - Assert.AreEqual(5, topic.References.GetTopic("BaseTopic").Id); + Assert.AreEqual(5, topic.References.GetValue("BaseTopic").Id); } @@ -379,7 +379,7 @@ public void IsDirty_ChangeCollections_ReturnsTrue() { var related = TopicFactory.Create("Related", "Page", 2); topic.Attributes.SetValue("Related", related.Key); - topic.References.SetTopic("Related", related); + topic.References.SetValue("Related", related); topic.Relationships.SetTopic("Related", related); Assert.IsTrue(topic.IsDirty(true)); @@ -401,7 +401,7 @@ public void MarkClean_ChangeCollection_ResetIsDirty() { var related = TopicFactory.Create("Related", "Page"); topic.Attributes.SetValue("Related", related.Key); - topic.References.SetTopic("Related", related); + topic.References.SetValue("Related", related); topic.Relationships.SetTopic("Related", related); topic.MarkClean(true); diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 8751136b..de67bf40 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -555,7 +555,7 @@ PropertyConfiguration configuration target.Attributes.SetInteger(configuration.AttributeKey, topicReference.Id); } else { - target.References.SetTopic(configuration.AttributeKey, topicReference); + target.References.SetValue(configuration.AttributeKey, topicReference); } } From 956c9f41b56080a8ceae2aced31bde9857101ca4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:03:04 -0800 Subject: [PATCH 555/778] Updated `Add()` to `SetValue()` Since `TopicReferenceCollection` is no longer a dictionary (74d5907), the `Add()` overload doesn't make as much sense. This was required to fulfill the `IDictionary<>` contract, but is largely redundant with `SetValue()` with the `TrackedCollection`. We could easily just add this as a proxy function, but `SetValue()` is more consistent with the broader semantics of the library, and especially since we don't want callers to need to worry about whether or not a record already exists. As such, calls to `Add()` have been renamed to `SetValue()`. Note: There is one exception to this. In the rare case that we want to add a new `TopicReference` class directly, we can still use the `Add()` method from `KeyedCollection`. This isn't normally preferred, as it's more cumbersome for callers to work with `TopicReference` records directly, instead of relying on `SetValue()` to handle them. But this is used as part of the `TopicReferenceCollection` unit tests for evaluating the business logic enforcement. --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 26 +++++++++---------- OnTopic/Topic.cs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index ec86d102..f4551f1d 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -36,7 +36,7 @@ public void Add_NewReference_IsDirty() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); Assert.AreEqual(1, topic.References.Count); Assert.IsTrue(topic.References.IsDirty()); @@ -52,7 +52,7 @@ public void Add_NewReference_IsDirty() { /// TopicReferenceDictionary.IsDirty()"/> is not set. /// [TestMethod] - public void SetTopic_NewReference_NotDirty() { + public void SetValue_NewReference_NotDirty() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); @@ -122,7 +122,7 @@ public void Add_NewReference_IncomingRelationshipSet() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); Assert.AreEqual(1, reference.IncomingRelationships.GetTopics("Reference").Count); @@ -143,7 +143,7 @@ public void Remove_ExistingReference_IncomingRelationshipRemoved() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); topic.References.Remove("Reference"); Assert.AreEqual(0, reference.IncomingRelationships.GetTopics("Reference").Count); @@ -187,7 +187,7 @@ public void SetTopic_NullReference_TopicRemoved() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); topic.References.SetValue("Reference", null); Assert.AreEqual(0, topic.References.Count); @@ -208,7 +208,7 @@ public void Add_NewReference_TopicIsDirty() { var topic = TopicFactory.Create("Topic", "Page", 1); var reference = TopicFactory.Create("Reference", "Page", 2); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); Assert.IsTrue(topic.IsDirty(true)); Assert.IsFalse(reference.IsDirty(true)); @@ -228,7 +228,7 @@ public void GetTopic_ExistingReference_ReturnsTopic() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); Assert.AreEqual(reference, topic.References.GetValue("Reference")); @@ -248,7 +248,7 @@ public void GetTopic_MissingReference_ReturnsNull() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("Reference", reference); + topic.References.SetValue("Reference", reference); Assert.IsNull(topic.References.GetValue("MissingReference")); @@ -270,7 +270,7 @@ public void GetTopic_InheritedReference_ReturnsTopic() { var reference = TopicFactory.Create("Reference", "Page"); topic.BaseTopic = baseTopic; - baseTopic.References.Add("Reference", reference); + baseTopic.References.SetValue("Reference", reference); Assert.AreEqual(reference, topic.References.GetValue("Reference")); @@ -293,7 +293,7 @@ public void GetTopic_InheritedReference_ReturnsNull() { var reference = TopicFactory.Create("Reference", "Page"); topic.BaseTopic = baseTopic; - baseTopic.References.Add("Reference", reference); + baseTopic.References.SetValue("Reference", reference); Assert.IsNull(topic.References.GetValue("MissingReference")); @@ -316,7 +316,7 @@ public void GetTopic_InheritedReferenceWithoutInheritance_ReturnsNull() { var reference = TopicFactory.Create("Reference", "Page"); topic.BaseTopic = baseTopic; - baseTopic.References.Add("Reference", reference); + baseTopic.References.SetValue("Reference", reference); Assert.IsNull(topic.References.GetValue("Reference", null, false, false)); @@ -335,7 +335,7 @@ public void Add_TopicReferenceWithBusinessLogic_IsReturned() { var topic = new CustomTopic("Test", "Page"); var reference = TopicFactory.Create("Reference", "Page"); - topic.References.Add("TopicReference", reference); + topic.References.SetValue("TopicReference", reference); Assert.AreEqual(reference, topic.TopicReference); @@ -357,7 +357,7 @@ public void Add_TopicReferenceWithBusinessLogic_ThrowsException() { var topic = new CustomTopic("Test", "Page"); var reference = TopicFactory.Create("Reference", "Container"); - topic.References.Add("TopicReference", reference); + topic.References.SetValue("TopicReference", reference); } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index fbf345c4..84d0e0b0 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -653,7 +653,7 @@ public Topic? BaseTopic { value != this, "A topic may not derive from itself." ); - References.SetTopic("BaseTopic", value); + References.SetValue("BaseTopic", value); } } From 3d3a6ba8acec3ddc011412c795beb44706da8aa8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:07:08 -0800 Subject: [PATCH 556/778] Updated XML Docs previously referring to `TopicReferenceDictionary` Between `TopicReferenceDictionary` being renamed to `TopicReferenceCollection` and being updated to derive from `TrackedCollection<>`, all XML Documentation referring to the `TopicReferenceDictionary` is now broken. The documentation is now updated to either point to `TopicReferenceCollection` or, if pointing to a derived member, the underlying `TrackedCollection<>`. This required quite a bit of (re)wrapping due to the longer fully-qualified name of the generic base collection. --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 96 ++++++++++--------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 +- .../References/ReferenceSetterAttribute.cs | 30 +++--- OnTopic/References/TopicReference.cs | 2 +- OnTopic/Topic.cs | 20 ++-- 5 files changed, 79 insertions(+), 73 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index f4551f1d..885eaff7 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -15,10 +15,11 @@ namespace OnTopic.Tests { | CLASS: TOPIC REFERENCE COLLECTION TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the , with a particular emphasis on the custom features - /// such as , , , and the cross-referencing of reciprocal - /// values in the property. + /// Provides unit tests for the , with a particular emphasis on the custom features + /// such as , , , and the cross-referencing of reciprocal values in the property. /// [TestClass] public class TopicReferenceCollectionTest { @@ -27,8 +28,8 @@ public class TopicReferenceCollectionTest { | TEST: ADD: NEW REFERENCE: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference, and confirms that - /// is correctly set. + /// Assembles a new , adds a new reference, and confirms that + /// is correctly set. /// [TestMethod] public void Add_NewReference_IsDirty() { @@ -47,9 +48,9 @@ public void Add_NewReference_IsDirty() { | TEST: SET TOPIC: NEW REFERENCE: NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , and confirms that is not set. + /// Assembles a new , adds a new reference using , and confirms that is not set. /// [TestMethod] public void SetValue_NewReference_NotDirty() { @@ -68,9 +69,9 @@ public void SetValue_NewReference_NotDirty() { | TEST: REMOVE: EXISTING REFERENCE: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a topic reference, removes that reference using , and confirms that is - /// set. + /// Assembles a new with a topic reference, removes that reference using , and confirms that is set. /// [TestMethod] public void Remove_ExistingReference_IsDirty() { @@ -90,9 +91,10 @@ public void Remove_ExistingReference_IsDirty() { | TEST: CLEAR: EXISTING REFERENCES: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , calls and confirms that is set. + /// Assembles a new , adds a new reference using , calls and confirms that is set. /// [TestMethod] public void Clear_ExistingReferences_IsDirty() { @@ -112,9 +114,9 @@ public void Clear_ExistingReferences_IsDirty() { | TEST: ADD: NEW REFERENCE: INCOMING RELATIONSHIP SET \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , and confirms that - /// reference is correctly set. + /// Assembles a new , adds a new reference using , and confirms that reference is correctly set. /// [TestMethod] public void Add_NewReference_IncomingRelationshipSet() { @@ -132,10 +134,10 @@ public void Add_NewReference_IncomingRelationshipSet() { | TEST: REMOVE: EXISTING REFERENCE: INCOMING RELATIONSHIP REMOVED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , removes the reference using , and confirms that the reference is correctly removed as - /// well. + /// Assembles a new , adds a new reference using , removes the reference + /// using , and confirms that the reference is correctly removed as well. /// [TestMethod] public void Remove_ExistingReference_IncomingRelationshipRemoved() { @@ -154,9 +156,10 @@ public void Remove_ExistingReference_IncomingRelationshipRemoved() { | TEST: SET TOPIC: EXISTING REFERENCE: TOPIC UPDATED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , updates the reference using , and confirms that the reference is correctly updated. + /// Assembles a new , adds a new reference using , updates the reference + /// using , and + /// confirms that the reference is correctly updated. /// [TestMethod] public void SetTopic_ExistingReference_TopicUpdated() { @@ -176,10 +179,10 @@ public void SetTopic_ExistingReference_TopicUpdated() { | TEST: SET TOPIC: NULL REFERENCE: TOPIC REMOVED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference using , updates the reference with a null value using , and confirms that the reference - /// is correctly removed. + /// Assembles a new , adds a new reference using , updates the reference + /// with a null value using , and confirms that the reference is correctly removed. /// [TestMethod] public void SetTopic_NullReference_TopicRemoved() { @@ -199,7 +202,7 @@ public void SetTopic_NullReference_TopicRemoved() { | TEST: ADD: NEW REFERENCE: TOPIC IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference, and confirms that + /// Assembles a new , adds a new reference, and confirms that /// is correctly set. /// [TestMethod] @@ -219,8 +222,9 @@ public void Add_NewReference_TopicIsDirty() { | TEST: GET TOPIC: EXISTING REFERENCE: RETURNS TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference, and confirms that - /// correctly returns the . + /// Assembles a new , adds a new reference, and confirms that + /// correctly returns the . /// [TestMethod] public void GetTopic_ExistingReference_ReturnsTopic() { @@ -238,9 +242,9 @@ public void GetTopic_ExistingReference_ReturnsTopic() { | TEST: GET TOPIC: MISSING REFERENCE: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new , adds a new reference, and confirms that - /// correctly returns null if an incorrect - /// referencedKey is entered. + /// Assembles a new , adds a new reference, and confirms that + /// correctly returns null if + /// an incorrect referencedKey is entered. /// [TestMethod] public void GetTopic_MissingReference_ReturnsNull() { @@ -258,9 +262,9 @@ public void GetTopic_MissingReference_ReturnsNull() { | TEST: GET TOPIC: INHERITED REFERENCE: RETURNS TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns the related topic reference. + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns the related topic reference. /// [TestMethod] public void GetTopic_InheritedReference_ReturnsTopic() { @@ -280,10 +284,10 @@ public void GetTopic_InheritedReference_ReturnsTopic() { | TEST: GET TOPIC: INHERITED REFERENCE: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if an incorrect referencedKey - /// is entered. + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if an incorrect referencedKey is + /// entered. /// [TestMethod] public void GetTopic_InheritedReference_ReturnsNull() { @@ -303,10 +307,10 @@ public void GetTopic_InheritedReference_ReturnsNull() { | TEST: GET TOPIC: INHERITED REFERENCE WITHOUT INHERITANCE: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromBase is - /// set to false. + /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromBase is set to + /// false. /// [TestMethod] public void GetTopic_InheritedReferenceWithoutInheritance_ReturnsNull() { diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 52168856..2572b7e5 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -593,8 +593,8 @@ public void Save_IsRecursive_SavesChild() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Saves a new with an unresolved and confirms that it successfully - /// resolves it by marking the collection as as false. + /// resolves it by marking the collection as as false. /// [TestMethod] public void Save_UnresolvedReference_Resolves() { diff --git a/OnTopic/References/ReferenceSetterAttribute.cs b/OnTopic/References/ReferenceSetterAttribute.cs index 322f9ee4..99c85614 100644 --- a/OnTopic/References/ReferenceSetterAttribute.cs +++ b/OnTopic/References/ReferenceSetterAttribute.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using OnTopic.Collections.Specialized; namespace OnTopic.References { @@ -11,28 +12,29 @@ namespace OnTopic.References { | CLASS: REFERENCE SETTER [ATTRIBUTE] \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Flags that a property should be used when setting a reference via . + /// Flags that a property should be used when setting a reference via . /// /// /// - /// When a call is made to the code will check - /// to see if a property with the same name as the reference key exists, and whether that property is decorated with the - /// (i.e., [ReferenceSetter]). If it is, then the update will be - /// routed through that property. This ensures that business logic is enforced by local properties, instead of allowing - /// business logic to be potentially bypassed by writing directly to the collection. + /// When a call is made to the code will check to see if a property with the same name as the reference key exists, and whether + /// that property is decorated with the (i.e., [ReferenceSetter]). If + /// it is, then the update will be routed through that property. This ensures that business logic is enforced by local + /// properties, instead of allowing business logic to be potentially bypassed by writing directly to the collection. /// /// - /// As an example, the property is adorned with the . As a result, if a client calls topic.References.SetTopic("BaseTopic", topic), then that update - /// will be routed through , thus enforcing any validation. + /// As an example, the property is adorned with the . + /// As a result, if a client calls topic.References.SetTopic("BaseTopic", topic), then that update will be + /// routed through , thus enforcing any validation. /// /// /// To ensure this logic, it is critical that implementers of ensure that the - /// property setters call the overload with the - /// final parameter set to false to disable the enforcement of business logic. Otherwise, an infinite loop will - /// occur. Calling that overload tells that the business logic has already been - /// enforced by the caller. + /// property setters call the overload with the final parameter set to false to disable the enforcement of business logic. + /// Otherwise, an infinite loop will occur. Calling that overload tells that the + /// business logic has already been enforced by the caller. /// /// [AttributeUsage(AttributeTargets.Property)] diff --git a/OnTopic/References/TopicReference.cs b/OnTopic/References/TopicReference.cs index 71650d4e..ee9a630d 100644 --- a/OnTopic/References/TopicReference.cs +++ b/OnTopic/References/TopicReference.cs @@ -23,7 +23,7 @@ namespace OnTopic.References { /// LastModified"/> date. /// /// - /// Typically, the will be exposed as part of a via + /// Typically, the will be exposed as part of a via /// the collection. /// /// diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 84d0e0b0..372c80e3 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -629,16 +629,16 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// to help avoid an infinite loop. /// /// - /// The underlying value of the is stored as a topic reference with the of BaseTopic in . If the hasn't been saved, then the relationship will be established, but the BaseTopic won't be persisted - /// to the underlying repository upon . That said, when is called, the will be reevaluated - /// and, if it has subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory - /// topic graphs to be constructed, while preventing invalid s from being persisted to the - /// underlying data storage. As a result, however, a referencing an that is - /// unsaved will need to be saved again once the has been saved, assuming it's otherwise outside - /// the scope of the original call. + /// The underlying value of the is stored as a topic reference with the of BaseTopic in . If the hasn't been + /// saved, then the relationship will be established, but the BaseTopic won't be persisted to the underlying + /// repository upon . That said, when is called, the will be reevaluated and, if it has + /// subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory topic graphs to + /// be constructed, while preventing invalid s from being persisted to the underlying data + /// storage. As a result, however, a referencing an that is unsaved will + /// need to be saved again once the has been saved, assuming it's otherwise outside the scope of + /// the original call. /// /// /// The that values should be inherited from, if not otherwise available. From 38515ec0b92956b2b9df3f6377652dd1c128b257 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:08:56 -0800 Subject: [PATCH 557/778] Updated test names to reflect `TopicReferenceCollection` changes Notably, renamed tests that started with `GetTopic` or `SetTopic` to instead start with `GetValue` or `SetTopic`. This corresponds to a previous update where the actual code references were changed (0544353). --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 885eaff7..79dc1250 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -45,7 +45,7 @@ public void Add_NewReference_IsDirty() { } /*========================================================================================================================== - | TEST: SET TOPIC: NEW REFERENCE: NOT DIRTY + | TEST: SET VALUE: NEW REFERENCE: NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , adds a new reference using reference is correctly updated. /// [TestMethod] - public void SetTopic_ExistingReference_TopicUpdated() { + public void SetValue_ExistingReference_TopicUpdated() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); @@ -176,7 +176,7 @@ public void SetTopic_ExistingReference_TopicUpdated() { } /*========================================================================================================================== - | TEST: SET TOPIC: NULL REFERENCE: TOPIC REMOVED + | TEST: SET VALUE: NULL REFERENCE: TOPIC REMOVED \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , and confirms that the reference is correctly removed. /// [TestMethod] - public void SetTopic_NullReference_TopicRemoved() { + public void SetValue_NullReference_TopicRemoved() { var topic = TopicFactory.Create("Topic", "Page"); var reference = TopicFactory.Create("Reference", "Page"); From 2722c1f6bece70b0298bce8cf8c184461f7b7bde Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:11:54 -0800 Subject: [PATCH 558/778] Replaced setter with new `TopicReference` instance The new `TopicReferenceCollection`, now deriving from `TrackedCollection<>`, no longer supports setting a topic value via the indexer. We could replace this with `SetValue()`, as we did elsewhere. But, since this unit test is evaluating back doors, it makes more sense to inject a new `TopicReference` instance directly via `Add()`. In production scenarios, this is almost never a preferred option, since working with the `TopicReference` record directly is a bit cumbersome; using `SetValue()` is much preferred. But since this is deliberately testing backdoors that might evade the logic of `SetValue()`, this is effective for the purpose of the unit test. --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 79dc1250..5fb78bbc 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -381,7 +381,7 @@ public void Set_TopicReferenceWithBusinessLogic_ThrowsException() { var topic = new CustomTopic("Test", "Page"); var reference = TopicFactory.Create("Reference", "Container"); - topic.References["TopicReference"] = reference; + topic.References.Add(new("TopicReference", reference)); } From e91a006fe54b05f007d9c1f9eedaed2c0f972555 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:14:19 -0800 Subject: [PATCH 559/778] Updated expectations of unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, setting a reference to `null` would remove it. That makes good sense. But now that the records represent `TopicReference` instances (with e.g. `IsDirty` and `Version`), it makes more sense to _update_ them to have a `null` `Value` instead of removing them, as that allows them to be tracked as `IsDirty`. Given that, I've updated the test of `RemoveItem()` to expect that the value still still be there—but then to verify that it's `null`. (Strictly speaking, this isn't necessary in that they are _also_ tracked via `DeletedItems`, but it also doesn't hurt.) --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 5fb78bbc..4da5a4b2 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -193,7 +193,7 @@ public void SetValue_NullReference_TopicRemoved() { topic.References.SetValue("Reference", reference); topic.References.SetValue("Reference", null); - Assert.AreEqual(0, topic.References.Count); + Assert.AreEqual(1, topic.References.Count); Assert.IsNull(topic.References.GetValue("Reference")); } From e7a552014f64dc753723813005fdcabd28d0741d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:16:24 -0800 Subject: [PATCH 560/778] Provided handling of potentially null topic references Now that `TopicReference.Value` is `null`, iterations over `Topic.References` must account for that possibility. That has always been done when calling `GetTopic()` (now `GetValue()`), but not when iterating over the collection. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 4 ++-- OnTopic/Repositories/TopicRepository.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index c38d8e61..3af70521 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -282,7 +282,7 @@ topic.References.Count is 0? from reference in topic.References select new XElement(_pagemapNamespace + "Attribute", new XAttribute("name", reference.Key), - new XText(reference.Value.GetUniqueKey().Replace("Root:", "", StringComparison.OrdinalIgnoreCase)) + new XText(reference.Value?.GetUniqueKey().Replace("Root:", "", StringComparison.OrdinalIgnoreCase)) ) ); diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index fa5a7143..9e93e691 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -710,8 +710,8 @@ private static void PersistReferences(Topic topic, DateTime version, SqlConnecti CommandType = CommandType.StoredProcedure }; - foreach (var relatedTopic in topic.References.Where(t => !t.Value.IsNew)) { - references.AddRow(relatedTopic.Key, relatedTopic.Value.Id); + foreach (var relatedTopic in topic.References.Where(t => !t.Value?.IsNew?? false)) { + references.AddRow(relatedTopic.Key, relatedTopic.Value!.Id); } // Add Parameters diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 28b3f9fd..b54af1cb 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -349,7 +349,7 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso \-----------------------------------------------------------------------------------------------------------------------*/ if ( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || - topic.References.Values.Any(t => t.Id < 0) + topic.References.Any(t => t.Value?.Id < 0) ) { unresolvedTopics.Add(topic); } From 4b00642c93905a02199bcc1346382f0dbe8e586f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:19:15 -0800 Subject: [PATCH 561/778] Replace `TryGetValue()` with `GetValue()` The `TopicReferenceDictionary` had a `TryGetValue()` method, since it implemented `IDictionary<>`. The `KeyedCollection<>` used by the new underlying `TrackedCollection` doesn't implement this. We could add one, but it doesn't add significantly to the capabilities of `GetValue()`, which wil check to see if a value exists, and if not return an optional default value. As such, the call to `TryGetValue()` is replaced with a call to `GetValue()`. (I refactored this a bit since another condition relies on the variable, and thus it needs to be defined in the parent scope. This is done when placing `TryGetValue()` instead a condition's _expression_, but wouldn't be done if I declared the variable within the _body_ of the condition.) --- OnTopic/Mapping/TopicMappingService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index db56ab3a..ea0731c2 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -281,6 +281,7 @@ private async Task SetPropertyAsync( \-----------------------------------------------------------------------------------------------------------------------*/ var configuration = new PropertyConfiguration(property, attributePrefix); var topicReferenceId = source.Attributes.GetInteger($"{configuration.AttributeKey}Id", 0); + var topicReference = source.References.GetValue(configuration.AttributeKey); if (topicReferenceId == 0 && configuration.AttributeKey.EndsWith("Id", StringComparison.OrdinalIgnoreCase)) { topicReferenceId = source.Attributes.GetInteger(configuration.AttributeKey, 0); @@ -314,7 +315,7 @@ private async Task SetPropertyAsync( } } else if ( - source.References.TryGetValue(configuration.AttributeKey, out var topicReference) && + topicReference is not null && relationships.HasFlag(Relationships.References) ) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); From 93491fc02735280b9aa06af09df7127d7cb554cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:21:29 -0800 Subject: [PATCH 562/778] Avoid infinite loop with `BaseTopic` getter `BaseTopic` is a topic reference. Topic references can be inherited from the base topic. As such, calling `BaseTopic` calls `GetValue()` which in turn calls `BaseTopic`, causing an infinite loop. This isn't usually a challenge; it's a unique issue specific to the fact that `BaseTopic` is explicitly called by `GetValue()`. It can be easily fixed by manually retrieving the `BaseTopic` reference, if it exists, from the `TopicReferenceCollection` instead of relying on the (generally preferred) `GetValue()` method. --- OnTopic/Topic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 372c80e3..ccf328db 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -647,7 +647,7 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// [ReferenceSetter] public Topic? BaseTopic { - get => References.GetTopic("BaseTopic", false); + get => References.Contains("BaseTopic") ? References["BaseTopic"].Value : null; set { Contract.Requires( value != this, From 3d5b4ef4cbf2c693a3ccbf9e382b3b9a60fa0678 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 14:27:28 -0800 Subject: [PATCH 563/778] Updated `TopicPropertyDispatcher` to operate off of `TopicReference.Value` The `TopicPropertyDispatcher` has custom handling for `AttributeValue` instances so that it can call the value from the `Value` object, while storing the `AttributeValue` object. Similar logic was needed for `TopicReference`, since `TopicReferenceCollection` now stores a wrapper object with metadata, instead of just a `Topic` reference. The implementation is slightly different as `HasSettableProperty` needs a reference to the target type (i.e., `Topic`), whereas `AttributeValue` didn't since it was relying on the underlying `TypeMemberInfoCollection` to use its built-in conversion of basic scalar values to handle this. This is a bit clumsy in that the `TopicPropertyDispatcher` only supports `AttributeValueCollection` and `TopicReferenceCollection`. As a result, having hard-coded exceptions for each of the primary customers isn't a great design. Ideally, this would be replaced with support for generic handling of e.g. `TrackedTopic`. As this is an internal library, that's not a priority, and the currenct approach is an easy way to maintain this functionality. --- OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 0a5f3160..b5b4969e 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -187,6 +187,9 @@ internal bool Register(string itemKey, TValueType? initialValue) { if (typeof(AttributeValue).IsAssignableFrom(type)) { type = null; } + else if (typeof(TopicReference).IsAssignableFrom(type)) { + type = (initialValue as TopicReference)?.Value?.GetType(); + } if ( _typeCache.HasSettableProperty(_associatedTopic.GetType(), itemKey, type) && !PropertyCache.ContainsKey(itemKey) @@ -265,12 +268,13 @@ internal bool Enforce(string itemKey, TValueType? initialObject) { ); } var attribute = initialObject as AttributeValue; + var topicReference = initialObject as TopicReference; try { if (attribute is not null) { _typeCache.SetPropertyValue(_associatedTopic, itemKey, attribute.Value); } - else { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, initialObject); + else if (topicReference is not null) { + _typeCache.SetPropertyValue(_associatedTopic, itemKey, topicReference.Value); } } catch (TargetInvocationException ex) { From 11506c3df380519ac7b82aab61bdca6e0024d639 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 15:57:38 -0800 Subject: [PATCH 564/778] Generalized `TopicPropertyDispatcher<>` for `TrackedItem<>` Since `TopicPropertyDispatcher<>` is exclusively used by `TrackedCollection<>` now, and `TrackedCollection<>` exclusively works with `TrackedItem<>` instances, we can update `TopicPropertyDispatcher` to be more intelligent by making its generics aware of `TrackedItem<>` and its `TValue` type. This allows us to get rid of a number of ugly conditions which checked to see if we were working with an `AttributeValue` or a `TopicReference` and respond accordingly. Now, instead, it just operates off of the underlying `TrackedItem<>`, thus providing a uniform treatment. As this required adding a new generic type parameter, I used the opportunity to a) standardize the generic type argument order between `TopicPropertyDispatcher<>` and `TrackedCollection<>`, and b) rewrap the comments to account for the longer names, while also using a more compact format to help reduce unnecessary wrapping. --- ...ckedCollection{TItem,TValue,TAttribute}.cs | 2 +- .../Reflection/TopicPropertyDispatcher.cs | 194 +++++++++--------- 2 files changed, 93 insertions(+), 103 deletions(-) diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index d6473784..2456accf 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -39,7 +39,7 @@ public abstract class TrackedCollection : /*========================================================================================================================== | DISPATCHER \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly TopicPropertyDispatcher _topicPropertyDispatcher; + private readonly TopicPropertyDispatcher _topicPropertyDispatcher; /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index b5b4969e..1f9e3bc5 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -18,7 +18,7 @@ namespace OnTopic.Internal.Reflection { | CLASS: TOPIC PROPERTY DISPATCHER \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// The allows a collection on a + /// The allows a collection on a /// entity to optionally route requests through properties on the corresponding which correspond to the /// item key, thus ensuring local state and any business logic enforced by the property setter are maintained. /// @@ -29,65 +29,65 @@ namespace OnTopic.Internal.Reflection { /// this logic is typically handled by property setters on , such as or . This introduces a potential backdoor, as updates made directly to the collection can /// bypass any business logic—such as data validation or local state management—handled by those property setters. The - /// class addresses this by allowing those collections - /// to route requests through appropriately decorated properties on prior to adding or setting a - /// value. + /// class addresses this by allowing those + /// collections to route requests through appropriately decorated properties on prior to adding or + /// setting a value. /// /// - /// The requires two type arguments. represents an attribute which must be present on each property setter. This helps avoid potential - /// ambiguities. For instance, if both and have the same - /// key, and that key maps to the identify of a property, the usage will be restricted based on - /// whether the expected attribute is used to decorate the property—for instance, the or . In practice, this is an unexpected situation since a) individual content - /// types cannot use the same key for both and , and b) even - /// if they did, these properties support different data types, and thus are not intercompatible. Nevertheless, these + /// The requires two type arguments. represents an attribute which must be present on each property setter. This helps avoid + /// potential ambiguities. For instance, if both and have + /// the same key, and that key maps to a property, the usage will be restricted based on whether the + /// expected attribute is used to decorate the property—for instance, the or . In practice, this is an unexpected situation since a) individual content types + /// cannot use the same key for both and , and b) even if + /// they did, these properties support different data types, and thus are not intercompatible. Nevertheless, these /// attributes provide an additional level of explicitness to avoid any ambiguity, and provide both developers as well as - /// the hints about what a property - /// is intended for. + /// the hints about what a + /// property is intended for. /// /// - /// The represents the value that is stored in the corresponding collection. This value - /// is saved as part of either or , - /// and can optionally be retrieved via . This is useful in case there - /// is data from the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as - /// , , and represents the value that is stored in the corresponding collection. This value is + /// saved as part of either or , and can + /// optionally be retrieved via . This is useful in case there is data from + /// the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we - /// allow the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as - /// critical for e.g. since the will be the same that's sent to the corresponding property, and thus is expected to be the same as the value set by the - /// property itself. + /// "/>. As such, by saving a reference to those as part of the process, we allow + /// the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as critical + /// for e.g. since the will be the same + /// that's sent to the corresponding property, and thus is expected to be the same as the value set by the property itself. /// /// - /// In a typical workflow, the method will end up getting once or twice. The - /// first occurs when a caller attempts to insert an item directly into a collection; if a corresponding property setter - /// is requested, then will call , - /// trigger the call to the corresponding property, and return false—indicating that the item should not be added - /// to the collection. In this case, the property setter will run its own business logic, then attempt to add or set the - /// item into the collection again. This time, the call to will prevent the - /// second call to from enforcing the business logic, thus preventing an - /// infinite loop. + /// In a typical workflow, the method will end up getting once or twice. The first + /// occurs when a caller attempts to insert an item directly into a collection; if a corresponding property setter is + /// requested, then will call , trigger the + /// call to the corresponding property, and return false—indicating that the item should not be added to the + /// collection. In this case, the property setter will run its own business logic, then attempt to add or set the item + /// into the collection again. This time, the call to will prevent the second call + /// to from enforcing the business logic, thus preventing an infinite loop. /// /// /// One caveat to this are cases where the caller attempts to set the value via the property directly, /// instead of adding the item directly to the corresponding collection—e.g., they call instead /// of e.g. the method /// from . In that case, the business logic will already have been enforced, but the method will not have been called. To mitigate the property setter getting - /// called twice, collection implementors are advised to offer an internal overload that allows an item to be added to the - /// collection while bypassing the business logic. For instance, this can be done using or ; in each case, - /// the internally accessible enforceBusinessLogic parameter allows a property setter to disable business logic. - /// Internally, this is done by calling , thus assuring that the business logic has already occurred. + /// cref="Register(String, TItem?)"/> method will not have been called. To mitigate the property setter getting called + /// twice, collection implementors are advised to offer an internal overload that allows an item to be added to the + /// collection while bypassing the business logic. For instance, this can be done using or ; in each case, the internally + /// accessible enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this + /// is done by calling , thus assuring that + /// the business logic has already occurred. /// /// - internal class TopicPropertyDispatcher + internal class TopicPropertyDispatcher where TAttributeType: Attribute - where TValueType: class + where TItem: TrackedItem + where TValue: class { /*========================================================================================================================== @@ -105,8 +105,8 @@ internal class TopicPropertyDispatcher | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class associated - /// with a specific . + /// Initializes a new instance of the class + /// associated with a specific . /// /// The whose properties should be called, when appropriate. internal TopicPropertyDispatcher(Topic associatedTopic) { @@ -117,78 +117,75 @@ internal TopicPropertyDispatcher(Topic associatedTopic) { | PROPERTY: PROPERTY CACHE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a local cache of objects, keyed by an associated itemKey, prior + /// Provides a local cache of objects, keyed by an associated itemKey, prior /// to having their business logic enforced. /// /// /// /// Whenever a property setter has been called, a record in the is added containing a) the - /// key associated with the item (and, therefore, property), and b) the original that + /// key associated with the item (and, therefore, property), and b) the original that /// was to be added to the collection. This registers that the business logic for that property has been enforced. /// /// /// There are two ways that a record is created in the . The typical way is to call , which will check to see if there is a corresponding property setter and, if there + /// ="Enforce(String, TItem?)"/>, which will check to see if there is a corresponding property setter and, if there /// is, will add a record to the cache, and call the property. The second way is to call to directly register that the property has already been executed. This is typically done by special + /// TItem?)"/> to directly register that the property has already been executed. This is typically done by special /// internal methods, called exclusively by the property setters themselves, with a enforceBusinessLogic /// parameter that is set to false; that prevents calls made directly to the property setter to bypass the /// dispatcher. /// /// /// There is only one way to remove an item from the once it's been created. This happens - /// when is called, and a record already exists. When this occurs, the record - /// is removed, and returns true without any further action. This + /// when is called, and a record already exists. When this occurs, the record + /// is removed, and returns true without any further action. This /// instructs the caller—i.e., a method on a collection responsible for adding or setting an item—that it can complete /// the request. /// /// - private Dictionary PropertyCache { get; } = new(); + private Dictionary PropertyCache { get; } = new(); /*========================================================================================================================== | REGISTER \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Instructs the that the business logic for a + /// Instructs the that the business logic for a /// corresponding property has been set, and does not need to be executed again. /// /// /// - /// The method is called by right - /// before it triggers a call to a corresponding property setter. This allows it to track that the business logic has - /// been enforced, and it doesn't need to make the call again on a round trip. + /// The method is called by right before it + /// triggers a call to a corresponding property setter. This allows it to track that the business logic has been + /// enforced, and it doesn't need to make the call again on a round trip. /// /// - /// The method can also be called directly by a collection to tell the - /// that business logic should not be enforced when - /// adding or setting an item. The typical use case for this is an internal method which allows the property setters - /// themselves to bypass business logic, thus preventing them from being called twice. These methods should be marked - /// internal to prevent external actors from bypassing the business logic; the purpose is to confirm that the business - /// logic has already been enforced, not to make the business logic optional. Two examples of this are the internal - /// enforceBusinessLogic parameters on method can also be called directly by a collection to tell the that business logic should not be enforced when adding or + /// setting an item. The typical use case for this is an internal method which allows the property setters to bypass + /// business logic, thus preventing them from being called twice. These methods should be marked internal to prevent + /// external actors from bypassing the business logic; the purpose is to confirm that the business logic has already + /// been enforced, not to make the business logic optional. Two examples of this are the internal + /// enforceBusinessLogic parameters on and . /// /// - /// It's worth noting that any calls to are invalidated the next time is called. As such, is not a way - /// to permanently disable calling a property setter. (The correct way to do that is to remove the property setter, or - /// at least its corresponding .) Instead, it only disables the next attempt to add - /// an item corresponding to that key—which, if correctly implemented, will be when the current is added to the collection. + /// It's worth noting that any calls to are invalidated the next time is called. As such, is not a way to permanently + /// disable calling a property setter. (The correct way to do that is to remove the property setter, or at least its + /// corresponding .) Instead, it only disables the next attempt to add an item + /// corresponding to that key—which, if correctly implemented, will be when the current + /// is added to the collection. /// /// /// - /// The key of the , which potentially corresponds to a property. + /// The key of the , which potentially corresponds to a property. /// - /// The object which is being inserted. - internal bool Register(string itemKey, TValueType? initialValue) { - var type = initialValue?.GetType(); - if (typeof(AttributeValue).IsAssignableFrom(type)) { - type = null; - } - else if (typeof(TopicReference).IsAssignableFrom(type)) { - type = (initialValue as TopicReference)?.Value?.GetType(); + /// The object which is being inserted. + internal bool Register(string itemKey, TItem? initialValue) { + var type = (Type?)null; + if (!MemberDispatcher.SettableTypes.Contains(typeof(TValue))) { + type = typeof(TValue); } if ( _typeCache.HasSettableProperty(_associatedTopic.GetType(), itemKey, type) && @@ -208,7 +205,7 @@ internal bool Register(string itemKey, TValueType? initialValue) { /// logic enforced. /// /// - /// The key of the , which potentially corresponds to a property. + /// The key of the , which potentially corresponds to a property. /// /// Returns true if the has been registered, otherwise false. internal bool IsRegistered(string itemKey) => IsRegistered(itemKey, out var _); @@ -218,43 +215,43 @@ internal bool Register(string itemKey, TValueType? initialValue) { /// logic enforced. Returns the that was registered as an out parameter. /// /// - /// The key of the , which potentially corresponds to a property. + /// The key of the , which potentially corresponds to a property. /// - /// The object which is being inserted. + /// The object which is being inserted. /// Returns true if the has been registered, otherwise false. - internal bool IsRegistered(string itemKey, [NotNullWhen(true)] out TValueType? initialObject) => + internal bool IsRegistered(string itemKey, [NotNullWhen(true)] out TItem? initialObject) => PropertyCache.TryGetValue(itemKey, out initialObject!); /*========================================================================================================================== | METHOD: ENFORCE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Inspects the requested to determine if the corresponding should be routed through the associated in order to enforce business logic. + /// Inspects the requested to determine if the corresponding + /// should be routed through the associated in order to enforce business logic. /// /// /// /// If a settable property is available on the associated corresponding to the , the call should be routed through that property to ensure that local business logic is enforced. This - /// is determined by looking for attribute, which confirms that a property with a + /// itemKey"/>, the call should be routed through that property to ensure that local business logic is enforced. This is + /// determined by looking for attribute, which confirms that a property with a /// matching name is aware of and intended to operate with a given collection. /// /// - /// The method should be called from an implementing collection prior to + /// The method should be called from an implementing collection prior to /// committing an add, insert, or set operation. That operation should only be completed if returns true; otherwise, the request will be routed through the corresponding property on - /// in order to enforce any business logic, after which the property will attempt to add the - /// property to the collection again. When is called a second time for the - /// same , it won't enforce the business logic, and will instead return true. + /// TItem?)"/> returns true; otherwise, the request will be routed through the corresponding property on in order to enforce any business logic, after which the property will attempt to add the property to + /// the collection again. When is called a second time for the same , it won't enforce the business logic, and will instead return true. /// /// /// - /// The key of the , which potentially corresponds to a property + /// The key of the , which potentially corresponds to a property /// setter. /// - /// The object which is being inserted. + /// The object which is being inserted. /// Returns true if the business logic has been enfored; otherwise false. - internal bool Enforce(string itemKey, TValueType? initialObject) { + internal bool Enforce(string itemKey, TItem? initialObject) { if (PropertyCache.ContainsKey(itemKey)) { PropertyCache.Remove(itemKey); return true; @@ -267,15 +264,8 @@ internal bool Enforce(string itemKey, TValueType? initialObject) { $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties." ); } - var attribute = initialObject as AttributeValue; - var topicReference = initialObject as TopicReference; try { - if (attribute is not null) { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, attribute.Value); - } - else if (topicReference is not null) { - _typeCache.SetPropertyValue(_associatedTopic, itemKey, topicReference.Value); - } + _typeCache.SetPropertyValue(_associatedTopic, itemKey, (TValue?)initialObject?.Value); } catch (TargetInvocationException ex) { if (PropertyCache.ContainsKey(itemKey)) { From 13d43d14d378712a4ac7beb37287b4933942142e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 16:00:01 -0800 Subject: [PATCH 565/778] Consolidate overloads of `SetPropertyValue()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reduces the amount of code by consolidating largely repetitive methods. In addition, however, it fixes a bug (? or at least unexpected behavior) where a caller with a generic type that of `string?` doesn't call the `string?` overload but rather the `object?` overload, unless it's explicitly converted to a string first. That's a confusing burden to put on callers, so it's easier to handle here—and especially given that the `MemberDispatcher` will frequently be called either with genetics. --- .../Internal/Reflection/MemberDispatcher.cs | 41 ++++--------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 70639a74..b5bc1a96 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -137,18 +137,20 @@ internal bool HasSettableProperty(Type type, string name, Type? targetType = nul | METHOD: SET PROPERTY VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Uses reflection to call a property, assuming that it is a) writable, and b) of type , - /// , or . + /// Uses reflection to call a property, assuming that it is a) writable, and b) of type , , or , or is otherwise compatible with the type. /// /// The object on which the property is defined. /// The name of the property to assess. /// The value to set on the property. - internal bool SetPropertyValue(object target, string name, string? value) { + internal bool SetPropertyValue(object target, string name, object? value) { Contract.Requires(target, nameof(target)); Contract.Requires(name, nameof(name)); - if (!HasSettableProperty(target.GetType(), name)) { + var isString = value?.GetType() == typeof(string); + + if (!HasSettableProperty(target.GetType(), name, isString? null : value?.GetType())) { return false; } @@ -156,7 +158,7 @@ internal bool SetPropertyValue(object target, string name, string? value) { Contract.Assume(property, $"The {name} property could not be retrieved."); - var valueObject = GetValueObject(property.PropertyType, value); + var valueObject = isString? GetValueObject(property.PropertyType, value as string) : value; if (valueObject is null) { return false; @@ -167,35 +169,6 @@ internal bool SetPropertyValue(object target, string name, string? value) { } - /// - /// Uses reflection to call a property, assuming that the property value is compatible with the - /// type. - /// - /// The object on which the property is defined. - /// The name of the property to assess. - /// The value to set on the property. - internal bool SetPropertyValue(object target, string name, object? value) { - - Contract.Requires(target, nameof(target)); - Contract.Requires(name, nameof(name)); - - if (!HasSettableProperty(target.GetType(), name, value?.GetType())) { - return false; - } - - var property = GetMember(target.GetType(), name); - - Contract.Assume(property, $"The {name} property could not be retrieved."); - - if (value is null) { - return false; - } - - property.SetValue(target, value); - return true; - - } - /*========================================================================================================================== | METHOD: HAS GETTABLE PROPERTY \-------------------------------------------------------------------------------------------------------------------------*/ From edc9f4b4a0fa0aa01a935f24d3d8f595076b106b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 16:15:10 -0800 Subject: [PATCH 566/778] Updated `IsSettableType` to honor polymorphism It's not entirely clear why this was working previously, as several unit tests worked with mapping derived types. Regardless, this is an easy and obvious fix that ensures that derived types can be set to properties of their parent type. --- OnTopic/Internal/Reflection/MemberDispatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index b5bc1a96..2ac9c548 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -409,7 +409,7 @@ method is not null && private static bool IsSettableType(Type sourceType, Type? targetType = null) { if (targetType is not null) { - return sourceType.Equals(targetType); + return sourceType.IsAssignableFrom(targetType); } return SettableTypes.Contains(sourceType); From f84ae667a6067b039619c3b528002f40429fe1a2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 17:05:16 -0800 Subject: [PATCH 567/778] Renamed `[Relationship()]` attribute to `[Collection()]` Originally, the `[Relationship()]` attribute was intended to disambiguate references to `Topic.Relationships` and `Topic.IncomingRelationships`. Almost as soon as it was conceived of, however, it was expanded to include e.g. `Topic.Children`, as well as mapped collections (i.e., strongly typed collections with compatible types), and nested topics (a specialized type of `Topic.Children`). Technically, we can argue that all of these are types of relationships. But the name remains ambiguous and unintuitive, since it sounds specific to `Topic.Relationships`. To mitigate that, it's being renamed to `[Collection()]`, which better articulates that it's clarifying the mapping between the source collection and the target collection, independent of whether or not it's a relationship attribute. --- ...nvalidRelationshipTypeTopicBindingModel.cs | 2 +- OnTopic.Tests/TopicMappingServiceTest.cs | 4 ++-- .../AmbiguousRelationTopicViewModel.cs | 4 ++-- .../ContentTypeDescriptorTopicViewModel.cs | 2 +- ...hipAttribute.cs => CollectionAttribute.cs} | 20 +++++++++---------- .../Mapping/Internal/PropertyConfiguration.cs | 6 +++--- .../Reverse/IReverseTopicMappingService.cs | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) rename OnTopic/Mapping/Annotations/{RelationshipAttribute.cs => CollectionAttribute.cs} (81%) diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index 921f188e..5dae7c18 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -25,7 +25,7 @@ public class InvalidRelationshipTypeTopicBindingModel : BasicTopicBindingModel { public InvalidRelationshipTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - [Relationship(RelationshipType.NestedTopics)] + [Collection(RelationshipType.NestedTopics)] public Collection ContentTypes { get; } = new(); } //Class diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 348e4064..bc1a5b02 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -384,11 +384,11 @@ public async Task Map_Relationships_SkipsDisabled() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Establishes a and tests whether it successfully derives values from the key and - /// type specified by . + /// type specified by . /// /// /// The uses to set the relationship key to AmbiguousRelationship and the + /// cref="Mapping.Annotations.CollectionAttribute"/> to set the relationship key to AmbiguousRelationship and the /// to . AmbiguousRelationship /// refers to a relationship that is both outgoing and incoming. It should be smart enough to a) look for the /// AmbigousRelationship instead of the RelationshipAlias, and b) source from the /// /// - /// The uses to set the relationship key to + /// The uses to set the relationship key to /// AmbiguousRelationship and the to . AmbiguousRelationship refers to a relationship that is both /// outgoing and incoming. @@ -27,7 +27,7 @@ namespace OnTopic.Tests.ViewModels { /// public class AmbiguousRelationTopicViewModel: KeyOnlyTopicViewModel { - [Relationship("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)] + [Collection("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)] public Collection RelationshipAlias { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index 40202b9c..635e3815 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -22,7 +22,7 @@ public class ContentTypeDescriptorTopicViewModel { public Collection AttributeDescriptors { get; } = new(); - [Relationship(RelationshipType.MappedCollection)] + [Collection(RelationshipType.MappedCollection)] [Follow(Relationships.None)] public Collection PermittedContentTypes { get; } = new(); diff --git a/OnTopic/Mapping/Annotations/RelationshipAttribute.cs b/OnTopic/Mapping/Annotations/CollectionAttribute.cs similarity index 81% rename from OnTopic/Mapping/Annotations/RelationshipAttribute.cs rename to OnTopic/Mapping/Annotations/CollectionAttribute.cs index 66af9c22..a1bc5d0d 100644 --- a/OnTopic/Mapping/Annotations/RelationshipAttribute.cs +++ b/OnTopic/Mapping/Annotations/CollectionAttribute.cs @@ -7,7 +7,7 @@ namespace OnTopic.Mapping.Annotations { /*============================================================================================================================ - | ATTRIBUTE: RELATIONSHIP + | ATTRIBUTE: COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides the with instructions as to which key to follow for the relationship. @@ -21,32 +21,32 @@ namespace OnTopic.Mapping.Annotations { /// /// /// This attribute instructs the to instead look for a specified key. This allows the - /// target property name to be decoupled from the source's relationship key. In addition, this attribute can be used to - /// specify the type of relationship expected, which is useful if there might be ambiguity between relationship names (for - /// example, if there is a with the same key as an ). + /// target property name to be decoupled from the source's collection key. In addition, this attribute can be used to + /// specify the type of collection expected, which is useful if there might be ambiguity between collection names (for + /// example, if there is a with the same key as an ), which is not an uncommon scenario. /// /// [System.AttributeUsage(System.AttributeTargets.Property)] - public sealed class RelationshipAttribute : System.Attribute { + public sealed class CollectionAttribute : System.Attribute { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Annotates a property with the by providing an . + /// Annotates a property with the by providing an . /// /// The key value of the relationships associated with the current property. - public RelationshipAttribute(string key) { + public CollectionAttribute(string key) { TopicFactory.ValidateKey(key, false); Key = key; } /// - /// Annotates a property with the by providing the . + /// Annotates a property with the by providing the . /// /// Optional. The type of collection the relationship is associated with. - public RelationshipAttribute(RelationshipType type = RelationshipType.Any) { + public CollectionAttribute(RelationshipType type = RelationshipType.Any) { Type = type; } diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index b322a7bd..d27c05a9 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -92,7 +92,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /*------------------------------------------------------------------------------------------------------------------------ | Attributes: Determine relationship key and type \-----------------------------------------------------------------------------------------------------------------------*/ - GetAttributeValue( + GetAttributeValue( property, a => { RelationshipKey = a.Key ?? RelationshipKey; @@ -248,7 +248,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// the DTO to be aliased to a different collection name on the source . /// /// - /// The property corresponds to the property. It + /// The property corresponds to the property. It /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey")]. /// /// @@ -269,7 +269,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// be ambiguous between multiple collections. /// /// - /// The property corresponds to the property. It + /// The property corresponds to the property. It /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey", /// RelationshipType.Children)]. /// diff --git a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs index da861e8e..1f9ebfe9 100644 --- a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs @@ -36,7 +36,7 @@ namespace OnTopic.Mapping.Reverse { /// Despite the differences between the and s, /// many attributes are able to be reused between them. For instance, the can still /// map a property on a binding model to an attribute of a different name on a , just as the can with relationships. Other attributes, however, provde no benefit in the reverse + /// cref="CollectionAttribute"/> can with relationships. Other attributes, however, provde no benefit in the reverse /// scenario, such as or , which really only make /// sense in creating a "produced view" that is a subset of the original model. That is valuable when creating a view /// model, but isn't a useful use case when working with binding models. From 1daf799eef0c9de4769aa251a2f948788af72764 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 17:28:26 -0800 Subject: [PATCH 568/778] Renamed `RelationshipType` to `CollectionType` This corresponds to the rename of the `[Relationship()]` attribute to `[Collection()]`. This also has a large downstream effect, since `PropertyConfiguration` has a `RelationshipType` property, and there are a lot of related `relationshipType` variables throughout the code base. --- ...nvalidRelationshipTypeTopicBindingModel.cs | 8 +++---- .../ReverseTopicMappingServiceTest.cs | 7 +++--- OnTopic.Tests/TopicMappingServiceTest.cs | 11 +++++----- .../AmbiguousRelationTopicViewModel.cs | 9 ++++---- .../ContentTypeDescriptorTopicViewModel.cs | 2 +- .../Annotations/CollectionAttribute.cs | 10 ++++----- ...{RelationshipType.cs => CollectionType.cs} | 9 ++++---- OnTopic/Mapping/Annotations/Relationships.cs | 8 +++---- .../Mapping/Internal/PropertyConfiguration.cs | 16 +++++++------- OnTopic/Mapping/Internal/RelationshipMap.cs | 22 +++++++++---------- .../Mapping/Reverse/BindingModelValidator.cs | 18 +++++++-------- OnTopic/Mapping/TopicMappingService.cs | 18 +++++++-------- 12 files changed, 68 insertions(+), 70 deletions(-) rename OnTopic/Mapping/Annotations/{RelationshipType.cs => CollectionType.cs} (84%) diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index 5dae7c18..f6fab89a 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -14,9 +14,9 @@ namespace OnTopic.Tests.BindingModels { | BINDING MODEL: RELATIONSHIP TYPE TOPIC (INVALID) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a custom binding model with an invalid —i.e., it refers to , even though the property is associated with a . An should be thrown when it is mapped. + /// Provides a custom binding model with an invalid —i.e., it refers to , even though the property is associated with a . + /// An should be thrown when it is mapped. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. @@ -25,7 +25,7 @@ public class InvalidRelationshipTypeTopicBindingModel : BasicTopicBindingModel { public InvalidRelationshipTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - [Collection(RelationshipType.NestedTopics)] + [Collection(CollectionType.NestedTopics)] public Collection ContentTypes { get; } = new(); } //Class diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index dbdc8b6d..8e1b081f 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -440,10 +440,9 @@ public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationExceptio | TEST: MAP: INVALID RELATIONSHIP TYPE: THROWS INVALID OPERATION EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Maps a content type that has a relationship an invalid —i.e., it refers to , even though the property is associated with a . This is invalid, and expected to throw an . + /// Maps a content type that has a relationship with an invalid —i.e., it refers to , even though the property is associated with a . This is invalid, and expected to throw an . /// [TestMethod] [ExpectedException(typeof(MappingModelValidationException))] diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index bc1a5b02..09960750 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -387,12 +387,11 @@ public async Task Map_Relationships_SkipsDisabled() { /// type specified by . /// /// - /// The uses to set the relationship key to AmbiguousRelationship and the - /// to . AmbiguousRelationship - /// refers to a relationship that is both outgoing and incoming. It should be smart enough to a) look for the - /// AmbigousRelationship instead of the RelationshipAlias, and b) source from the collection. + /// The uses to set the relationship key to AmbiguousRelationship and the to . AmbiguousRelationship refers to a relationship that is + /// both outgoing and incoming. It should be smart enough to a) look for the AmbigousRelationship instead of the + /// RelationshipAlias, and b) source from the collection. /// [TestMethod] public async Task Map_AlternateRelationship_ReturnsCorrectRelationship() { diff --git a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs index bc81aff0..d3fdbb6b 100644 --- a/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs @@ -16,10 +16,9 @@ namespace OnTopic.Tests.ViewModels { /// /// /// - /// The uses to set the relationship key to - /// AmbiguousRelationship and the to . AmbiguousRelationship refers to a relationship that is both - /// outgoing and incoming. + /// The uses to set the relationship key to + /// AmbiguousRelationship and the to . + /// AmbiguousRelationship refers to a relationship that is both outgoing and incoming. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. @@ -27,7 +26,7 @@ namespace OnTopic.Tests.ViewModels { /// public class AmbiguousRelationTopicViewModel: KeyOnlyTopicViewModel { - [Collection("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)] + [Collection("AmbiguousRelationship", Type= CollectionType.IncomingRelationship)] public Collection RelationshipAlias { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index 635e3815..1f35107b 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -22,7 +22,7 @@ public class ContentTypeDescriptorTopicViewModel { public Collection AttributeDescriptors { get; } = new(); - [Collection(RelationshipType.MappedCollection)] + [Collection(CollectionType.MappedCollection)] [Follow(Relationships.None)] public Collection PermittedContentTypes { get; } = new(); diff --git a/OnTopic/Mapping/Annotations/CollectionAttribute.cs b/OnTopic/Mapping/Annotations/CollectionAttribute.cs index a1bc5d0d..1f05030d 100644 --- a/OnTopic/Mapping/Annotations/CollectionAttribute.cs +++ b/OnTopic/Mapping/Annotations/CollectionAttribute.cs @@ -43,10 +43,10 @@ public CollectionAttribute(string key) { } /// - /// Annotates a property with the by providing the . + /// Annotates a property with the by providing the . /// /// Optional. The type of collection the relationship is associated with. - public CollectionAttribute(RelationshipType type = RelationshipType.Any) { + public CollectionAttribute(CollectionType type = CollectionType.Any) { Type = type; } @@ -54,7 +54,7 @@ public CollectionAttribute(RelationshipType type = RelationshipType.Any) { | PROPERTY: KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets the value of the relationship key. + /// Gets the value of the collection key. /// public string? Key { get; } @@ -62,10 +62,10 @@ public CollectionAttribute(RelationshipType type = RelationshipType.Any) { | PROPERTY: TYPE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets the value of the relationship type. + /// Gets the value of the . /// #pragma warning disable CA1019 // Define accessors for attribute arguments - public RelationshipType Type { get; set; } + public CollectionType Type { get; set; } #pragma warning restore CA1019 // Define accessors for attribute arguments } //Class diff --git a/OnTopic/Mapping/Annotations/RelationshipType.cs b/OnTopic/Mapping/Annotations/CollectionType.cs similarity index 84% rename from OnTopic/Mapping/Annotations/RelationshipType.cs rename to OnTopic/Mapping/Annotations/CollectionType.cs index 1020191e..febbea37 100644 --- a/OnTopic/Mapping/Annotations/RelationshipType.cs +++ b/OnTopic/Mapping/Annotations/CollectionType.cs @@ -7,15 +7,16 @@ namespace OnTopic.Mapping.Annotations { /*============================================================================================================================ - | ENUM: RELATIONSHIP TYPE + | ENUM: COLLECTION TYPE \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Enum that allows a relationship to be specified. + /// Enum that allows a collection to be specified. /// /// - /// This differs from , which allows multiple relationships to be specified. + /// This differs from , which allows multiple collections to be specified, and also + /// includes the as a source. /// - public enum RelationshipType { + public enum CollectionType { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member diff --git a/OnTopic/Mapping/Annotations/Relationships.cs b/OnTopic/Mapping/Annotations/Relationships.cs index 628214a3..a3211cd3 100644 --- a/OnTopic/Mapping/Annotations/Relationships.cs +++ b/OnTopic/Mapping/Annotations/Relationships.cs @@ -23,7 +23,7 @@ namespace OnTopic.Mapping.Annotations { /// relevant to the class and its view models. /// /// - /// This differs from , which only allows one relationship to be specified. + /// This differs from , which only allows one collection to be specified. /// /// [Flags] @@ -49,7 +49,7 @@ public enum Relationships { | CHILDREN \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Map references, or properties marked as . + /// Map references, or properties marked as . /// Children = 1 << 1, @@ -57,7 +57,7 @@ public enum Relationships { | RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Map references, or properties marked as . + /// Map references, or properties marked as . /// Relationships = 1 << 2, @@ -66,7 +66,7 @@ public enum Relationships { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Map references, or properties marked as . + /// cref="CollectionType.IncomingRelationship"/>. /// IncomingRelationships = 1 << 3, diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index d27c05a9..284501ad 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -68,7 +68,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" DefaultValue = null; InheritValue = false; RelationshipKey = AttributeKey; - RelationshipType = RelationshipType.Any; + CollectionType = CollectionType.Any; CrawlRelationships = Relationships.None; MetadataKey = null; DisableMapping = false; @@ -96,12 +96,12 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" property, a => { RelationshipKey = a.Key ?? RelationshipKey; - RelationshipType = a.Type; + CollectionType = a.Type; } ); if (RelationshipKey.Equals("Children", StringComparison.OrdinalIgnoreCase)) { - RelationshipType = RelationshipType.Children; + CollectionType = CollectionType.Children; } /*------------------------------------------------------------------------------------------------------------------------ @@ -264,17 +264,17 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// By default, a collection property on a DTO class will attempt to find a match from, in order, , , and, finally, . - /// If the is set, however, then the will only + /// If the is set, however, then the will only /// map the collection to a relationship of that type. This can be valuable when the might /// be ambiguous between multiple collections. /// /// - /// The property corresponds to the property. It + /// The property corresponds to the property. It /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey", - /// RelationshipType.Children)]. + /// CollectionType.Children)]. /// /// - public RelationshipType RelationshipType { get; set; } + public CollectionType CollectionType { get; set; } /*========================================================================================================================== | PROPERTY: CRAWL RELATIONSHIPS @@ -286,7 +286,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// /// By default, the all relationships will be mapped on the target DTO, unless the caller specifies otherwise. On any - /// related DTOs, however, only will be mapped. So, if a mapped DTO has a + /// related DTOs, however, only will be mapped. So, if a mapped DTO has a /// collection for children, relationships, or even a parent property then any relationships on those DTOs will /// not be mapped. This behavior can be changed by specifying the flag, which allows /// one or multiple relationships to be specified for a given property. diff --git a/OnTopic/Mapping/Internal/RelationshipMap.cs b/OnTopic/Mapping/Internal/RelationshipMap.cs index 50bc8236..77fa9066 100644 --- a/OnTopic/Mapping/Internal/RelationshipMap.cs +++ b/OnTopic/Mapping/Internal/RelationshipMap.cs @@ -12,11 +12,11 @@ namespace OnTopic.Mapping.Internal { | CLASS: RELATIONSHIP MAP \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a mapping of the relationship between and . + /// Provides a mapping of the relationship between and . /// /// - /// While the and enumerations are distinct, there are times when - /// a single needs to be related to an item in the collection of . + /// While the and enumerations are distinct, there are times when + /// a single needs to be related to an item in the collection of . /// This mapping makes that feasible. /// static internal class RelationshipMap { @@ -26,13 +26,13 @@ static internal class RelationshipMap { \-------------------------------------------------------------------------------------------------------------------------*/ static RelationshipMap() { - var mappings = new Dictionary { - { RelationshipType.Any, Relationships.None }, - { RelationshipType.Children, Relationships.Children }, - { RelationshipType.Relationship, Relationships.Relationships }, - { RelationshipType.NestedTopics, Relationships.None }, - { RelationshipType.MappedCollection, Relationships.MappedCollections }, - { RelationshipType.IncomingRelationship, Relationships.IncomingRelationships } + var mappings = new Dictionary { + { CollectionType.Any, Relationships.None }, + { CollectionType.Children, Relationships.Children }, + { CollectionType.Relationship, Relationships.Relationships }, + { CollectionType.NestedTopics, Relationships.None }, + { CollectionType.MappedCollection, Relationships.MappedCollections }, + { CollectionType.IncomingRelationship, Relationships.IncomingRelationships } }; Mappings = mappings; @@ -42,7 +42,7 @@ static RelationshipMap() { /*========================================================================================================================== | PROPERTY: MAPPINGS \-------------------------------------------------------------------------------------------------------------------------*/ - static internal Dictionary Mappings { get; } + static internal Dictionary Mappings { get; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 8590f366..f6f9db6d 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -162,8 +162,8 @@ static internal void ValidateProperty( var configuration = new PropertyConfiguration(property, attributePrefix); var compositeAttributeKey = configuration.AttributeKey; var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey); - var childRelationships = new[] { RelationshipType.Children, RelationshipType.NestedTopics }; - var relationships = new[] { RelationshipType.Relationship, RelationshipType.IncomingRelationship }; + var childCollections = new[] { CollectionType.Children, CollectionType.NestedTopics }; + var relationships = new[] { CollectionType.Relationship, CollectionType.IncomingRelationship }; var listType = (Type?)null; /*------------------------------------------------------------------------------------------------------------------------ @@ -197,7 +197,7 @@ static internal void ValidateProperty( /*------------------------------------------------------------------------------------------------------------------------ | Handle children \-----------------------------------------------------------------------------------------------------------------------*/ - if (configuration.RelationshipType is RelationshipType.Children) { + if (configuration.CollectionType is CollectionType.Children) { throw new MappingModelValidationException( $"The {nameof(ReverseTopicMappingService)} does not support mapping child topics. This property should be " + $"removed from the binding model, or otherwise decorated with the {nameof(DisableMappingAttribute)} to prevent " + @@ -247,7 +247,7 @@ listType is not null ) { throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + - $"{configuration.RelationshipType}, but the generic type '{listType.Name}' does not implement the " + + $"{configuration.CollectionType}, but the generic type '{listType.Name}' does not implement the " + $"{nameof(ITopicBindingModel)} interface. This is required for binding models. If this collection is not intended " + $"to be mapped as a {ModelType.NestedTopic} then update the definition in the associated " + $"{nameof(ContentTypeDescriptor)}. If this collection is not intended to be mapped at all, include the " + @@ -325,11 +325,11 @@ [AllowNull]Type listType /*------------------------------------------------------------------------------------------------------------------------ | Validate relationship type \-----------------------------------------------------------------------------------------------------------------------*/ - if (!new[] { RelationshipType.Any, RelationshipType.Relationship }.Contains(configuration.RelationshipType)) { + if (!new[] { CollectionType.Any, CollectionType.Relationship }.Contains(configuration.CollectionType)) { throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class maps to a relationship attribute " + - $"'{attributeDescriptor.Key}', but is configured as a {configuration.RelationshipType}. The property should be " + - $"flagged as either {nameof(RelationshipType.Any)} or {nameof(RelationshipType.Relationship)}." + $"'{attributeDescriptor.Key}', but is configured as a {configuration.CollectionType}. The property should be " + + $"flagged as either {nameof(CollectionType.Any)} or {nameof(CollectionType.Relationship)}." ); } @@ -339,9 +339,9 @@ [AllowNull]Type listType if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) { throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + - $"{configuration.RelationshipType}, but the generic type '{listType?.Name}' does not implement the " + + $"{configuration.CollectionType}, but the generic type '{listType?.Name}' does not implement the " + $"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + - $"intended to be mapped as a {configuration.RelationshipType} then update the definition in the associated " + + $"intended to be mapped as a {configuration.CollectionType} then update the definition in the associated " + $"{nameof(ContentTypeDescriptor)}. If this collection is not intended to be mapped at all, include the " + $"{nameof(DisableMappingAttribute)} to exclude it from mapping." ); diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index ea0731c2..c5325893 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -522,13 +522,13 @@ private IList GetSourceCollection(Topic source, Relationships relationshi \-----------------------------------------------------------------------------------------------------------------------*/ var listSource = (IList)Array.Empty(); var relationshipKey = configuration.RelationshipKey; - var relationshipType = configuration.RelationshipType; + var collectionType = configuration.CollectionType; /*------------------------------------------------------------------------------------------------------------------------ | Handle children \-----------------------------------------------------------------------------------------------------------------------*/ listSource = GetRelationship( - RelationshipType.Children, + CollectionType.Children, s => true, () => source.Children.ToList() ); @@ -537,7 +537,7 @@ private IList GetSourceCollection(Topic source, Relationships relationshi | Handle (outgoing) relationships \-----------------------------------------------------------------------------------------------------------------------*/ listSource = GetRelationship( - RelationshipType.Relationship, + CollectionType.Relationship, source.Relationships.Contains, () => source.Relationships.GetTopics(relationshipKey) ); @@ -546,7 +546,7 @@ private IList GetSourceCollection(Topic source, Relationships relationshi | Handle nested topics, or children corresponding to the property name \-----------------------------------------------------------------------------------------------------------------------*/ listSource = GetRelationship( - RelationshipType.NestedTopics, + CollectionType.NestedTopics, source.Children.Contains, () => source.Children[relationshipKey].Children ); @@ -555,7 +555,7 @@ private IList GetSourceCollection(Topic source, Relationships relationshi | Handle (incoming) relationships \-----------------------------------------------------------------------------------------------------------------------*/ listSource = GetRelationship( - RelationshipType.IncomingRelationship, + CollectionType.IncomingRelationship, source.IncomingRelationships.Contains, () => source.IncomingRelationships.GetTopics(relationshipKey) ); @@ -576,7 +576,7 @@ private IList GetSourceCollection(Topic source, Relationships relationshi typeof(Topic).IsAssignableFrom(sourcePropertyValue[0]?.GetType()) ) { listSource = GetRelationship( - RelationshipType.MappedCollection, + CollectionType.MappedCollection, s => true, () => sourcePropertyValue.Cast().ToList() ); @@ -609,12 +609,12 @@ private IList GetSourceCollection(Topic source, Relationships relationshi /*------------------------------------------------------------------------------------------------------------------------ | Provide local function for evaluating current relationship \-----------------------------------------------------------------------------------------------------------------------*/ - IList GetRelationship(RelationshipType relationship, Func contains, Func> getTopics) { + IList GetRelationship(CollectionType relationship, Func contains, Func> getTopics) { var targetRelationships = RelationshipMap.Mappings[relationship]; var preconditionsMet = listSource.Count == 0 && - (relationshipType is RelationshipType.Any || relationshipType.Equals(relationship)) && - (relationshipType is RelationshipType.Children || relationship is not RelationshipType.Children) && + (collectionType is CollectionType.Any || collectionType.Equals(relationship)) && + (collectionType is CollectionType.Children || relationship is not CollectionType.Children) && (targetRelationships is Relationships.None || relationships.HasFlag(targetRelationships)) && contains(configuration.RelationshipKey); return preconditionsMet? getTopics() : listSource; From 44dc3eb157972dc87ce9b53fc055783c95831261 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Feb 2021 17:35:29 -0800 Subject: [PATCH 569/778] Renamed `PropertyConfiguration.RelationshipKey` to `CollectionKey` With the renaming of `[Relationship()]` to `[Collection()]` (f84ae66), it makes sense to rename the `PropertyConfiguration`'s `RelationshipKey` property to `CollectionKey`, since it maps to the `CollectionAttribute.Key` property. --- OnTopic/Mapping/Internal/PropertyConfiguration.cs | 14 +++++++------- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 284501ad..70a33b72 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -67,7 +67,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" AttributePrefix = attributePrefix; DefaultValue = null; InheritValue = false; - RelationshipKey = AttributeKey; + CollectionKey = AttributeKey; CollectionType = CollectionType.Any; CrawlRelationships = Relationships.None; MetadataKey = null; @@ -95,12 +95,12 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue( property, a => { - RelationshipKey = a.Key ?? RelationshipKey; + CollectionKey = a.Key ?? CollectionKey; CollectionType = a.Type; } ); - if (RelationshipKey.Equals("Children", StringComparison.OrdinalIgnoreCase)) { + if (CollectionKey.Equals("Children", StringComparison.OrdinalIgnoreCase)) { CollectionType = CollectionType.Children; } @@ -244,15 +244,15 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// So, for instance, if the property on the DTO class is called Cousins then the will search , , and, finally, for an object named Cousins. - /// If the is set, however, then that value is used instead, thus allowing the property on + /// If the is set, however, then that value is used instead, thus allowing the property on /// the DTO to be aliased to a different collection name on the source . /// /// - /// The property corresponds to the property. It + /// The property corresponds to the property. It /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey")]. /// /// - public string RelationshipKey { get; set; } + public string CollectionKey { get; set; } /*========================================================================================================================== | PROPERTY: RELATIONSHIP TYPE @@ -265,7 +265,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// By default, a collection property on a DTO class will attempt to find a match from, in order, , , and, finally, . /// If the is set, however, then the will only - /// map the collection to a relationship of that type. This can be valuable when the might + /// map the collection to a relationship of that type. This can be valuable when the might /// be ambiguous between multiple collections. /// /// diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index c5325893..e4ffb092 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -521,7 +521,7 @@ private IList GetSourceCollection(Topic source, Relationships relationshi | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ var listSource = (IList)Array.Empty(); - var relationshipKey = configuration.RelationshipKey; + var relationshipKey = configuration.CollectionKey; var collectionType = configuration.CollectionType; /*------------------------------------------------------------------------------------------------------------------------ @@ -616,7 +616,7 @@ IList GetRelationship(CollectionType relationship, Func con (collectionType is CollectionType.Any || collectionType.Equals(relationship)) && (collectionType is CollectionType.Children || relationship is not CollectionType.Children) && (targetRelationships is Relationships.None || relationships.HasFlag(targetRelationships)) && - contains(configuration.RelationshipKey); + contains(configuration.CollectionKey); return preconditionsMet? getTopics() : listSource; } From 191262a54ae69ffd7baefc0d389e997b1fe2f35c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 13:10:19 -0800 Subject: [PATCH 570/778] Renamed `Relationships` enum to `AssociationTypes` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The name `Relationships` has always been ambiguous, since it can refer both to `Topic.Relationships`, as well as other types of topic associations, such as `Topic.Parent` or `Topic.References`. To avoid that ambiguity, we're moving toward the more general name `Associations`. Ideally, the `OnTopic.References` namespace—which provides types associated with topic associations—will be renamed to `OnTopic.Associations`. To avoid a conflict between the `Associations` namespace and an `Associations` enum, the enum is being named `AssociationTypes`. I don't love this name, but it's better than having two different labels for the same concept in the library. This has a lot of downstream impacts, such as properties on `MappedTopicCacheEntry`, the parameters on `ITopicMappingService`, the `RelationshipMap` class, and properties on the `PropertyConfiguration` class. Those downstream impacts will be addressed in subsequent updates. --- .../DummyTopicMappingService.cs | 6 ++-- OnTopic.Tests/TopicMappingServiceTest.cs | 14 ++++---- .../ViewModels/AscendentTopicViewModel.cs | 4 +-- .../ViewModels/CircularTopicViewModel.cs | 4 +-- .../ViewModels/DescendentTopicViewModel.cs | 4 +-- .../ContentTypeDescriptorTopicViewModel.cs | 2 +- .../ViewModels/RelationTopicViewModel.cs | 2 +- .../RelationWithChildrenTopicViewModel.cs | 2 +- OnTopic.ViewModels/TopicViewModel.cs | 10 +++--- .../{Relationships.cs => AssociationTypes.cs} | 31 ++++++++-------- OnTopic/Mapping/Annotations/CollectionType.cs | 2 +- .../Mapping/Annotations/FollowAttribute.cs | 4 +-- OnTopic/Mapping/CachedTopicMappingService.cs | 14 ++++---- .../HierarchicalTopicMappingService{T}.cs | 2 +- OnTopic/Mapping/ITopicMappingService.cs | 9 ++--- .../Mapping/Internal/MappedTopicCacheEntry.cs | 16 ++++----- .../Mapping/Internal/PropertyConfiguration.cs | 4 +-- OnTopic/Mapping/Internal/RelationshipMap.cs | 24 ++++++------- OnTopic/Mapping/TopicMappingService.cs | 36 +++++++++---------- 19 files changed, 97 insertions(+), 93 deletions(-) rename OnTopic/Mapping/Annotations/{Relationships.cs => AssociationTypes.cs} (78%) diff --git a/OnTopic.TestDoubles/DummyTopicMappingService.cs b/OnTopic.TestDoubles/DummyTopicMappingService.cs index ba282e33..37cbc802 100644 --- a/OnTopic.TestDoubles/DummyTopicMappingService.cs +++ b/OnTopic.TestDoubles/DummyTopicMappingService.cs @@ -36,21 +36,21 @@ public DummyTopicMappingService() { \-------------------------------------------------------------------------------------------------------------------------*/ /// [return: NotNullIfNotNull("topic")] - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) where T : class, new() + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, Relationships relationships = Relationships.All) + public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) => throw new NotImplementedException(); } //Class diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 09960750..5fafb570 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -305,23 +305,23 @@ public async Task Map_AlternateAttributeKey_ReturnsMappedModel() { | TEST: MAPPED TOPIC CACHE ENTRY: GET MISSING RELATIONSHIPS: RETURNS DIFFERENCE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a with a set of , and then confirms that - /// its correctly returns the missing + /// Establishes a with a set of , and then confirms that + /// its correctly returns the missing /// relationships. /// [TestMethod] public void MappedTopicCacheEntry_GetMissingRelationships_ReturnsDifference() { var cacheEntry = new MappedTopicCacheEntry() { - Relationships = Relationships.Children | Relationships.Parents + Relationships = AssociationTypes.Children | AssociationTypes.Parents }; - var relationships = Relationships.Children | Relationships.References; + var relationships = AssociationTypes.Children | AssociationTypes.References; var difference = cacheEntry.GetMissingRelationships(relationships); - Assert.IsTrue(difference.HasFlag(Relationships.References)); - Assert.IsFalse(difference.HasFlag(Relationships.Children)); - Assert.IsFalse(difference.HasFlag(Relationships.Parents)); + Assert.IsTrue(difference.HasFlag(AssociationTypes.References)); + Assert.IsFalse(difference.HasFlag(AssociationTypes.Children)); + Assert.IsFalse(difference.HasFlag(AssociationTypes.Parents)); } diff --git a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs index a50dac93..a5f48cb1 100644 --- a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs @@ -11,7 +11,7 @@ namespace OnTopic.Tests.ViewModels { | VIEW MODEL: ASCENDENT \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a simple view model with a single property () for mapping ascendent relationships. + /// Provides a simple view model with a single property () for mapping ascendent associations. /// /// /// @@ -24,7 +24,7 @@ namespace OnTopic.Tests.ViewModels { /// public class AscendentTopicViewModel: KeyOnlyTopicViewModel { - [Follow(Relationships.Parents)] + [Follow(AssociationTypes.Parents)] public AscendentTopicViewModel? Parent { get; set; } } //Class diff --git a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs index 3009c666..3d89fb60 100644 --- a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs @@ -19,10 +19,10 @@ namespace OnTopic.Tests.ViewModels { /// public class CircularTopicViewModel { - [Follow(Relationships.Parents)] + [Follow(AssociationTypes.Parents)] public CircularTopicViewModel? Parent { get; set; } - [Follow(Relationships.Children | Relationships.Parents)] + [Follow(AssociationTypes.Children | AssociationTypes.Parents)] public Collection Children { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index 2c982d55..18ccdcd4 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -13,7 +13,7 @@ namespace OnTopic.Tests.ViewModels { | VIEW MODEL: DESCENDENT \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a simple view model with a single property () for mapping descending relationships. + /// Provides a simple view model with a single property () for mapping descending associations. /// /// /// @@ -26,7 +26,7 @@ namespace OnTopic.Tests.ViewModels { /// public record DescendentTopicViewModel: TopicViewModel { - [Follow(Relationships.Children)] + [Follow(AssociationTypes.Children)] public TopicViewModelCollection Children { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index 1f35107b..f509d9ab 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -23,7 +23,7 @@ public class ContentTypeDescriptorTopicViewModel { public Collection AttributeDescriptors { get; } = new(); [Collection(CollectionType.MappedCollection)] - [Follow(Relationships.None)] + [Follow(AssociationTypes.None)] public Collection PermittedContentTypes { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs index c6eccfc8..d285c689 100644 --- a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs @@ -25,7 +25,7 @@ namespace OnTopic.Tests.ViewModels { /// public class RelationTopicViewModel: KeyOnlyTopicViewModel { - [Follow(Relationships.Children)] + [Follow(AssociationTypes.Children)] public Collection Cousins { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs index 7a1f759c..4ecc1ae8 100644 --- a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs @@ -25,7 +25,7 @@ namespace OnTopic.Tests.ViewModels { /// public class RelationWithChildrenTopicViewModel: RelationTopicViewModel { - [Follow(Relationships.Relationships)] + [Follow(AssociationTypes.Relationships)] public Collection Children { get; } = new(); } //Class diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 0f43f34a..1cdc8c3c 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -86,12 +86,12 @@ public record TopicViewModel: ITopicViewModel { /// /// /// If the current is being mapped as part of another , then the - /// property will only be mapped if that relationship includes a - /// with a value including . If it does, all topics will be mapped - /// up to the root of the site. No other relationships on the view models will be mapped, even if - /// they are annotated with a . + /// property will only be mapped if that association includes a with a + /// value including . If it does, all topics will be mapped up + /// to the root of the site. No other associations on the view models will be mapped, even if they + /// are annotated with a . /// - [Follow(Relationships.Parents)] + [Follow(AssociationTypes.Parents)] public TopicViewModel? Parent { get; init; } } //Class diff --git a/OnTopic/Mapping/Annotations/Relationships.cs b/OnTopic/Mapping/Annotations/AssociationTypes.cs similarity index 78% rename from OnTopic/Mapping/Annotations/Relationships.cs rename to OnTopic/Mapping/Annotations/AssociationTypes.cs index a3211cd3..ae2d4ff7 100644 --- a/OnTopic/Mapping/Annotations/Relationships.cs +++ b/OnTopic/Mapping/Annotations/AssociationTypes.cs @@ -9,31 +9,34 @@ namespace OnTopic.Mapping.Annotations { /*============================================================================================================================ - | ENUM: RELATIONSHIPS + | ENUM: ASSOCIATION TYPES \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Allows one or more relationship types to be specified. + /// Allows one or more associations types to be specified. /// /// /// - /// The and use the enum to - /// determine what relationships should be mapped—or followed as part of the mapping process. This helps constrain the - /// scope of the object graph to only include the data needed for a given view, or vice verse. That said, the enum can be used any place where the code needs to model multiple types of relationships - /// relevant to the class and its view models. + /// The and use the enum to + /// determine what associations should be mapped—or followed—as part of the mapping process. This helps constrain the + /// scope of the object graph to only include the data needed for a given view, or vice verse. That said, the enum can be used any place where the code needs to model multiple types of associations relevant + /// to the class and its view models. /// /// - /// This differs from , which only allows one collection to be specified. + /// The enum is similar to the enum—and, in fact, they + /// share several members. They differ in that exclusively models collections, + /// whereas also models other types of associations, such as , + /// and allows multiple associations to be selected. /// /// [Flags] - public enum Relationships { + public enum AssociationTypes { /*========================================================================================================================== | NONE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Do not follow any relationships. + /// Do not follow any associations. /// None = 0, @@ -65,8 +68,8 @@ public enum Relationships { | INCOMING RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Map references, or properties marked as . + /// Map references, or properties marked as . /// IncomingRelationships = 1 << 3, @@ -89,8 +92,8 @@ public enum Relationships { /// Map topic pointer references, such as . /// /// - /// By convention, types refer to a , , or property identifier ending in Id. + /// By convention, types refer to a , , or property identifier ending in Id. /// References = 1 << 5, diff --git a/OnTopic/Mapping/Annotations/CollectionType.cs b/OnTopic/Mapping/Annotations/CollectionType.cs index febbea37..3595c589 100644 --- a/OnTopic/Mapping/Annotations/CollectionType.cs +++ b/OnTopic/Mapping/Annotations/CollectionType.cs @@ -13,7 +13,7 @@ namespace OnTopic.Mapping.Annotations { /// Enum that allows a collection to be specified. /// /// - /// This differs from , which allows multiple collections to be specified, and also + /// This differs from , which allows multiple collections to be specified, and also /// includes the as a source. /// public enum CollectionType { diff --git a/OnTopic/Mapping/Annotations/FollowAttribute.cs b/OnTopic/Mapping/Annotations/FollowAttribute.cs index d81c6532..df80baf1 100644 --- a/OnTopic/Mapping/Annotations/FollowAttribute.cs +++ b/OnTopic/Mapping/Annotations/FollowAttribute.cs @@ -36,7 +36,7 @@ public sealed class FollowAttribute : System.Attribute { /// Annotates a property with the by providing an . /// /// The specific relationships that should be crawled. - public FollowAttribute(Relationships relationships) { + public FollowAttribute(AssociationTypes relationships) { Relationships = relationships; } @@ -46,7 +46,7 @@ public FollowAttribute(Relationships relationships) { /// /// Gets the type(s) of relationships that should be recursed over. /// - public Relationships Relationships { get; } + public AssociationTypes Relationships { get; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/CachedTopicMappingService.cs b/OnTopic/Mapping/CachedTopicMappingService.cs index e4b1ddf3..6916c37f 100644 --- a/OnTopic/Mapping/CachedTopicMappingService.cs +++ b/OnTopic/Mapping/CachedTopicMappingService.cs @@ -28,7 +28,7 @@ public class CachedTopicMappingService : ITopicMappingService { /*========================================================================================================================== | ESTABLISH CACHE \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly ConcurrentDictionary<(int, Type?, Relationships), object> _cache = new(); + private readonly ConcurrentDictionary<(int, Type?, AssociationTypes), object> _cache = new(); /*========================================================================================================================== | CONSTRUCTOR @@ -45,7 +45,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) { + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -83,7 +83,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) where T : class, new() { + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -121,7 +121,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, Relationships relationships = Relationships.All) { + public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -171,8 +171,8 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /// The internal will potentially add two entries to the cache for every view model. /// /// - /// The first will be bound to the , view model , and the mapped. + /// The first will be bound to the , view model , and the mapped. /// /// /// The second will assume a null , and can be used for scenarios where the is @@ -198,7 +198,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /// The view model object to cache; can be any POCO object. /// A Tuple{T1, T2, T3} representing the cache key. /// The . - private object? CacheViewModel(string contentType, object viewModel, (int, Type?, Relationships) cacheKey) { + private object? CacheViewModel(string contentType, object viewModel, (int, Type?, AssociationTypes) cacheKey) { if (cacheKey.Item1 > 0 && cacheKey.Item2 is not null && !viewModel.GetType().Equals(typeof(object))) { _cache.TryAdd(cacheKey, viewModel); } diff --git a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs index 1ce8a8d1..c6bd77c9 100644 --- a/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs +++ b/OnTopic/Mapping/Hierarchical/HierarchicalTopicMappingService{T}.cs @@ -177,7 +177,7 @@ private static int DistanceFromRoot(Topic? sourceTopic) { /*------------------------------------------------------------------------------------------------------------------------ | Map object \-----------------------------------------------------------------------------------------------------------------------*/ - viewModel = await _topicMappingService.MapAsync(sourceTopic, Relationships.None).ConfigureAwait(false); + viewModel = await _topicMappingService.MapAsync(sourceTopic, AssociationTypes.None).ConfigureAwait(false); Contract.Assume( viewModel, diff --git a/OnTopic/Mapping/ITopicMappingService.cs b/OnTopic/Mapping/ITopicMappingService.cs index cb0e97e1..769988f7 100644 --- a/OnTopic/Mapping/ITopicMappingService.cs +++ b/OnTopic/Mapping/ITopicMappingService.cs @@ -30,7 +30,8 @@ public interface ITopicMappingService { /// Because the class is using reflection to determine the target View Models, the return type is . /// These results may need to be cast to a specific type, depending on the context. That said, strongly-typed views /// should be able to cast the object to the appropriate View Model type. If the type of the View Model is known - /// upfront, and it's imperative that it be strongly typed, then prefer . + /// upfront, and it's imperative that it be strongly typed, then prefer . /// /// /// Because the target object is being dynamically constructed, it must implement a default constructor. @@ -39,7 +40,7 @@ public interface ITopicMappingService { /// The entity to derive the data from. /// Determines what relationships the mapping should follow, if any. /// An instance of the dynamically determined View Model with properties appropriately mapped. - Task MapAsync(Topic? topic, Relationships relationships = Relationships.All); + Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All); /*========================================================================================================================== | METHOD: MAP (GENERIC) @@ -58,7 +59,7 @@ public interface ITopicMappingService { /// /// An instance of the requested View Model with properties appropriately mapped. /// - Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) where T : class, new(); + Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new(); /*========================================================================================================================== | METHOD: MAP (INSTANCES) @@ -73,7 +74,7 @@ public interface ITopicMappingService { /// /// An instance of the requested View Model instance with properties appropriately mapped. /// - Task MapAsync(Topic? topic, object target, Relationships relationships = Relationships.All); + Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All); } //Interface } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs index adec97a9..66073821 100644 --- a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs @@ -17,9 +17,9 @@ namespace OnTopic.Mapping.Internal { /// In addition to the actual , this also includes a property for /// tracking what relationships were mapped to the . This allows the to be update the cached object with any missing relationships, which can be identified using the - /// method. In turn, the cache can then be updated to reflect those - /// new relationships by using . This ensures that even if a topic has - /// already been mapped, its scope can be expanded without duplicating effort. + /// method. In turn, the cache can then be updated to reflect those + /// new relationships by using . This ensures that even if a topic + /// has already been mapped, its scope can be expanded without duplicating effort. /// public class MappedTopicCacheEntry { @@ -37,25 +37,25 @@ public class MappedTopicCacheEntry { /// /// Provides a reference to the relationships that the was mapped with. /// - public Relationships Relationships { get; set; } = Relationships.None; + public AssociationTypes Relationships { get; set; } = AssociationTypes.None; /*========================================================================================================================== | METHOD: GET MISSING RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a target , identifies any relationships not covered by and returns them as a new instance. + /// /> and returns them as a new instance. /// - public Relationships GetMissingRelationships(Relationships relationships) => Relationships ^ (relationships | Relationships); + public AssociationTypes GetMissingRelationships(AssociationTypes relationships) => Relationships ^ (relationships | Relationships); /*========================================================================================================================== | METHOD: ADD MISSING RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a target , adds any missing to the property. + /// AssociationTypes"/> to the property. /// - public void AddMissingRelationships(Relationships relationships) => Relationships = relationships | Relationships; + public void AddMissingRelationships(AssociationTypes relationships) => Relationships = relationships | Relationships; } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 70a33b72..0f871bb7 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -69,7 +69,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" InheritValue = false; CollectionKey = AttributeKey; CollectionType = CollectionType.Any; - CrawlRelationships = Relationships.None; + CrawlRelationships = AssociationTypes.None; MetadataKey = null; DisableMapping = false; AttributeFilters = new(); @@ -296,7 +296,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// property. It can be assigned by decorating a DTO property with e.g. [Follow(Relationships.Children)]. /// /// - public Relationships CrawlRelationships { get; set; } + public AssociationTypes CrawlRelationships { get; set; } /*========================================================================================================================== | PROPERTY: METADATA KEY diff --git a/OnTopic/Mapping/Internal/RelationshipMap.cs b/OnTopic/Mapping/Internal/RelationshipMap.cs index 77fa9066..9861240f 100644 --- a/OnTopic/Mapping/Internal/RelationshipMap.cs +++ b/OnTopic/Mapping/Internal/RelationshipMap.cs @@ -12,12 +12,12 @@ namespace OnTopic.Mapping.Internal { | CLASS: RELATIONSHIP MAP \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a mapping of the relationship between and . + /// Provides a mapping of the relationship between and . /// /// - /// While the and enumerations are distinct, there are times when - /// a single needs to be related to an item in the collection of . - /// This mapping makes that feasible. + /// While the and enumerations are distinct, there are times + /// when a single needs to be related to an item in the collection of . This mapping makes that feasible. /// static internal class RelationshipMap { @@ -26,13 +26,13 @@ static internal class RelationshipMap { \-------------------------------------------------------------------------------------------------------------------------*/ static RelationshipMap() { - var mappings = new Dictionary { - { CollectionType.Any, Relationships.None }, - { CollectionType.Children, Relationships.Children }, - { CollectionType.Relationship, Relationships.Relationships }, - { CollectionType.NestedTopics, Relationships.None }, - { CollectionType.MappedCollection, Relationships.MappedCollections }, - { CollectionType.IncomingRelationship, Relationships.IncomingRelationships } + var mappings = new Dictionary { + { CollectionType.Any, AssociationTypes.None }, + { CollectionType.Children, AssociationTypes.Children }, + { CollectionType.Relationship, AssociationTypes.Relationships }, + { CollectionType.NestedTopics, AssociationTypes.None }, + { CollectionType.MappedCollection, AssociationTypes.MappedCollections }, + { CollectionType.IncomingRelationship, AssociationTypes.IncomingRelationships } }; Mappings = mappings; @@ -42,7 +42,7 @@ static RelationshipMap() { /*========================================================================================================================== | PROPERTY: MAPPINGS \-------------------------------------------------------------------------------------------------------------------------*/ - static internal Dictionary Mappings { get; } + static internal Dictionary Mappings { get; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index e4ffb092..5dfb9e0f 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -60,7 +60,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService \-------------------------------------------------------------------------------------------------------------------------*/ /// [return: NotNullIfNotNull("topic")] - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) => + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) => await MapAsync(topic, relationships, new()).ConfigureAwait(false); /// @@ -72,7 +72,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// Because the class is using reflection to determine the target View Models, the return type is . /// These results may need to be cast to a specific type, depending on the context. That said, strongly-typed views /// should be able to cast the object to the appropriate View Model type. If the type of the View Model is known - /// upfront, and it is imperative that it be strongly-typed, prefer . + /// upfront, and it is imperative that it be strongly-typed, prefer . /// /// /// Because the target object is being dynamically constructed, it must implement a default constructor. @@ -80,7 +80,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with /// recursion in case is referred to multiple times (e.g., a Children collection with - /// set to include ). + /// set to include ). /// /// /// The entity to derive the data from. @@ -90,7 +90,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// An instance of the dynamically determined View Model with properties appropriately mapped. private async Task MapAsync( Topic? topic, - Relationships relationships, + AssociationTypes relationships, MappedTopicCache cache, string? attributePrefix = null ) { @@ -109,7 +109,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService if (cache.TryGetValue(topic.Id, out var cacheEntry)) { target = cacheEntry.MappedTopic; - if (cacheEntry.GetMissingRelationships(relationships) == Relationships.None) { + if (cacheEntry.GetMissingRelationships(relationships) == AssociationTypes.None) { return target; } } @@ -149,7 +149,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, Relationships relationships = Relationships.All) where T : class, new() { + public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() { if (typeof(Topic).IsAssignableFrom(typeof(T))) { return topic as T; } @@ -160,7 +160,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, Relationships relationships = Relationships.All) { + public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) { Contract.Requires(target, nameof(target)); return await MapAsync(topic, target, relationships, new()).ConfigureAwait(false); } @@ -175,8 +175,8 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// The prefix to apply to the attributes. /// /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with - /// recursion in case is referred to multiple times (e.g., a Children collection with - /// set to include ). + /// recursion in case is referred to multiple times (e.g., a Children collection with set to include ). /// /// /// The target view model with the properties appropriately mapped. @@ -184,7 +184,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService private async Task MapAsync( Topic? topic, object target, - Relationships relationships, + AssociationTypes relationships, MappedTopicCache cache, string? attributePrefix = null ) { @@ -212,7 +212,7 @@ private async Task MapAsync( if (cache.TryGetValue(topic.Id, out var cacheEntry)) { relationships = cacheEntry.GetMissingRelationships(relationships); target = cacheEntry.MappedTopic; - if (relationships == Relationships.None) { + if (relationships == AssociationTypes.None) { return cacheEntry.MappedTopic; } cacheEntry.AddMissingRelationships(relationships); @@ -260,7 +260,7 @@ private async Task MapAsync( private async Task SetPropertyAsync( Topic source, object target, - Relationships relationships, + AssociationTypes relationships, PropertyInfo property, MappedTopicCache cache, string? attributePrefix = null, @@ -309,18 +309,18 @@ private async Task SetPropertyAsync( else if (typeof(IList).IsAssignableFrom(property.PropertyType)) { await SetCollectionValueAsync(source, target, relationships, configuration, cache).ConfigureAwait(false); } - else if (configuration.AttributeKey is "Parent" && relationships.HasFlag(Relationships.Parents)) { + else if (configuration.AttributeKey is "Parent" && relationships.HasFlag(AssociationTypes.Parents)) { if (source.Parent is not null) { await SetTopicReferenceAsync(source.Parent, target, configuration, cache).ConfigureAwait(false); } } else if ( topicReference is not null && - relationships.HasFlag(Relationships.References) + relationships.HasFlag(AssociationTypes.References) ) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); } - else if (topicReferenceId > 0 && relationships.HasFlag(Relationships.References)) { + else if (topicReferenceId > 0 && relationships.HasFlag(AssociationTypes.References)) { topicReference = _topicRepository.Load(topicReferenceId, source); if (topicReference is not null) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); @@ -439,7 +439,7 @@ private static void SetScalarValue(Topic source, object target, PropertyConfigur private async Task SetCollectionValueAsync( Topic source, object target, - Relationships relationships, + AssociationTypes relationships, PropertyConfiguration configuration, MappedTopicCache cache ) { @@ -508,7 +508,7 @@ MappedTopicCache cache /// /// The with details about the property's attributes. /// - private IList GetSourceCollection(Topic source, Relationships relationships, PropertyConfiguration configuration) { + private IList GetSourceCollection(Topic source, AssociationTypes relationships, PropertyConfiguration configuration) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -615,7 +615,7 @@ IList GetRelationship(CollectionType relationship, Func con listSource.Count == 0 && (collectionType is CollectionType.Any || collectionType.Equals(relationship)) && (collectionType is CollectionType.Children || relationship is not CollectionType.Children) && - (targetRelationships is Relationships.None || relationships.HasFlag(targetRelationships)) && + (targetRelationships is AssociationTypes.None || relationships.HasFlag(targetRelationships)) && contains(configuration.CollectionKey); return preconditionsMet? getTopics() : listSource; } From a99321478ebc652e1192af7d66addc8acd1a0fbd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 13:49:43 -0800 Subject: [PATCH 571/778] Renamed `MappedTopicCacheEntry` members to use `Associations` To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I've renamed the `MappedTopicCacheEntry`'s members to `Associations`, `GetMissingAssociations()` and `AddMissingAssociations()`. While I was at it, I made some minor cleanup to the inline documentation. --- OnTopic.Tests/TopicMappingServiceTest.cs | 14 ++++---- .../Mapping/Internal/MappedTopicCacheEntry.cs | 34 +++++++++---------- OnTopic/Mapping/TopicMappingService.cs | 12 +++---- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 5fafb570..2e000b5d 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -302,22 +302,22 @@ public async Task Map_AlternateAttributeKey_ReturnsMappedModel() { } /*========================================================================================================================== - | TEST: MAPPED TOPIC CACHE ENTRY: GET MISSING RELATIONSHIPS: RETURNS DIFFERENCE + | TEST: MAPPED TOPIC CACHE ENTRY: GET MISSING ASSOCIATIONS: RETURNS DIFFERENCE \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Establishes a with a set of , and then confirms that - /// its correctly returns the missing - /// relationships. + /// its correctly returns the missing + /// associations. /// [TestMethod] - public void MappedTopicCacheEntry_GetMissingRelationships_ReturnsDifference() { + public void MappedTopicCacheEntry_GetMissingAssociations_ReturnsDifference() { var cacheEntry = new MappedTopicCacheEntry() { - Relationships = AssociationTypes.Children | AssociationTypes.Parents + Associations = AssociationTypes.Children | AssociationTypes.Parents }; - var relationships = AssociationTypes.Children | AssociationTypes.References; + var associations = AssociationTypes.Children | AssociationTypes.References; - var difference = cacheEntry.GetMissingRelationships(relationships); + var difference = cacheEntry.GetMissingAssociations(associations); Assert.IsTrue(difference.HasFlag(AssociationTypes.References)); Assert.IsFalse(difference.HasFlag(AssociationTypes.Children)); diff --git a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs index 66073821..42475118 100644 --- a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs @@ -14,12 +14,12 @@ namespace OnTopic.Mapping.Internal { /// Provides an entry to tracking an object mapped using the . /// /// - /// In addition to the actual , this also includes a property for - /// tracking what relationships were mapped to the . This allows the to be update the cached object with any missing relationships, which can be identified using the - /// method. In turn, the cache can then be updated to reflect those - /// new relationships by using . This ensures that even if a topic - /// has already been mapped, its scope can be expanded without duplicating effort. + /// In addition to the actual , this also includes a property for + /// tracking what associations were mapped to the . This allows the to be update the cached object with any missing associations, which can be identified using the method. In turn, the cache can then be updated to reflect those new + /// associations by using . This ensures that even if a topic has + /// already been mapped, its scope can be expanded without duplicating effort. /// public class MappedTopicCacheEntry { @@ -32,30 +32,30 @@ public class MappedTopicCacheEntry { public object MappedTopic { get; set; } = null!; /*========================================================================================================================== - | PROPERTY: RELATIONSHIPS + | PROPERTY: ASSOCIATIONS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a reference to the relationships that the was mapped with. + /// Provides a reference to the associations that the was mapped with. /// - public AssociationTypes Relationships { get; set; } = AssociationTypes.None; + public AssociationTypes Associations { get; set; } = AssociationTypes.None; /*========================================================================================================================== - | METHOD: GET MISSING RELATIONSHIPS + | METHOD: GET MISSING ASSOCIATIONS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a target , identifies any relationships not covered by and returns them as a new instance. + /// Given a target , identifies any associations not covered by + /// and returns them as a new instance. /// - public AssociationTypes GetMissingRelationships(AssociationTypes relationships) => Relationships ^ (relationships | Relationships); + public AssociationTypes GetMissingAssociations(AssociationTypes associations) => Associations ^ (associations | Associations); /*========================================================================================================================== - | METHOD: ADD MISSING RELATIONSHIPS + | METHOD: ADD MISSING ASSOCIATIONS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a target , adds any missing to the property. + /// Given a target , adds any missing to the property. /// - public void AddMissingRelationships(AssociationTypes relationships) => Relationships = relationships | Relationships; + public void AddMissingAssociations(AssociationTypes associations) => Associations = associations | Associations; } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 5dfb9e0f..b5b9ea5c 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -109,7 +109,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService if (cache.TryGetValue(topic.Id, out var cacheEntry)) { target = cacheEntry.MappedTopic; - if (cacheEntry.GetMissingRelationships(relationships) == AssociationTypes.None) { + if (cacheEntry.GetMissingAssociations(relationships) == AssociationTypes.None) { return target; } } @@ -206,23 +206,23 @@ private async Task MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Handle cached objects >------------------------------------------------------------------------------------------------------------------------- - | If the cache contains an entry, check to make sure it includes all of the requested relationships. If it does, return - | it. If it doesn't, determine the missing relationships and request to have those mapped. + | If the cache contains an entry, check to make sure it includes all of the requested associations. If it does, return it. + | If it doesn't, determine the missing associations and request to have those mapped. \-----------------------------------------------------------------------------------------------------------------------*/ if (cache.TryGetValue(topic.Id, out var cacheEntry)) { - relationships = cacheEntry.GetMissingRelationships(relationships); + relationships = cacheEntry.GetMissingAssociations(relationships); target = cacheEntry.MappedTopic; if (relationships == AssociationTypes.None) { return cacheEntry.MappedTopic; } - cacheEntry.AddMissingRelationships(relationships); + cacheEntry.AddMissingAssociations(relationships); } else if (!topic.IsNew) { cache.GetOrAdd( topic.Id, new MappedTopicCacheEntry() { MappedTopic = target, - Relationships = relationships + Associations = relationships } ); } From 2fd74703d92ee310fb927b5cab78e427ccdf4540 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 13:57:43 -0800 Subject: [PATCH 572/778] Rename `ITopicMappingService.MapAsync()` parameter to `associations` To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I've renamed the `ITopicMappingService`'s `MapAsync()` methods to use the parameter `associations` instead of `relationships`. This change was applied to all concrete implementations of `ITopicMappingService`, as well as their internal variable references. --- .../DummyTopicMappingService.cs | 6 +- OnTopic/Mapping/CachedTopicMappingService.cs | 18 ++--- OnTopic/Mapping/ITopicMappingService.cs | 12 ++-- OnTopic/Mapping/TopicMappingService.cs | 66 +++++++++---------- 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/OnTopic.TestDoubles/DummyTopicMappingService.cs b/OnTopic.TestDoubles/DummyTopicMappingService.cs index 37cbc802..684aa463 100644 --- a/OnTopic.TestDoubles/DummyTopicMappingService.cs +++ b/OnTopic.TestDoubles/DummyTopicMappingService.cs @@ -36,21 +36,21 @@ public DummyTopicMappingService() { \-------------------------------------------------------------------------------------------------------------------------*/ /// [return: NotNullIfNotNull("topic")] - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) where T : class, new() => throw new NotImplementedException(); /*========================================================================================================================== | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) + public async Task MapAsync(Topic? topic, object target, AssociationTypes associations = AssociationTypes.All) => throw new NotImplementedException(); } //Class diff --git a/OnTopic/Mapping/CachedTopicMappingService.cs b/OnTopic/Mapping/CachedTopicMappingService.cs index 6916c37f..fba62202 100644 --- a/OnTopic/Mapping/CachedTopicMappingService.cs +++ b/OnTopic/Mapping/CachedTopicMappingService.cs @@ -45,7 +45,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) { + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -55,7 +55,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure cache is populated \-----------------------------------------------------------------------------------------------------------------------*/ - var cacheKey = (topic.Id, (Type?)null, relationships); + var cacheKey = (topic.Id, (Type?)null, associations); if(_cache.TryGetValue(cacheKey, out var viewModel)) { return viewModel; } @@ -63,7 +63,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Process result \-----------------------------------------------------------------------------------------------------------------------*/ - viewModel = await _topicMappingService.MapAsync(topic, relationships).ConfigureAwait(false); + viewModel = await _topicMappingService.MapAsync(topic, associations).ConfigureAwait(false); /*------------------------------------------------------------------------------------------------------------------------ | Return (cached) result @@ -83,7 +83,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() { + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) where T : class, new() { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -93,7 +93,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure cache is populated \-----------------------------------------------------------------------------------------------------------------------*/ - var cacheKey = (topic.Id, typeof(T), relationships); + var cacheKey = (topic.Id, typeof(T), associations); if (_cache.TryGetValue(cacheKey, out var viewModel)) { return (T)viewModel; } @@ -101,7 +101,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Process result \-----------------------------------------------------------------------------------------------------------------------*/ - viewModel = await _topicMappingService.MapAsync(topic, relationships).ConfigureAwait(false); + viewModel = await _topicMappingService.MapAsync(topic, associations).ConfigureAwait(false); /*------------------------------------------------------------------------------------------------------------------------ | Return (cached) result @@ -121,7 +121,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) { + public async Task MapAsync(Topic? topic, object target, AssociationTypes associations = AssociationTypes.All) { /*------------------------------------------------------------------------------------------------------------------------ | Handle null source @@ -136,7 +136,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Ensure cache is populated \-----------------------------------------------------------------------------------------------------------------------*/ - var cacheKey = (topic.Id, target.GetType(), relationships); + var cacheKey = (topic.Id, target.GetType(), associations); if (_cache.TryGetValue(cacheKey, out var viewModel)) { return viewModel; } @@ -144,7 +144,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) { /*------------------------------------------------------------------------------------------------------------------------ | Process result \-----------------------------------------------------------------------------------------------------------------------*/ - viewModel = await _topicMappingService.MapAsync(topic, relationships).ConfigureAwait(false); + viewModel = await _topicMappingService.MapAsync(topic, associations).ConfigureAwait(false); /*------------------------------------------------------------------------------------------------------------------------ | Return (cached) result diff --git a/OnTopic/Mapping/ITopicMappingService.cs b/OnTopic/Mapping/ITopicMappingService.cs index 769988f7..faa3f65b 100644 --- a/OnTopic/Mapping/ITopicMappingService.cs +++ b/OnTopic/Mapping/ITopicMappingService.cs @@ -38,9 +38,9 @@ public interface ITopicMappingService { /// /// /// The entity to derive the data from. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// An instance of the dynamically determined View Model with properties appropriately mapped. - Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All); + Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All); /*========================================================================================================================== | METHOD: MAP (GENERIC) @@ -55,11 +55,11 @@ public interface ITopicMappingService { /// /// /// The entity to derive the data from. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// /// An instance of the requested View Model with properties appropriately mapped. /// - Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new(); + Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) where T : class, new(); /*========================================================================================================================== | METHOD: MAP (INSTANCES) @@ -70,11 +70,11 @@ public interface ITopicMappingService { /// /// The entity to derive the data from. /// The data transfer object to populate. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// /// An instance of the requested View Model instance with properties appropriately mapped. /// - Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All); + Task MapAsync(Topic? topic, object target, AssociationTypes associations = AssociationTypes.All); } //Interface } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index b5b9ea5c..75e0fe2d 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -60,8 +60,8 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService \-------------------------------------------------------------------------------------------------------------------------*/ /// [return: NotNullIfNotNull("topic")] - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) => - await MapAsync(topic, relationships, new()).ConfigureAwait(false); + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) => + await MapAsync(topic, associations, new()).ConfigureAwait(false); /// /// Given a topic, will identify any View Models named, by convention, "{ContentType}TopicViewModel" and populate them @@ -84,13 +84,13 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// /// The entity to derive the data from. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// An instance of the dynamically determined View Model with properties appropriately mapped. private async Task MapAsync( Topic? topic, - AssociationTypes relationships, + AssociationTypes associations, MappedTopicCache cache, string? attributePrefix = null ) { @@ -109,7 +109,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService if (cache.TryGetValue(topic.Id, out var cacheEntry)) { target = cacheEntry.MappedTopic; - if (cacheEntry.GetMissingAssociations(relationships) == AssociationTypes.None) { + if (cacheEntry.GetMissingAssociations(associations) == AssociationTypes.None) { return target; } } @@ -141,7 +141,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /*------------------------------------------------------------------------------------------------------------------------ | Provide mapping \-----------------------------------------------------------------------------------------------------------------------*/ - return await MapAsync(topic, target, relationships, cache, attributePrefix).ConfigureAwait(false); + return await MapAsync(topic, target, associations, cache, attributePrefix).ConfigureAwait(false); } @@ -149,20 +149,20 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService | METHOD: MAP (T) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, AssociationTypes relationships = AssociationTypes.All) where T : class, new() { + public async Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All) where T : class, new() { if (typeof(Topic).IsAssignableFrom(typeof(T))) { return topic as T; } - return (T?)await MapAsync(topic, new T(), relationships).ConfigureAwait(false); + return (T?)await MapAsync(topic, new T(), associations).ConfigureAwait(false); } /*========================================================================================================================== | METHOD: MAP (OBJECTS) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public async Task MapAsync(Topic? topic, object target, AssociationTypes relationships = AssociationTypes.All) { + public async Task MapAsync(Topic? topic, object target, AssociationTypes associations = AssociationTypes.All) { Contract.Requires(target, nameof(target)); - return await MapAsync(topic, target, relationships, new()).ConfigureAwait(false); + return await MapAsync(topic, target, associations, new()).ConfigureAwait(false); } /// @@ -170,7 +170,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// The entity to derive the data from. /// The target object to map the data to. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// @@ -184,7 +184,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService private async Task MapAsync( Topic? topic, object target, - AssociationTypes relationships, + AssociationTypes associations, MappedTopicCache cache, string? attributePrefix = null ) { @@ -210,19 +210,19 @@ private async Task MapAsync( | If it doesn't, determine the missing associations and request to have those mapped. \-----------------------------------------------------------------------------------------------------------------------*/ if (cache.TryGetValue(topic.Id, out var cacheEntry)) { - relationships = cacheEntry.GetMissingAssociations(relationships); + associations = cacheEntry.GetMissingAssociations(associations); target = cacheEntry.MappedTopic; - if (relationships == AssociationTypes.None) { + if (associations == AssociationTypes.None) { return cacheEntry.MappedTopic; } - cacheEntry.AddMissingAssociations(relationships); + cacheEntry.AddMissingAssociations(associations); } else if (!topic.IsNew) { cache.GetOrAdd( topic.Id, new MappedTopicCacheEntry() { MappedTopic = target, - Associations = relationships + Associations = associations } ); } @@ -232,7 +232,7 @@ private async Task MapAsync( \-----------------------------------------------------------------------------------------------------------------------*/ var taskQueue = new List(); foreach (var property in _typeCache.GetMembers(target.GetType())) { - taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache, attributePrefix, cacheEntry != null)); + taskQueue.Add(SetPropertyAsync(topic, target, associations, property, cache, attributePrefix, cacheEntry != null)); } await Task.WhenAll(taskQueue.ToArray()).ConfigureAwait(false); @@ -252,7 +252,7 @@ private async Task MapAsync( /// /// The entity to derive the data from. /// The target object to map the data to. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// Information related to the current property. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. @@ -260,7 +260,7 @@ private async Task MapAsync( private async Task SetPropertyAsync( Topic source, object target, - AssociationTypes relationships, + AssociationTypes associations, PropertyInfo property, MappedTopicCache cache, string? attributePrefix = null, @@ -272,7 +272,7 @@ private async Task SetPropertyAsync( \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(source, nameof(source)); Contract.Requires(target, nameof(target)); - Contract.Requires(relationships, nameof(relationships)); + Contract.Requires(associations, nameof(associations)); Contract.Requires(property, nameof(property)); Contract.Requires(cache, nameof(cache)); @@ -307,20 +307,20 @@ private async Task SetPropertyAsync( SetScalarValue(source, target, configuration); } else if (typeof(IList).IsAssignableFrom(property.PropertyType)) { - await SetCollectionValueAsync(source, target, relationships, configuration, cache).ConfigureAwait(false); + await SetCollectionValueAsync(source, target, associations, configuration, cache).ConfigureAwait(false); } - else if (configuration.AttributeKey is "Parent" && relationships.HasFlag(AssociationTypes.Parents)) { + else if (configuration.AttributeKey is "Parent" && associations.HasFlag(AssociationTypes.Parents)) { if (source.Parent is not null) { await SetTopicReferenceAsync(source.Parent, target, configuration, cache).ConfigureAwait(false); } } else if ( topicReference is not null && - relationships.HasFlag(AssociationTypes.References) + associations.HasFlag(AssociationTypes.References) ) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); } - else if (topicReferenceId > 0 && relationships.HasFlag(AssociationTypes.References)) { + else if (topicReferenceId > 0 && associations.HasFlag(AssociationTypes.References)) { topicReference = _topicRepository.Load(topicReferenceId, source); if (topicReference is not null) { await SetTopicReferenceAsync(topicReference, target, configuration, cache).ConfigureAwait(false); @@ -332,7 +332,7 @@ topicReference is not null && await MapAsync( source, targetProperty, - relationships, + associations, cache, configuration.AttributePrefix ).ConfigureAwait(false); @@ -431,7 +431,7 @@ private static void SetScalarValue(Topic source, object target, PropertyConfigur /// /// The source from which to pull the value. /// The target DTO on which to set the property value. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// /// The with details about the property's attributes. /// @@ -439,7 +439,7 @@ private static void SetScalarValue(Topic source, object target, PropertyConfigur private async Task SetCollectionValueAsync( Topic source, object target, - AssociationTypes relationships, + AssociationTypes associations, PropertyConfiguration configuration, MappedTopicCache cache ) { @@ -448,7 +448,7 @@ MappedTopicCache cache | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(source, nameof(source)); - Contract.Requires(relationships, nameof(relationships)); + Contract.Requires(associations, nameof(associations)); Contract.Requires(configuration, nameof(configuration)); Contract.Requires(cache, nameof(cache)); @@ -475,7 +475,7 @@ MappedTopicCache cache /*------------------------------------------------------------------------------------------------------------------------ | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ - var sourceList = GetSourceCollection(source, relationships, configuration); + var sourceList = GetSourceCollection(source, associations, configuration); /*------------------------------------------------------------------------------------------------------------------------ | Validate that source collection was identified @@ -504,17 +504,17 @@ MappedTopicCache cache /// on the target DTO. /// /// The source from which to pull the value. - /// Determines what relationships the mapping should follow, if any. + /// Determines what associations the mapping should follow, if any. /// /// The with details about the property's attributes. /// - private IList GetSourceCollection(Topic source, AssociationTypes relationships, PropertyConfiguration configuration) { + private IList GetSourceCollection(Topic source, AssociationTypes associations, PropertyConfiguration configuration) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters \-----------------------------------------------------------------------------------------------------------------------*/ Contract.Requires(source, nameof(source)); - Contract.Requires(relationships, nameof(relationships)); + Contract.Requires(associations, nameof(associations)); Contract.Requires(configuration, nameof(configuration)); /*------------------------------------------------------------------------------------------------------------------------ @@ -615,7 +615,7 @@ IList GetRelationship(CollectionType relationship, Func con listSource.Count == 0 && (collectionType is CollectionType.Any || collectionType.Equals(relationship)) && (collectionType is CollectionType.Children || relationship is not CollectionType.Children) && - (targetRelationships is AssociationTypes.None || relationships.HasFlag(targetRelationships)) && + (targetRelationships is AssociationTypes.None || associations.HasFlag(targetRelationships)) && contains(configuration.CollectionKey); return preconditionsMet? getTopics() : listSource; } From e347e4322c37c893a7cac9fc3103465ebe3a7f38 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:00:02 -0800 Subject: [PATCH 573/778] Update TopicMappingService.cs To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I renamed the (private) `SetPropertyAsync()` method's `mapRelationshipsOnly` to `mapAssociationsOnly`. --- OnTopic/Mapping/TopicMappingService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 75e0fe2d..ef1eaf35 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -256,7 +256,7 @@ private async Task MapAsync( /// Information related to the current property. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. - /// Determines if properties not associated with properties should be mapped. + /// Determines if properties not associated with associations should be mapped. private async Task SetPropertyAsync( Topic source, object target, @@ -264,7 +264,7 @@ private async Task SetPropertyAsync( PropertyInfo property, MappedTopicCache cache, string? attributePrefix = null, - bool mapRelationshipsOnly = false + bool mapAssociationsOnly = false ) { /*------------------------------------------------------------------------------------------------------------------------ @@ -290,7 +290,7 @@ private async Task SetPropertyAsync( /*------------------------------------------------------------------------------------------------------------------------ | Assign default value \-----------------------------------------------------------------------------------------------------------------------*/ - if (!mapRelationshipsOnly && configuration.DefaultValue is not null) { + if (!mapAssociationsOnly && configuration.DefaultValue is not null) { property.SetValue(target, configuration.DefaultValue); } @@ -303,7 +303,7 @@ private async Task SetPropertyAsync( else if (SetCompatibleProperty(source, target, configuration)) { //Performed 1:1 mapping between source and target } - else if (!mapRelationshipsOnly && _typeCache.HasSettableProperty(target.GetType(), property.Name)) { + else if (!mapAssociationsOnly && _typeCache.HasSettableProperty(target.GetType(), property.Name)) { SetScalarValue(source, target, configuration); } else if (typeof(IList).IsAssignableFrom(property.PropertyType)) { From e6cbc4b5dcc3a56aa3cbcd795abe0ad86c813e7d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:04:23 -0800 Subject: [PATCH 574/778] Update TopicMappingService.cs To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a) as well as the previous rename of `RelationshipType` to the more specific `CollectionType` (1daf799), I renamed the (local) `GetRelationship()` method to `getCollection()` , the `relationshipKey` variable to `collectionKey`, and the `targetRelationships` variable to `targetAssociations`. --- OnTopic/Mapping/TopicMappingService.cs | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index ef1eaf35..20b9a0f3 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -521,13 +521,13 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat | Establish source collection to store topics to be mapped \-----------------------------------------------------------------------------------------------------------------------*/ var listSource = (IList)Array.Empty(); - var relationshipKey = configuration.CollectionKey; + var collectionKey = configuration.CollectionKey; var collectionType = configuration.CollectionType; /*------------------------------------------------------------------------------------------------------------------------ | Handle children \-----------------------------------------------------------------------------------------------------------------------*/ - listSource = GetRelationship( + listSource = getCollection( CollectionType.Children, s => true, () => source.Children.ToList() @@ -536,35 +536,35 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat /*------------------------------------------------------------------------------------------------------------------------ | Handle (outgoing) relationships \-----------------------------------------------------------------------------------------------------------------------*/ - listSource = GetRelationship( + listSource = getCollection( CollectionType.Relationship, source.Relationships.Contains, - () => source.Relationships.GetTopics(relationshipKey) + () => source.Relationships.GetTopics(collectionKey) ); /*------------------------------------------------------------------------------------------------------------------------ | Handle nested topics, or children corresponding to the property name \-----------------------------------------------------------------------------------------------------------------------*/ - listSource = GetRelationship( + listSource = getCollection( CollectionType.NestedTopics, source.Children.Contains, - () => source.Children[relationshipKey].Children + () => source.Children[collectionKey].Children ); /*------------------------------------------------------------------------------------------------------------------------ | Handle (incoming) relationships \-----------------------------------------------------------------------------------------------------------------------*/ - listSource = GetRelationship( + listSource = getCollection( CollectionType.IncomingRelationship, source.IncomingRelationships.Contains, - () => source.IncomingRelationships.GetTopics(relationshipKey) + () => source.IncomingRelationships.GetTopics(collectionKey) ); /*------------------------------------------------------------------------------------------------------------------------ | Handle other strongly typed source collections \-----------------------------------------------------------------------------------------------------------------------*/ //The following allows a target collection to be mapped to an IList source collection. This is valuable for custom, - //curated collections defined on e.g. derivatives of Topic, but which don't otherwise map to a specific relationship type. + //curated collections defined on e.g. derivatives of Topic, but which don't otherwise map to a specific collection type. //For example, the ContentTypeDescriptor's AttributeDescriptors collection, which provides a rollup of //AttributeDescriptors from the current ContentTypeDescriptor, as well as all of its ascendents. if (listSource.Count == 0) { @@ -575,7 +575,7 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat sourcePropertyValue.Count > 0 && typeof(Topic).IsAssignableFrom(sourcePropertyValue[0]?.GetType()) ) { - listSource = GetRelationship( + listSource = getCollection( CollectionType.MappedCollection, s => true, () => sourcePropertyValue.Cast().ToList() @@ -607,15 +607,15 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat return listSource; /*------------------------------------------------------------------------------------------------------------------------ - | Provide local function for evaluating current relationship + | Provide local function for evaluating current collection \-----------------------------------------------------------------------------------------------------------------------*/ - IList GetRelationship(CollectionType relationship, Func contains, Func> getTopics) { - var targetRelationships = RelationshipMap.Mappings[relationship]; + IList getCollection(CollectionType collection, Func contains, Func> getTopics) { + var targetAssociations = RelationshipMap.Mappings[collection]; var preconditionsMet = listSource.Count == 0 && - (collectionType is CollectionType.Any || collectionType.Equals(relationship)) && - (collectionType is CollectionType.Children || relationship is not CollectionType.Children) && - (targetRelationships is AssociationTypes.None || associations.HasFlag(targetRelationships)) && + (collectionType is CollectionType.Any || collectionType.Equals(collection)) && + (collectionType is CollectionType.Children || collection is not CollectionType.Children) && + (targetAssociations is AssociationTypes.None || associations.HasFlag(targetAssociations)) && contains(configuration.CollectionKey); return preconditionsMet? getTopics() : listSource; } From f96f7d6940087051fb58c37cb8a50ed004efbf46 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:10:18 -0800 Subject: [PATCH 575/778] Renamed `RelationshipMap` to `AssociationMap` The `RelationshipMap` is an `internal` class that provides a mapping between the `Relationships` enum and the `RelationshipType` enum. Those enums have been renamed to `AssociationTypes` (191262a) and `CollectionType` (1daf799) respectively, and so the `RelationshipMap` identifier no longer makes sense. As such, it's been renamed to `AssociationMap`. --- .../Internal/{RelationshipMap.cs => AssociationMap.cs} | 6 +++--- OnTopic/Mapping/TopicMappingService.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename OnTopic/Mapping/Internal/{RelationshipMap.cs => AssociationMap.cs} (95%) diff --git a/OnTopic/Mapping/Internal/RelationshipMap.cs b/OnTopic/Mapping/Internal/AssociationMap.cs similarity index 95% rename from OnTopic/Mapping/Internal/RelationshipMap.cs rename to OnTopic/Mapping/Internal/AssociationMap.cs index 9861240f..97598847 100644 --- a/OnTopic/Mapping/Internal/RelationshipMap.cs +++ b/OnTopic/Mapping/Internal/AssociationMap.cs @@ -9,7 +9,7 @@ namespace OnTopic.Mapping.Internal { /*============================================================================================================================ - | CLASS: RELATIONSHIP MAP + | CLASS: ASSOCIATION MAP \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a mapping of the relationship between and . @@ -19,12 +19,12 @@ namespace OnTopic.Mapping.Internal { /// when a single needs to be related to an item in the collection of . This mapping makes that feasible. /// - static internal class RelationshipMap { + static internal class AssociationMap { /*========================================================================================================================== | CONSTRUCTOR (STATIC) \-------------------------------------------------------------------------------------------------------------------------*/ - static RelationshipMap() { + static AssociationMap() { var mappings = new Dictionary { { CollectionType.Any, AssociationTypes.None }, diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 20b9a0f3..51847895 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -610,7 +610,7 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat | Provide local function for evaluating current collection \-----------------------------------------------------------------------------------------------------------------------*/ IList getCollection(CollectionType collection, Func contains, Func> getTopics) { - var targetAssociations = RelationshipMap.Mappings[collection]; + var targetAssociations = AssociationMap.Mappings[collection]; var preconditionsMet = listSource.Count == 0 && (collectionType is CollectionType.Any || collectionType.Equals(collection)) && From 78600fd1d1d31cea77d638001e6d4175401febed Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:16:30 -0800 Subject: [PATCH 576/778] Renamed `[Follow()]`'s parameter, property to `Associations` To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I renamed the `[Follow()]` attribute's parameter from `relationships` to `associations`, and its property from `Relationships` to `Associations`. --- OnTopic/Mapping/Annotations/FollowAttribute.cs | 14 +++++++------- OnTopic/Mapping/Internal/PropertyConfiguration.cs | 5 ++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/OnTopic/Mapping/Annotations/FollowAttribute.cs b/OnTopic/Mapping/Annotations/FollowAttribute.cs index df80baf1..ea0e5981 100644 --- a/OnTopic/Mapping/Annotations/FollowAttribute.cs +++ b/OnTopic/Mapping/Annotations/FollowAttribute.cs @@ -21,8 +21,8 @@ namespace OnTopic.Mapping.Annotations { /// /// /// The overrides this behavior. If set, the will - /// populate the specified on the related topics. By default, it will crawl all - /// relationships, but the flag can optionally be used to specify one or multiple + /// populate the specified on the related topics. By default, it will crawl all + /// relationships, but the flag can optionally be used to specify one or multiple /// relationship types, thus providing fine-tune control. /// /// @@ -33,11 +33,11 @@ public sealed class FollowAttribute : System.Attribute { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Annotates a property with the by providing an . + /// Annotates a property with the by providing an . /// - /// The specific relationships that should be crawled. - public FollowAttribute(AssociationTypes relationships) { - Relationships = relationships; + /// The specific associations that should be crawled. + public FollowAttribute(AssociationTypes associations) { + Associations = associations; } /*========================================================================================================================== @@ -46,7 +46,7 @@ public FollowAttribute(AssociationTypes relationships) { /// /// Gets the type(s) of relationships that should be recursed over. /// - public AssociationTypes Relationships { get; } + public AssociationTypes Associations { get; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 0f871bb7..505cc01e 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -10,7 +10,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using OnTopic.Attributes; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Annotations; @@ -83,7 +82,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue(property, a => AttributeKey = attributePrefix + a.Key); GetAttributeValue(property, a => MapToParent = true); GetAttributeValue(property, a => AttributePrefix += (a.AttributePrefix?? property.Name)); - GetAttributeValue(property, a => CrawlRelationships = a.Relationships); + GetAttributeValue(property, a => CrawlRelationships = a.Associations); GetAttributeValue(property, a => FlattenChildren = true); GetAttributeValue(property, a => MetadataKey = a.Key); GetAttributeValue(property, a => DisableMapping = true); @@ -292,7 +291,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// one or multiple relationships to be specified for a given property. /// /// - /// The property corresponds to the + /// The property corresponds to the /// property. It can be assigned by decorating a DTO property with e.g. [Follow(Relationships.Children)]. /// /// From 7c150d81f84bfbad9442de02594ad4c0fcfcd70f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:24:09 -0800 Subject: [PATCH 577/778] Renamed `[Follow()]` to `[Include()]` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `[Follow()]` attribute name isn't bad, but it's potentially misleading in that it isn't a recusive instruction. For this reason, it's being renamed to `[Include()]`. Not only does this better communicate that this exclusively impacts the models in the associated property, but not their associations, but it is also more consistent with the Entity Framework's nomenclature for navigation properties—which are effectively synonymous with OnTopic's associations. While we're not generally striving to maintain feature or identifier parity with Entity Framework, it's useful to leverage familiar terminology when the concepts cleanly map, as is the case here. --- .../ViewModels/AscendentTopicViewModel.cs | 2 +- .../ViewModels/CircularTopicViewModel.cs | 4 +-- .../ViewModels/DescendentTopicViewModel.cs | 2 +- .../ContentTypeDescriptorTopicViewModel.cs | 2 +- .../ViewModels/RelationTopicViewModel.cs | 2 +- .../RelationWithChildrenTopicViewModel.cs | 2 +- OnTopic.ViewModels/TopicViewModel.cs | 6 ++-- .../Mapping/Annotations/AssociationTypes.cs | 2 +- .../Mapping/Annotations/FlattenAttribute.cs | 2 +- ...FollowAttribute.cs => IncludeAttribute.cs} | 28 +++++++++---------- .../Mapping/Internal/PropertyConfiguration.cs | 4 +-- OnTopic/Mapping/TopicMappingService.cs | 4 +-- 12 files changed, 29 insertions(+), 31 deletions(-) rename OnTopic/Mapping/Annotations/{FollowAttribute.cs => IncludeAttribute.cs} (59%) diff --git a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs index a5f48cb1..f215a9a6 100644 --- a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs @@ -24,7 +24,7 @@ namespace OnTopic.Tests.ViewModels { /// public class AscendentTopicViewModel: KeyOnlyTopicViewModel { - [Follow(AssociationTypes.Parents)] + [Include(AssociationTypes.Parents)] public AscendentTopicViewModel? Parent { get; set; } } //Class diff --git a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs index 3d89fb60..ad767a93 100644 --- a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs @@ -19,10 +19,10 @@ namespace OnTopic.Tests.ViewModels { /// public class CircularTopicViewModel { - [Follow(AssociationTypes.Parents)] + [Include(AssociationTypes.Parents)] public CircularTopicViewModel? Parent { get; set; } - [Follow(AssociationTypes.Children | AssociationTypes.Parents)] + [Include(AssociationTypes.Children | AssociationTypes.Parents)] public Collection Children { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index 18ccdcd4..05b4997a 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -26,7 +26,7 @@ namespace OnTopic.Tests.ViewModels { /// public record DescendentTopicViewModel: TopicViewModel { - [Follow(AssociationTypes.Children)] + [Include(AssociationTypes.Children)] public TopicViewModelCollection Children { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs index f509d9ab..9e6b9686 100644 --- a/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/Metadata/ContentTypeDescriptorTopicViewModel.cs @@ -23,7 +23,7 @@ public class ContentTypeDescriptorTopicViewModel { public Collection AttributeDescriptors { get; } = new(); [Collection(CollectionType.MappedCollection)] - [Follow(AssociationTypes.None)] + [Include(AssociationTypes.None)] public Collection PermittedContentTypes { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs index d285c689..88c00a9f 100644 --- a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs @@ -25,7 +25,7 @@ namespace OnTopic.Tests.ViewModels { /// public class RelationTopicViewModel: KeyOnlyTopicViewModel { - [Follow(AssociationTypes.Children)] + [Include(AssociationTypes.Children)] public Collection Cousins { get; } = new(); } //Class diff --git a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs index 4ecc1ae8..c71d59ae 100644 --- a/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/RelationWithChildrenTopicViewModel.cs @@ -25,7 +25,7 @@ namespace OnTopic.Tests.ViewModels { /// public class RelationWithChildrenTopicViewModel: RelationTopicViewModel { - [Follow(AssociationTypes.Relationships)] + [Include(AssociationTypes.Relationships)] public Collection Children { get; } = new(); } //Class diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 1cdc8c3c..e4be7670 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -86,12 +86,12 @@ public record TopicViewModel: ITopicViewModel { /// /// /// If the current is being mapped as part of another , then the - /// property will only be mapped if that association includes a with a + /// property will only be mapped if that association includes a with a /// value including . If it does, all topics will be mapped up /// to the root of the site. No other associations on the view models will be mapped, even if they - /// are annotated with a . + /// are annotated with a . /// - [Follow(AssociationTypes.Parents)] + [Include(AssociationTypes.Parents)] public TopicViewModel? Parent { get; init; } } //Class diff --git a/OnTopic/Mapping/Annotations/AssociationTypes.cs b/OnTopic/Mapping/Annotations/AssociationTypes.cs index ae2d4ff7..e6464919 100644 --- a/OnTopic/Mapping/Annotations/AssociationTypes.cs +++ b/OnTopic/Mapping/Annotations/AssociationTypes.cs @@ -16,7 +16,7 @@ namespace OnTopic.Mapping.Annotations { /// /// /// - /// The and use the enum to + /// The and use the enum to /// determine what associations should be mapped—or followed—as part of the mapping process. This helps constrain the /// scope of the object graph to only include the data needed for a given view, or vice verse. That said, the enum can be used any place where the code needs to model multiple types of associations relevant diff --git a/OnTopic/Mapping/Annotations/FlattenAttribute.cs b/OnTopic/Mapping/Annotations/FlattenAttribute.cs index 539c5022..448d7767 100644 --- a/OnTopic/Mapping/Annotations/FlattenAttribute.cs +++ b/OnTopic/Mapping/Annotations/FlattenAttribute.cs @@ -16,7 +16,7 @@ namespace OnTopic.Mapping.Annotations { /// /// /// By default, will populate all items in a collection—and, if the is defined, then also include their specified relationships. The is defined, then also include their specified relationships. The allows all subsequent children to not only be included, but to be elevated to a single /// list. This can be especially useful when combined with e.g. as well as /// strongly-typed collections (e.g., of a specific view model type), as it allows a list to provide, effectively, search diff --git a/OnTopic/Mapping/Annotations/FollowAttribute.cs b/OnTopic/Mapping/Annotations/IncludeAttribute.cs similarity index 59% rename from OnTopic/Mapping/Annotations/FollowAttribute.cs rename to OnTopic/Mapping/Annotations/IncludeAttribute.cs index ea0e5981..91c97d5f 100644 --- a/OnTopic/Mapping/Annotations/FollowAttribute.cs +++ b/OnTopic/Mapping/Annotations/IncludeAttribute.cs @@ -7,44 +7,42 @@ namespace OnTopic.Mapping.Annotations { /*============================================================================================================================ - | ATTRIBUTE: FOLLOW + | ATTRIBUTE: INCLUDE \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Instructs the to continue following relationships on that property. Optionally - /// specifies which relationships should be followed. + /// Instructs the to include the specified on that property. /// /// /// - /// By default, will populate all relationships on the initial data transfer object, - /// but won't continue to do so on related objects. So, for instance, a Children collection will cause all children - /// to be loaded, but the mapper won't populate their Children (assuming that property is set). + /// By default, will populate all associations on the initial data transfer object, + /// but won't continue to do so on associated objects. So, for instance, a Children collection will cause all + /// children to be loaded, but the mapper won't populate their Children (assuming that property is + /// available). /// /// - /// The overrides this behavior. If set, the will - /// populate the specified on the related topics. By default, it will crawl all - /// relationships, but the flag can optionally be used to specify one or multiple - /// relationship types, thus providing fine-tune control. + /// The overrides this behavior. If set, the will + /// populate the specified on the associated topics. /// /// [System.AttributeUsage(System.AttributeTargets.Property)] - public sealed class FollowAttribute : System.Attribute { + public sealed class IncludeAttribute : System.Attribute { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Annotates a property with the by providing an . + /// Annotates a property with the by providing an . /// /// The specific associations that should be crawled. - public FollowAttribute(AssociationTypes associations) { + public IncludeAttribute(AssociationTypes associations) { Associations = associations; } /*========================================================================================================================== - | PROPERTY: RELATIONSHIPS + | PROPERTY: ASSOCIATIONS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets the type(s) of relationships that should be recursed over. + /// Gets the type(s) of associations that should be recursed over. /// public AssociationTypes Associations { get; } diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 505cc01e..1a159e19 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -82,7 +82,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue(property, a => AttributeKey = attributePrefix + a.Key); GetAttributeValue(property, a => MapToParent = true); GetAttributeValue(property, a => AttributePrefix += (a.AttributePrefix?? property.Name)); - GetAttributeValue(property, a => CrawlRelationships = a.Associations); + GetAttributeValue(property, a => CrawlRelationships = a.Associations); GetAttributeValue(property, a => FlattenChildren = true); GetAttributeValue(property, a => MetadataKey = a.Key); GetAttributeValue(property, a => DisableMapping = true); @@ -291,7 +291,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// one or multiple relationships to be specified for a given property. /// /// - /// The property corresponds to the + /// The property corresponds to the /// property. It can be assigned by decorating a DTO property with e.g. [Follow(Relationships.Children)]. /// /// diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 51847895..42be904b 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -80,7 +80,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with /// recursion in case is referred to multiple times (e.g., a Children collection with - /// set to include ). + /// set to include ). /// /// /// The entity to derive the data from. @@ -176,7 +176,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with /// recursion in case is referred to multiple times (e.g., a Children collection with set to include ). + /// ="IncludeAttribute"/> set to include ). /// /// /// The target view model with the properties appropriately mapped. From c16c0a8c000b2e1627d49c8b9816337bee691043 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:27:42 -0800 Subject: [PATCH 578/778] Renamed `CrawlRelationships` to `IncludeAssociations` To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a) as well as the rename of the `[Follow()]` attribute to `[Include()]` (7c150d8), I renamed the `CrawlRelationships` property to `IncludeAssociations`. --- .../Mapping/Internal/PropertyConfiguration.cs | 24 +++++++++---------- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 1a159e19..dd37fd72 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -68,7 +68,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" InheritValue = false; CollectionKey = AttributeKey; CollectionType = CollectionType.Any; - CrawlRelationships = AssociationTypes.None; + IncludeAssociations = AssociationTypes.None; MetadataKey = null; DisableMapping = false; AttributeFilters = new(); @@ -82,7 +82,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue(property, a => AttributeKey = attributePrefix + a.Key); GetAttributeValue(property, a => MapToParent = true); GetAttributeValue(property, a => AttributePrefix += (a.AttributePrefix?? property.Name)); - GetAttributeValue(property, a => CrawlRelationships = a.Associations); + GetAttributeValue(property, a => IncludeAssociations = a.Associations); GetAttributeValue(property, a => FlattenChildren = true); GetAttributeValue(property, a => MetadataKey = a.Key); GetAttributeValue(property, a => DisableMapping = true); @@ -276,26 +276,26 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" public CollectionType CollectionType { get; set; } /*========================================================================================================================== - | PROPERTY: CRAWL RELATIONSHIPS + | PROPERTY: INCLUDE ASSOCIATIONS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Determines which relationships, if any, the service should crawl for the current + /// Determines which associations, if any, the service should be included for the current /// property. /// /// /// - /// By default, the all relationships will be mapped on the target DTO, unless the caller specifies otherwise. On any - /// related DTOs, however, only will be mapped. So, if a mapped DTO has a - /// collection for children, relationships, or even a parent property then any relationships on those DTOs will - /// not be mapped. This behavior can be changed by specifying the flag, which allows - /// one or multiple relationships to be specified for a given property. + /// By default, the all associations will be mapped on the target model, unless the caller specifies otherwise. On any + /// associated models, however, only will be mapped. So, if a mapped model has + /// a collection for children, relationships, or even a parent property then any associations on those models + /// will not be mapped. This behavior can be changed by specifying the flag, which + /// allows one or multiple relationships to be specified for a given property. /// /// - /// The property corresponds to the - /// property. It can be assigned by decorating a DTO property with e.g. [Follow(Relationships.Children)]. + /// The property corresponds to the + /// property. It can be assigned by decorating a model property with e.g. [Include(Relationships.Children)]. /// /// - public AssociationTypes CrawlRelationships { get; set; } + public AssociationTypes IncludeAssociations { get; set; } /*========================================================================================================================== | PROPERTY: METADATA KEY diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 42be904b..d1470c65 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -693,7 +693,7 @@ configuration.ContentTypeFilter is not null && //Map child topic to target DTO var childDto = (object)childTopic; if (!typeof(Topic).IsAssignableFrom(listType)) { - taskQueue.Add(MapAsync(childTopic, configuration.CrawlRelationships, cache)); + taskQueue.Add(MapAsync(childTopic, configuration.IncludeAssociations, cache)); } else { AddToList(childDto); @@ -771,7 +771,7 @@ MappedTopicCache cache \-----------------------------------------------------------------------------------------------------------------------*/ var topicDto = (object?)null; try { - topicDto = await MapAsync(source, configuration.CrawlRelationships, cache).ConfigureAwait(false); + topicDto = await MapAsync(source, configuration.IncludeAssociations, cache).ConfigureAwait(false); } catch (InvalidTypeException) { //Disregard errors caused by unmapped view models; those are functionally equivalent to IsAssignableFrom() mismatches From acb64e930a3e234c139e0cd1af64ad7f46fdb526 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 14:31:04 -0800 Subject: [PATCH 579/778] Updated relevant references of `follow` to `include` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To correspond with the rename of the `[Follow()]` attribute to `[Include()]` (7c150d8), I reworded references to "follow" to use the "include" nomenclature instead—while being careful to only reword those usages that pertained to mapping associations. --- OnTopic/Mapping/ITopicMappingService.cs | 6 +++--- OnTopic/Mapping/TopicMappingService.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/OnTopic/Mapping/ITopicMappingService.cs b/OnTopic/Mapping/ITopicMappingService.cs index faa3f65b..e3ba283a 100644 --- a/OnTopic/Mapping/ITopicMappingService.cs +++ b/OnTopic/Mapping/ITopicMappingService.cs @@ -38,7 +38,7 @@ public interface ITopicMappingService { /// /// /// The entity to derive the data from. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// An instance of the dynamically determined View Model with properties appropriately mapped. Task MapAsync(Topic? topic, AssociationTypes associations = AssociationTypes.All); @@ -55,7 +55,7 @@ public interface ITopicMappingService { /// /// /// The entity to derive the data from. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// /// An instance of the requested View Model with properties appropriately mapped. /// @@ -70,7 +70,7 @@ public interface ITopicMappingService { /// /// The entity to derive the data from. /// The data transfer object to populate. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// /// An instance of the requested View Model instance with properties appropriately mapped. /// diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index d1470c65..85f331ab 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -84,7 +84,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// /// The entity to derive the data from. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// An instance of the dynamically determined View Model with properties appropriately mapped. @@ -170,7 +170,7 @@ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService /// /// The entity to derive the data from. /// The target object to map the data to. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. /// @@ -252,7 +252,7 @@ private async Task MapAsync( /// /// The entity to derive the data from. /// The target object to map the data to. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// Information related to the current property. /// A cache to keep track of already-mapped object instances. /// The prefix to apply to the attributes. @@ -431,7 +431,7 @@ private static void SetScalarValue(Topic source, object target, PropertyConfigur /// /// The source from which to pull the value. /// The target DTO on which to set the property value. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// /// The with details about the property's attributes. /// @@ -504,7 +504,7 @@ MappedTopicCache cache /// on the target DTO. /// /// The source from which to pull the value. - /// Determines what associations the mapping should follow, if any. + /// Determines what associations the mapping should include, if any. /// /// The with details about the property's attributes. /// From 0e32098b07831879b07910138eb5276abc5bd3de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 15:30:25 -0800 Subject: [PATCH 580/778] Renamed relevant references of `relationships` to `associations` As we've worked to disambiguate the term "relationships" from "collections" (1daf799) and "relationships" (191262a) by establishing the more general term "associations", we should update the XML Docs to maintain this nomenclature. Most of these are not references to e.g. the `AssociationTypes` enum, but rather discuss the concept in more abstract terms. By consistently using "associations" here instead of "relationships", we help reinforce the preferred vocabulary, and reduce ambiguities. This update required care to differentiate between references to "relationship" that referred to the broader concept, and references to "relationship" that referred, specifically, to e.g. `Topic.Relationships`, as the latter should be retained. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 14 +++--- OnTopic.Data.Sql/SqlTopicRepository.cs | 6 +-- ...idRelationshipBaseTypeTopicBindingModel.cs | 2 +- OnTopic.Tests/TopicTest.cs | 4 +- .../Specialized/ReadOnlyTopicMultiMap.cs | 2 +- .../Collections/Specialized/TopicMultiMap.cs | 2 +- .../Mapping/Annotations/AssociationTypes.cs | 2 +- .../Annotations/CollectionAttribute.cs | 13 +++--- .../Annotations/FilterByAttributeAttribute.cs | 7 ++- .../Annotations/FilterByContentType.cs | 6 +-- .../Mapping/Annotations/FlattenAttribute.cs | 9 ++-- .../Annotations/MapToParentAttribute.cs | 2 +- .../Mapping/Internal/PropertyConfiguration.cs | 44 +++++++++---------- .../Reverse/IReverseTopicMappingService.cs | 6 +-- OnTopic/Models/IRelatedTopicBindingModel.cs | 9 ++-- OnTopic/Repositories/ITopicRepository.cs | 12 ++--- OnTopic/Repositories/TopicRepository.cs | 13 +++--- OnTopic/Topic.cs | 8 ++-- 18 files changed, 79 insertions(+), 82 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 358e4236..93936bd3 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -39,12 +39,12 @@ internal static class SqlDataReaderExtensions { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a from a call to the GetTopics stored procedure, will extract a list of - /// topics and populate their attributes, relationships, and children. + /// topics and populate their attributes, associations, and children. /// /// The with output from the GetTopics stored procedure. /// - /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references - /// and relationships, including , are integrated with existing entities. + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic + /// associations—such as references, relationships, and —are integrated with existing entities. /// /// /// Specified whether the target collection value should be marked as dirty, assuming the value changes. By default, it @@ -394,7 +394,7 @@ private static void SetRelationships(this IDataReader reader, TopicIndex topics, /// Adds topic references to their associated topics. /// /// - /// Topics can be cross-referenced with each other topics via a one-to-one relationships. Once the topics are populated in + /// Topics can be cross-referenced with each other topics via a one-to-one associations. Once the topics are populated in /// memory, loop through the data to create these associations. /// /// The with output from the GetTopics stored procedure. @@ -411,7 +411,7 @@ private static void SetReferences(this IDataReader reader, TopicIndex topics, bo | Identify attributes \-----------------------------------------------------------------------------------------------------------------------*/ var sourceTopicId = reader.GetTopicId("Source_TopicID"); - var relationshipKey = reader.GetString("ReferenceKey"); + var referenceKey = reader.GetString("ReferenceKey"); var targetTopicId = reader.GetNullableTopicId("Target_TopicID"); /*------------------------------------------------------------------------------------------------------------------------ @@ -432,9 +432,9 @@ private static void SetReferences(this IDataReader reader, TopicIndex topics, bo } /*------------------------------------------------------------------------------------------------------------------------ - | Set relationship on object + | Set reference on object \-----------------------------------------------------------------------------------------------------------------------*/ - current.References.SetValue(relationshipKey, referenced, markDirty); + current.References.SetValue(referenceKey, referenced, markDirty); } diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 9e93e691..2edd7f10 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -207,10 +207,10 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic ); /*------------------------------------------------------------------------------------------------------------------------ - | Clear relationships, topic references + | Clear associations >------------------------------------------------------------------------------------------------------------------------- | Because we don't (currently) track version as part of the .NET data model for relationships or topic references, there's - | no easy way to determine if a relationship should be deleted when doing a rollback. As such, existing relationships + | no easy way to determine if an association should be deleted when doing a rollback. As such, existing associations | should be deleted, assuming a `referenceTopic` is passed, and it contains the `topicId`. \-----------------------------------------------------------------------------------------------------------------------*/ var topic = (Topic?)null; @@ -375,7 +375,7 @@ bool persistRelationships /*------------------------------------------------------------------------------------------------------------------------ | Detect whether anything has changed >------------------------------------------------------------------------------------------------------------------------- - | If no relationships have changed, and no attributes values have changed, and there aren't any mismatched attributes in + | If no associations have changed, and no attributes values have changed, and there aren't any mismatched attributes in | their respective lists, then there isn't anything new to persist to the database, and thus no benefit to executing the | current command. A more aggressive version of this would wrap much of the below logic in this, but this is just meant | as a quick fix to reduce the overhead of recursive saves. diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs index 73f7d6e9..0b4aa1e8 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs @@ -14,7 +14,7 @@ namespace OnTopic.Tests.BindingModels { | BINDING MODEL: RELATIONSHIP BASE TYPE TOPIC (INVALID) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a custom binding model with an invalid base type for a relationship—i.e., one that doesn't implement the . An should be thrown when it is mapped. /// /// diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index b281449b..7df5b628 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -298,8 +298,8 @@ public void BaseTopic_UpdateValue_ReturnsExpectedValue() { | TEST: BASE TOPIC: RESAVED VALUE: RETURNS EXPECTED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a base topic to an unsaved topic entity, then saves the entity and reestablishes the relationship. Ensures - /// that the base topic is correctly set as a entry. + /// Sets a base topic to an unsaved topic entity, then saves the entity and reestablishes the reference. Ensures that the + /// base topic is correctly set as a entry. /// [TestMethod] public void BaseTopic_ResavedValue_ReturnsExpectedValue() { diff --git a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index a6179de9..5a92a94e 100644 --- a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -135,7 +135,7 @@ public ReadOnlyTopicCollection GetAllTopics() => new(Source.SelectMany(list => list.Values).Distinct().ToList()); /// - /// Retrieves a list of all related objects, independent of relationship key, filtered by content + /// Retrieves a list of all related objects, independent of key, filtered by content /// type. /// /// diff --git a/OnTopic/Collections/Specialized/TopicMultiMap.cs b/OnTopic/Collections/Specialized/TopicMultiMap.cs index 260aaa55..9c8eb431 100644 --- a/OnTopic/Collections/Specialized/TopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/TopicMultiMap.cs @@ -119,7 +119,7 @@ public bool Remove(string key, Topic topic) { } /*------------------------------------------------------------------------------------------------------------------------ - | Remove relationship + | Remove topic \-----------------------------------------------------------------------------------------------------------------------*/ topics.Remove(topic); diff --git a/OnTopic/Mapping/Annotations/AssociationTypes.cs b/OnTopic/Mapping/Annotations/AssociationTypes.cs index e6464919..54292b89 100644 --- a/OnTopic/Mapping/Annotations/AssociationTypes.cs +++ b/OnTopic/Mapping/Annotations/AssociationTypes.cs @@ -101,7 +101,7 @@ public enum AssociationTypes { | ALL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Map all relationship types. + /// Map all association types. /// All = Parents | Children | Relationships | IncomingRelationships | MappedCollections | References diff --git a/OnTopic/Mapping/Annotations/CollectionAttribute.cs b/OnTopic/Mapping/Annotations/CollectionAttribute.cs index 1f05030d..6cfc11c4 100644 --- a/OnTopic/Mapping/Annotations/CollectionAttribute.cs +++ b/OnTopic/Mapping/Annotations/CollectionAttribute.cs @@ -10,14 +10,15 @@ namespace OnTopic.Mapping.Annotations { | ATTRIBUTE: COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides the with instructions as to which key to follow for the relationship. + /// Provides the with instructions as to which collection to map to. /// /// /// /// By default, implementations will attempt to map a collection property to the first - /// relationship it finds with a key that matches the property name. For example, if a property is named "Categories" it - /// will first look in for a relationship named "Categories", then it will search for a set of nested topics, and finally . + /// collection it finds with a key that matches the property name. For example, if a property is named Categories + /// it will first look in for a relationship named Categories, then it will + /// search , then for a set of nested topics, and finally . /// /// /// This attribute instructs the to instead look for a specified key. This allows the @@ -36,7 +37,7 @@ public sealed class CollectionAttribute : System.Attribute { /// /// Annotates a property with the by providing an . /// - /// The key value of the relationships associated with the current property. + /// The key value of the collection associated with the current property. public CollectionAttribute(string key) { TopicFactory.ValidateKey(key, false); Key = key; @@ -45,7 +46,7 @@ public CollectionAttribute(string key) { /// /// Annotates a property with the by providing the . /// - /// Optional. The type of collection the relationship is associated with. + /// Optional. The type of collection the collection is associated with. public CollectionAttribute(CollectionType type = CollectionType.Any) { Type = type; } diff --git a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs index 89984278..7cabab49 100644 --- a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs +++ b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs @@ -13,10 +13,9 @@ namespace OnTopic.Mapping.Annotations { /// Flags that a collection property should be filtered by a specified attributeKey and attributeValue. /// /// - /// By default, will add any corresponding relationships to a collection, assuming they - /// are assignable to the collection's base type. With the [FilterByAttribute(attributeKey, attributeValue)] - /// attribute, the collection will instead be filtered to only those topics that have the specified attribute value - /// assigned. + /// By default, will add any corresponding topics to a collection, assuming they are + /// assignable to the collection's base type. With the [FilterByAttribute(attributeKey, attributeValue)] attribute, + /// the collection will instead be filtered to only those topics that have the specified attribute value assigned. /// [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=true, Inherited=true)] public sealed class FilterByAttributeAttribute : System.Attribute { diff --git a/OnTopic/Mapping/Annotations/FilterByContentType.cs b/OnTopic/Mapping/Annotations/FilterByContentType.cs index 5d93d4dc..53aa8786 100644 --- a/OnTopic/Mapping/Annotations/FilterByContentType.cs +++ b/OnTopic/Mapping/Annotations/FilterByContentType.cs @@ -13,9 +13,9 @@ namespace OnTopic.Mapping.Annotations { /// Flags that a collection property should be filtered by a specified ContentType. /// /// - /// By default, will add any corresponding relationships to a collection, assuming they - /// are assignable to the collection's base type. With the [FilterByContentType(contentType)] attribute, the - /// collection will instead be filtered to only those topics that have the specified content type. + /// By default, will add any corresponding topic to a collection, assuming they are + /// assignable to the collection's base type. With the [FilterByContentType(contentType)] attribute, the collection + /// will instead be filtered to only those topics that have the specified content type. /// [System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=true, Inherited=true)] public sealed class FilterByContentTypeAttribute : System.Attribute { diff --git a/OnTopic/Mapping/Annotations/FlattenAttribute.cs b/OnTopic/Mapping/Annotations/FlattenAttribute.cs index 448d7767..9b4ae6d0 100644 --- a/OnTopic/Mapping/Annotations/FlattenAttribute.cs +++ b/OnTopic/Mapping/Annotations/FlattenAttribute.cs @@ -16,11 +16,10 @@ namespace OnTopic.Mapping.Annotations { /// /// /// By default, will populate all items in a collection—and, if the is defined, then also include their specified relationships. The allows all subsequent children to not only be included, but to be elevated to a single - /// list. This can be especially useful when combined with e.g. as well as - /// strongly-typed collections (e.g., of a specific view model type), as it allows a list to provide, effectively, search - /// results. + /// cref="IncludeAttribute"/> is defined, then also include their specified associations. The allows all subsequent children to not only be included, but to be elevated to a single list. This can be especially + /// useful when combined with e.g. as well as strongly-typed collections (e.g., + /// of a specific view model type), as it allows a list to provide, effectively, search results. /// /// [System.AttributeUsage(System.AttributeTargets.Property)] diff --git a/OnTopic/Mapping/Annotations/MapToParentAttribute.cs b/OnTopic/Mapping/Annotations/MapToParentAttribute.cs index fd98eb7c..fcfa244d 100644 --- a/OnTopic/Mapping/Annotations/MapToParentAttribute.cs +++ b/OnTopic/Mapping/Annotations/MapToParentAttribute.cs @@ -16,7 +16,7 @@ namespace OnTopic.Mapping.Annotations { /// the provided key. /// /// - /// By default, complex property objects that don't map to known attributes (e.g., a compatible property) or relationships + /// By default, complex property objects that don't map to known attributes (e.g., a compatible property) or associations /// are bypassed. The informs the to treat the /// properties of such complex objects as members of the parent . By default, these will be prefixed by /// the name of the property that the complex object is assigned to. Optionally, however, this may be overwritten—including diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index dd37fd72..9f91abb0 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -89,7 +89,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" GetAttributeValue(property, a => ContentTypeFilter = a.ContentType); /*------------------------------------------------------------------------------------------------------------------------ - | Attributes: Determine relationship key and type + | Attributes: Determine collection key and type \-----------------------------------------------------------------------------------------------------------------------*/ GetAttributeValue( property, @@ -183,7 +183,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// /// Be aware that the should only apply to actual attributes on the mapped entity; it is not intended to be applied to e.g. collections or relationships. + /// cref="Topic"/> entity; it is not intended to be applied to e.g. collections or associations. /// /// /// The property corresponds to the @@ -226,51 +226,51 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// /// The property corresponds to the being set on a given - /// property. It can be assigned by decorating a DTO property with e.g. [Inherit]. + /// property. It can be assigned by decorating a model property with e.g. [Inherit]. /// /// public bool InheritValue { get; set; } /*========================================================================================================================== - | PROPERTY: RELATIONSHIP KEY + | PROPERTY: COLLECTION KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// The name of the relationship that a collection should map to. Defaults to the property name on DTO. + /// The name of the collection that a collection should map to. Defaults to the property name on DTO. /// /// /// - /// By default, a collection property on a DTO class will be mapped to a corresponding relationship of the same name. - /// So, for instance, if the property on the DTO class is called Cousins then the will search , , and, finally, for an object named Cousins. - /// If the is set, however, then that value is used instead, thus allowing the property on - /// the DTO to be aliased to a different collection name on the source . + /// By default, a collection property on a model class will be mapped to a corresponding collection of the same name. + /// So, for instance, if the property on the model class is called Cousins then the will search , , , and, finally, for an object named Cousins. If + /// the is set, however, then that value is used instead, thus allowing the property on the + /// model to be aliased to a different collection name on the source . /// /// /// The property corresponds to the property. It - /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey")]. + /// can be assigned by decorating a model property with e.g. [Collection("AlternateCollectionKey")]. /// /// public string CollectionKey { get; set; } /*========================================================================================================================== - | PROPERTY: RELATIONSHIP TYPE + | PROPERTY: COLLECTION TYPE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Determines the type of relationship that a collection property corresponds to. + /// Determines the type of collection that a collection property corresponds to. /// /// /// - /// By default, a collection property on a DTO class will attempt to find a match from, in order, , , and, finally, . - /// If the is set, however, then the will only - /// map the collection to a relationship of that type. This can be valuable when the might - /// be ambiguous between multiple collections. + /// By default, a collection property on a model class will attempt to find a match from, in order, , , , and, finally, . If the is set, however, then the will only map the collection to a collection of that type. This can be valuable when the might be ambiguous between multiple collections. /// /// /// The property corresponds to the property. It - /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey", - /// CollectionType.Children)]. + /// can be assigned by decorating a model property with e.g. [Collection("AlternateCollectionKey", CollectionType. + /// Children)]. /// /// public CollectionType CollectionType { get; set; } @@ -288,7 +288,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// associated models, however, only will be mapped. So, if a mapped model has /// a collection for children, relationships, or even a parent property then any associations on those models /// will not be mapped. This behavior can be changed by specifying the flag, which - /// allows one or multiple relationships to be specified for a given property. + /// allows one or multiple associations to be specified for a given property. /// /// /// The property corresponds to the diff --git a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs index 1f9ebfe9..b1649a57 100644 --- a/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/IReverseTopicMappingService.cs @@ -28,15 +28,15 @@ namespace OnTopic.Mapping.Reverse { /// from the binding model regarding what properties are intended for—especially in terms of e.g aliased relationships. /// /// - /// The current design of is not intended to support mapping relationships, as + /// The current design of is not intended to support mapping associations, as /// this overlaps with a broader concern of merging topics with an existing graph (e.g., as part of an - /// . + /// . It will map the associations to those topics, but not those topics themselves. /// /// /// Despite the differences between the and s, /// many attributes are able to be reused between them. For instance, the can still /// map a property on a binding model to an attribute of a different name on a , just as the can with relationships. Other attributes, however, provde no benefit in the reverse + /// cref="CollectionAttribute"/> can with collections. Other attributes, however, provde no benefit in the reverse /// scenario, such as or , which really only make /// sense in creating a "produced view" that is a subset of the original model. That is valuable when creating a view /// model, but isn't a useful use case when working with binding models. diff --git a/OnTopic/Models/IRelatedTopicBindingModel.cs b/OnTopic/Models/IRelatedTopicBindingModel.cs index 45473dcc..42104014 100644 --- a/OnTopic/Models/IRelatedTopicBindingModel.cs +++ b/OnTopic/Models/IRelatedTopicBindingModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; -using OnTopic.Mapping.Reverse; namespace OnTopic.Models { @@ -12,12 +11,12 @@ namespace OnTopic.Models { | INTERFACE: RELATED TOPIC BINDING MODEL \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for binding a relationship of a binding model to an existing . + /// Provides a generic data transfer topic for binding an association of a binding model to an existing . /// /// - /// It is strictly required that any binding models used as relationships implement the interface for the default to correctly - /// identify and map a relationship back to a . + /// It is strictly required that any binding models used as associations implement the interface for the default to correctly identify and map an association back + /// to a . /// public interface IRelatedTopicBindingModel { diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 598be865..465ed990 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -74,8 +74,8 @@ public interface ITopicRepository { /// /// The topic identifier. /// - /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references - /// and relationships, including , are integrated with existing entities. + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic + /// associations—such as references, relationships, and —are integrated with existing entities. /// /// Determines whether or not to recurse through and load a topic's children. /// A topic object. @@ -87,8 +87,8 @@ public interface ITopicRepository { /// /// The fully-qualified unique topic key. /// - /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references - /// and relationships, including , are integrated with existing entities. + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic + /// associations—such as references, relationships, and —are integrated with existing entities. /// /// Determines whether or not to recurse through and load a topic's children. /// A topic object. @@ -105,8 +105,8 @@ public interface ITopicRepository { /// The topic identifier. /// The version. /// - /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic references - /// and relationships, including , are integrated with existing entities. + /// When loading a single topic or branch, offers a reference topic graph that can be used to ensure that topic + /// associations—such as references, relationships, and —are integrated with existing entities. /// /// A topic object. Topic? Load(int topicId, DateTime version, Topic? referenceTopic = null); diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index b54af1cb..c31c5db9 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -272,7 +272,7 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv Save(topic, isRecursive, unresolvedTopics, version); /*------------------------------------------------------------------------------------------------------------------------ - | Attempt to resolve outstanding relationships + | Attempt to resolve outstanding associations \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var unresolvedTopic in unresolvedTopics.ToList()) { unresolvedTopics.Remove(unresolvedTopic); @@ -307,7 +307,7 @@ public override sealed void Save([ValidatedNotNull] Topic topic, bool isRecursiv /// cref="Topic.Relationships"/> or —can't yet be persisted because the target hasn't yet been saved, and thus the is still set to -1. To mitigate /// this, the allows this private overload to keep track of unresolved - /// relationships. The public overload uses this list to resave any topics + /// associations. The public overload uses this list to resave any topics /// that include such references. This adds some overhead due to the duplicate , but /// helps avoid potential data loss when working with complex topic graphs. /// @@ -341,11 +341,10 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso } /*------------------------------------------------------------------------------------------------------------------------ - | Handle unresolved references + | Handle unresolved associations >------------------------------------------------------------------------------------------------------------------------- - | If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been - | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The - | same can be done for Base Topic references, which are effectively establish a 1:1 relationship. + | If it's a recursive save and there are any unresolved associations, come back to this after the topic graph has been + | saved; that ensures that any associations within the topic graph have been saved and can be properly persisted. \-----------------------------------------------------------------------------------------------------------------------*/ if ( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) || @@ -795,8 +794,8 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { continue; }; - // Ignore relationships and nested topics if (attribute.ModelType is ModelType.Relationship or ModelType.NestedTopic) { + // Ignore associations continue; } diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index ccf328db..7ec737c5 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -61,7 +61,7 @@ public class Topic { public Topic(string key, string contentType, Topic? parent = null, int id = -1) { /*------------------------------------------------------------------------------------------------------------------------ - | Set relationships + | Set collections \-----------------------------------------------------------------------------------------------------------------------*/ Children = new(); Attributes = new(this); @@ -136,7 +136,7 @@ public int Id { /// The current 's parent . /// /// - /// While topics may be represented as a network graph via relationships, they are physically stored and primarily + /// While topics may be represented as a network graph via associations, they are physically stored and primarily /// represented via a hierarchy. As such, each topic may have at most a single parent. Note that the root node will /// have a null parent. /// @@ -631,7 +631,7 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) /// /// The underlying value of the is stored as a topic reference with the of BaseTopic in . If the hasn't been - /// saved, then the relationship will be established, but the BaseTopic won't be persisted to the underlying + /// saved, then the reference will be established, but the BaseTopic won't be persisted to the underlying /// repository upon . That said, when is called, the will be reevaluated and, if it has /// subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory topic graphs to @@ -697,7 +697,7 @@ public Topic? BaseTopic { /// The references property exposes a with child topics representing named references (e.g., /// BaseTopic for a ). /// - /// The current 's relationships. + /// The current 's references. public TopicReferenceCollection References { get; } /*========================================================================================================================== From 743588911eef908b410d91eaebfe3a683a4aaf0e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 15:53:15 -0800 Subject: [PATCH 581/778] Renamed `IRelatedTopicBindingModel` to `IAssociatedTopicBindingModel` The `RelatedTopicBindingModel` is intended to ensure that topic binding models can refer to related topics and have those relationships persisted. With the introduction of topic references, however, we've reevaluated the "relationship" nomenclature, since a related topic could be a relationship or a reference. The preferred terminology for referring to _both_ of these is "associations". Given that, we've introduced a new `IAssociatedTopicBindingModel` interface and a new `AssociatedTopicBindingModel` implementation. --- .../ContentTypeDescriptorTopicBindingModel.cs | 2 +- .../InvalidReferenceTypeTopicBindingModel.cs | 4 ++-- .../InvalidRelationshipBaseTypeTopicBindingModel.cs | 2 +- .../InvalidRelationshipListTypeTopicBindingModel.cs | 2 +- .../InvalidRelationshipTypeTopicBindingModel.cs | 2 +- .../BindingModels/ReferenceTopicBindingModel.cs | 2 +- OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 10 +++++----- ...indingModel.cs => AssociatedTopicBindingModel.cs} | 12 ++++++------ OnTopic/Mapping/Reverse/BindingModelValidator.cs | 10 +++++----- .../Mapping/Reverse/ReverseTopicMappingService.cs | 6 +++--- ...ndingModel.cs => IAssociatedTopicBindingModel.cs} | 11 ++++++----- 11 files changed, 32 insertions(+), 31 deletions(-) rename OnTopic.ViewModels/BindingModels/{RelatedTopicBindingModel.cs => AssociatedTopicBindingModel.cs} (74%) rename OnTopic/Models/{IRelatedTopicBindingModel.cs => IAssociatedTopicBindingModel.cs} (81%) diff --git a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs index 7e621373..2b14faa3 100644 --- a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs @@ -21,7 +21,7 @@ public class ContentTypeDescriptorTopicBindingModel : BasicTopicBindingModel { public ContentTypeDescriptorTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - public Collection ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); public Collection Attributes { get; } = new(); diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs index c8f0e658..26980d74 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs @@ -14,8 +14,8 @@ namespace OnTopic.Tests.BindingModels { | BINDING MODEL: REFERENCE TYPE TOPIC (INVALID) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a custom binding model with an invalid reference type—i.e., one that doesn't implement . An should be thrown when it is mapped. + /// Provides a custom binding model with an invalid reference type—i.e., one that doesn't implement . An should be thrown when it is mapped. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs index 0b4aa1e8..14e2e2ce 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs @@ -15,7 +15,7 @@ namespace OnTopic.Tests.BindingModels { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a custom binding model with an invalid base type for an association—i.e., one that doesn't implement the . An should be thrown when it is mapped. + /// cref="IAssociatedTopicBindingModel"/>. An should be thrown when it is mapped. /// /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs index 1dc8909b..88af3b68 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs @@ -25,7 +25,7 @@ public class InvalidRelationshipListTypeTopicBindingModel : BasicTopicBindingMod public InvalidRelationshipListTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - public Dictionary ContentTypes { get; } = new(); + public Dictionary ContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs index f6fab89a..13126ea8 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs @@ -26,7 +26,7 @@ public class InvalidRelationshipTypeTopicBindingModel : BasicTopicBindingModel { public InvalidRelationshipTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } [Collection(CollectionType.NestedTopics)] - public Collection ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index 64f79c40..91419232 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -22,7 +22,7 @@ public class ReferenceTopicBindingModel : BasicTopicBindingModel { public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttributeDescriptor") { } - public RelatedTopicBindingModel? BaseTopic { get; set; } + public AssociatedTopicBindingModel? BaseTopic { get; set; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 8e1b081f..f9618585 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -422,8 +422,8 @@ public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() { | TEST: MAP: INVALID RELATIONSHIP BASE TYPE: THROWS INVALID OPERATION EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Maps a content type that has a relationship whose type doesn't implement . This - /// is invalid, and expected to throw an . + /// Maps a content type that has a relationship whose type doesn't implement . + /// This is invalid, and expected to throw an . /// [TestMethod] [ExpectedException(typeof(MappingModelValidationException))] @@ -478,9 +478,9 @@ public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationExceptio | TEST: MAP: INVALID TOPIC REFERENCE TYPE: THROWS INVALID OPERATION EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Maps a content type that has a reference that implements an invalid type—i.e., it implements a , even though references are expected to return a type implementing . This is invalid, and expected to throw an . + /// Maps a content type that has a reference that implements an invalid type—i.e., it implements a , even though references are expected to return a type implementing . This is invalid, and expected to throw an . /// [TestMethod] [ExpectedException(typeof(MappingModelValidationException))] diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs similarity index 74% rename from OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs rename to OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs index 480bea9f..55a9c4e0 100644 --- a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs @@ -10,18 +10,18 @@ namespace OnTopic.ViewModels.BindingModels { /*============================================================================================================================ - | CLASS: RELATED TOPIC BINDING MODEL + | CLASS: ASSOCIATED TOPIC BINDING MODEL \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for binding a relationship of a binding model to an existing . + /// Provides a generic data transfer topic for binding an association of a binding model to an existing . /// /// - /// While implementors may choose to create a custom implementation, the out-of-the- - /// box implementation satisfies all of the requirements of the . The only reason to implement a custom definition is if the caller needs additional + /// While implementors may choose to create a custom implementation, the out-of- + /// the-box implementation satisfies all of the requirements of the . The only reason to implement a custom definition is if the caller needs additional /// metadata for separate validation or processing. /// - public record RelatedTopicBindingModel : IRelatedTopicBindingModel { + public record AssociatedTopicBindingModel : IAssociatedTopicBindingModel { /*========================================================================================================================== | PROPERTY: UNIQUE KEY diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index f6f9db6d..b58bc71f 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -260,13 +260,13 @@ listType is not null \-----------------------------------------------------------------------------------------------------------------------*/ if ( attributeDescriptor.ModelType is ModelType.Reference && - !typeof(IRelatedTopicBindingModel).IsAssignableFrom(propertyType) + !typeof(IAssociatedTopicBindingModel).IsAssignableFrom(propertyType) ) { throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{ModelType.Reference}, but the generic type '{propertyType.Name}' does not implement the " + - $"{nameof(IRelatedTopicBindingModel)} interface. This is required for references. If this property is not intended " + - $"to be mapped as a {ModelType.Reference} then update the definition in the associated " + + $"{nameof(IAssociatedTopicBindingModel)} interface. This is required for references. If this property is not " + + $"intended to be mapped as a {ModelType.Reference} then update the definition in the associated " + $"{nameof(ContentTypeDescriptor)}. If this property is not intended to be mapped at all, include the " + $"{nameof(DisableMappingAttribute)} to exclude it from mapping." ); @@ -336,11 +336,11 @@ [AllowNull]Type listType /*------------------------------------------------------------------------------------------------------------------------ | Validate the correct base class for relationships \-----------------------------------------------------------------------------------------------------------------------*/ - if (!typeof(IRelatedTopicBindingModel).IsAssignableFrom(listType)) { + if (!typeof(IAssociatedTopicBindingModel).IsAssignableFrom(listType)) { throw new MappingModelValidationException( $"The '{property.Name}' property on the '{sourceType.Name}' class has been determined to be a " + $"{configuration.CollectionType}, but the generic type '{listType?.Name}' does not implement the " + - $"{nameof(IRelatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + + $"{nameof(IAssociatedTopicBindingModel)} interface. This is required for binding models. If this collection is not " + $"intended to be mapped as a {configuration.CollectionType} then update the definition in the associated " + $"{nameof(ContentTypeDescriptor)}. If this collection is not intended to be mapped at all, include the " + $"{nameof(DisableMappingAttribute)} to exclude it from mapping." diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index de67bf40..a851aed6 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -422,7 +422,7 @@ PropertyConfiguration configuration var sourceList = (IList?)configuration.Property.GetValue(source, null); if (sourceList is null) { - sourceList = new List(); + sourceList = new List(); } /*------------------------------------------------------------------------------------------------------------------------ @@ -433,7 +433,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Set relationships for each \-----------------------------------------------------------------------------------------------------------------------*/ - foreach (IRelatedTopicBindingModel relationship in sourceList) { + foreach (IAssociatedTopicBindingModel relationship in sourceList) { var targetTopic = _topicRepository.Load(relationship.UniqueKey, target); if (targetTopic is null) { throw new MappingModelValidationException( @@ -524,7 +524,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Retrieve source value \-----------------------------------------------------------------------------------------------------------------------*/ - var modelReference = (IRelatedTopicBindingModel?)configuration.Property.GetValue(source); + var modelReference = (IAssociatedTopicBindingModel?)configuration.Property.GetValue(source); /*------------------------------------------------------------------------------------------------------------------------ | Bypass if reference (or value) is null (or empty) diff --git a/OnTopic/Models/IRelatedTopicBindingModel.cs b/OnTopic/Models/IAssociatedTopicBindingModel.cs similarity index 81% rename from OnTopic/Models/IRelatedTopicBindingModel.cs rename to OnTopic/Models/IAssociatedTopicBindingModel.cs index 42104014..68a0d153 100644 --- a/OnTopic/Models/IRelatedTopicBindingModel.cs +++ b/OnTopic/Models/IAssociatedTopicBindingModel.cs @@ -4,21 +4,22 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using OnTopic.Mapping.Reverse; namespace OnTopic.Models { /*============================================================================================================================ - | INTERFACE: RELATED TOPIC BINDING MODEL + | INTERFACE: ASSOCIATED TOPIC BINDING MODEL \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides a generic data transfer topic for binding an association of a binding model to an existing . /// /// - /// It is strictly required that any binding models used as associations implement the interface for the default to correctly identify and map an association back - /// to a . + /// It is strictly required that any binding models used as associations implement the interface for the default to correctly identify + /// and map an association back to a . /// - public interface IRelatedTopicBindingModel { + public interface IAssociatedTopicBindingModel { /*========================================================================================================================== | PROPERTY: UNIQUE KEY From cf7badfe9ca2065291b8dbab49a43d4d280669e4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 15:56:54 -0800 Subject: [PATCH 582/778] Reintroduced `IRelatedTopicBindingModel` as deprecated In the previous commit, we renamed `IRelatedTopicBindingModel` to `IAssociatedTopicBindingModel` to ensure that it accounted for both relationships and topic references, thus using the preferred "associations" nomenclature. As `IRelatedTopicBindingModel` hadn't been marked as deprecated, this could caused confusion. To help mitigate that, the `IRelatedTopicBindingModel` is being reintroduced as a deprecated so that consumers will be giving instructions on how to migrate their implementations. These will be removed with OnTopic 5.1.0. --- .../BindingModels/RelatedTopicBindingModel.cs | 31 +++++++++++++++++++ OnTopic/Models/IRelatedTopicBindingModel.cs | 29 +++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs create mode 100644 OnTopic/Models/IRelatedTopicBindingModel.cs diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs new file mode 100644 index 00000000..ae6baf18 --- /dev/null +++ b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs @@ -0,0 +1,31 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Mapping.Reverse; +using OnTopic.Models; + +namespace OnTopic.ViewModels.BindingModels { + + /*============================================================================================================================ + | CLASS: RELATED TOPIC BINDING MODEL + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a generic data transfer topic for binding a relationship of a binding model to an existing . + /// + /// + /// While implementors may choose to create a custom implementation, the out-of- + /// the-box implementation satisfies all of the requirements of the . The only reason to implement a custom definition is if the caller needs additional + /// metadata for separate validation or processing. + /// + [Obsolete( + "The RelatedTopicBindingModel has been replaced by the AssociatedTopicBindingModel. Please update references.", + true + )] + public record RelatedTopicBindingModel: AssociatedTopicBindingModel { + + } //Class +} //Namespaces \ No newline at end of file diff --git a/OnTopic/Models/IRelatedTopicBindingModel.cs b/OnTopic/Models/IRelatedTopicBindingModel.cs new file mode 100644 index 00000000..184eb052 --- /dev/null +++ b/OnTopic/Models/IRelatedTopicBindingModel.cs @@ -0,0 +1,29 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Mapping.Reverse; + +namespace OnTopic.Models { + + /*============================================================================================================================ + | INTERFACE: RELATED TOPIC BINDING MODEL + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a generic data transfer topic for binding an association of a binding model to an existing . + /// + /// + /// It is strictly required that any binding models used as associations implement the interface for the default to correctly identify and map an association back + /// to a . + /// + [Obsolete( + "The IRelatedTopicBindingModel has been replaced by the IAssociatedTopicBindingModel. Please update references.", + true + )] + public interface IRelatedTopicBindingModel: IAssociatedTopicBindingModel { + + } //Class +} //Namespace \ No newline at end of file From 213a46b82a81fea42d4fa372033d7b67a7ddd5a5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 16:05:12 -0800 Subject: [PATCH 583/778] Renamed `OnTopic.References` to `OnTopic.Associations` The introduction of topic references has caused confusion and ambiguity in the naming convention for relationships. Previously, the only relationships to other topics we had were `Topic.Relationships`. But, now, we also have `Topic.References` as well. Referring to both of these as "relationships" is thus confusing since it _could_ refer to `Topic.Relationships` specifically, but could _also_ refer to `Topic.References`. An early attempt to mitigate this had resulted in the `OnTopic.References` nomenclature. That's no better, as it _could_ refer to `Topic.References` specifically, but could _also_ refer to `Topic.Relationships`. To avoid confusion, those are now being referred to collectively as topic _associations_. We've already updated e.g. the `Relationships` enum to `AssociationTypes` (191262a), along with a number of related changes. We're now renaming `OnTopic.References` to `OnTopic.Associations`. --- OnTopic.Tests/Entities/CustomTopic.cs | 2 +- OnTopic.Tests/SqlTopicRepositoryTest.cs | 2 +- OnTopic.Tests/TopicReferenceCollectionTest.cs | 2 +- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 2 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 2 +- .../{References => Associations}/ReferenceSetterAttribute.cs | 2 +- OnTopic/{References => Associations}/TopicReference.cs | 2 +- .../{References => Associations}/TopicReferenceCollection.cs | 2 +- .../{References => Associations}/TopicRelationshipMultiMap.cs | 2 +- OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/Topic.cs | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) rename OnTopic/{References => Associations}/ReferenceSetterAttribute.cs (98%) rename OnTopic/{References => Associations}/TopicReference.cs (99%) rename OnTopic/{References => Associations}/TopicReferenceCollection.cs (99%) rename OnTopic/{References => Associations}/TopicRelationshipMultiMap.cs (99%) diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs index 8679bc8f..8141faea 100644 --- a/OnTopic.Tests/Entities/CustomTopic.cs +++ b/OnTopic.Tests/Entities/CustomTopic.cs @@ -7,7 +7,7 @@ using System.Globalization; using OnTopic.Attributes; using OnTopic.Internal.Diagnostics; -using OnTopic.References; +using OnTopic.Associations; namespace OnTopic.Tests.Entities { diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 90515352..844cf342 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -9,7 +9,7 @@ using System.Xml; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Data.Sql; -using OnTopic.References; +using OnTopic.Associations; using OnTopic.Tests.Schemas; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 4da5a4b2..5786afb9 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.References; +using OnTopic.Associations; using OnTopic.Tests.Entities; using OnTopic.Collections.Specialized; diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 49271292..c8676252 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -7,7 +7,7 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Collections.Specialized; -using OnTopic.References; +using OnTopic.Associations; namespace OnTopic.Tests { diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 2572b7e5..44ccc13e 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -10,7 +10,7 @@ using OnTopic.Collections.Specialized; using OnTopic.Data.Caching; using OnTopic.Metadata; -using OnTopic.References; +using OnTopic.Associations; using OnTopic.Repositories; using OnTopic.TestDoubles; using OnTopic.TestDoubles.Metadata; diff --git a/OnTopic/References/ReferenceSetterAttribute.cs b/OnTopic/Associations/ReferenceSetterAttribute.cs similarity index 98% rename from OnTopic/References/ReferenceSetterAttribute.cs rename to OnTopic/Associations/ReferenceSetterAttribute.cs index 99c85614..da8b8369 100644 --- a/OnTopic/References/ReferenceSetterAttribute.cs +++ b/OnTopic/Associations/ReferenceSetterAttribute.cs @@ -6,7 +6,7 @@ using System; using OnTopic.Collections.Specialized; -namespace OnTopic.References { +namespace OnTopic.Associations { /*============================================================================================================================ | CLASS: REFERENCE SETTER [ATTRIBUTE] diff --git a/OnTopic/References/TopicReference.cs b/OnTopic/Associations/TopicReference.cs similarity index 99% rename from OnTopic/References/TopicReference.cs rename to OnTopic/Associations/TopicReference.cs index ee9a630d..6ac1f741 100644 --- a/OnTopic/References/TopicReference.cs +++ b/OnTopic/Associations/TopicReference.cs @@ -8,7 +8,7 @@ using OnTopic.Metadata; using OnTopic.Repositories; -namespace OnTopic.References { +namespace OnTopic.Associations { /*============================================================================================================================ | CLASS: TOPIC REFERENCE diff --git a/OnTopic/References/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs similarity index 99% rename from OnTopic/References/TopicReferenceCollection.cs rename to OnTopic/Associations/TopicReferenceCollection.cs index 3114d699..e8d7bc67 100644 --- a/OnTopic/References/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -7,7 +7,7 @@ using OnTopic.Collections.Specialized; using OnTopic.Repositories; -namespace OnTopic.References { +namespace OnTopic.Associations { /*============================================================================================================================ | CLASS: TOPIC REFERENCE COLLECTION diff --git a/OnTopic/References/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs similarity index 99% rename from OnTopic/References/TopicRelationshipMultiMap.cs rename to OnTopic/Associations/TopicRelationshipMultiMap.cs index 54db4e7f..d8c92159 100644 --- a/OnTopic/References/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -8,7 +8,7 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Repositories; -namespace OnTopic.References { +namespace OnTopic.Associations { /*============================================================================================================================ | CLASS: TOPIC RELATIONSHIP MULTIMAP diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 1f9e3bc5..f54d3947 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -10,7 +10,7 @@ using System.Runtime.ExceptionServices; using OnTopic.Attributes; using OnTopic.Collections.Specialized; -using OnTopic.References; +using OnTopic.Associations; namespace OnTopic.Internal.Reflection { diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 1ceebc0a..9819c91c 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -9,7 +9,7 @@ using OnTopic.Collections; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; -using OnTopic.References; +using OnTopic.Associations; namespace OnTopic.Metadata { diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 7ec737c5..4e879676 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -13,7 +13,7 @@ using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Metadata; -using OnTopic.References; +using OnTopic.Associations; namespace OnTopic { From f77cf59fd3a8d2fd94954365041d0fbd8b3089b9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 16:26:25 -0800 Subject: [PATCH 584/778] Accounted for `Topic.References` on `Delete()` Previously, the `TopicRepository`'s base `Delete()` class accounted for `Topic.Relationships` by removing any outgoing or incoming relationships that would be affected by the operation, thus ensuring that no orphaned topic references were persisted in memory from other parts of the topic graph. This helps ensure there aren't any legacy references maintained in the cache after a delete. While this accounted for `Topic.Relationships`, however, it didn't account for the new `Topic.References`. This is corrected in this update. --- OnTopic/Repositories/TopicRepository.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index c31c5db9..9d931283 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -599,6 +599,17 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi } } + /*------------------------------------------------------------------------------------------------------------------------ + | Remove references + \-----------------------------------------------------------------------------------------------------------------------*/ + foreach (var descendantTopic in descendantTopics) { + foreach (var reference in descendantTopic.References) { + if (reference.Value is not null && !descendantTopics.Contains(reference.Value)) { + descendantTopic.References.Remove(reference.Key); + } + } + } + /*------------------------------------------------------------------------------------------------------------------------ | Remove incoming relationships \-----------------------------------------------------------------------------------------------------------------------*/ @@ -606,7 +617,12 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi foreach (var relationship in descendantTopic.IncomingRelationships) { foreach (var relatedTopic in relationship.Values.ToList()) { if (!descendantTopics.Contains(relatedTopic)) { - relatedTopic.Relationships.RemoveTopic(relationship.Key, descendantTopic); + if (relatedTopic.Relationships.Contains(relationship.Key, descendantTopic)) { + relatedTopic.Relationships.RemoveTopic(relationship.Key, descendantTopic); + } + else if (relatedTopic.References.Contains(relationship.Key)) { + relatedTopic.References.Remove(relationship.Key); + } } } } From 80c901767987eaf6a04611d988171bf20a3b442e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sat, 6 Feb 2021 16:31:32 -0800 Subject: [PATCH 585/778] Bypass `ModelType.Reference` as part of `GetUnmatchedAttributes()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, `GetUnmatchedAttributes()` ignored `AttributeDescriptor`s on the source `ContentTypeDescriptor` with a `ModelType` of `Relationship` or `NestedTopic` since those aren't actually stored as attributes. This failed to include `Reference` types, however. As a result, this meant that any topics with a topic reference attribute would always pass those topic references as null attributes to ensure they were deleted. Technically, this doesn't hurt anything—but it's also entirely unnecessary. To mitigate this, the `ModelType.Reference` is included as part of this condition. This breaks a unit test. This isn't because the underlying functionality stopped working, but because the content type being evaluated—`ContentTypeDescriptor`—only had one actual attribute—`Title`—and that was deliberately being set in the unit test. Previously, this failed to be recognized, however, because `ContentTypeDescriptor` had a `BaseTopic` reference. With the exclusion of `ModelType.Reference`, that was no longer being returned. This is easily fixed by updating the unit test to evaluate a `Page` content type instead, which has additional attributes defined. --- OnTopic.Tests/TopicRepositoryBaseTest.cs | 2 +- OnTopic/Repositories/TopicRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 44ccc13e..2b3dc731 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -392,7 +392,7 @@ public void GetAttributes_ArbitraryAttributeWithLongValue_ReturnsAsExtendedAttri [TestMethod] public void GetUnmatchedAttributes_ReturnsAttributes() { - var topic = TopicFactory.Create("Test", "ContentTypeDescriptor", 1); + var topic = TopicFactory.Create("Test", "Page", 1); topic.Attributes.SetValue("Title", "Title"); diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 9d931283..ac9efc2e 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -810,8 +810,8 @@ protected IEnumerable GetUnmatchedAttributes(Topic topic) { continue; }; - if (attribute.ModelType is ModelType.Relationship or ModelType.NestedTopic) { // Ignore associations + if (attribute.ModelType is ModelType.Relationship or ModelType.NestedTopic or ModelType.Reference) { continue; } From 4bf2642246079a2f1f3c3b093532a59513c3e433 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Sun, 7 Feb 2021 13:04:22 -0800 Subject: [PATCH 586/778] Updated documentation with new syntax Updated documentation to refer to e.g. `[Include(AssociationTypes)]` and `[Collection(CollectionType)]` instead of the legacy `[Follow(Relationships)]` and `[Relationships(RelationshipType)]`. --- OnTopic.ViewModels/README.md | 3 ++- OnTopic/Mapping/README.md | 38 +++++++++++++++---------------- OnTopic/Mapping/Reverse/README.md | 2 +- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md index 69277a62..56c2b476 100644 --- a/OnTopic.ViewModels/README.md +++ b/OnTopic.ViewModels/README.md @@ -42,6 +42,7 @@ Installation can be performed by providing a ` to the `OnTo - [`ContentItemTopicViewModel`](Items/ContentItemTopicViewModel.cs) - [`LookupListItemTopicViewModel`](Items/LookupListItemTopicViewModel.cs) - [`SlideTopicViewModel`](Items/SlideTopicViewModel.cs) +- [`AssociatedTopicBindingModel`](BindingModels/AssociatedTopicBindingModel.cs) - [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) - [`TopicViewModelCollection<>`](Collections/TopicViewModelCollection.cs) @@ -54,7 +55,7 @@ For applications with a large number of view models, it may be preferable to use > *Note:* If a base class is overwritten then topics that derive from the original version will continue to do so unless they are _also_ overwritten. For example, if a `Theme` property is added to a customer-specific `PageTopicViewModel`, the `Theme` property won't be available on e.g. `SlideShowTopicViewModel` unless it is _also_ overwritten by the customer to inherit from their `PageTopicViewModel`. ## Design Considerations -As view models, not all attributes and relationships are exposed. The properties chosen are optimized around values that are expected to be of common interest to most views. +As view models, not all attributes and associations are exposed. The properties chosen are optimized around values that are expected to be of common interest to most views. ### Default Constructor All of the view models assume a default constructor (e.g., `new TopicViewModel()`). This is necessary to provide compatibility with the `TopicMappingService` which will attempt to create new instances of view models based on the default constructor. diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 120041dc..9f45806c 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -1,5 +1,5 @@ # Topic Mapping Service -The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract interface for a service that maps `Topic` entities to any arbitrary data transfer object. It is intended primarily to aid in mapping the `Topic` entity to view model instances, such as the ones provided in the [`ViewModels` assembly](../../Ignia.Topics.ViewModels) +The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract interface for a service that maps `Topic` entities to any arbitrary data transfer object. It is intended primarily to aid in mapping the `Topic` entity to view model instances, such as the ones provided in the [`ViewModels` assembly](../../OnTopic.ViewModels) ### Contents - [`TopicMappingService`](#topicmappingservice) @@ -64,7 +64,7 @@ Would be mapped to an `AuthorTopicViewModel` if `topic.Attributes.GetValue("Auth #### Parent If a property is named `Parent`, then the `TopicMappingService` will pull the value from `topic.Parent`. This acts as a special version of a [Topic Reference](#references). -> *Note:* By default, collections and reference properties of related topics will not be pulled. For instance, if a `TopicViewModel` has a `Children` collection, then the relationships, nested topics, and children of those instances will not be populated. This is meant to constrain the size of the object graph delivered. +> *Note:* By default, associations to other topics stored in collections or reference properties of associated topics will not be pulled. For instance, if a `TopicViewModel` has a `Children` collection, then the relationships, references, nested topics, and children of those instances will not be populated. This is meant to constrain the size of the object graph delivered. ### Example The following is an example of a data transfer object (specifically, a view model) that `TopicMappingService` might consume: @@ -85,7 +85,7 @@ In this example, the properties would map to: - `CustomPropertyB`: An `int` attribute named `CustomPropertyB` or a method named `GetCustomPropertyB()`. - `LastModified`: A `DateTime` attribute named `LastModified` or a method named `GetLastModified()`. - `Children`: The `Children` collection, with each child mapped to a `TopicViewModel`. -- `Cousins`: A relationship or nested topic set named `Cousins`, with each relationship mapped to a `TopicViewModel`. +- `Cousins`: A relationship or nested topic set named `Cousins`, with each association mapped to a `TopicViewModel`. - `NestedTopics`: A relationship or nested topic set named `NestedTopics` (the name doesn't have any special meaning). ## Attributes @@ -101,8 +101,8 @@ To support the mapping, a variety of `Attribute` classes are provided for decora - E.g., If child objects have a `TopicLookup` referencing the `Countries` metadata collection. - **`[AttributeKey(key)]`**: Instructs the `TopicMappingService` to use the specified `key` instead of the property name when calling `topic.Attributes.GetValue()`. - **`[FilterByAttribute(key, value)]`**: Ensures that all items in a collection have an attribute named "Key" with a value of "Value"; all else will be excluded. Multiple instances can be stacked. -- **`[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. -- **`[Follow(relationships)]`**: Instructs the code to populate the specified relationships on any view models within a collection. +- **`[Collection(key, type)]`**: For a collection, optionally specifies the name of the key to look for, instead of the property name, and the collection type, in case the key name is ambiguous. +- **`[Include(associationTypes)]`**: Instructs the code to populate the specified associations on any view models within a collection. - **`[Flatten]`**: Includes all descendants for every item in the collection. If the collection enforces uniqueness, duplicates will be removed. - **`[MapToParent]`**: Allows the attributes of a topic to be applied to a child complex object, optionally including a prefix. - **`[DisableMapping]`**: Prevents the mapping service from attempting to map the property to an attribute. @@ -123,14 +123,14 @@ public class CompanyTopicViewModel { [Metadata("Countries")] public TopicViewModelCollection Countries { get; set; } - [Relationship("Companies", Type=RelationshipType.IncomingRelationship)] - [Follow(Relationships.Children)] + [Collection("Companies", Type=CollectionType.IncomingRelationship)] + [Include(AssociationTypes.Children)] public TopicViewModelCollection CaseStudies { get; set; } - [Follow(Relationships.Relationships)] + [Include(AssociationTypes.Relationships)] public TopicViewModelCollection Children { get; set; } - [Relationship("Employees", Type=RelationshipType.NestedTopics)] + [Collection("Employees", Type=CollectionType.NestedTopics)] [FilterByAttribute("IsActive", "1")] [FilterByAttribute("Role", "Account Manager")] [Flatten] @@ -143,17 +143,17 @@ In this example, the properties would map to: - `CompanyName`: Would default to "Ignia" if not otherwise set; would throw an error if the value exceeded 100 characters. - `HideFromDirectory`: An attribute named `IsHidden` or a method named `GetIsHidden()`. If null, will look in `Parent` topics. - `Countries`: Loads all `LookupListItem` instances in the `Root:Configuration:Metadata:Countries` metadata collection. -- `CaseStudies`: A collection of `CaseStudy` topics pointing to the current `Company` via a "Companies" relationship. Will load the children of each case study. -- `Children`: A collection of child topics, with all relationships (but not e.g. grandchildren) loaded. +- `CaseStudies`: A collection of `CaseStudy` topics pointing to the current `Company` via a "Companies" association. Will load the children of each case study. +- `Children`: A collection of child topics, with all associations (but not e.g. grandchildren) loaded. - `Contacts`: A list of `Employee` nested topics, filtered by those with `IsActive` set to `1` (`true`) and `Role` set to "Account Manager". Includes any descendants of the nested topics that meet the previous criteria. -> *Note*: Often times, data transfer objects won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional help. For instance, the `[Relationship(…)]` attribute is useful if the relationship key is ambiguous between outgoing relationships and incoming relationships. +> *Note*: Often times, data transfer objects won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional help. For instance, the `[Collection(…)]` attribute is useful if the collection key is ambiguous between outgoing relationships and incoming relationships. ## Polymorphism If a reference type (e.g., `TopicViewModel Parent`) or a strongly-typed collection property (e.g., `List`) are defined, then any target instances must be assignable by the base type (in these cases, `TopicViewModel`). If they cannot be, then they will not be included; no error will occur. ### Filtering -This can be useful for filtering a collection. For instance, if a `CompanyTopicViewModel` includes an `Employees` collection of type `List` then it will only be populated by topics that can be mapped to either `ManagerTopicViewModel` or a derivative (perhaps, `ExecutiveTopicViewModel`). Other types (e.g., `EmployeeTopicViewModel`) will be excluded, even though they might otherwise be referenced by the `Employees` relationship. +This can be useful for filtering a collection. For instance, if a `CompanyTopicViewModel` includes an `Employees` collection of type `List` then it will only be populated by topics that can be mapped to either `ManagerTopicViewModel` or a derivative (perhaps, `ExecutiveTopicViewModel`). Other types (e.g., `EmployeeTopicViewModel`) will be excluded, even though they might otherwise be referenced by the `Employees` collection. > *Note:* For this reason, it is recommended that view models use inheritance based on the content type hierarchy. This provides an intuitive mapping to content type definitions, avoids needing to redefine base properties, and allows for polymorphism in assigning derived types. @@ -164,13 +164,13 @@ While it's not a best practice, this also works for strongly-typed collections o By default, the `TopicMappingService` will cache a reference to all `MemberInfo` objects associated with each of view model it maps. That mitigates much of the performance hit associated with the use of reflection. Despite that, simply setting properties—and, especially, on large object graphs—can require a lot of processing time. To address this, OnTopic also offers two approaches. ### Internal Caching -When a request is made to `TopicMappingService`, and internal cache is constructed. If any mapping requests refer to a `Topic` that's already been mapped as part of the _current_ object graph, then that object will be returned. This prevents unnecessary duplication of mapping, and also avoids the potential for infinite loops. For instance, if a view model includes `Children`, and those children are set to `[Follow(Relationships.Parents)]`, the `TopicMappingService` will point back to the originally-mapped `Parent` object, instead of mapping a new instance of that `Topic`. +When a request is made to `TopicMappingService`, and internal cache is constructed. If any mapping requests refer to a `Topic` that's already been mapped as part of the _current_ object graph, then that object will be returned. This prevents unnecessary duplication of mapping, and also avoids the potential for infinite loops. For instance, if a view model includes `Children`, and those children are set to `[Include(AssociationTypes.Parents)]`, the `TopicMappingService` will point back to the originally-mapped `Parent` object, instead of mapping a new instance of that `Topic`. ### `CachedTopicMappingService` -The [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which accepts a concrete implementation of an `ITopicMappingService`, provides caching across requests based on `topic.Id`, `Type`, and `Relationships`. Because the cache is based on all three of these, it will differentiate between the results of e.g., -- `topicMappingService.Map(topic, Relationships.All)` -- `topicMappingService.Map(topic, Relationships.Children)` -- `topicMappingService.Map(topic, Relationships.Children)` +The [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which accepts a concrete implementation of an `ITopicMappingService`, provides caching across requests based on `topic.Id`, `Type`, and `AssociationTypes`. Because the cache is based on all three of these, it will differentiate between the results of e.g., +- `topicMappingService.Map(topic, AssociationTypes.All)` +- `topicMappingService.Map(topic, AssociationTypes.Children)` +- `topicMappingService.Map(topic, AssociationTypes.Children)` To implement the caching decorator, use the following construction as a Singleton lifestyle in your composer: ``` @@ -185,7 +185,7 @@ var cachedTopicMappingService = new CachedTopicMappingService(topicMappingServic #### Limitations While the `CachedTopicMappingService` can be useful for particular scenarios, it introduces several limitations that should be accounted for. -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. +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. associations in multiple graphs. 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. 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. diff --git a/OnTopic/Mapping/Reverse/README.md b/OnTopic/Mapping/Reverse/README.md index 03f78806..53e0cb0d 100644 --- a/OnTopic/Mapping/Reverse/README.md +++ b/OnTopic/Mapping/Reverse/README.md @@ -8,7 +8,7 @@ The [`IReverseTopicMappingService`](IReverseTopicMappingService.cs) and its conc - [Complex Models](#complex-models) ## Interfaces -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. +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 associations, it expects implementation of the `IAssociatedTopicBindingModel`, which has a single `UniqueKey` property. ## Model Validation 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. From 85c386fdb10e4723951aeb8f78a7413da5e60a1b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 12:52:57 -0800 Subject: [PATCH 587/778] Prevent `MarkClean()` or `IsClean()` if `Topic.IsNew` A `Topic` instance that hasn't been saved can _only_ be dirty, by definition. As such, any efforts to mark a collection associated with a new `Topic` as clean, or to modify the collection with an `!markDirty` parameter should not be honored. There's no need to check this condition from `IsDirty`; instead, we can just make sure it's checked during any entry points, such as e.g. `MarkClean()`, `SetValue()` or `SetTopic()`, `InsertItem()`, and/or `SetItem()`. --- .../Associations/TopicRelationshipMultiMap.cs | 16 +++++++-- ...ckedCollection{TItem,TValue,TAttribute}.cs | 33 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index d8c92159..4724be2a 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -187,7 +187,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo var wasDirty = _dirtyKeys.IsDirty(relationshipKey); if (!topics.Contains(topic)) { _storage.Add(relationshipKey, topic); - if (markDirty.HasValue && !markDirty.Value && !wasDirty) { + if (!_parent.IsNew && markDirty.HasValue && !markDirty.Value && !wasDirty) { MarkClean(relationshipKey); } else { @@ -245,10 +245,20 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ /// - public void MarkClean() => _dirtyKeys.MarkClean(); + public void MarkClean() { + if (_parent.IsNew) { + return; + } + _dirtyKeys.MarkClean(); + } /// - public void MarkClean(string key) => _dirtyKeys.MarkClean(key); + public void MarkClean(string key) { + if (_parent.IsNew) { + return; + } + _dirtyKeys.MarkClean(key); + } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index 2456accf..8c2d0d44 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -146,6 +146,9 @@ public bool IsDirty(string key) { /// cref="Topic.VersionHistory"/>. /// public void MarkClean(DateTime? version) { + if (AssociatedTopic.IsNew) { + return; + } foreach (var trackedItem in Items.Where(a => a.IsDirty).ToArray()) { SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); } @@ -169,7 +172,10 @@ public void MarkClean(DateTime? version) { /// cref="Topic.VersionHistory"/>. /// public void MarkClean(string key, DateTime? version) { - if (Contains(key)) { + if (AssociatedTopic.IsNew) { + return; + } + else if (Contains(key)) { var trackedItem = this[key]; if (trackedItem.IsDirty) { SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); @@ -444,7 +450,10 @@ internal void SetValue( \-----------------------------------------------------------------------------------------------------------------------*/ else if (originalItem is not null) { var markAsDirty = originalItem.IsDirty; - if (markDirty.HasValue) { + if (AssociatedTopic.IsNew) { + markAsDirty = true; + } + else if (markDirty.HasValue) { markAsDirty = markDirty.Value; } else if (originalItem.Value != value) { @@ -474,7 +483,7 @@ internal void SetValue( updatedItem = new TItem() { Key = key, Value = value, - IsDirty = markDirty ?? true, + IsDirty = AssociatedTopic.IsNew || (markDirty ?? true), LastModified = version?? DateTime.UtcNow }; } @@ -534,6 +543,11 @@ internal void SetValue( /// protected override void InsertItem(int index, TItem item) { Contract.Requires(item, nameof(item)); + if (AssociatedTopic.IsNew && !item.IsDirty) { + item = item with { + IsDirty = true + }; + } if (_topicPropertyDispatcher.Enforce(item.Key, item)) { if (!Contains(item.Key)) { base.InsertItem(index, item); @@ -567,6 +581,11 @@ protected override void InsertItem(int index, TItem item) { /// The object which is being inserted. protected override void SetItem(int index, TItem item) { Contract.Requires(item, nameof(item)); + if (AssociatedTopic.IsNew && !item.IsDirty) { + item = item with { + IsDirty = true + }; + } if (_topicPropertyDispatcher.Enforce(item.Key, item)) { base.SetItem(index, item); if (DeletedItems.Contains(item.Key)) { @@ -588,7 +607,9 @@ protected override void SetItem(int index, TItem item) { /// protected override void RemoveItem(int index) { var trackedItem = this[index]; - DeletedItems.Add(trackedItem.Key); + if (!AssociatedTopic.IsNew) { + DeletedItems.Add(trackedItem.Key); + } base.RemoveItem(index); } @@ -604,7 +625,9 @@ protected override void RemoveItem(int index) { /// cref="TrackedItem{T}"/>s are marked as . /// protected override void ClearItems() { - DeletedItems.AddRange(Items.Select(a => a.Key)); + if (!AssociatedTopic.IsNew) { + DeletedItems.AddRange(Items.Select(a => a.Key)); + } base.ClearItems(); } From ee048f7aed1f75c8c007fafbeef8135b63fc3dbb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 12:59:24 -0800 Subject: [PATCH 588/778] Update `IsDirty` unit tests to operate off of "saved" topics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the previous update, I prevented attributes, references, and relationships from being marked as clean if the topic was new, since a new topic is, by definition, dirty. (The one exception to this being the removal of an item.) This means that the unit tests for evaluating `IsDirty` needed to be updated to be "saved" entities—i.e., topics with an `Id`. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 12 ++++++------ OnTopic.Tests/TopicReferenceCollectionTest.cs | 4 ++-- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 4 ++-- OnTopic.Tests/TopicTest.cs | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 560535c0..05430ca6 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -302,7 +302,7 @@ public void SetValue_ValueChanged_IsDirty() { [TestMethod] public void Clear_ExistingValues_IsDirty() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); topic.Attributes.SetValue("Foo", "Bar", false); @@ -323,7 +323,7 @@ public void Clear_ExistingValues_IsDirty() { [TestMethod] public void SetValue_ValueUnchanged_IsNotDirty() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); topic.Attributes.SetValue("Fah", "Bar", false); topic.Attributes.SetValue("Fah", "Bar"); @@ -361,7 +361,7 @@ public void IsDirty_DirtyValues_ReturnsTrue() { [TestMethod] public void IsDirty_DeletedValues_ReturnsTrue() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); topic.Attributes.SetValue("Foo", "Bar"); topic.Attributes.Remove("Foo"); @@ -421,7 +421,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { [TestMethod] public void IsDirty_MarkClean_UpdatesLastModified() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); var version = DateTime.Now.AddDays(5); topic.Attributes.SetValue("Baz", "Foo"); @@ -443,7 +443,7 @@ public void IsDirty_MarkClean_UpdatesLastModified() { [TestMethod] public void IsDirty_MarkClean_ReturnsFalse() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); topic.Attributes.SetValue("Foo", "Bar"); topic.Attributes.SetValue("Baz", "Foo"); @@ -467,7 +467,7 @@ public void IsDirty_MarkClean_ReturnsFalse() { [TestMethod] public void IsDirty_MarkAttributeClean_ReturnsFalse() { - var topic = TopicFactory.Create("Test", "Container"); + var topic = TopicFactory.Create("Test", "Container", 1); topic.Attributes.SetValue("Foo", "Bar"); topic.Attributes.MarkClean("Foo"); diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 5786afb9..d02dded1 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -55,8 +55,8 @@ public void Add_NewReference_IsDirty() { [TestMethod] public void SetValue_NewReference_NotDirty() { - var topic = TopicFactory.Create("Topic", "Page"); - var reference = TopicFactory.Create("Reference", "Page"); + var topic = TopicFactory.Create("Topic", "Page", 1); + var reference = TopicFactory.Create("Reference", "Page", 2); topic.References.SetValue("Reference", reference, false); diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index c8676252..159c8933 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -192,9 +192,9 @@ public void SetTopic_IsDirty() { [TestMethod] public void SetTopic_IsDuplicate_IsNotDirty() { - var topic = TopicFactory.Create("Test", "Page"); + var topic = TopicFactory.Create("Test", "Page", 1); var relationships = new TopicRelationshipMultiMap(topic); - var related = TopicFactory.Create("Topic", "Page"); + var related = TopicFactory.Create("Topic", "Page", 2); relationships.SetTopic("Related", related); relationships.MarkClean(); diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 7df5b628..56fbd485 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -397,8 +397,8 @@ public void IsDirty_ChangeCollections_ReturnsTrue() { [TestMethod] public void MarkClean_ChangeCollection_ResetIsDirty() { - var topic = TopicFactory.Create("Topic", "Page"); - var related = TopicFactory.Create("Related", "Page"); + var topic = TopicFactory.Create("Topic", "Page", 1); + var related = TopicFactory.Create("Related", "Page", 2); topic.Attributes.SetValue("Related", related.Key); topic.References.SetValue("Related", related); From 61fe810b98f4f6c121c0f4a25a013f282bfd2565 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:10:53 -0800 Subject: [PATCH 589/778] Implemented key-tracking for `Topic.IsDirty()` Previously, `Topic` implemented an incredibly basic `IsDirty()` tracking via a single `_isDirty` boolean field. This has been improved to use the `DirtyKeyCollection`, thus allowing it to more intelligently track _which_ fields are dirty, if any. This will allow for more granular checking of dirty status in future commits, if needed. --- OnTopic/Topic.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 4e879676..a51d89d4 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -34,7 +34,7 @@ public class Topic { private int _id = -1; private string? _originalKey; private Topic? _parent; - private bool _isDirty; + readonly DirtyKeyCollection _dirtyKeys = new(); /*========================================================================================================================== | CONSTRUCTOR @@ -189,7 +189,7 @@ public string ContentType { return; } else if (_contentType is not null || IsNew) { - _isDirty = true; + _dirtyKeys.MarkDirty("ContentType"); } _contentType = value; } @@ -221,7 +221,7 @@ public string Key { return; } else if (_key is not null || IsNew) { - _isDirty = true; + _dirtyKeys.MarkDirty("Key"); } if (_originalKey is null) { _originalKey = _key; @@ -571,16 +571,17 @@ public string GetWebPath() { /// modified. /// public bool IsDirty(bool checkCollections = false, bool excludeLastModified = false) { - if (!_isDirty && checkCollections) { - _isDirty = Attributes.IsDirty(excludeLastModified); + var isDirty = _dirtyKeys.IsDirty(); + if (isDirty && checkCollections) { + isDirty = Attributes.IsDirty(excludeLastModified); } - if (!_isDirty && checkCollections) { - _isDirty = Relationships.IsDirty(); + if (isDirty && checkCollections) { + isDirty = Relationships.IsDirty(); } - if (!_isDirty && checkCollections) { - _isDirty = References.IsDirty(); + if (!isDirty && checkCollections) { + isDirty = References.IsDirty(); } - return _isDirty; + return isDirty; } /*========================================================================================================================== @@ -598,7 +599,7 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa /// VersionHistory"/>. /// public void MarkClean(bool includeCollections = false, DateTime? version = null) { - _isDirty = false; + _dirtyKeys.MarkClean(); if (includeCollections) { Attributes.MarkClean(version); Relationships.MarkClean(); From e6d15abf2ae1c671d37b4f7aebb3f5ac2395f898 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:16:05 -0800 Subject: [PATCH 590/778] Applied `ITrackDirtyKeys` to `Topic` To help enforce consistency between classes, the `ITrackDirtyKeys` interface has been applied to `Topic`, thus ensuring it can be interacted with in the same ways as other classes that provide tracking of dirty keys, such as `TopicReferenceCollection`, `AttributeValueCollection`, and `TopicRelationshipMultiMap`. This required some juggling of the overloads to ensure that the contract was adhered to while still supporting extended scenarios not covered by the interface. --- OnTopic/Topic.cs | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index a51d89d4..3c1fa8c9 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -24,7 +24,7 @@ namespace OnTopic { /// The Topic object is a simple container for a particular node in the topic hierarchy. It contains the metadata associated /// with the particular node, a list of children, etc. /// - public class Topic { + public class Topic: ITrackDirtyKeys { /*========================================================================================================================== | PRIVATE VARIABLES @@ -555,6 +555,10 @@ public string GetWebPath() { /*========================================================================================================================== | METHOD: IS DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public bool IsDirty() => IsDirty(false, false); + /// /// Determines if the topic is dirty, optionally checking and . /// @@ -570,7 +574,7 @@ public string GetWebPath() { /// Returns true if the , , or, optionally, any collections have been /// modified. /// - public bool IsDirty(bool checkCollections = false, bool excludeLastModified = false) { + public bool IsDirty(bool checkCollections, bool excludeLastModified = false) { var isDirty = _dirtyKeys.IsDirty(); if (isDirty && checkCollections) { isDirty = Attributes.IsDirty(excludeLastModified); @@ -584,9 +588,32 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa return isDirty; } + /// + public bool IsDirty(string key) => IsDirty(key, false); + + + /// + public bool IsDirty(string key, bool checkCollections) { + var isDirty = _dirtyKeys.IsDirty(key); + if (isDirty && checkCollections) { + isDirty = Attributes.IsDirty(key); + } + if (isDirty && checkCollections) { + isDirty = Relationships.IsDirty(key); + } + if (!isDirty && checkCollections) { + isDirty = References.IsDirty(key); + } + return isDirty; + } + /*========================================================================================================================== | METHOD: MARK CLEAN \-------------------------------------------------------------------------------------------------------------------------*/ + + /// + public void MarkClean() => MarkClean(false); + /// /// Resets the status of the —and, optionally, that of all collections, using the /// parameter. @@ -598,7 +625,7 @@ public bool IsDirty(bool checkCollections = false, bool excludeLastModified = fa /// The value that the attributes were last saved. This corresponds to the . /// - public void MarkClean(bool includeCollections = false, DateTime? version = null) { + public void MarkClean(bool includeCollections, DateTime? version = null) { _dirtyKeys.MarkClean(); if (includeCollections) { Attributes.MarkClean(version); @@ -607,6 +634,21 @@ public void MarkClean(bool includeCollections = false, DateTime? version = null) } } + /// + public void MarkClean(string key) { + MarkClean(key, false); + } + + /// + public void MarkClean(string key, bool includeCollections) { + _dirtyKeys.MarkClean(key); + if (includeCollections) { + Attributes.MarkClean(key); + Relationships.MarkClean(key); + References.MarkClean(key); + } + } + #endregion #region Relationship and Collection Properties From 4f0b50c29f43e1633c454a40af1045f58c51192c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:18:04 -0800 Subject: [PATCH 591/778] Tidied up implementation of `IsDirty()` --- OnTopic/Topic.cs | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 3c1fa8c9..95b835f0 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -575,36 +575,41 @@ public string GetWebPath() { /// modified. /// public bool IsDirty(bool checkCollections, bool excludeLastModified = false) { - var isDirty = _dirtyKeys.IsDirty(); - if (isDirty && checkCollections) { - isDirty = Attributes.IsDirty(excludeLastModified); + if (_dirtyKeys.IsDirty()) { + return true; } - if (isDirty && checkCollections) { - isDirty = Relationships.IsDirty(); + else if (!checkCollections) { + return false; } - if (!isDirty && checkCollections) { - isDirty = References.IsDirty(); + else if ( + Attributes.IsDirty(excludeLastModified) || + Relationships.IsDirty() || + References.IsDirty() + ) { + return true; } - return isDirty; + return false; } /// public bool IsDirty(string key) => IsDirty(key, false); - /// public bool IsDirty(string key, bool checkCollections) { - var isDirty = _dirtyKeys.IsDirty(key); - if (isDirty && checkCollections) { - isDirty = Attributes.IsDirty(key); + if (_dirtyKeys.IsDirty(key)) { + return true; } - if (isDirty && checkCollections) { - isDirty = Relationships.IsDirty(key); + else if (!checkCollections) { + return false; } - if (!isDirty && checkCollections) { - isDirty = References.IsDirty(key); + else if ( + Attributes.IsDirty(key) || + Relationships.IsDirty(key) || + References.IsDirty(key) + ) { + return true; } - return isDirty; + return false; } /*========================================================================================================================== From af53497c32a1a4d48068d7073b0e2ed492f9290c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:22:38 -0800 Subject: [PATCH 592/778] Added in `IsNew` handling for `Topic.IsDirty()`, `MarkClean()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As with the collections (85c386f), a `Topic` that `IsNew` is, by definition, dirty—and, therefore, should not be permitted to be marked clean via e.g. `MarkClean()`. Technically, there isn't any need to check `IsNew` on `IsDirty()`, since the topic cannot be `MarkClean()'ed if it `IsNew`. That said, this is a bit faster than doing a lookup against the `DirtyKeyCollection` if it's guaranteed to be dirty. Note that this shortcut _only_ applies to `Topic`, and not its collections, since a new `Topic` _will_ have dirty attributes, but a `Topic`'s collections may be empty—and, thus, clean. --- OnTopic/Topic.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 95b835f0..4d66eeec 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -575,7 +575,7 @@ public string GetWebPath() { /// modified. /// public bool IsDirty(bool checkCollections, bool excludeLastModified = false) { - if (_dirtyKeys.IsDirty()) { + if (IsNew || _dirtyKeys.IsDirty()) { return true; } else if (!checkCollections) { @@ -596,7 +596,7 @@ public bool IsDirty(bool checkCollections, bool excludeLastModified = false) { /// public bool IsDirty(string key, bool checkCollections) { - if (_dirtyKeys.IsDirty(key)) { + if (IsNew || _dirtyKeys.IsDirty(key)) { return true; } else if (!checkCollections) { @@ -631,6 +631,9 @@ public bool IsDirty(string key, bool checkCollections) { /// VersionHistory"/>. /// public void MarkClean(bool includeCollections, DateTime? version = null) { + if (IsNew) { + return; + } _dirtyKeys.MarkClean(); if (includeCollections) { Attributes.MarkClean(version); @@ -641,11 +644,17 @@ public void MarkClean(bool includeCollections, DateTime? version = null) { /// public void MarkClean(string key) { + if (IsNew) { + return; + } MarkClean(key, false); } /// public void MarkClean(string key, bool includeCollections) { + if (IsNew) { + return; + } _dirtyKeys.MarkClean(key); if (includeCollections) { Attributes.MarkClean(key); From ebd6d3c6df91d284afc561fca0179cc3be5fce57 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:26:19 -0800 Subject: [PATCH 593/778] Mark `Parent` as dirty during update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, there was no tracking of `IsDirty` for resetting the `Parent`. This was intentional, since changes to the parent are handled separated from changes to other properties—i.e., via `ITopicRepository.Move()`, not `ITopicRepository.Save()`—and, as such, we didn't want _moving_ a `Topic` to mark it as needing to be _saved_. This prevented `Save()`, however, from detecting if a `Topic` needed to _also_ be `Move()`d as part of the operation, a feature we've long supported. This is, admittedly, a rare use case, but can come in handy when programmatically working with the topic graph—e.g., when making multiple changes, then doing a recursive `Save()`. The actual implementation of this tracking will be implemented in a subsequent commit. --- OnTopic/Topic.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 4d66eeec..ac4d4050 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -485,6 +485,7 @@ public void SetParent(Topic parent, Topic? sibling = null) { } var insertAt = (sibling is not null)? parent.Children.IndexOf(sibling)+1 : 0; parent.Children.Insert(insertAt, this); + _dirtyKeys.MarkDirty("Parent"); /*------------------------------------------------------------------------------------------------------------------------ | Set parent values @@ -594,6 +595,7 @@ public bool IsDirty(bool checkCollections, bool excludeLastModified = false) { /// public bool IsDirty(string key) => IsDirty(key, false); + /// public bool IsDirty(string key, bool checkCollections) { if (IsNew || _dirtyKeys.IsDirty(key)) { @@ -620,8 +622,8 @@ public bool IsDirty(string key, bool checkCollections) { public void MarkClean() => MarkClean(false); /// - /// Resets the status of the —and, optionally, that of all collections, using the - /// parameter. + /// Resets the status of the —and, optionally, that of all collections, using + /// the parameter. /// /// /// Determines if , , and should be included. From f5ba7a8808c4a3d07ca2eed5645b061adaadf76e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 13:37:35 -0800 Subject: [PATCH 594/778] Implement `Move()` on `Save()` if `Parent` has changed With the newly (re)implemented change tracking of `Parent` (ebd6d3c), the `TopicRepository` is able to fix the implementation of committing a `Move()` on `Save()` if the `Parent` has been modified. This is a rare scenario, as typically a `Topic` will be moved via `ITopicRepository.Move()` and _then_ saved via `ITopicRepository.Save()`. In programmatic updates to the graph, however, it's conceivable that the parent will be modified _without_ calling `Move()`, in which case that update should be committed as part of `Save()`. This is a logical assumption from the implementations perspective, and thus helps ensure the integrity of the graph. --- OnTopic/Repositories/TopicRepository.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index ac9efc2e..502f6cfa 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -368,7 +368,7 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso /*---------------------------------------------------------------------------------------------------------------------- | Perform reordering and/or move \---------------------------------------------------------------------------------------------------------------------*/ - if (topic.Parent is not null && topic.Attributes.IsDirty("ParentId") && !topic.IsNew) { + if (topic.Parent is not null && !topic.IsNew && topic.IsDirty("Parent")) { var topicIndex = topic.Parent.Children.IndexOf(topic); if (topicIndex > 0) { Move(topic, topic.Parent, topic.Parent.Children[topicIndex - 1]); @@ -425,7 +425,7 @@ _contentTypeDescriptors is not null && | Recurse over children \-----------------------------------------------------------------------------------------------------------------------*/ if (isRecursive) { - foreach (var childTopic in topic.Children) { + foreach (var childTopic in topic.Children.ToList()) { Save(childTopic, isRecursive, unresolvedTopics, version); } } @@ -501,6 +501,13 @@ topic.Parent is not null && topic.SetParent(target, sibling); } + /*------------------------------------------------------------------------------------------------------------------------ + | Mark clean + >------------------------------------------------------------------------------------------------------------------------- + | To prevent the move from being recommitted the next time the topic is saved, mark "Parent" as clean. + \-----------------------------------------------------------------------------------------------------------------------*/ + topic.MarkClean("Parent"); + /*------------------------------------------------------------------------------------------------------------------------ | If a content type descriptor is being moved to a new parent, refresh cache >------------------------------------------------------------------------------------------------------------------------- From b0787c644dfa120f6386a686275f8ba6aba4cfb4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 14:07:53 -0800 Subject: [PATCH 595/778] Prevent `MarkClean()` if `TValue` is a `Topic.IsNew` A `TItem` should not be allowed to be set to `!IsDirty` if the `TValue` is a `Topic` and `Topic.IsNew`. That's an indication that the item has not yet been persisted. To facilitate this, the `AllowClean()` helper method has been introduced. **Note:** Technically, this only needs to be enforced on `InsertItem()` and `SetItem()`. Setting a value is a potentially expensive operation, however, and especially if business logic needs to be enforced. As such, it makes sense to validate this as part of `MarkClean()` as well, to ensure that we're not calling `SetValue()` even though the operation will end up being skipped. --- ...ckedCollection{TItem,TValue,TAttribute}.cs | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index 8c2d0d44..d4989171 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -150,7 +150,9 @@ public void MarkClean(DateTime? version) { return; } foreach (var trackedItem in Items.Where(a => a.IsDirty).ToArray()) { - SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); + if (AllowClean(trackedItem)) { + SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); + } } DeletedItems.Clear(); } @@ -177,7 +179,7 @@ public void MarkClean(string key, DateTime? version) { } else if (Contains(key)) { var trackedItem = this[key]; - if (trackedItem.IsDirty) { + if (trackedItem.IsDirty && AllowClean(trackedItem)) { SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); } } @@ -543,7 +545,7 @@ internal void SetValue( /// protected override void InsertItem(int index, TItem item) { Contract.Requires(item, nameof(item)); - if (AssociatedTopic.IsNew && !item.IsDirty) { + if (!AllowClean(item)) { item = item with { IsDirty = true }; @@ -581,7 +583,7 @@ protected override void InsertItem(int index, TItem item) { /// The object which is being inserted. protected override void SetItem(int index, TItem item) { Contract.Requires(item, nameof(item)); - if (AssociatedTopic.IsNew && !item.IsDirty) { + if (!AllowClean(item)) { item = item with { IsDirty = true }; @@ -631,6 +633,30 @@ protected override void ClearItems() { base.ClearItems(); } + /*========================================================================================================================== + | METHOD: ALLOW CLEAN? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines if a is permitted to be marked as not . + /// + /// + /// If the is or the is and the is , then + /// should never be set to false. + /// + /// The object which is being inserted. + protected bool AllowClean(TItem item) { + Contract.Requires(item, nameof(item)); + var topic = item.Value as Topic; + if (topic is not null && topic.IsNew) { + return false; + } + if (AssociatedTopic.IsNew && !item.IsDirty) { + return false; + } + return true; + } + /*========================================================================================================================== | OVERRIDE: GET KEY FOR ITEM \-------------------------------------------------------------------------------------------------------------------------*/ From d551442bcc9fa1e1cd482e7dc1ae5862d3e25d83 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 14:17:44 -0800 Subject: [PATCH 596/778] Introduce `IEnumerable` extension methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `TopicCollectionExtension` methods provide some basic extension methods for evaluating all topics in an `IEnumerable` collection. These are implemented as extensions so that they don't need to be separately defined on e.g. `TopicCollection`, `KeyedTopicCollection`, &c. This allows these methods to be applied to all of those at once—assuming, of course, that `OnTopic.Querying` is in scope. To start with, two extension methods are introduced. One will determine if any of the topics `IsDirty`. The other will determine if any of the topics `IsNew`. These are pretty trivial shortcuts. We could implement this via a delegate as well—but, at that point, callers might as well simply call LINQ's `Any()` directly. --- OnTopic/Querying/TopicCollectionExtensions.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 OnTopic/Querying/TopicCollectionExtensions.cs diff --git a/OnTopic/Querying/TopicCollectionExtensions.cs b/OnTopic/Querying/TopicCollectionExtensions.cs new file mode 100644 index 00000000..75d1f99e --- /dev/null +++ b/OnTopic/Querying/TopicCollectionExtensions.cs @@ -0,0 +1,49 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.Generic; +using System.Linq; +using OnTopic.Collections; + +namespace OnTopic.Querying { + + /*============================================================================================================================ + | CLASS: TOPIC COLLECTION (EXTENSIONS) + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides extensions for querying , , and related + /// collections via the generic interface. + /// + public static class TopicCollectionExtensions { + + /*========================================================================================================================== + | METHOD: ANY DIRTY? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether any of the instances in the collection are marked as . + /// + /// + /// This does not determine if the collection itself is dirty—it only determines if any instances in + /// the collection are . This distinction is important. For example, if a clean is added to the collection, then the collection will be dirty—but + /// will be false. + /// + /// The collection of instances to operate against. + /// Returns true if any of the instances are . + public static bool AnyDirty(this IEnumerable topics) => topics.Any(t => t.IsDirty(true)); + + /*========================================================================================================================== + | METHOD: ANY NEW? + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Determines whether any of the instances in the collection are marked as . + /// + /// The collection of instances to operate against. + /// Returns true if any of the instances are . + public static bool AnyNew(this IEnumerable topics) => topics.Any(t => t.IsNew); + + } +} From 34a80cb5d7b0a4197e9746803fae63e62367662f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 14:20:48 -0800 Subject: [PATCH 597/778] Prevent `MarkClean()` if any related items are `Topic.IsNew` A relationship should not be permitted to be marked as clean if any of the related topics `IsNew`; that fact suggests that the relationship is not, in fact, clean (since not all items in it have been saved). This parallels the similar implementation on `TrackedCollection<>` (b0787c6), and helps make `MarkClean()` more intelligent. --- OnTopic/Associations/TopicRelationshipMultiMap.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index 4724be2a..9d259d04 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -6,6 +6,7 @@ using System; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; +using OnTopic.Querying; using OnTopic.Repositories; namespace OnTopic.Associations { @@ -249,7 +250,11 @@ public void MarkClean() { if (_parent.IsNew) { return; } - _dirtyKeys.MarkClean(); + foreach (var relationship in _storage) { + if (!relationship.Values.AnyNew()) { + _dirtyKeys.MarkClean(relationship.Key); + } + } } /// @@ -257,7 +262,9 @@ public void MarkClean(string key) { if (_parent.IsNew) { return; } - _dirtyKeys.MarkClean(key); + if (Contains(key) && !_storage[key].Values.AnyNew()) { + _dirtyKeys.MarkClean(key); + } } } //Class From 2dd8e03aa3b70548aa65369912378c18ba8a1df6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 14:33:26 -0800 Subject: [PATCH 598/778] Centralized `MarkClean()` logic for associations to `TopicRepository` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that `TopicReferenceCollection.MarkClean()` (b0787c6) and `TopicRelationshipCollection.MarkClean()` (34a80cb) will only mark their respective collections as clean if no items within them are `IsNew`, we can easily move the `MarkClean()` logic out of `SqlTopicRepository` and into `TopicRepository` without needing to recreate the logic for ensuring that, in fact, all references were saved. (Technically, this logic is still recreated—but it's now handled centrally as part of the individual collection's `MarkClean()` handling, thus preventing collections from being inadvertantly being marked as clean when not all references within them have been saved.) This further reduces the amount of business logic that individual `ITopicRepository` implementations need to worry about by centralizing it in the `TopicRepository` base class. This allows individual `ITopicRepository` implementations to focus exclusively on data persistence, while all of the complexities of enforcing business logic are handled by `TopicRepository`. While I was at it, I cleaned up the logic within `PersistRelations` and `PersistReferences` so that they don't necessitate creating variables that are only used once. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 36 +++++++------------------ OnTopic/Repositories/TopicRepository.cs | 10 +++++++ 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 2edd7f10..afb1ab75 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -4,15 +4,12 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using System.Collections.Generic; using System.Data; -using System.Data.SqlTypes; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; using Microsoft.Data.SqlClient; -using OnTopic.Collections; using OnTopic.Data.Sql.Models; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; @@ -498,8 +495,6 @@ bool persistRelationships PersistReferences(topic, version, connection); } - topic.Attributes.MarkClean(version); - } /*------------------------------------------------------------------------------------------------------------------------ @@ -641,21 +636,19 @@ private static void PersistRelations(Topic topic, DateTime version, SqlConnectio \---------------------------------------------------------------------------------------------------------------------*/ foreach (var key in topic.Relationships.Keys) { - var relatedTopics = topic.Relationships.GetTopics(key); - var topicId = topic.Id.ToString(CultureInfo.InvariantCulture); - var savedTopics = relatedTopics.Where(t => !t.IsNew).Select(m => m.Id); - using var targetIds = new TopicListDataTable(); using var command = new SqlCommand("UpdateRelationships", connection) { CommandType = CommandType.StoredProcedure }; - foreach (var targetTopicId in savedTopics) { - targetIds.AddRow(targetTopicId); + foreach (var targetTopic in topic.Relationships.GetTopics(key)) { + if (!targetTopic.IsNew) { + targetIds.AddRow(targetTopic.Id); + } } // Add Parameters - command.AddParameter("TopicID", topicId); + command.AddParameter("TopicID", topic.Id.ToString(CultureInfo.InvariantCulture)); command.AddParameter("RelationshipKey", key); command.AddParameter("RelatedTopics", targetIds); command.AddParameter("Version", version); @@ -663,11 +656,6 @@ private static void PersistRelations(Topic topic, DateTime version, SqlConnectio command.ExecuteNonQuery(); - //Reset isDirty, assuming there aren't any unresolved references - if (savedTopics.Count() == relatedTopics.Count) { - topic.Relationships.MarkClean(key); - } - } } @@ -704,29 +692,25 @@ private static void PersistReferences(Topic topic, DateTime version, SqlConnecti \-----------------------------------------------------------------------------------------------------------------------*/ try { - var topicId = topic.Id.ToString(CultureInfo.InvariantCulture); using var references = new TopicReferencesDataTable(); using var command = new SqlCommand("UpdateReferences", connection) { CommandType = CommandType.StoredProcedure }; - foreach (var relatedTopic in topic.References.Where(t => !t.Value?.IsNew?? false)) { - references.AddRow(relatedTopic.Key, relatedTopic.Value!.Id); + foreach (var relatedTopic in topic.References) { + if (!relatedTopic.Value?.IsNew?? false) { + references.AddRow(relatedTopic.Key, relatedTopic.Value!.Id); + } } // Add Parameters - command.AddParameter("TopicID", topicId); + command.AddParameter("TopicID", topic.Id.ToString(CultureInfo.InvariantCulture)); command.AddParameter("ReferencedTopics", references); command.AddParameter("Version", version); command.AddParameter("DeleteUnmatched", topic.References.IsFullyLoaded); command.ExecuteNonQuery(); - //Reset isDirty, assuming there aren't any unresolved references - if (references.Rows.Count == topic.References.Count) { - topic.References.MarkClean(); - } - } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 502f6cfa..80e47b85 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -358,6 +358,16 @@ private void Save([NotNull]Topic topic, bool isRecursive, TopicCollection unreso \-----------------------------------------------------------------------------------------------------------------------*/ SaveTopic(topic, version, !isRecursive || !unresolvedTopics.Contains(topic)); + /*------------------------------------------------------------------------------------------------------------------------ + | Mark as clean + \-----------------------------------------------------------------------------------------------------------------------*/ + topic.Attributes.MarkClean(version); + + if (!unresolvedTopics.Contains(topic)) { + topic.References.MarkClean(); + topic.Relationships.MarkClean(); + } + /*------------------------------------------------------------------------------------------------------------------------ | Update version history \-----------------------------------------------------------------------------------------------------------------------*/ From 14cdb659f3b9db6f72dbfc92bb1bae343831c4d5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 14:35:17 -0800 Subject: [PATCH 599/778] Use consistent `Relationships` nomenclature in `PersistRelationships()` Previously, the private `PersistRelations()` method broke with the convention of referring to relationships as "Relationships`. This was even inconsistent with the local `persistRelationships` variable used to determine if `PersistRelationships()` should be called. This makes the nomenclature more consistent. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index afb1ab75..85f59851 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -488,7 +488,7 @@ bool persistRelationships ); if (persistRelationships && areRelationshipsDirty) { - PersistRelations(topic, version, connection); + PersistRelationships(topic, version, connection); } if (persistRelationships && areReferencesDirty) { @@ -612,14 +612,14 @@ protected override sealed void DeleteTopic(Topic topic) { } /*========================================================================================================================== - | METHOD: PERSIST RELATIONS + | METHOD: PERSIST RELATIONSHIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Internal method that saves topic relationships to the n:n mapping table in SQL. /// /// The topic object whose relationships should be persisted. /// The SQL connection. - private static void PersistRelations(Topic topic, DateTime version, SqlConnection connection) { + private static void PersistRelationships(Topic topic, DateTime version, SqlConnection connection) { /*------------------------------------------------------------------------------------------------------------------------ | Return blank if the topic has no relations. From 1222bd0dfafd7c854989fd879af88f79a98a71f7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:04:48 -0800 Subject: [PATCH 600/778] Prevent `!markDirty` if target `Topic.IsNew` If a topic is being added to `Topic.Relationships` and that topic `IsNew`, then the caller should not be able to prevent the relationships from being marked as `!IsDirty()` since, by definition, the target topic could not have been saved. --- OnTopic/Associations/TopicRelationshipMultiMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index 9d259d04..bb2d5464 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -188,7 +188,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo var wasDirty = _dirtyKeys.IsDirty(relationshipKey); if (!topics.Contains(topic)) { _storage.Add(relationshipKey, topic); - if (!_parent.IsNew && markDirty.HasValue && !markDirty.Value && !wasDirty) { + if (!_parent.IsNew && !topic.IsNew && markDirty.HasValue && !markDirty.Value && !wasDirty) { MarkClean(relationshipKey); } else { From cbd49c1090fe894e1889df1bfce21f2b89beb459 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:08:27 -0800 Subject: [PATCH 601/778] Unit test: Prevent clean `TrackedItem` being added to `Topic.IsNew` If a `Topic` `IsNew` then any modifications to it should be marked as `IsDirty`, regardless of whether or not the caller requested that they be marked as such. This prevents scenarios where attributes are orphaned because they were marked as clean even though the topic hadn't yet been saved. This test evaluates the backdoor of inserting a new `AttributeValue` with `IsDirty` explicitly set to `false`. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 05430ca6..0635e9df 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -476,6 +476,31 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { } + /*========================================================================================================================== + | TEST: IS DIRTY: ADD CLEAN ATTRIBUTE TO NEW TOPIC: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Populates a associated with an + /// with a that is not marked as and then confirms that + /// returns true. + /// + [TestMethod] + public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { + + var topic = TopicFactory.Create("Test", "Container"); + + topic.Attributes.Add( + new() { + Key = "Foo", + Value = "Bar", + IsDirty = false + } + ); + + Assert.IsTrue(topic.Attributes.IsDirty()); + + } + /*========================================================================================================================== | TEST: SET VALUE: INVALID VALUE: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ From dc08f2e3b3d3e884d9f4162128db5116c723f9d1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:09:45 -0800 Subject: [PATCH 602/778] Unit test: Prevent `MarkClean()` on `Topic.IsNew` If a `Topic` `IsNew` then it should never be able to be marked as clean. This prevents scenarios where attributes are orphaned because they were marked as clean even though the topic hadn't yet been saved. This test evaluates an explicit call to `MarkClean()` even on a `Topic.IsNew`. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 0635e9df..386fa8a1 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -501,6 +501,27 @@ public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { } + /*========================================================================================================================== + | TEST: IS DIRTY: MARK NEW TOPIC AS CLEAN: RETURNS TRUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Populates a associated with an + /// with a and then confirms that returns true for that attribute after calling . + /// + [TestMethod] + public void IsDirty_MarkNewTopicAsClean_ReturnsTrue() { + + var topic = TopicFactory.Create("Test", "Container"); + + topic.Attributes.SetValue("Foo", "Bar"); + topic.Attributes.MarkClean(); + + Assert.IsTrue(topic.Attributes.IsDirty()); + + } + /*========================================================================================================================== | TEST: SET VALUE: INVALID VALUE: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ From 2cd0c5f0321c6f8aca4d1966e2fc8ff9f9de0f5f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:11:13 -0800 Subject: [PATCH 603/778] Unit test: Prevent `!markDirty` being set on `Topic.IsNew` If a `Topic` `IsNew` then any modifications to its relationships should be marked as `IsDirty`, regardless of whether or not the caller requested that they be marked as such. This prevents scenarios where relationships are orphaned because they were marked as clean even though the parent topic hadn't yet been saved. --- .../TopicRelationshipMultiMapTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 159c8933..8d443a6e 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -336,5 +336,27 @@ public void ClearTopics_NoTopics_IsNotDirty() { } + /*========================================================================================================================== + | TEST: SET TOPIC: NEW PARENT: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds an existing to a associated with a and confirms that returns true + /// even if is called with the + /// markDirty parameter set to false. + /// + [TestMethod] + public void SetTopic_NewParent_IsDirty() { + + var topic = TopicFactory.Create("Test", "Page"); + var relationships = new TopicRelationshipMultiMap(topic); + var related = TopicFactory.Create("Topic", "Page", 1); + + relationships.SetTopic("Related", related, false); + + Assert.IsTrue(relationships.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file From 249d049c6097992079d9ae7cd1df0157df43b030 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:12:48 -0800 Subject: [PATCH 604/778] Unit test: Prevent `!markDirty` being set with `Topic.IsNew` target If a new `Topic` is being added to a `Topic.Relationships`, then it should not be allowed to be marked as `!IsDirty`, regardless of whether or not the caller requested that. This prevents scenarios where relationships are orphaned because they were marked as clean even though the target topic hadn't yet been saved. --- .../TopicRelationshipMultiMapTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 8d443a6e..32c99904 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -358,5 +358,27 @@ public void SetTopic_NewParent_IsDirty() { } + /*========================================================================================================================== + | TEST: SET TOPIC: NEW TOPIC: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Adds a new to a associated with an existing and confirms that returns true even if is called with the markDirty parameter + /// set to false. + /// + [TestMethod] + public void SetTopic_NewTopic_IsDirty() { + + var topic = TopicFactory.Create("Test", "Page", 1); + var relationships = new TopicRelationshipMultiMap(topic); + var related = TopicFactory.Create("Topic", "Page"); + + relationships.SetTopic("Related", related, false); + + Assert.IsTrue(relationships.IsDirty()); + + } + } //Class } //Namespace \ No newline at end of file From 6c6a1b526bf68f2ddaacf90f88d4ddecdd8710d9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 15:19:36 -0800 Subject: [PATCH 605/778] Unit test: Prevent `!markDirty` being set with `Topic.IsNew` target If a new `Topic` is being added to a `Topic.References`, then it should not be allowed to be marked as `!IsDirty`, regardless of whether or not the caller requested that. This prevents scenarios where references are orphaned because they were marked as clean even though the target topic hadn't yet been saved. This is similar to a unit test on the `AttributeValueCollection` tests (dc08f2e), except that it evaluates whether the _target_ value `IsNew`, not the `AssociatedTopic`. --- OnTopic.Tests/TopicReferenceCollectionTest.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index d02dded1..150e9797 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -8,6 +8,7 @@ using OnTopic.Associations; using OnTopic.Tests.Entities; using OnTopic.Collections.Specialized; +using System.Collections.ObjectModel; namespace OnTopic.Tests { @@ -110,6 +111,27 @@ public void Clear_ExistingReferences_IsDirty() { } + /*========================================================================================================================== + | TEST: ADD: NEW TOPIC: IS DIRTY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Assembles a new and adds a new reference using with set to false + /// , confirming that remains true since + /// the target is unsaved. + /// + [TestMethod] + public void Add_NewTopic_IsDirty() { + + var topic = TopicFactory.Create("Topic", "Page", 1); + var reference = TopicFactory.Create("Reference", "Page"); + + topic.References.SetValue("Reference", reference, false); + + Assert.IsTrue(topic.References.IsDirty()); + + } + /*========================================================================================================================== | TEST: ADD: NEW REFERENCE: INCOMING RELATIONSHIP SET \-------------------------------------------------------------------------------------------------------------------------*/ From 64ae337cb81a8a738bc1c69fe1be2ac4a4427d6b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 16:59:55 -0800 Subject: [PATCH 606/778] Wrapped the `CreateTopic` stored procedure in a transaction While it's generally a best practice to wrap stored procedures in transactions, and especially when they involve multiple CUD operations, it's especially important when modifying the nested set since errors caused during these operations can result in corrupting the hierarchy. Given this, the `MoveTopic` and `DeleteTopic` stored procedures were already wrapped in transactions, but the `CreateTopic` stored procedure wasn't. This corrects that issue by wrapping it in similar transaction logic, along with `TRY`/`CATCH` blocks --- .../Stored Procedures/CreateTopic.sql | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 99b9f407..6900ea67 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -14,6 +14,37 @@ CREATE PROCEDURE [dbo].[CreateTopic] @Version DATETIME2(7) = NULL AS +-------------------------------------------------------------------------------------------------------------------------------- +-- DECLARE VARIABLES +-------------------------------------------------------------------------------------------------------------------------------- +DECLARE @IsNestedTransaction BIT; +DECLARE @TopicID INT; + +BEGIN TRY + +-------------------------------------------------------------------------------------------------------------------------------- +-- BEGIN TRANSACTION +-------------------------------------------------------------------------------------------------------------------------------- +-- ### NOTE JJC20210218: By necessity, this procedure potentially makes a massive number of changes to the Topics table's nested +-- set. During the execution, the nested set hierarchy WILL be in an inconsistent state. Read operations during that time are +-- very likely to be corrupted. As such, it's critical that the updates made as part of this procedure be isolated from other +-- reads being performed on the system. Further, we don't want any writes being made to the Topics table during this time—see +-- notes below regarding TABLOCK. By combining SERIALIZABLE with TABLOCK, we ensure that a) readers get a stable state, while b) +-- writers are prevented from concurrently modifying the table. Fortunately, these types of operations should be pretty +-- uncommon! The nested set model is very much optimized for read performance and presumes a relatively stable data set. +-------------------------------------------------------------------------------------------------------------------------------- +IF (@@TRANCOUNT = 0) + BEGIN + SET @IsNestedTransaction = 0; + BEGIN TRANSACTION; + END +ELSE + BEGIN + SET @IsNestedTransaction = 1; + END + +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE + -------------------------------------------------------------------------------------------------------------------------------- -- SET DEFAULT VERSION DATETIME -------------------------------------------------------------------------------------------------------------------------------- @@ -76,8 +107,6 @@ Values ( @Version ) -DECLARE @TopicID INT - SELECT @TopicID = SCOPE_IDENTITY() -------------------------------------------------------------------------------------------------------------------------------- @@ -128,6 +157,27 @@ IF @ReferenceCount > 0 1 END +-------------------------------------------------------------------------------------------------------------------------------- +-- COMMIT TRANSACTION +-------------------------------------------------------------------------------------------------------------------------------- +IF (@@TRANCOUNT > 0 AND @IsNestedTransaction = 0) + BEGIN + COMMIT + END +END TRY + +-------------------------------------------------------------------------------------------------------------------------------- +-- HANDLE ERRORS +-------------------------------------------------------------------------------------------------------------------------------- +BEGIN CATCH + IF (@@TRANCOUNT > 0 AND @IsNestedTransaction = 0) + BEGIN + ROLLBACK; + END; + THROW + RETURN; +END CATCH + -------------------------------------------------------------------------------------------------------------------------------- -- RETURN TOPIC ID -------------------------------------------------------------------------------------------------------------------------------- From 2da9974f0885b50adbe2785010dbcc4228ee2bcb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 8 Feb 2021 17:03:53 -0800 Subject: [PATCH 607/778] Implemented `TABLOCK` for the `CreateTopic` stored procedure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While we generally try to avoid aggressive locks such as `TABLOCK`, it becomes important when working with the nested set since minor operations—such as inserting a single record in this case—impact all records after the insert, due to their `RangeRight` and possibly `RangeLeft` being modified. Given that, we want to prevent any concurrent writes or even reads from occuring during this time, as otherwise they could end up with an inconsistent hierarchical state. Fortunately, writes to the hierarchy are generally pretty uncommon—and OnTopic is broadly optimized for read operations, right down to the decision to use a nested set hierarchy in the first place. --- .../Stored Procedures/CreateTopic.sql | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index 6900ea67..d963b075 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -60,10 +60,17 @@ SET @RangeRight = 0 -------------------------------------------------------------------------------------------------------------------------------- -- RESERVE SPACE FOR NEW CHILD. -------------------------------------------------------------------------------------------------------------------------------- +-- ### NOTE JJC20210218: We usually avoid broad hints like TABLOCK. That said, the create operation requires multiple operations +-- against the topics table which will fail if the topic range shifts. Locking the table helps ensure that data integrity issues +-- aren't introduced by concurrent modification of the nested set. Because this is being done within a SERIALIZABLE isolation +-- level, this lock will be maintained for the duration of the transaction. +-------------------------------------------------------------------------------------------------------------------------------- IF (@ParentID IS NOT NULL) BEGIN SELECT @RangeRight = RangeRight FROM Topics + WITH ( TABLOCK + ) WHERE TopicID = @ParentID UPDATE Topics @@ -85,6 +92,8 @@ ELSE BEGIN SELECT @RangeRight = MAX(RangeRight) + 1 FROM Topics + WITH ( TABLOCK + ) END -------------------------------------------------------------------------------------------------------------------------------- From 74d7cee7d00737801ee3ed0eb1538565bd43c08a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:11:43 -0800 Subject: [PATCH 608/778] Ensured (consistent) syntax highlighting hints Ensured all code blocks had a language hint, and that they consistently used `csharp` for C# code (whereas many previously used `c#`). --- OnTopic.AspNetCore.Mvc/README.md | 8 ++++---- OnTopic.Data.Caching/README.md | 2 +- OnTopic.Data.Sql/README.md | 2 +- OnTopic/Mapping/Hierarchical/README.md | 4 ++-- OnTopic/Mapping/README.md | 6 +++--- OnTopic/Mapping/Reverse/README.md | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md index 85ec6eee..34163c60 100644 --- a/OnTopic.AspNetCore.Mvc/README.md +++ b/OnTopic.AspNetCore.Mvc/README.md @@ -97,7 +97,7 @@ Installation can be performed by providing a ` to the `OnTo ### Application In the `Startup` class, OnTopic's ASP.NET Core support can be registered by calling the `AddTopicSupport()` extension method: -```c# +```csharp public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc().AddTopicSupport(); @@ -107,7 +107,7 @@ public class Startup { > *Note:* This will register the `TopicViewLocationExpander`, `TopicViewResultExecutor`, as well as all [Controllers](#controllers) that ship with `OnTopic.AspNetCore.Mvc`. In addition, within the same `ConfigureServices()` method, you will need to establish a class that implements `IControllerActivator` and `IViewComponentActivator`, and will represent the site's _Composition Root_ for dependency injection. This will typically look like: -```c# +```csharp var activator = new OrganizationNameControllerActivator(Configuration.GetConnectionString("OnTopic") services.AddSingleton(activator); services.AddSingleton(activator); @@ -120,7 +120,7 @@ See [Composition Root](#composition-root) below for information on creating an i ### Route Configuration When registering routes via `Startup.Configure()` you may register any routes for OnTopic using the extension method: -```c# +```csharp public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseEndpoints(endpoints => { @@ -135,7 +135,7 @@ public class Startup { ### Composition Root As OnTopic relies on constructor injection, the application must be configured in a **Composition Root**—in the case of ASP.NET Core, that means a custom controller activator for controllers, and view component activator for view components. For controllers, the basic structure of this might look like: -```c# +```csharp var sqlTopicRepository = new SqlTopicRepository(connectionString); var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository); var topicViewModelLookupService = new TopicViewModelLookupService(); diff --git a/OnTopic.Data.Caching/README.md b/OnTopic.Data.Caching/README.md index 2c6feddc..7d7a3c2b 100644 --- a/OnTopic.Data.Caching/README.md +++ b/OnTopic.Data.Caching/README.md @@ -26,7 +26,7 @@ Installation can be performed by providing a ` to the `OnTo > *Note:* This package is currently only available on Ignia's private **NuGet** repository. For access, please contact [Ignia](http://www.ignia.com/). ## Usage -```c# +```csharp var sqlTopicRepository = new SqlTopicRepository(connectionString); var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository); diff --git a/OnTopic.Data.Sql/README.md b/OnTopic.Data.Sql/README.md index cef68ae2..8149f333 100644 --- a/OnTopic.Data.Sql/README.md +++ b/OnTopic.Data.Sql/README.md @@ -24,7 +24,7 @@ Installation can be performed by providing a ` to the `OnTo > *Note:* This package is currently only available on Ignia's private **NuGet** repository. For access, please contact [Ignia](http://www.ignia.com/). ## Usage -```c# +```csharp var sqlTopicRepository = new SqlTopicRepository(connectionString); var rootTopic = sqlTopicRepository.Load(); ``` diff --git a/OnTopic/Mapping/Hierarchical/README.md b/OnTopic/Mapping/Hierarchical/README.md index be491e2e..705792e9 100644 --- a/OnTopic/Mapping/Hierarchical/README.md +++ b/OnTopic/Mapping/Hierarchical/README.md @@ -23,7 +23,7 @@ The [`CachedHierarchicalTopicMappingService`](CachedHierarchicalTopicMappingS ## Example The first code block demonstrates how to construct a new instance of a `IHierarchicalTopicMappingService`. In this case, it wraps the default `HierarchicalTopicMappingService` in a `CachedHierarchicalTopicMappingService` for caching, and maps children to the `NavigationTopicViewModel` class from the [`Ignia.Topics.ViewModels`](../../../Ignia.Topics.ViewModels/) project. Typically, this would be done in the _Composition Root_ of an application, with the service passed into e.g. a `Controller` as an `IHierarchicalTopicMappingService` dependency. -``` +```csharp var hierarchicalTopicMappingService = new CachedHierarchicalTopicMappingService( new HierarchicalTopicMappingService( _topicRepository, @@ -37,7 +37,7 @@ Once the `IHierarchicalTopicMappingService` is constructed, it can by calling 2. **`int tiers = 1`:** The number of tiers to crawl. While the `TopicMappingService` implementation will crawl indefinitely, given the right conditions, the `IHierarchicalTopicMappingService` can be constrained to a particular depth by the caller. 3. **`Func validationDelegate = null`:** A validation function that accepts a `Topic` as input and returns `true` if the `Topic` (and its descendants) should be included, and otherwise `false`. -``` +```csharp await hierarchicalTopicMappingService.GetRootViewModelAsync( hierarchicalTopicMappingService.GetHierarchicalRoot(currentTopic, 2, "Web"), 2, diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 9f45806c..0682d1fe 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -68,8 +68,8 @@ If a property is named `Parent`, then the `TopicMappingService` will pull the va ### Example The following is an example of a data transfer object (specifically, a view model) that `TopicMappingService` might consume: -``` public class CustomTopicViewModel { +```csharp public TopicViewModel Parent { get; set; } public string CustomPropertyA { get; set; } public int CustomPropertyB { get; set; } @@ -109,8 +109,8 @@ To support the mapping, a variety of `Attribute` classes are provided for decora ### Example The following is an example of a data transfer object that implements the above attributes: -``` public class CompanyTopicViewModel { +```csharp [DefaultValue("Ignia")] [MaxLength(100)] @@ -173,7 +173,7 @@ The [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which - `topicMappingService.Map(topic, AssociationTypes.Children)` To implement the caching decorator, use the following construction as a Singleton lifestyle in your composer: -``` +```csharp var topicRepository = new SqlTopicRepository(…); var topicMappingService = new TopicMappingService(topicRepository); var cachedTopicMappingService = new CachedTopicMappingService(topicMappingService); diff --git a/OnTopic/Mapping/Reverse/README.md b/OnTopic/Mapping/Reverse/README.md index 53e0cb0d..7dead4b2 100644 --- a/OnTopic/Mapping/Reverse/README.md +++ b/OnTopic/Mapping/Reverse/README.md @@ -23,7 +23,7 @@ Because the `ReverseTopicMappingService` doesn't map directly to the `ITopicRepo ## Complex Models The `ReverseTopicMappingService` allows complex models with nested objects to be mapped to a single `Topic` by using the `[MapToParent]` attribute. By default, any properties on a complex property will be prefixed with the property name. This prefix can be modified—or even removed—by passing an `AttributePrefix` argument to `[MapToParent]`. For example: -``` +```csharp public class ContentBindingModel: ITopicBindingModel { public string Key { get; set; } public string ContentType { get; set; } From 7720e58c08574d8a16d568a084c476f96ecd951e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:15:30 -0800 Subject: [PATCH 609/778] Updated to account for change in `@DeleteRelationships` handling Previously, relationships could be deleted by passing a `@DeleteRelationships` parameter to the `UpdateTopic` stored procedure. This was a clumsy approach, and removed in OnTopic 4.0.0. In OnTopic 5.0.0, the `UpdateRelationships` and `UpdateReferences` stored procedures now include an optional `@DeleteUnmatched` option which effectively does the same thing, but only deletes those who don't have new values added. (Further, as these tables are versioned, "delete" means creating deletion records, not actually deleting previous records.) --- OnTopic.Data.Sql.Database/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 2aafd66e..70b29350 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -33,11 +33,11 @@ The following is a summary of the most relevant stored procedures. - **[`CreateTopic`](Stored%20Procedures/CreateTopic.sql)**: Creates a new topic based on a `@ParentId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Returns a new `@TopicId`. - **[`DeleteTopic`](Stored%20Procedures/DeleteTopic.sql)**: Deletes an existing topic based on a `@TopicId`. - **[`MoveTopic`](Stored%20Procedures/MoveTopic.sql)**: Moves an existing topic based on a `@TopicId`, `@ParentId`, and `@SiblingId`. -- **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Optionally deletes all relationships; these will need to be re-added using `UpdateRelationships`. Old attributes are persisted as previous versions. +- **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Old attributes are persisted as previous versions. - **[`UpdateAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the indexed attributes, optionally removing any whose values aren't matched in the provided `@Attributes` parameter. - **[`UpdateExtendedAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the extended attributes, assuming the `@ExtendedAttributes` parameter doesn't match the previous value. -- **[`UpdateReferences`](Stored%20Procedures/UpdateReferences.sql)**: Associates a reference with a topic based on a `@TopicId` and a `TopicReferences` array of `@ReferencKey`s and `@Target_TopicId`s. -- **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). +- **[`UpdateReferences`](Stored%20Procedures/UpdateReferences.sql)**: Associates a reference with a topic based on a `@TopicId` and a `TopicReferences` array of `@ReferencKey`s and `@Target_TopicId`s. Optionally deletes unmatched references. +- **[`UpdateRelationships`](Stored%20Procedures/UpdateRelationships.sql)**: Associates a relationship with a topic based on a `@TopicId`, `TopicList` array of `@Target_TopicIds`, and a `@RelationshipKey` (which can be any string label). Optionally deletes unmatched relationships. ## Functions - **[`GetTopicID`](Functions/GetTopicID.sql)**: Retrieves a topic's `TopicId` based on a corresponding `@UniqueKey` (e.g., `Root:Configuration`). From e08f7e5f7a86762d5a7997afcf0bd7d6277b84f1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:16:19 -0800 Subject: [PATCH 610/778] Updated disclaimer regarding versioning to only apply to `Topics` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Relationships` table—as well as the new `TopicReferences` table—now supports versioning, so this disclaimer is out-of-date. --- OnTopic.Data.Sql.Database/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 70b29350..29206d1a 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -20,7 +20,7 @@ The following is a summary of the most relevant tables. - **[`TopicReferences`](Tables/TopicReferences.sql)**: Represents (1:1) references between topics, segmented by a `ReferenceKey`. - **[`Relationships`](Tables/Relationships.sql)**: Represents (1:n) relationships between topics, segmented by a `RelationshipKey`. -> *Note:* Neither `Topics` nor `Relationships` are subject to tracking versions. Changes to these records are permanent. +> *Note:* The `Topics` table is not subject to tracking versions. Changes to core topic values, such as `TopicKey`, `ContentType`, and `ParentID`, are permanent. ## Stored Procedures The following is a summary of the most relevant stored procedures. From 39426ab750721a92799d18ff56e48d4e1bd9734e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:18:38 -0800 Subject: [PATCH 611/778] Consolidate build badges exclusively on main `README.md` Previously, two of these were on the main `README`, while three of them were on the `OnTopic` `README`; this change consolidates them on the main `README` exclusively. --- OnTopic/README.md | 4 ---- README.md | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index ba369653..9c4b0190 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -1,9 +1,5 @@ # OnTopic Library -[![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) -![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) -[![OnTopic package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/fb67677f-2b83-4318-9007-0c46b4da55c1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=fb67677f-2b83-4318-9007-0c46b4da55c1&preferRelease=true) - The `OnTopic` assembly represents the core domain layer of the OnTopic library. It includes the primary entity ([`Topic`](Topic.cs)), abstractions (e.g., [`ITopicRepository`](Repositories/ITopicRepository.cs)), and associated classes (e.g., [`KeyedTopicCollection<>`](Collections/KeyedTopicCollection{T}.cs)). ### Contents diff --git a/README.md b/README.md index f36af5db..6f9245c2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # OnTopic Library The OnTopic library is a .NET-based content management system (CMS) designed around structured schemas ("Content Types") and optimized to simplify team-based workflows with distinct roles for content owners, backend developers, and graphic producers. -[![OnTopic package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/fb67677f-2b83-4318-9007-0c46b4da55c1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=fb67677f-2b83-4318-9007-0c46b4da55c1&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) +[![OnTopic package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/fb67677f-2b83-4318-9007-0c46b4da55c1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=fb67677f-2b83-4318-9007-0c46b4da55c1&preferRelease=true) ### Roles The OnTopic library acknowledges that the roles of developers, designers, and content owners are usually compartmentalized and, thus, optimizes for the needs of each. From 0751471ed7067b098dbe194fe497ee7f3667abfa Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:20:48 -0800 Subject: [PATCH 612/778] Refactored topic collection list as graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The purpose of the various topic collections is fairly self-explanatory based on their names, so having a description for each didn't add much value—while organizing them as a list made the various dimensions confusing. To mitigate that, I've refactored the topic collection list as a grid contrasting read-write to read-only against unkeyed, keyed, and generic collections. This is easier to read. --- OnTopic/README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index 9c4b0190..315400ff 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -48,13 +48,20 @@ Out of the box, the OnTopic library contains two specially derived topics for su - **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeValueCollection`](Attributes/AttributeValueCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. ## Collections -In addition to the above key classes, the `OnTopic` assembly contains a number of specialized collections. These include: -- **[`KeyedTopicCollection{T}`](Collections/KeyedTopicCollection{T}.cs)**: A `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - - **[`KeyedTopicCollection`](Collections/KeyedTopicCollection.cs)**: A `KeyedCollection` of `Topic` keyed by `Id` and `Key`. -- **[`ReadOnlyKeyedTopicCollection{T}`](Collections/ReadOnlyKeyedTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`. - - **[`ReadOnlyKeyedTopicCollection`](Collections/ReadOnlyKeyedTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`. -- **[`TopicMultiMap`](Collections/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection name. - - **[`ReadOnlyTopicMultiMap`](Collections/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. +The `OnTopic` assembly contains a number of generic, keyed, and/or read-only collections for working with topics. These include: + +| | Read-Write | Read-Only +| ----------------------------- | ------------------------------------- | ------------------------------------- +| Unkeyed | [`TopicCollection`][1] | [`ReadOnlyTopicCollection`][4] +| Keyed | [`KeyedTopicCollection`][2] | [`ReadOnlyKeyedTopicCollection`][5] +| Keyed (Generic) | [`KeyedTopicCollection`][3] | [`ReadOnlyKeyedTopicCollection`][6] + +[1]: Collections/TopicCollection.cs +[2]: Collections/KeyedTopicCollection.cs +[3]: Collections/KeyedTopicCollection{T}.cs +[4]: Collections/ReadOnlyTopicCollection.cs +[5]: Collections/ReadOnlyKeyedTopicCollection.cs +[6]: Collections/ReadOnlyKeyedTopicCollection{T}.cs ### Specialty Collections - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. From b3189c56d2b4008c4dc162ad68c5a1578416c958 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:25:54 -0800 Subject: [PATCH 613/778] Fixed paths pointing to legacy namespaces --- OnTopic/Mapping/Hierarchical/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic/Mapping/Hierarchical/README.md b/OnTopic/Mapping/Hierarchical/README.md index 705792e9..bc7daffd 100644 --- a/OnTopic/Mapping/Hierarchical/README.md +++ b/OnTopic/Mapping/Hierarchical/README.md @@ -13,7 +13,7 @@ While the [`TopicMappingService`](../README.md) is capable of populating trees o 2. The topics included can be constrained by specifying a method or lamda expression that accepts a `Topic` as the parameter, and returns `true` (if the `Topic` should be mapped) or `false` (if it should be skipped). 3. The type that all _children_ will be mapped to can be specified, instead of letting the model type be determined exclusively by the `Topic.ContentType` property. -In many cases, these are not needed. They do, however, provide additional flexibility for particular scenarios. For example, these are valuable for constructing the navigation used by e.g. the [`LayoutControllerBase`](../../../Ignia.Topics.Web.Mvc/Controllers/LayoutControllerBase{T}.cs), which should be restricted to three tiers, should be mapped to a [`NavigationTopicViewModel`](../../../Ignia.Topics.ViewModels/NavigationTopicViewModel.cs), and, in the case of the many navigation, should exclude any topics of the content type `PageGroup`. +In many cases, these are not needed. They do, however, provide additional flexibility for particular scenarios. For example, these are valuable for constructing the navigation used by e.g. the [`MenuViewComponentBase`](../../../OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs), which should be restricted to three tiers, should be mapped to a [`NavigationTopicViewModel`](../../../OnTopic.ViewModels/NavigationTopicViewModel.cs), and, in the case of the many navigation, should exclude any topics of the content type `PageGroup`. ## `CachedHierarchicalTopicMappingService` @@ -22,7 +22,7 @@ The [`CachedHierarchicalTopicMappingService`](CachedHierarchicalTopicMappingS > *Note:* As with the `CachedTopicMappingService`, the `CachedHierarchicalTopicMapping` service should be used with caution. It will not be (immediately) updated if the underlying database or topic graph are updated. And since the topic graph is already cached, it effectively doubles the memory footprint of the graph by storing it both as topics as well as view models. That said, this is useful for large view model graphs that are frequently reused—such as those that show up in the navigation of a site. ## Example -The first code block demonstrates how to construct a new instance of a `IHierarchicalTopicMappingService`. In this case, it wraps the default `HierarchicalTopicMappingService` in a `CachedHierarchicalTopicMappingService` for caching, and maps children to the `NavigationTopicViewModel` class from the [`Ignia.Topics.ViewModels`](../../../Ignia.Topics.ViewModels/) project. Typically, this would be done in the _Composition Root_ of an application, with the service passed into e.g. a `Controller` as an `IHierarchicalTopicMappingService` dependency. +The first code block demonstrates how to construct a new instance of a `IHierarchicalTopicMappingService`. In this case, it wraps the default `HierarchicalTopicMappingService` in a `CachedHierarchicalTopicMappingService` for caching, and maps children to the `NavigationTopicViewModel` class from the [`Ignia.Topics.ViewModels`](../../../OnTopic.ViewModels/) project. Typically, this would be done in the _Composition Root_ of an application, with the service passed into e.g. a `Controller` as an `IHierarchicalTopicMappingService` dependency. ```csharp var hierarchicalTopicMappingService = new CachedHierarchicalTopicMappingService( new HierarchicalTopicMappingService( From 85d6daff224ede5209606df9cae27009413f0add Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:26:58 -0800 Subject: [PATCH 614/778] Added section on extensibility to main `README` --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6f9245c2..a3a47973 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ This is contrasted to most traditional CMSs, which attempt to coordinate all of ### Multi-Device Optimized In addition, OnTopic is optimized for multi-client/multi-device scenarios since the content editor focuses exclusively on structured data. This allows entirely distinct presentation layers to be established. For instance, the same content can be accessed by an iOS app, a website, and even a web-based API for third-party consumption. By contrast, most CMSs are designed for one client only: a website (which may be mobile-friendly via responsive templates.) +### Extensible +Fundamentally, OnTopic is based on structured schemas ("Content Types") which can be modified via the editor itself. This allows new data structures to be introduced without needing to modify the database or creating extensive plugins. So, for example, if a site includes job postings, it might create a `JobPosting` content type that describes the structure of a job posting, such as _job title_, _job description_, _job requirements_, &c. By contrast, some CMSs�such as WordPress�try to fit all items into a single data model�such as a _blog post_�or require extensive customizations of database objects and intermediate queries in order to extend the data model. OnTopic is designed with extensibility in mind, so updates to the data model are comparatively trivial to implement. + ## Library ### Domain Layer From 2e4a7801800867341086c590f00fc95da3fcc487 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:31:08 -0800 Subject: [PATCH 615/778] Emphasized relevant keywords to reinforce point --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a3a47973..700a8d04 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # OnTopic Library -The OnTopic library is a .NET-based content management system (CMS) designed around structured schemas ("Content Types") and optimized to simplify team-based workflows with distinct roles for content owners, backend developers, and graphic producers. +The OnTopic library is a .NET Core-based content management system (CMS) designed around structured schemas ("Content Types") and optimized to simplify team-based workflows with distinct roles for content owners, backend developers, and graphic producers. [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) ![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) @@ -8,14 +8,14 @@ The OnTopic library is a .NET-based content management system (CMS) designed aro ### Roles The OnTopic library acknowledges that the roles of developers, designers, and content owners are usually compartmentalized and, thus, optimizes for the needs of each. -- **Content owners** have access to an *[editor](https://github.com/OnTopicCMS/OnTopic-Editor-AspNetCore)* that focuses exclusively on exposing *structured data*; this includes support for custom content types (e.g., "Job Posting", "Blog Post", &c.) +- **Content owners** have access to an [editor](https://github.com/OnTopicCMS/OnTopic-Editor-AspNetCore) that focuses _exclusively_ on exposing *structured data*; this includes support for custom content types (e.g., "Job Posting", "Blog Post", &c.) - **Backend developers** have access to *data repositories*, *services*, and a rich *entity* model in C# for consuming the structured data and implementing any *business logic* via code. - **Frontend developers** have access to light-weight *views* based on purpose-built *view models*, thus allowing them to focus exclusively on presentation concerns, without any platform-specific knowledge. -This is contrasted to most traditional CMSs, which attempt to coordinate all of these via an editor by exposing design responsibilities (via themes, templates, and layouts) as well as development responsibilities (via plug-ins or components). This works well for a small project without distinct design or development resources, but introduces a lot of complexity for more mature teams with well-established roles. +This is contrasted to most traditional CMSs, which attempt to coordinate all of these via an editor by exposing design responsibilities (via themes, templates, and layouts) as well as development responsibilities (via plugins or components). This works well for a small project without distinct design or development resources, but introduces a lot of complexity for more mature teams with well-established roles. ### Multi-Device Optimized -In addition, OnTopic is optimized for multi-client/multi-device scenarios since the content editor focuses exclusively on structured data. This allows entirely distinct presentation layers to be established. For instance, the same content can be accessed by an iOS app, a website, and even a web-based API for third-party consumption. By contrast, most CMSs are designed for one client only: a website (which may be mobile-friendly via responsive templates.) +In addition, OnTopic is optimized for multi-client/multi-device scenarios since the content editor focuses _exclusively_ on structured data. This allows entirely distinct presentation layers to be established without the CMS attempting attempting to influence or determing design decisions via e.g. per-page layout. For instance, the same content can be accessed by an iOS app, a website, and even a web-based API for third-party consumption. By contrast, most CMSs are designed for one client only: a website (which may be mobile-friendly via responsive templates.) ### Extensible Fundamentally, OnTopic is based on structured schemas ("Content Types") which can be modified via the editor itself. This allows new data structures to be introduced without needing to modify the database or creating extensive plugins. So, for example, if a site includes job postings, it might create a `JobPosting` content type that describes the structure of a job posting, such as _job title_, _job description_, _job requirements_, &c. By contrast, some CMSs�such as WordPress�try to fit all items into a single data model�such as a _blog post_�or require extensive customizations of database objects and intermediate queries in order to extend the data model. OnTopic is designed with extensibility in mind, so updates to the data model are comparatively trivial to implement. From e5953abc3f586caeedd8a7e36f68ff5f5696b1d4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:33:51 -0800 Subject: [PATCH 616/778] Removed references to legacy projects, added references to new projects We're no longer actively maintaining the ASP.NET Web Forms or ASP.NET MVC Framework versions of OnTopic, and they will **not** work with OnTopic 5.x. As such, references to these projects are removed. While I was at it, introduced references to new projects such as the new SQL unit tests and the Data Transfer library. --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 700a8d04..1d101170 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This is contrasted to most traditional CMSs, which attempt to coordinate all of In addition, OnTopic is optimized for multi-client/multi-device scenarios since the content editor focuses _exclusively_ on structured data. This allows entirely distinct presentation layers to be established without the CMS attempting attempting to influence or determing design decisions via e.g. per-page layout. For instance, the same content can be accessed by an iOS app, a website, and even a web-based API for third-party consumption. By contrast, most CMSs are designed for one client only: a website (which may be mobile-friendly via responsive templates.) ### Extensible -Fundamentally, OnTopic is based on structured schemas ("Content Types") which can be modified via the editor itself. This allows new data structures to be introduced without needing to modify the database or creating extensive plugins. So, for example, if a site includes job postings, it might create a `JobPosting` content type that describes the structure of a job posting, such as _job title_, _job description_, _job requirements_, &c. By contrast, some CMSs�such as WordPress�try to fit all items into a single data model�such as a _blog post_�or require extensive customizations of database objects and intermediate queries in order to extend the data model. OnTopic is designed with extensibility in mind, so updates to the data model are comparatively trivial to implement. +Fundamentally, OnTopic is based on structured schemas ("Content Types") which can be modified via the editor itself. This allows new data structures to be introduced without needing to modify the database or creating extensive plugins. So, for example, if a site includes job postings, it might create a `JobPosting` content type that describes the structure of a job posting, such as _job title_, _job description_, _job requirements_, &c. By contrast, some CMSs—such as WordPress—try to fit all items into a single data model—such as a _blog post_—or require extensive customizations of database objects and intermediate queries in order to extend the data model. OnTopic is designed with extensibility in mind, so updates to the data model are comparatively trivial to implement. ## Library @@ -27,24 +27,23 @@ Fundamentally, OnTopic is based on structured schemas ("Content Types") which ca ### Data Access Layer - **[`OnTopic.Data.Sql`](OnTopic.Data.Sql/README.md)**: [`ITopicRepository`](OnTopic/Repositories/ITopicRepository.cs) implementation for storing and retrieving [`Topic`](OnTopic/Topic.cs) entities in a Microsoft SQL Server database. - - **[`OnTopic.Data.Sql.Database`](OnTopic.Data.Sql.Database/README.md)**: Microsoft SQL Server database schema, including tables, views, functions, and stored procedures needed to support the [`OnTopic.Data.Sql`](OnTopic.Data.Sql/README.md) library. -- **[`OnTopic.Data.Caching`](OnTopic.Data.Caching/README.md)**: [`ITopicRepository`](OnTopic/Repositories/ITopicRepository.cs) façade that caches data accessed in memory for fast subsequent retrieval. + - **[`OnTopic.Data.Sql.Database`](OnTopic.Data.Sql.Database/README.md)**: Microsoft SQL Server database schema, including tables, views, types, functions, and stored procedures needed to support the [`OnTopic.Data.Sql`](OnTopic.Data.Sql/README.md) library. +- **[`OnTopic.Data.Caching`](OnTopic.Data.Caching/README.md)**: [`ITopicRepository`](OnTopic/Repositories/ITopicRepository.cs) decorator that caches data accessed in memory for fast subsequent retrieval. > *Note*: Additional data access layers can be created by implementing the [`ITopicRepository`](OnTopic/Repositories/ITopicRepository.cs) interface. ### Presentation Layer -- **[`OnTopic.AspNetCore.Mvc`](OnTopic.AspNetCore.Mvc/README.md)**: ASP.NET Core 2.x implementation, including a default [`TopicController`](OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs), allowing templates to be created using `*.cshtml` pages and _view components_. -- **[`OnTopic.Web.Mvc`](https://github.com/OnTopicCMS/OnTopic-MVC)**: ASP.NET MVC 5.x implementation, including a default [`TopicController`](https://github.com/OnTopicCMS/OnTopic-MVC/blob/master/OnTopic.Web.Mvc/Controllers/TopicController.cs), allowing templates to be created using `*.cshtml` pages. -- **[`OnTopic.Web`](https://github.com/OnTopicCMS/OnTopic-WebForms/)**: Legacy ASP.NET WebForms implementation, allowing templates to be created using `*.aspx` pages. This is considered obsolete, and intended exclusively for migration to new versions. -- **[`OnTopic.ViewModels`](OnTopic.ViewModels/README.md)**: Standard view models for exposing factory=default schemas of shared content types. These can be extended, overwritten, or ignored entirely by the presentation layer implementation; they are provided for convenience. +- **[`OnTopic.AspNetCore.Mvc`](OnTopic.AspNetCore.Mvc/README.md)**: ASP.NET Core implementation, including a default [`TopicController`](OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs), allowing templates to be created using `*.cshtml` pages and _view components_. Supports both ASP.NET Core 3.x and ASP.NET Core 5.x. +- **[`OnTopic.ViewModels`](OnTopic.ViewModels/README.md)**: Standard view models using C# 9 records for exposing factory-default schemas of shared content types. These can be extended, overwritten, or ignored entirely by the presentation layer implementation; they are provided for convenience. ### Unit Tests - **[`OnTopic.Tests`](OnTopic.Tests)**: .NET Unit Tests, broken down by target class. - **[`OnTopic.AspNetCore.Mvc.Tests`](OnTopic.AspNetCore.Mvc.Tests)**: .NET Unit Tests for the `OnTopic.AspNetCore.Mvc` implementation. +- **[`OnTopic.Data.Sql.Database.Tests`](OnTopic.Data.Sql.Database.Tests)**: SQL Server Data Tools (SSDT) unit tests for evaluating the functionality of stored procedures and functions against a local SQL Server database. ### Editor -- **[`OnTopic.Editor.AspNetCore`](https://github.com/OnTopicCMS/OnTopic-Editor-AspNetCore/)**: ASP.NET Core 3.1 implementation of the editor interface. -- **[`OnTopic.Editor`](https://github.com/OnTopicCMS/OnTopic-Editor-WebForms/)**: Legacy ASP.NET WebForms implementation of the editor interface. +- **[`OnTopic.Editor.AspNetCore`](https://github.com/OnTopicCMS/OnTopic-Editor-AspNetCore/)**: ASP.NET Core implementation of the editor interface. Supports both ASP.NET Core 3.x and ASP.NET Core 5.x. +- **[`OnTopic.Data.Transfer`](https://github.com/OnTopicCMS/OnTopic-Data-Transfer/)**: .NET Standard library for serializing and deserializing `Topic` entities into a data interchange format which can be used to import or export topic graphs via JSON. ## Credits OnTopic is owned and maintained by [Ignia](http://www.ignia.com/). \ No newline at end of file From 1cd126365213cf772728330d4ad5a2e02b07401d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:37:27 -0800 Subject: [PATCH 617/778] Reviewed and updated `README` for `OnTopic` namespace Clarified language, expanded on concepts, and factored in updates specific to OnTopic 5.0.0, such as use of `AttributeDescriptor` derivatives within external plugins, and the ability for `DynamicTopicViewModelLookupService` to look for classes that end in `ViewModel`, not just `TopicViewModel`. Also cross-referenced the `OnTopic.ViewModels` library. --- OnTopic/README.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index 315400ff..a6f40b95 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -14,37 +14,37 @@ The `OnTopic` assembly represents the core domain layer of the OnTopic library. - [View Models](#view-models) ## Entities -- **[`Topic`](Topic.cs)**: This is the core business object in OnTopic. +- **[`Topic`](Topic.cs)**: This is the core entity in OnTopic, and models all attributes, relationships, and references associated with a topic record. -> *Note*: Any class that derives from `Topic` and is named `{ContentType}Topic` will automatically be loaded by [`TopicFactory.Create()`](TopicFactory.cs), thus allowing content type specific business logic to be added to any `Topic` instance. +> *Note*: Any class that derives from `Topic` will automatically be loaded by [`TopicFactory.Create()`](TopicFactory.cs), thus allowing content type specific business logic to be added to any `Topic` instance. This is especially useful for custom `AttributeDescriptor` types used to extend the OnTopic Editor. ### Editor Out of the box, the OnTopic library contains two specially derived topics for supporting core infrastructure requirements: -- **[`ContentTypeDescriptor`](Metadata/ContentTypeDescriptor.cs)**: A `ContentTypeDescriptor` is composed of multiple `AttributeDescriptor` instances which describe the schema of a content type. This is primarily used by editors. +- **[`ContentTypeDescriptor`](Metadata/ContentTypeDescriptor.cs)**: A `ContentTypeDescriptor` is composed of multiple `AttributeDescriptor` instances which describe the schema of a content type. This is primarily used by the OnTopic Editor. - **[`AttributeDescriptor`](Metadata/AttributeDescriptor.cs)**: An `AttributeDescriptor` describes a single attribute on a `ContentTypeDescriptor`. This includes the `AttributeType`, `Description`, `DisplayGroup`, and whether or not it's required (`IsRequired`). -> As of v4.0.0, there are additionally attribute _type_ descriptors which are derived from `AttributeDescriptor`, such as [`BooleanAttribute`](Metadata/AttributeTypes/BooleanAttribute.cs). Currently, these don't fully model their corresponding content types—that is done via view models in the OnTopic Editor. By deriving them in the `OnTopic` library, however, we ensure that they are still created as derivatives of `AttributeDescriptor`, and thus properly included anywhere that `AttributeDescriptor`s are used. +> *Note*: In addition, the OnTopic Editor can be extended through types derived from `AttributeDescriptor`, such as the `BooleanAttributeDescriptor`. Plugins may optionally implement these in order to overwrite the `EditorType` or `ModelType` of the attribute, and thus determine where and how their values will be stored. ## Key Abstractions -- **[`ITopicRepository`](Repositories/ITopicRepository.cs)**: Defines the data access layer interface, with `Load()`, `Save()`, `Delete()`, `Move()`, and `Rollback()` methods. -- **[`ITopicMappingService`](Mapping/README.md)**: Defines interface for a service that can convert a `Topic` class into any arbitrary data transfer object based on predetermined conventions—or vice versa (via the `IReverseTopicMappingService`. -- **[`IHierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: Defines an interface for applying the `ITopicMappingService` to hierarchical data with constraints on depth. Used primarily for mapping navigation, such as in the [`NavigationTopicViewComponentBase`](../OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs). -- **[`ITypeLookupService`](lookup/ITypeLookupService.cs)**: Defines the interface that can identify `Type` objects based on a `GetType(typeName)` query. Used by e.g. `ITopicMappingService` to find corresponding `TopicViewModel` classes to map to. +- **[`ITopicRepository`](Repositories/ITopicRepository.cs)**: Defines an interface for data access, with `Load()`, `Save()`, `Delete()`, `Move()`, `Refresh()`, and `Rollback()` methods. +- **[`ITopicMappingService`](Mapping/README.md)**: Defines an interface for a service that can convert a `Topic` into any arbitrary data transfer object based on predetermined conventions—or vice versa (via the [`IReverseTopicMappingService`](Mapping/Reverse/IReverseTopicMappingService.cs)). +- **[`IHierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: Defines an interface for applying the `ITopicMappingService` to hierarchical data with constraints on depth. Used primarily for mapping data for navigation components, such as the [`NavigationTopicViewComponentBase`](../OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs). +- **[`ITypeLookupService`](lookup/ITypeLookupService.cs)**: Defines an interface that can identify `Type` objects based on a `Lookup(typeName)` query. Used by e.g. `ITopicMappingService` to find corresponding `TopicViewModel` classes to map to. ## Implementations -- **[`TopicMappingService`](Mapping/README.md)**: A default implementation of the `ITopicMappingService`, with built-in conventions that should address that majority of mapping requirements. This also includes a number of attributes for annotating view models with hints that the `TopicMappingService` can use in populating target objects. - - **[`CachedTopicMappingService`](Mapping/README.md)**: Provides an optional caching layer for the `TopicMappingService`—or any `ITopicMappingService` implementation. -- **[`ReverseTopicMappingService`](Mapping/Reverse/README.md)**: A default implementation of the `IReverseTopicMappingService`, honoring similar conventions and attribute hints as the `TopicMappingService`. +- **[`TopicMappingService`](Mapping/README.md)**: A default implementation of the `ITopicMappingService`, with built-in conventions that should address the majority of mapping requirements. This also includes a number of attributes for annotating view models with hints that the `TopicMappingService` can use to fine-tune the mapping process. + - **[`CachedTopicMappingService`](Mapping/README.md)**: Provides an optional caching layer for decorating the `TopicMappingService`—or any `ITopicMappingService` implementation. +- **[`ReverseTopicMappingService`](Mapping/Reverse/README.md)**: A default implementation of the `IReverseTopicMappingService`, honoring similar conventions and attribute hints as the `TopicMappingService`. Useful for merging binding models with topics as part of form processing. - **[`HierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: A default implementation of the `IHierarchicalTopicMappingService`, which accepts an `ITopicMappingService` for mapping each individual node in the hierarchy. - **[`CachedHierarchicalTopicMappingService`](Mapping/Hierarchical/README.md)**: Provides an optional caching layer for the `HierarchicalTopicMappingService`—or any `IHierarchicalTopicMappingService` implementation. -- **[`StaticTypeLookupService`](Lookup/StaticTypeLookupService.cs)**: A basic implementation of the `ITypeLookupService` interface that allows types to be explicitly registered; useful when a small number of types are expected. - - **[`DynamicTypeLookupService`](Lookup/DynamicTypeLookupService.cs)**: A reflection-based implementation of the `ITypeLookupService` interface that looks up types from all loaded assemblies based on a `Func` delegate. +- **[`StaticTypeLookupService`](Lookup/StaticTypeLookupService.cs)**: A basic implementation of the `ITypeLookupService` interface that allows types to be explicitly registered; useful when a small number of well-known types are expected. + - **[`DynamicTypeLookupService`](Lookup/DynamicTypeLookupService.cs)**: A reflection-based implementation of the `ITypeLookupService` interface that looks up types from all loaded assemblies based on a `Func` delegate passed to the constructor. - **[`DynamicTopicLookupService`](Lookup/DynamicTopicLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that derive from `Topic`; this is the default implementation for `TopicFactory`. - - **[`DynamicTopicViewModeLookupService`](Lookup/DynamicTopicViewModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that end with `TopicViewModel`; this is useful for the `TopicMappingService`. + - **[`DynamicTopicViewModeLookupService`](Lookup/DynamicTopicViewModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that end with `ViewModel`; this is useful for the `TopicMappingService`. - **[`DynamicTopicBindingModelLookupService`](Lookup/DynamicTopicBindingModelLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that implement `ITopicBindingModel`; this is useful for the `ReverseTopicMappingService`. ## Extension Methods -- **[`Querying`](Querying/TopicExtensions.cs)**: The `TopicExtensions` class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate. +- **[`Querying`](Querying/TopicExtensions.cs)**: The [`TopicExtensions`](Querying/TopicExtensions.cs) class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate. There are also specialty extensions for querying [`IEnumerable`](Querying/TopicCollectionExtensions.cs). - **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeValueCollection`](Attributes/AttributeValueCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. ## Collections @@ -72,9 +72,11 @@ The following are intended to provide support for the Editor domain objects, `Co - **[`AttributeDescriptorCollection`](Metadata/AttributeDescriptorCollection.cs)**: A `KeyedCollection` of `AttributeDescriptor` objects keyed by `Id` and `Key`. ## View Models -The core Topic library has been designed to be view model agnostic; i.e., view models should be defined for the specific presentation framework (e.g., ASP.NET MVC) and customer. That said, to facilitate reusability of features that work with view models, several interfaces are defined which can be applied as appropriate. These include: -- **[`ITopicViewModel`](Models/ITopicViewModel.cs)**: Includes universal properties such as `Key`, `Id`, and `ContentType`. - - **[`IPageTopicViewModel`](Models/IPageTopicViewModel.cs)**: Includes page-specific properties such as `Title`, `MetaKeywords`, and `WebPath`. +The core Topic library has been designed to be view model agnostic; i.e., view models should be defined for the specific presentation framework (e.g., ASP.NET Core) and customer. That said, to facilitate reusability of features that work with view models, several interfaces are defined which can be applied as appropriate. These include: +- **[`ITopicViewModel`](Models/ITopicViewModel.cs)**: Includes universal properties such as `Key`, `UniqueKey`, `Id`, `ContentType`, and `Title`. + - **[`IPageTopicViewModel`](Models/IPageTopicViewModel.cs)**: Includes page-specific properties such as `MetaKeywords` and `MetaDescription`. - **[`INavigationTopicViewModel`](Models/INavigationTopicViewModel{T}.cs)**: Includes `IPageTopicViewModel`, `Children`, and an `IsSelected()` view logic handler, for use with navigation menus. - **[`ITopicBindingModel`](Models/ITopicBindingModel.cs)**: Includes the bare minimum properties—namely `Key` and `ContentType`—needed to support a binding model that will be consumed by the `IReverseTopicMappingService`. - **[`IRelatedTopicBindingModel`](Models/IRelatedTopicBindingModel.cs)**: Includes the bare minimum properties—namely `UniqueKey`—needed to reference another topic on a binding model that will be consumed by the `IReverseTopicMappingService`. + +In addition to these interfaces, a set of concrete implementations of view models corresponding to the default schemas for the out-of-the-box content types can be found in the [`OnTopic.ViewModels`](../OnTopic.ViewModels/README.md) package. \ No newline at end of file From 468149955a7df9b1046e076c39a954dcf128d07a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:38:38 -0800 Subject: [PATCH 618/778] Introduced full set of specialized collections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With OnTopic 5.0.0, we're introducing quite a few specialized collections. This ensures those are fully documented—including the derived types such as `AttributeValueCollection`, `TopicRelationshipMultiMap`, and `TopicReferenceCollection`. --- OnTopic/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index a6f40b95..a125904e 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -64,7 +64,13 @@ The `OnTopic` assembly contains a number of generic, keyed, and/or read-only col [6]: Collections/ReadOnlyKeyedTopicCollection{T}.cs ### Specialty Collections -- **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `KeyedCollection` of `AttributeValue` instances keyed by `AttributeValue.Key`. +The `OnTopic.Collections.Specialized` namespace includes a number of collections that are used by the OnTopic library, but won't generally be used directly by implementors, except as exposed by the core library. These include: +- **[`TrackedCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedItem` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. + - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedCollection` of [`AttributeValue`](Attributes/AttributeValue.cs) instances keyed by `AttributeValue.Key`; exposed by `Topic.Attributes`. + - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedCollection` of [`TopicReference`](Associations/TopicReference.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. +- **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. + - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. + - **[`TopicRelationshipMultiMap`](associations/TopicRelationshipMultiMap.cs)**: A `TopicMultiMap` of [`KeyValuesPair`](Collections/Specialized/KeyValuesPair.cs) instances keyed by `KeyValuesPair.Key`; exposed by `Topic.Relationships`. ### Editor The following are intended to provide support for the Editor domain objects, `ContentTypeDescriptor` and `AttributeDescriptor`. From 4aa305c58dd02549c908597b55c772225d0f47ea Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:42:23 -0800 Subject: [PATCH 619/778] Reviewed, updated `README` for `OnTopic.Mapping.Reverse` namespace Clarified language, expanded on concepts. As there are no major updates to the `ReverseTopicMappingService` in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. Added the preexisting _Interfaces_ section to the table of contents. --- OnTopic/Mapping/Reverse/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/OnTopic/Mapping/Reverse/README.md b/OnTopic/Mapping/Reverse/README.md index 7dead4b2..dd6b4a92 100644 --- a/OnTopic/Mapping/Reverse/README.md +++ b/OnTopic/Mapping/Reverse/README.md @@ -1,22 +1,23 @@ # Reverse Topic Mapping Service -The [`IReverseTopicMappingService`](IReverseTopicMappingService.cs) and its concrete implementation, [`ReverseTopicMappingService`](ReverseTopicMappingService.cs), provide handling for mapping data transfer objects (and typing binding models) _back_ to topics. Generally, it follows similar conventions as the [`ITopicMappingService`](../README.md) and honors most of the same attribute hints, albeit with some important restrictions and limitations outlined below. +The [`IReverseTopicMappingService`](IReverseTopicMappingService.cs) and its concrete implementation, [`ReverseTopicMappingService`](ReverseTopicMappingService.cs), provide handling for mapping binding models _back_ to topics. Generally, it follows similar conventions as the [`ITopicMappingService`](../README.md) and honors most of the same attribute hints—albeit with some important restrictions and limitations outlined below. ### Contents +- [Interfaces](#interfaces) - [Model Validation](#model-validation) - [`ITopicRepository` Integration](#itopicrepository-integration) - [Mapping Hierarchies](#mapping-hierarchies) - [Complex Models](#complex-models) ## Interfaces -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 associations, it expects implementation of the `IAssociatedTopicBindingModel`, which has a single `UniqueKey` property. +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 associations, such as relationships and topic references, it expects implementation of the `IAssociatedTopicBindingModel`, which has a single `UniqueKey` property. ## Model Validation -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. +The `ReverseTopicMappingService` is deliberately more conservative than the `TopicMappingService` since assumptions in mapping won't just impact the data available to a view, but may be committed to the persistence store. 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`](../MappingModelValidationException.cs) is thrown. -> _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. +> _Important:_ If a binding model contains properties that are not intended to be mapped, they must be explicitly excluded from mapping using the `[DisableMapping]` attribute. ## `ITopicRepository` Integration -While the `ReverseTopicMappingService` maintains a dependency on the `ITopicRepository`, it makes no effort to look up and map to an existing topic. If an existing `Topic` is passed in, it will map to it. Otherwise, it will always return a new `Topic`. It is up to the controller to set the parent topic, if appropriate, and call `ITopicRepository.Save()`. This allows more flexibility, and avoids potential security issues, by allowing the caller to explicitly control what topics are modified based on its own business logic. +While the `ReverseTopicMappingService` maintains a dependency on the `ITopicRepository`, it makes no effort to look up and map to an existing topic. If an existing `Topic` is passed in, it will map to it. Otherwise, it will always return a new `Topic` instance. It is up to the controller to set the parent topic, if appropriate, and call `ITopicRepository.Save()`. This allows more flexibility, and avoids potential security issues by allowing the caller to explicitly control what topics are modified based on its own business logic. ## Mapping Hierarchies Because the `ReverseTopicMappingService` doesn't map directly to the `ITopicRepository`, it makes no effort to crawl a hierarchy of binding models. If this is needed, the calling code can iterate over the hierarchy, determining how best to handle e.g. new v. existing topics. That said, this is generally not expected to be needed, since form pages will usually only model a single topic. @@ -31,4 +32,4 @@ public class ContentBindingModel: ITopicBindingModel { public AddressBindingModel BillingContact { get; set; } } ``` -In this case, a `City` property on the `AddressBindingModel` would attempt to bind to a `BillingCity` attribute on the target `Topic`. If the `[MapToParent]` attribute is not present, then properties with complex types are ignored. +In this case, a `City` property on the `AddressBindingModel` would attempt to bind to a `BillingCity` attribute on the target `Topic`. If the `[MapToParent]` attribute is not present, then properties with complex types are ignored. \ No newline at end of file From d9881612ca1e63c3403f9c720fc32e701bdf4f22 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:44:39 -0800 Subject: [PATCH 620/778] Reviewed, updated `README` for `OnTopic.Mapping` namespace Clarified language, expanded on concepts. As there are no major updates to the `TopicMappingService` in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. Preferred term "model" or "view model" over "data transfer object". Removed references to legacy OnTopic 3.x concepts such as `LayoutControllerBase`. --- OnTopic/Mapping/README.md | 83 ++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 0682d1fe..d00c459b 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -1,5 +1,5 @@ # Topic Mapping Service -The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract interface for a service that maps `Topic` entities to any arbitrary data transfer object. It is intended primarily to aid in mapping the `Topic` entity to view model instances, such as the ones provided in the [`ViewModels` assembly](../../OnTopic.ViewModels) +The [`ITopicMappingService`](ITopicMappingService.cs) defines a service for mapping `Topic` entities to any arbitrary model class. It is intended primarily to aid in mapping the `Topic` entity to view model instances, such as the ones provided in the [`ViewModels`](../../OnTopic.ViewModels/README.md) package. ### Contents - [`TopicMappingService`](#topicmappingservice) @@ -29,37 +29,40 @@ The [`TopicMappingService`](TopicMappingService.cs) provides a concrete implemen - The data transfer object type can be explicitly specified via the `ITopicMappingService.Map<>()` method. - E.g., `topicMappingService.Map(topic)` will map the `topic` to the `TopicViewModel` class. - The data transfer object can also be assigned by convention via the `ITopicMappingService.Map()` method. - - The target type must be named `ContentTypeTopicViewModel`, based on the `ContentType` of the source `Topic`. - - E.g., If the source topic's `ContentType` is "Example", it will look for `ExampleTopicViewModel`. -> *Note:* Data transfer objects must have a default constructor so that either of the above methods can dynamically construct a new instance of them. + - The target type must be named `{ContentType}TopicViewModel` or `{ContentType}ViewModel`, based on the `ContentType` of the source `Topic`. + - E.g., If the source topic's `ContentType` is "Example", it will look for `ExampleTopicViewModel` or `ExampleViewModel`. + +> *Note:* Data transfer objects _must_ have a parameterless constructor so that either of the above methods can dynamically construct a new instance of the model classes. ### Properties -The mapping service will automatically attempt to map any properties on a data transfer object to values on the source `Topic`. To do so, it uses the following conventions: +The mapping service will automatically attempt to map any properties on a model to values on the source `Topic`. To do so, it uses the following conventions: #### Scalar Values If a property is of the type `bool`, `int`, `string`, or `DateTime`, then: -- Will pull the value from a parameterless getter method with the same name. -- Will pull the value from a property of the same name. -- Otherwise, will pull the value from the `topic.Attributes.GetValue()` method. +- It will pull the value from a parameterless getter method with the same name. +- It will pull the value from a property of the same name. +- Otherwise, it will pull the value from the `topic.Attributes.GetValue()` method. -For example, if a property on a view model is named `Author`, it will automatically look, in order, for: +For example, if a property on a view model is named `Author`, it will automatically look for, in order: - `topic.GetAuthor()` - `topic.Author` - `topic.Attributes.GetValue("Author")` #### Collections If a property implements `IList` (e.g., `List<>`, `Collection<>`, `TopicViewModelCollection<>`), then: -- Will pull the value from a collection with the same name as the property. -- If the property is explicitly named `Children` then it will load the `topic.Children`. -- Will search, in order, `topic.Relationships`, `topic.IncomingRelationships`, `topic.Children`. +- It will pull the value from a collection with the same name as the property. +- If the property is explicitly named `Children`, then it will load the `topic.Children`. +- It will search, in order, `topic.Relationships`, `topic.IncomingRelationships`, and finally `topic.Children`. - E.g., If a `List<>` property is named `Cousins` then it might match `topic.Relationships.GetTopics("Cousins")`. #### References -Topic references relate a single topic to another topic. If a property corresponds to an attribute named `{Property}Id`, that identifier refers to a `Topic`, and that `Topic` maps to an object that is assignable to the original property, then the `Topic` will be loaded, mapped, and assigned to that property. For instance, the following property: -``` -public AuthorTopicViewModel Author { get; set; } +Topic references relate a single topic to another topic by key. If a property corresponds to the key of a topic reference, and that `Topic` maps to an object that is assignable to the original property, then the `Topic` will be loaded, mapped, and assigned to that property. For instance, the following property: +```csharp +public AuthorViewModel Author { get; set; } ``` -Would be mapped to an `AuthorTopicViewModel` if `topic.Attributes.GetValue("AuthorId")` returns the identifier of a `Topic` with a `ContentType` set to `Author`. +Would be mapped to an `AuthorTopicViewModel` if `topic.References.GetValue("Author")` returns a `Topic` with a `ContentType` set to `Author`. + +> *_Note_*: For backward compatibility, topic references can also be created from attributes following the `{Property}Id` nomenclature—e.g., `topic.Attributes.GetValue("AuthorId")`. Implementors should prefer `topic.References` instead. #### Parent If a property is named `Parent`, then the `TopicMappingService` will pull the value from `topic.Parent`. This acts as a special version of a [Topic Reference](#references). @@ -67,9 +70,9 @@ If a property is named `Parent`, then the `TopicMappingService` will pull the va > *Note:* By default, associations to other topics stored in collections or reference properties of associated topics will not be pulled. For instance, if a `TopicViewModel` has a `Children` collection, then the relationships, references, nested topics, and children of those instances will not be populated. This is meant to constrain the size of the object graph delivered. ### Example -The following is an example of a data transfer object (specifically, a view model) that `TopicMappingService` might consume: -public class CustomTopicViewModel { +The following is an example of a view model that `TopicMappingService` might consume: ```csharp +public class CustomViewModel { public TopicViewModel Parent { get; set; } public string CustomPropertyA { get; set; } public int CustomPropertyB { get; set; } @@ -89,28 +92,28 @@ In this example, the properties would map to: - `NestedTopics`: A relationship or nested topic set named `NestedTopics` (the name doesn't have any special meaning). ## Attributes -To support the mapping, a variety of `Attribute` classes are provided for decorating data transfer objects. +To fine-tune the mapping process, a variety of `[Attribute]`s are provided for decorating models: - **`[Validation]`**: Enforces the rules established by any of the [`ValidationAttribute`](https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.validationattribute%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) subclasses. - This includes e.g., `[Required]`, `[MaxLength()]`, `[RegularExpression()]`, &c. -- **`[DefaultValue()]`**: Sets the default value for the property. -- **`[Inherit]`**: If the corresponding call comes back null, checks parent topics until a value is found. +- **`[DefaultValue()]`**: Sets the default value for the property, if one isn't explicitly provided. +- **`[Inherit]`**: If the corresponding call comes back `null`, checks parent topics until a value is found. - This is the equivalent of calling `topic.Attributes.GetValue(attributeKey, true)`. - **`[Metadata(key)]`**: Populates a collection with a list of `LookupItem` values from `Root:Configuration:Metadata`. - This is useful for including a list of items to filter collections by. - E.g., If child objects have a `TopicLookup` referencing the `Countries` metadata collection. -- **`[AttributeKey(key)]`**: Instructs the `TopicMappingService` to use the specified `key` instead of the property name when calling `topic.Attributes.GetValue()`. -- **`[FilterByAttribute(key, value)]`**: Ensures that all items in a collection have an attribute named "Key" with a value of "Value"; all else will be excluded. Multiple instances can be stacked. -- **`[Collection(key, type)]`**: For a collection, optionally specifies the name of the key to look for, instead of the property name, and the collection type, in case the key name is ambiguous. -- **`[Include(associationTypes)]`**: Instructs the code to populate the specified associations on any view models within a collection. +- **`[AttributeKey(key)]`**: Instructs the `TopicMappingService` to use the specified `key` instead of the property name when calling `topic.Attributes.GetValue()` or `topic.References.GetValue()`. +- **`[FilterByAttribute(key, value)]`**: Ensures that all items in a collection have an attribute named `key` with a value of `value`; all others will be excluded. Multiple instances can be stacked together. +- **`[Collection(key, type)]`**: For a collection, optionally specifies the `key` to look for, instead of the property name, and the `CollectionType`, in case the `key` is ambiguous. +- **`[Include(associationTypes)]`**: Instructs the code to populate the specified associations on any view models within a collection, thus expanding the scope of the mapping process. - **`[Flatten]`**: Includes all descendants for every item in the collection. If the collection enforces uniqueness, duplicates will be removed. -- **`[MapToParent]`**: Allows the attributes of a topic to be applied to a child complex object, optionally including a prefix. +- **`[MapToParent(prefix)]`**: Allows the attributes from a single topic to be applied to a complex child view model, optionally mapping attributes that begin with the supplied `prefix`. - **`[DisableMapping]`**: Prevents the mapping service from attempting to map the property to an attribute. ### Example -The following is an example of a data transfer object that implements the above attributes: -public class CompanyTopicViewModel { +The following is an example of a model that implements the above attributes: ```csharp +public class CompanyViewModel { [DefaultValue("Ignia")] [MaxLength(100)] @@ -141,16 +144,16 @@ public class CompanyTopicViewModel { In this example, the properties would map to: - `CompanyName`: Would default to "Ignia" if not otherwise set; would throw an error if the value exceeded 100 characters. -- `HideFromDirectory`: An attribute named `IsHidden` or a method named `GetIsHidden()`. If null, will look in `Parent` topics. +- `HideFromDirectory`: Maps to an attribute named `IsHidden` or a method named `GetIsHidden()`. If `null`, will search the `Parent` topics. - `Countries`: Loads all `LookupListItem` instances in the `Root:Configuration:Metadata:Countries` metadata collection. -- `CaseStudies`: A collection of `CaseStudy` topics pointing to the current `Company` via a "Companies" association. Will load the children of each case study. -- `Children`: A collection of child topics, with all associations (but not e.g. grandchildren) loaded. -- `Contacts`: A list of `Employee` nested topics, filtered by those with `IsActive` set to `1` (`true`) and `Role` set to "Account Manager". Includes any descendants of the nested topics that meet the previous criteria. +- `CaseStudies`: A collection of `CaseStudy` topics pointing to the current company via an incoming `Companies` relationship. Will additionally load the children of each case study. +- `Children`: A collection of child topics. Will additionally load the relationships of each child topic. +- `Contacts`: A list of `Employee` nested topics, filtered by those with `IsActive` set to `1` (`true`) and `Role` set to `Account Manager`. Additionally includes any descendants of the nested topics that meet the previous criteria. -> *Note*: Often times, data transfer objects won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional help. For instance, the `[Collection(…)]` attribute is useful if the collection key is ambiguous between outgoing relationships and incoming relationships. +> *Note*: Often times, models won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional hints. For instance, the `[Collection(…)]` attribute is useful if the collection key is ambiguous between outgoing relationships and incoming relationships. ## Polymorphism -If a reference type (e.g., `TopicViewModel Parent`) or a strongly-typed collection property (e.g., `List`) are defined, then any target instances must be assignable by the base type (in these cases, `TopicViewModel`). If they cannot be, then they will not be included; no error will occur. +If a reference type (e.g., `TopicViewModel Parent`) or a strongly-typed collection property (e.g., `List`) is defined, then any target instances must be assignable by the base type (in these cases, `TopicViewModel`). If they cannot be, then they will not be included; no error will occur. ### Filtering This can be useful for filtering a collection. For instance, if a `CompanyTopicViewModel` includes an `Employees` collection of type `List` then it will only be populated by topics that can be mapped to either `ManagerTopicViewModel` or a derivative (perhaps, `ExecutiveTopicViewModel`). Other types (e.g., `EmployeeTopicViewModel`) will be excluded, even though they might otherwise be referenced by the `Employees` collection. @@ -158,28 +161,28 @@ This can be useful for filtering a collection. For instance, if a `CompanyTopicV > *Note:* For this reason, it is recommended that view models use inheritance based on the content type hierarchy. This provides an intuitive mapping to content type definitions, avoids needing to redefine base properties, and allows for polymorphism in assigning derived types. ### Topics -While it's not a best practice, this also works for strongly-typed collections of `Topic` objects. Typically, collections should return view models, but if the collection is strongly-typed to `Topic` (or a derivative) then the source `Topic` will not be mapped, and will be used as-is assuming it implements (or derives from) the target `Topic` type. This can be useful for scenarios where a view needs full access to the object graph (such as the `SitemapController`). In such cases, it is impractical to map the entirety of an object graph, along with all attributes, to a corresponding view model graph, and makes more sense to simply return the `Topic` graph. +While it's not a best practice, this also works for strongly-typed collections of `Topic` objects. Typically, collections should return view models, but if the collection is strongly typed to `Topic` (or a derivative) then the source `Topic` will not be mapped, and will be used as-is, assuming it implements (or derives from) the target `Topic` type. This can be useful for scenarios where a view needs full access to the object graph. In such cases, it is impractical to map the entirety of an object graph, along with all attributes, to a corresponding view model graph, and makes more sense to simply return the `Topic` graph. ## Caching By default, the `TopicMappingService` will cache a reference to all `MemberInfo` objects associated with each of view model it maps. That mitigates much of the performance hit associated with the use of reflection. Despite that, simply setting properties—and, especially, on large object graphs—can require a lot of processing time. To address this, OnTopic also offers two approaches. ### Internal Caching -When a request is made to `TopicMappingService`, and internal cache is constructed. If any mapping requests refer to a `Topic` that's already been mapped as part of the _current_ object graph, then that object will be returned. This prevents unnecessary duplication of mapping, and also avoids the potential for infinite loops. For instance, if a view model includes `Children`, and those children are set to `[Include(AssociationTypes.Parents)]`, the `TopicMappingService` will point back to the originally-mapped `Parent` object, instead of mapping a new instance of that `Topic`. +When a request is made to `TopicMappingService`, and internal cache is constructed. If any mapping requests refer to a `Topic` that's already been mapped as part of the _current_ object graph, then that object will be returned. This prevents unnecessary duplication of mapping, and also avoids the potential for infinite loops. For instance, if a view model includes `Children`, and those children are set to `[Include(AssociationTypes.Parents)]`, the `TopicMappingService` will point back to the originally-mapped `Parent` object, instead of mapping a new instance of that `Topic`. If the mapping calls to include additional associations that weren't originally mapped, then those will be added to the cahced instance. ### `CachedTopicMappingService` -The [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which accepts a concrete implementation of an `ITopicMappingService`, provides caching across requests based on `topic.Id`, `Type`, and `AssociationTypes`. Because the cache is based on all three of these, it will differentiate between the results of e.g., +The [`CachedTopicMappingService`](CachedTopicMappingService.cs) decorator, which accepts a concrete implementation of an `ITopicMappingService`, provides caching across requests based on `topic.Id`, `Type`, and `AssociationTypes`. Because the cache is based on all three of these, it will differentiate between the results of e.g., - `topicMappingService.Map(topic, AssociationTypes.All)` - `topicMappingService.Map(topic, AssociationTypes.Children)` - `topicMappingService.Map(topic, AssociationTypes.Children)` -To implement the caching decorator, use the following construction as a Singleton lifestyle in your composer: +To implement the caching decorator, use the following construction as a singleton lifestyle in your composer: ```csharp var topicRepository = new SqlTopicRepository(…); var topicMappingService = new TopicMappingService(topicRepository); var cachedTopicMappingService = new CachedTopicMappingService(topicMappingService); ``` -> _**Important**_: Due to limitations discussed below, the application of the `CachedTopicMappingService` is quite restricted. It is likely inapprorpiate for page content, since that wouldn't reflect changes made via the editor. And it isn't appropriate for e.g. the `LayoutControllerBase{T}`, since it manually constructs its tree. +> _**Important**_: Due to limitations discussed below, the application of the `CachedTopicMappingService` is quite restricted. It is likely inapprorpiate for page content, since that won't reflect changes made via the editor. #### Limitations @@ -187,7 +190,7 @@ While the `CachedTopicMappingService` can be useful for particular scenarios, it 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. associations in multiple graphs. 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. -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. +3. If a graph is manually constructed (by e.g. programmatically mapping `Children`) then the first instance of a topic will be cached independent of the parent graph, thus potentially allowing it to be shared between multiple graphs. This can introduce concerns if edge maintenance is important (e.g., one instance should include children, while another does not). ## Exceptions The topic mapping services will throw a [`TopicMappingException`](TopicMappingException.cs) if a foreseeable exception occurs. Specifically, the exceptions expected will be: From cf2473736c95ce3f8f6ad8ad497f0110863721a2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:45:14 -0800 Subject: [PATCH 621/778] Fixed reference to renamed `[Include()]` attribute The `[Follows()]` attribute was renamed to `[Include()]`. --- OnTopic/Mapping/Hierarchical/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Mapping/Hierarchical/README.md b/OnTopic/Mapping/Hierarchical/README.md index bc7daffd..86c88a2d 100644 --- a/OnTopic/Mapping/Hierarchical/README.md +++ b/OnTopic/Mapping/Hierarchical/README.md @@ -7,7 +7,7 @@ The [`IHierarchicalTopicMappingService`](IHierarchicalTopicMappingService{T}. - [Example](#example-2) ## Motivation -While the [`TopicMappingService`](../README.md) is capable of populating trees on its own, it is exclusively bound to honoring the rules defined by the attributes (such as `[Follow(relationships)]` and `[Flatten]`). By contrast, the `IHierarchicalTopicMappingService` offers three additional capabilities: +While the [`TopicMappingService`](../README.md) is capable of populating trees on its own, it is exclusively bound to honoring the rules defined by the attributes (such as `[Include(associationTypes)]` and `[Flatten]`). By contrast, the `IHierarchicalTopicMappingService` offers three additional capabilities: 1. The number of tiers in the hierarchy can be restricted to a set number (via the `tiers` parameter on `GetRootViewModelAsync()` and `GetViewModelAsync()`). 2. The topics included can be constrained by specifying a method or lamda expression that accepts a `Topic` as the parameter, and returns `true` (if the `Topic` should be mapped) or `false` (if it should be skipped). From 6a6c6e90f67ee5fdfd6bb0af41327eaf2ace76cf Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 13:45:37 -0800 Subject: [PATCH 622/778] Accounted for new `TopicReferences` table in `GetTopics` summary --- OnTopic.Data.Sql.Database/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 29206d1a..3a848075 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -26,7 +26,7 @@ The following is a summary of the most relevant tables. The following is a summary of the most relevant stored procedures. ### Querying -- **[`GetTopics`](Stored%20Procedures/GetTopics.sql)**: Based on an optional `@TopicId` or `@TopicKey`, retrieves a hierarchy of topics, sorted by hierarchy, alongside separate data sets for corresponding records from `Attributes`, `ExtendedAttributes`, `Relationships`, and version history. Only retrieves the latest version data for each topic. +- **[`GetTopics`](Stored%20Procedures/GetTopics.sql)**: Based on an optional `@TopicId` or `@TopicKey`, retrieves a hierarchy of topics, sorted by hierarchy, alongside separate data sets for corresponding records from `Attributes`, `ExtendedAttributes`, `Relationships`, `TopicReferences`, and version history. Only retrieves the latest version data for each topic. - **[`GetTopicVersion`](Stored%20Procedures/GetTopicVersion.sql)**: Retrieves a single instance of a topic based on a `@TopicId` and `@Version`. Not that the `@Version` must include miliseconds. ### Updating From 52da0bc0628ce98341b725d92ce967b5d903b20c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 14:00:51 -0800 Subject: [PATCH 623/778] Reviewed, updated `README` for `OnTopic.Mapping.Hierarchical` namespace Clarified language, expanded on concepts. As there are no major updates to the `HierarchicalTopicMappingService` in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. The biggest update was a bug fix in the sample code which excluded topics of the content type `PageGroup`, instead of topics whose _parent_ are of the content type `PageGroup`. This reflects, in turn, a bug fix which enforces this logic in OnTopic 5.0.0, whereas previously the validation function inadvertently ran on the parent topic, not each individual topic. --- OnTopic/Mapping/Hierarchical/README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/OnTopic/Mapping/Hierarchical/README.md b/OnTopic/Mapping/Hierarchical/README.md index 86c88a2d..88683a62 100644 --- a/OnTopic/Mapping/Hierarchical/README.md +++ b/OnTopic/Mapping/Hierarchical/README.md @@ -1,5 +1,5 @@ # Hierarchical Topic Mapping Service -The [`IHierarchicalTopicMappingService`](IHierarchicalTopicMappingService{T}.cs) and its concrete implementation, [`HierarchicalTopicMappingService`](HierarchicalTopicMappingService{T}.cs), provide special handling for traversing hierarchical trees of view models. +The [`IHierarchicalTopicMappingService`](IHierarchicalTopicMappingService{T}.cs) and its concrete implementation, [`HierarchicalTopicMappingService`](HierarchicalTopicMappingService{T}.cs), provide special handling for traversing hierarchical trees of view models. ### Contents - [Motivation](#motivation) @@ -9,12 +9,11 @@ The [`IHierarchicalTopicMappingService`](IHierarchicalTopicMappingService{T}. ## Motivation While the [`TopicMappingService`](../README.md) is capable of populating trees on its own, it is exclusively bound to honoring the rules defined by the attributes (such as `[Include(associationTypes)]` and `[Flatten]`). By contrast, the `IHierarchicalTopicMappingService` offers three additional capabilities: -1. The number of tiers in the hierarchy can be restricted to a set number (via the `tiers` parameter on `GetRootViewModelAsync()` and `GetViewModelAsync()`). -2. The topics included can be constrained by specifying a method or lamda expression that accepts a `Topic` as the parameter, and returns `true` (if the `Topic` should be mapped) or `false` (if it should be skipped). +1. The number of tiers in the hierarchy can be restricted to a set number (via the `tiers` parameter on `GetRootViewModelAsync()` and `GetViewModelAsync()`), even if the `[Include()]` rules would allow full unlimited recursion. +2. The topics included can be constrained by specifying a validation method or lamda expression that accepts a `Topic` as the parameter, and returns either `true` (if the `Topic` should be mapped) or `false` (if it should be skipped). 3. The type that all _children_ will be mapped to can be specified, instead of letting the model type be determined exclusively by the `Topic.ContentType` property. -In many cases, these are not needed. They do, however, provide additional flexibility for particular scenarios. For example, these are valuable for constructing the navigation used by e.g. the [`MenuViewComponentBase`](../../../OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs), which should be restricted to three tiers, should be mapped to a [`NavigationTopicViewModel`](../../../OnTopic.ViewModels/NavigationTopicViewModel.cs), and, in the case of the many navigation, should exclude any topics of the content type `PageGroup`. - +In many cases, these are not needed. They do, however, provide additional flexibility for particular scenarios. For example, these are valuable for constructing the navigation used by e.g. the [`MenuViewComponentBase`](../../../OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs), which should be restricted to three tiers, should be mapped to a [`NavigationTopicViewModel`](../../../OnTopic.ViewModels/NavigationTopicViewModel.cs), and, in the case of the menu navigation, should exclude any topics of the content type `PageGroup`. ## `CachedHierarchicalTopicMappingService` The [`CachedHierarchicalTopicMappingService`](CachedHierarchicalTopicMappingService{T}.cs) caches entries keyed based on the `Topic.Id` of the root `Topic` as well as the `T` argument of the type. Because of the mechanics of the `HierarchicalTopicMappingService`, this cannot simply use the `CachedTopicMappingService` for caching, since each tier of navigation is mapped independently. This is necessary to apply the above business logic to the hierarchy, but makes it impossible for the `CachedTopicMappingService` to understand whether each request is suitable for caching. @@ -22,7 +21,7 @@ The [`CachedHierarchicalTopicMappingService`](CachedHierarchicalTopicMappingS > *Note:* As with the `CachedTopicMappingService`, the `CachedHierarchicalTopicMapping` service should be used with caution. It will not be (immediately) updated if the underlying database or topic graph are updated. And since the topic graph is already cached, it effectively doubles the memory footprint of the graph by storing it both as topics as well as view models. That said, this is useful for large view model graphs that are frequently reused—such as those that show up in the navigation of a site. ## Example -The first code block demonstrates how to construct a new instance of a `IHierarchicalTopicMappingService`. In this case, it wraps the default `HierarchicalTopicMappingService` in a `CachedHierarchicalTopicMappingService` for caching, and maps children to the `NavigationTopicViewModel` class from the [`Ignia.Topics.ViewModels`](../../../OnTopic.ViewModels/) project. Typically, this would be done in the _Composition Root_ of an application, with the service passed into e.g. a `Controller` as an `IHierarchicalTopicMappingService` dependency. +The first code block demonstrates how to construct a new instance of a `IHierarchicalTopicMappingService`. In this case, it wraps the default `HierarchicalTopicMappingService` in a `CachedHierarchicalTopicMappingService` for caching, and maps children to the `NavigationTopicViewModel` class from the [`Ignia.Topics.ViewModels`](../../../OnTopic.ViewModels/) project. Typically, this would be done in the _Composition Root_ of an application, with the service passed into e.g. a `MenuViewComponent` as an `IHierarchicalTopicMappingService` dependency. ```csharp var hierarchicalTopicMappingService = new CachedHierarchicalTopicMappingService( new HierarchicalTopicMappingService( @@ -31,22 +30,22 @@ var hierarchicalTopicMappingService = new CachedHierarchicalTopicMappingService< ); ); ``` -Once the `IHierarchicalTopicMappingService` is constructed, it can by calling the main entry point, `GetRootViewModelAsync()`, which accepts three arguments: +Once the `IHierarchicalTopicMappingService` is initialized, the hierarchical mapping can be constructed by calling the main entry point, `GetRootViewModelAsync()`, which accepts three arguments: -1. **`Topic sourceTopic`:** The topic representing the root of the hierarchy. This could be the root topic in the database, but will more likely be the root of a subtree. +1. **`Topic sourceTopic`:** The topic representing the root of the hierarchy. This could be the root topic in the database, but will more likely be the root of a subtree relative to the current request (e.g., `Root:Web`). 2. **`int tiers = 1`:** The number of tiers to crawl. While the `TopicMappingService` implementation will crawl indefinitely, given the right conditions, the `IHierarchicalTopicMappingService` can be constrained to a particular depth by the caller. -3. **`Func validationDelegate = null`:** A validation function that accepts a `Topic` as input and returns `true` if the `Topic` (and its descendants) should be included, and otherwise `false`. +3. **`Func validationDelegate = null`:** A validation function that accepts a `Topic` as input and returns either `true` if the `Topic` (and its descendants) should be included, and otherwise `false`. ```csharp await hierarchicalTopicMappingService.GetRootViewModelAsync( hierarchicalTopicMappingService.GetHierarchicalRoot(currentTopic, 2, "Web"), 2, - t => t.ContentType != "PageGroup" + t => t.Parent?.ContentType != "PageGroup" ).ConfigureAwait(false), ``` In this code example, the following arguments are used: -1. **`Topic sourceTopic`:** The `GetHierarchicalRoot()` helper function is used to find a root that is at the second tier of the topic graph (right below the database root), but within the path of the current topic. So, for instance, if the current topic is at `Root:Customers:Support:Email`, then the `GetHierarchicalRoot()` would return `Root:Customers`. -2. **`int tiers = 1`:** The number of tiers is set to 2. So in the above example, `Root:Customers:Support:Email` would be included (since `Email` is two tiers from the hierarchical root), but e.g. `Root:Customers:Support:Email:Priority` wouldn't be. ' -3. **`Func validationDelegate = null`:** The validation delegate will reject any topics of the type `PageGroup`. Typically, pages of type `PageGroup` have their own internal navigation, which shouldn't be duplicated in the primary navigation of the site. \ No newline at end of file +1. **`Topic sourceTopic`:** The `GetHierarchicalRoot()` helper method is used to find a root that is at the second tier of the topic graph (right below the database root), but within the path of the current topic. So, for instance, if the current topic is at `Root:Customers:Support:Email`, then the `GetHierarchicalRoot()` would return `Root:Customers`. +2. **`int tiers = 1`:** The number of tiers is set to 2. So in the above example, `Root:Customers:Support:Email` would be included (since `Email` is two tiers from the hierarchical root), but e.g. `Root:Customers:Support:Email:Priority` wouldn't be. +3. **`Func validationDelegate = null`:** The validation delegate will reject any topics under a `PageGroup`. Typically, pages of type `PageGroup` have their own internal navigation, which shouldn't be duplicated in the primary navigation of the site. \ No newline at end of file From dd1394d19e58f130491191224764a852141caa89 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 14:07:24 -0800 Subject: [PATCH 624/778] Standardized build badges across projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While the main page implemented all three build badges—for the NuGet package version, Azure Pipelines build process, and the Azure Release—the child pages only included the first two. This also undoes the change I previously made to the `OnTopic` project's `README`, which had moved its badges to the solution's `README` (39426ab). This was in error since `OnTopic` has its own package, just as (most of) the other projects in the solution. --- OnTopic.AspNetCore.Mvc/README.md | 1 + OnTopic.Data.Caching/README.md | 1 + OnTopic.Data.Sql/README.md | 1 + OnTopic.TestDoubles/README.md | 1 + OnTopic.ViewModels/README.md | 1 + OnTopic/README.md | 4 ++++ 6 files changed, 9 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md index 34163c60..7bc0b2f8 100644 --- a/OnTopic.AspNetCore.Mvc/README.md +++ b/OnTopic.AspNetCore.Mvc/README.md @@ -3,6 +3,7 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util [![OnTopic.AspNetCore.Mvc package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/4db5e20c-69c6-4134-823a-c3de06d1176e/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=4db5e20c-69c6-4134-823a-c3de06d1176e&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) ### Contents - [Components](#components) diff --git a/OnTopic.Data.Caching/README.md b/OnTopic.Data.Caching/README.md index 7d7a3c2b..b53ab4a9 100644 --- a/OnTopic.Data.Caching/README.md +++ b/OnTopic.Data.Caching/README.md @@ -3,6 +3,7 @@ The `CachedTopicRepository` provides an in-memory wrapper around an `ITopicRepos [![OnTopic.Data.Caching package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/3dfb3a0a-c049-407d-959e-546f714dcd0f/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3dfb3a0a-c049-407d-959e-546f714dcd0f&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) ### Contents - [Functionality](#functionality) diff --git a/OnTopic.Data.Sql/README.md b/OnTopic.Data.Sql/README.md index 8149f333..780ff3f5 100644 --- a/OnTopic.Data.Sql/README.md +++ b/OnTopic.Data.Sql/README.md @@ -3,6 +3,7 @@ The `SqlTopicRepository` provides an implementation of the `ITopicRepository` in [![OnTopic.Data.Sql package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/15c8a666-efa5-4b23-b08b-1de907478d2d/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=15c8a666-efa5-4b23-b08b-1de907478d2d&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) > *Note:* The schema for the Microsoft SQL Server implementation can be found at [`OnTopic.Data.Sql.Database`](../OnTopic.Data.Sql.Database/README.md). It is not currently distributed as part of the `SqlTopicRepository` and must be deployed separately. diff --git a/OnTopic.TestDoubles/README.md b/OnTopic.TestDoubles/README.md index 8be2351a..7f173d72 100644 --- a/OnTopic.TestDoubles/README.md +++ b/OnTopic.TestDoubles/README.md @@ -3,6 +3,7 @@ Provides common test doubles for use in testing the **OnTopic Library**. [![OnTopic.TestDoubles package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/3a741b7a-7fa1-4bdb-bc55-efbac3f04e6c/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3a741b7a-7fa1-4bdb-bc55-efbac3f04e6c&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) ### Contents - [Installation](#installation) diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md index 56c2b476..287d5b2e 100644 --- a/OnTopic.ViewModels/README.md +++ b/OnTopic.ViewModels/README.md @@ -3,6 +3,7 @@ The `OnTopic.ViewModels` assembly includes default implementations of basic view [![OnTopic.ViewModels package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/b22ec8a0-3966-4dc8-8bf5-69e6264dabd1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=b22ec8a0-3966-4dc8-8bf5-69e6264dabd1&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) > *Note:* It is not necessary to use or derive from these view models. They are provided exclusively for convenience so implementers don't need to recreate basic data models. diff --git a/OnTopic/README.md b/OnTopic/README.md index a125904e..c78b2bd3 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -2,6 +2,10 @@ The `OnTopic` assembly represents the core domain layer of the OnTopic library. It includes the primary entity ([`Topic`](Topic.cs)), abstractions (e.g., [`ITopicRepository`](Repositories/ITopicRepository.cs)), and associated classes (e.g., [`KeyedTopicCollection<>`](Collections/KeyedTopicCollection{T}.cs)). +[![OnTopic package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/fb67677f-2b83-4318-9007-0c46b4da55c1/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=fb67677f-2b83-4318-9007-0c46b4da55c1&preferRelease=true) +[![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) + ### Contents - [Entities](#entities) - [Editor](#editor) From b8ced5a14c0f550a4208736bfdd15582992d9a78 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 14:12:29 -0800 Subject: [PATCH 625/778] Reviewed, updated `README` for `OnTopic.Data.Caching` namespace Clarified language, expanded on concepts. As there are no major updates to the `CachedTopicRepository` in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. In addition to changing the version number for the package, I also emphasized that this decorator is recommended for web applications, in particular. --- OnTopic.Data.Caching/README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/OnTopic.Data.Caching/README.md b/OnTopic.Data.Caching/README.md index b53ab4a9..6c7e3c79 100644 --- a/OnTopic.Data.Caching/README.md +++ b/OnTopic.Data.Caching/README.md @@ -1,5 +1,5 @@ # OnTopic Cached Repository -The `CachedTopicRepository` provides an in-memory wrapper around an `ITopicRepository` implementation. +The `CachedTopicRepository` decorates another `ITopicRepository` implementation with an in-memory cache. It is recommended that web applications decorate their `ITopicRepository` implementation. [![OnTopic.Data.Caching package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/3dfb3a0a-c049-407d-959e-546f714dcd0f/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3dfb3a0a-c049-407d-959e-546f714dcd0f&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) @@ -11,7 +11,7 @@ The `CachedTopicRepository` provides an in-memory wrapper around an `ITopicRepos - [Usage](#usage) ## Functionality -When topics are requested, they are pulled from the cache, if they exist; otherwise, they are pulled from the underlying `ITopicRepository` implementation, and then cached. Similarly, when topics are e.g. saved, the updated versions are persisted to the underlying `ITopicRepository`, and then updated in the cache. +When topics are requested, they are pulled from the cache, if they exist; otherwise, they are pulled from the underlying `ITopicRepository` implementation, and then cached. Similarly, when topics are e.g. saved or moved, the updated versions are persisted to the underlying `ITopicRepository`, and then updated in the cache. ## Installation Installation can be performed by providing a ` to the `OnTopic.Data.Caching` **NuGet** package. @@ -19,7 +19,7 @@ Installation can be performed by providing a ` to the `OnTo … - + ``` @@ -28,8 +28,7 @@ Installation can be performed by providing a ` to the `OnTo ## Usage ```csharp -var sqlTopicRepository = new SqlTopicRepository(connectionString); -var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository); - -var rootTopic = cachedTopicRepository.Load(); +var sqlTopicRepository = new SqlTopicRepository(connectionString); +var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository); +var rootTopic = cachedTopicRepository.Load(); ``` \ No newline at end of file From c8f112905f980aff02e8d2dd1d2d31986ba52143 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 15:02:34 -0800 Subject: [PATCH 626/778] Reviewed, updated `README` for `OnTopic.AspNetCore.Mvc` namespace Clarified language, expanded on concepts. Where there are no major updates to the `OnTopic.AspNetCore.Mvc` project in OnTopic 5.0.0, there were quite a few updates from OnTopic 4.x that weren't fully documented, and are now included. This includes, for example, the `PageLevelNavigationViewComponent`, the `TopicRouteValueTransformer`, as well as the `IEndpointRouteBuilder` extension methods for handling areas, such as `MapTopicAreaRoute()` and `MapImplicitAreaControllerRoute()`. --- OnTopic.AspNetCore.Mvc/README.md | 54 +++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md index 7bc0b2f8..38eed59e 100644 --- a/OnTopic.AspNetCore.Mvc/README.md +++ b/OnTopic.AspNetCore.Mvc/README.md @@ -1,5 +1,5 @@ -# OnTopic for ASP.NET Core 3.x -The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for utilizing OnTopic with the ASP.NET Core 3.x Framework. It is the recommended client for working with OnTopic. +# OnTopic for ASP.NET Core 3.x, 5.x +The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for utilizing OnTopic with ASP.NET Core 3.x and ASP.NET Core 5.x. It is the recommended client for working with OnTopic. [![OnTopic.AspNetCore.Mvc package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/4db5e20c-69c6-4134-823a-c3de06d1176e/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=4db5e20c-69c6-4134-823a-c3de06d1176e&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) @@ -21,20 +21,27 @@ The `OnTopic.AspNetCore.Mvc` assembly provides a default implementation for util ## Components There are five key components at the heart of the ASP.NET Core implementation. - **`TopicController`**: This is a default controller instance that can be used for _any_ topic path. It will automatically validate that the `Topic` exists, that it is not disabled (`!IsDisabled`), and will honor any redirects (e.g., if the `Url` attribute is filled out). Otherwise, it will return a `TopicViewResult` based on a view model, view name, and content type. -- **`TopicViewLocationExpander`**: Assists the out-of-the-box Razor view engine in locating views associated with OnTopic, e.g. by looking in `~/Views/ContentTypes/ContentType.cshtml`, or `~/Views/ContentType/View.cshtml`. See [View Locations](#view-locations) below. -- **`TopicViewResultExecutor`**: When the `TopicController` returns a `TopicViewResult`, the `TopicViewResultExecutor` takes over and attempts to identify the correct view based on the `accept` headers, `view` query string parameter, topic's default `View` attribute and, finally, the topic's `ContentType` attribute. See [View Matching](#view-matching) below. +- **`TopicRouteValueTransformer`**: A `DynamicRouteValueTransformer` for use with the ASP.NET Core's `MapDynamicControllerRoute()` method, allowing for route parameters to be implicitly inferred; notably, it will use the `area` as the default `controller` and `rootTopic`, if those route parameters are not otherwise defined. +- **`TopicViewLocationExpander`**: Assists the out-of-the-box Razor view engine in locating views associated with OnTopic, e.g. by looking in `~/Views/ContentTypes/{ContentType}.cshtml`, or `~/Views/{ContentType}/{View}.cshtml`. See [View Locations](#view-locations) below. +- **`TopicViewResultExecutor`**: When the `TopicController` returns a `TopicViewResult`, the `TopicViewResultExecutor` takes over and attempts to identify the correct view based on the `accept` headers, `?view=` query string parameter, topic's default `View` attribute and, finally, the topic's `ContentType` attribute. See [View Matching](#view-matching) below. - **`ServiceCollectionExtensions`**: A set of extensions to be used in an ASP.NET Core website's `Startup` class that automatically handle registering services, controllers, and other extensions from `OnTopic.AspNetCore.Mvc`. - **`ITopicRepositoryExtensions`**: A set of extensions that allows loading topics based on an ASP.NET Core `RouteData` collection, including `OnTopic` route variables, such as `path` and `contenttype`. -> **Note:** In [`OnTopic.Web.Mvc`](https://github.com/OnTopic/OnTopic-MVC/), the `TopicViewEngine` took on the responsibilities now handled by the `TopicViewLocationExpander`, `TopicViewResult` responsibilities for `TopicViewResultExecutor`, and the `MvcTopicRoutingService` responsibilities for `ITopicRepositoryExtensions`. - ## Controllers and View Components -There are six main controllers and view components that ship with the ASP.NET Core implementation. In addition to the core **`TopicController`**, these include the following ancillary classes: -- **`RedirectController`**: Provides a single `Redirect` action which can be bound to a route such as `/Topic/{ID}/`; this provides support for permanent URLs that are independent of the `GetWebPath()`. -- **`SitemapController`**: Provides a single `Sitemap` action which recurses over the entire Topic graph, including all attributes, and returns an XML document with a sitemaps.org schema. -- **`MenuViewComponentBase`**: Provides support for a navigation menu by automatically mapping the top three tiers of the current namespace (e.g., `Web`, its children, and grandchildren). Can accept any `INavigationTopicViewModel` as a generic argument; that will be used as the view model for each mapped instance. - -> **Note:** There is not a practical way for ASP.NET Core to provide routing for generic controllers and view components. As such, these _must_ be subclassed by each implementation. The derived class needn't do anything outside of provide a specific type reference to the generic base. +There are five main controllers and view components that ship with the ASP.NET Core implementation. In addition to the core **`TopicController`**, these include the following ancillary classes: +- **[`RedirectController`](Controllers/RedirectController.cs)**: Provides a single `Redirect` action which can be bound to a route such as `/Topic/{ID}/`; this provides support for permanent URLs that are independent of the `GetWebPath()`. +- **[`SitemapController`](Controllers/SitemapController.cs)**: Provides a single `Sitemap` action which recurses over the entire Topic graph, including all attributes, and returns an XML document with a sitemaps.org schema. +- **[`MenuViewComponentBase`](Components/MenuViewComponentBase{T}.cs)**: Provides support for a navigation menu by automatically mapping the top three tiers of the current namespace (e.g., `Web`, its children, and grandchildren). Can accept any `INavigationTopicViewModel` as a generic argument; that will be used as the view model for each mapped instance. +- **[`PageLevelNavigationViewComponentBase`](Components/PageLevelNavigationViewComponentBase{T}.cs)**: Provides support for page-level navigation by automatically mapping the child topics from the nearest `PageGroup`. Can accept any `INavigationTopicViewModel` as a generic argument; that will be used as the view model for each mapped instance. + +> **Note:** There is no practical way for ASP.NET Core to provide routing for generic controllers and view components. As such, these _must_ be subclassed by each implementation. The derived class needn't do anything outside of provide a specific type reference to the generic base. For example: +> ```csharp +> public class MenuViewComponent: MenuViewComponentBase { +> public MenuViewComponent( +> ITopicRepository topicRepository, +> IHierarchicalTopicMappingService hierarchicalTopicMappingService +> ): base(topicRepository, hierarchicalTopicMappingService) {} +> } ## View Conventions By default, OnTopic matches views based on the current topic's `ContentType` and, if available, `View`. @@ -47,6 +54,8 @@ There are multiple ways for a view to be set. The `TopicViewResultExecutor` will - **`View`** attribute (i.e., `topic.View`) - **`ContentType`** attribute (i.e., `topic.ContentType`) +This allows multiple views to be available for any individual content type, thus allowing pages using the same content type to potentially be rendered with different layouts or, even, different content types (e.g., JSON vs. HTML). + ### View Locations For each of the above [View Matching](#view-matching) rules, the `TopicViewLocationExpander` will search the following locations for a matching view: - `~/Views/{Controller}/{View}.cshtml` @@ -58,7 +67,7 @@ For each of the above [View Matching](#view-matching) rules, the `TopicViewLocat - `~/Views/ContentTypes/{View}.cshtml` - `~/Views/Shared/{View}.cshtml` -> *Note:* After searching each of these locations for each of the [View Matching](#view-matching) rules, control will be handed over to the [`RazorViewEngine`](https://msdn.microsoft.com/en-us/library/system.web.mvc.razorviewengine%28v=vs.118%29.aspx?f=255&MSPPError=-2147217396), which will search the out-of-the-box default locations for ASP.NET MVC. +> *Note:* After searching each of these locations for each of the [View Matching](#view-matching) rules, control will be handed over to the [`RazorViewEngine`](https://msdn.microsoft.com/en-us/library/system.web.mvc.razorviewengine%28v=vs.118%29.aspx?f=255&MSPPError=-2147217396), which will search the out-of-the-box default locations for ASP.NET Core. ### Example If the `topic.ContentType` is `ContentList` and the `Accept` header is `application/json` then the `TopicViewResult` and `TopicViewEngine` would coordinate to search the following paths: @@ -89,7 +98,7 @@ Installation can be performed by providing a ` to the `OnTo … - + ``` @@ -105,11 +114,11 @@ public class Startup { } } ``` -> *Note:* This will register the `TopicViewLocationExpander`, `TopicViewResultExecutor`, as well as all [Controllers](#controllers) that ship with `OnTopic.AspNetCore.Mvc`. +> *Note:* This will register the `TopicViewLocationExpander`, `TopicViewResultExecutor`, `TopicRouteValueTransformer`, as well as all [Controllers](#controllers) that ship with `OnTopic.AspNetCore.Mvc`. In addition, within the same `ConfigureServices()` method, you will need to establish a class that implements `IControllerActivator` and `IViewComponentActivator`, and will represent the site's _Composition Root_ for dependency injection. This will typically look like: ```csharp -var activator = new OrganizationNameControllerActivator(Configuration.GetConnectionString("OnTopic") +var activator = new OrganizationNameActivator(Configuration.GetConnectionString("OnTopic")) services.AddSingleton(activator); services.AddSingleton(activator); ``` @@ -125,9 +134,16 @@ When registering routes via `Startup.Configure()` you may register any routes fo public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseEndpoints(endpoints => { - endpoints.MapTopicRoute("Web"); - endpoints.MapTopicRedirect(); + + endpoints.MapTopicAreaRoute(); // {area:exists}/{**path} + endpoints.MapImplicitAreaControllerRoute(); // {area:exists}/{action=Index} + endpoints.MapDefaultControllerRoute(); // {controller=Home}/{action=Index}/{id?} + endpoints.MapDefaultAreaControllerRoute(); // {area:exists}/{controller}/{action=Index}/{id?} + + endpoints.MapTopicRoute("Web"); // Web/{**path} + endpoints.MapTopicRedirect(); // Topic/{topicId} endpoints.MapControllers(); + }); } } @@ -151,4 +167,4 @@ return controllerType.Name switch { ``` For a complete reference template, including the ancillary controllers, view components, and a more maintainable structure, see the [`OrganizationNameActivator.cs`](https://gist.github.com/JeremyCaney/00c04b1b9f40d9743793cd45dfaaa606) Gist. Optionally, you may use a dependency injection container. -> *Note:* The default `TopicController` will automatically identify the current topic (based on the `RouteData`), map the current topic to a corresponding view model (based on [the `TopicMappingService` conventions](../OnTopic/Mapping/README.md)), and then return a corresponding view (based on the [view conventions](#view-conventions)). For most applications, this is enough. If custom mapping rules or additional presentation logic are needed, however, implementors can subclass `TopicController`. +> *Note:* The default `TopicController` will automatically identify the current topic (based on the `RouteData`), map the current topic to a corresponding view model (based on [the `TopicMappingService` conventions](../OnTopic/Mapping/README.md)), and then return a corresponding view (based on the [view conventions](#view-conventions)). For most applications, this is enough. If custom mapping rules or additional presentation logic are needed, however, implementors can subclass `TopicController`. \ No newline at end of file From 08ac1e73f6f164370378414479d1a149ef60296d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 15:13:56 -0800 Subject: [PATCH 627/778] Reviewed, updated `README` for `OnTopic.ViewModels` namespace Clarified language, expanded on concepts. As there are no major updates to the `OnTopic.ViewModels` project in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. In addition to changing the version number for the package, I also expanded on the behavior of the `DynamicTopicViewModelLookupService`, and preferred the more accurate term "parameterless constructor" to "default constructor". --- OnTopic.ViewModels/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md index 287d5b2e..7da678b7 100644 --- a/OnTopic.ViewModels/README.md +++ b/OnTopic.ViewModels/README.md @@ -13,7 +13,7 @@ The `OnTopic.ViewModels` assembly includes default implementations of basic view - [Usage](#usage) - [`DynamicTopicViewModelLookupService`](#DynamicTopicViewModelLookupService) - [Design Considerations](#design-considerations) - - [Default Constructor](#default-constructor) + - [Parameterless Constructor](#parameterless-constructor) - [Inheritance](#inheritance) ## Installation @@ -22,7 +22,7 @@ Installation can be performed by providing a ` to the `OnTo … - + ``` @@ -51,18 +51,17 @@ Installation can be performed by providing a ` to the `OnTo By default, the [`OnTopic.AspNetCore.Mvc`](../OnTopic.AspNetCore.Mvc/README.md)'s [`TopicController`](../OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs) uses the out-of-the-box [`TopicMappingService`](../OnTopic/Mapping) to map topics to view models. For applications primarily relying on the out-of-the-box view models, it is recommended that the [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) be used; this includes all of the out-of-the-box view models, and can be derived to add application-specific view models. ### `DynamicTopicViewModelLookupService` -For applications with a large number of view models, it may be preferable to use the `DynamicTopicViewModelLookupService`, which will attempt to map topics to view models based on the naming convention `{ContentType}TopicViewModel`, from any assembly or namespace. If the `OnTopic.ViewModels.dll` is in an application's `/bin` directory then these view models will be available to the lookup service and, thus, the mapping service. If any classes with the same name are available in _any other assembly or namespace_ then they will override the `ViewModels` from this assembly. That allows these classes to be treated as default fallbacks. +For applications with a large number of view models, it may be preferable to use the `DynamicTopicViewModelLookupService`, which will attempt to map topics to view models from any assembly or namespace based on the naming convention, `{ContentType}TopicViewModel` or `{ContentType}ViewModel`. If a reference to the `OnTopic.ViewModels` package is included in a project's `csproj`, then these view models will be available to the lookup service and, thus, the mapping service. If any classes with the same name are available in _any other assembly or namespace_ then they will override the `ViewModels` from this assembly. That allows these classes to be treated as default fallbacks. -> *Note:* If a base class is overwritten then topics that derive from the original version will continue to do so unless they are _also_ overwritten. For example, if a `Theme` property is added to a customer-specific `PageTopicViewModel`, the `Theme` property won't be available on e.g. `SlideShowTopicViewModel` unless it is _also_ overwritten by the customer to inherit from their `PageTopicViewModel`. +> *Note:* If a base class is overwritten, topics that derive from the original version will continue to do so unless they are _also_ overwritten. For example, if a `Theme` property is added to a customer-specific `PageTopicViewModel`, the `Theme` property won't be available on e.g. `SlideShowTopicViewModel` unless it is _also_ overwritten by the customer to inherit from their custom `PageTopicViewModel`. ## Design Considerations As view models, not all attributes and associations are exposed. The properties chosen are optimized around values that are expected to be of common interest to most views. -### Default Constructor -All of the view models assume a default constructor (e.g., `new TopicViewModel()`). This is necessary to provide compatibility with the `TopicMappingService` which will attempt to create new instances of view models based on the default constructor. +### Parameterless Constructor +All of the view models assume a parameterless constructor (e.g., `new TopicViewModel()`), which can optionally be the default constructor if no other constructors are required. This is necessary to provide compatibility with the `TopicMappingService`, which will attempt to create new instances of view models based on the the topic's `ContentType`, using the view models parameterless constructor. ### Inheritance The view models map to the hierarchy of the content types in OnTopic, with each view model only including properties that are _specific_ to that content type. So, for example, [`PageTopicViewModel`](PageTopicViewModel.cs) includes a `Body` property, which is introduced by the `Page` content type, but doesn't include e.g. `Key`, `ContentType`, or `Title`; these are all inherited from the base [`TopicViewModel`](TopicViewModel.cs). -This is advantageous not only because it effectively models the familiar content type hierarchy, but also because it allows for polymorphism in the mapping library. So, for example, if a property accepts a `List` then this can contain any view models that implement or derive from `PageTopicViewModel` (e.g., `SlideshowTopicViewModel`, `VideoTopicViewModel`, &c.). - +This is advantageous not only because it effectively models the familiar content type hierarchy, but also because it allows for polymorphism in the mapping library. So, for example, if a property accepts a `Collection`, then this can also contain any view models that derive from the `PageTopicViewModel` (e.g., `SlideshowTopicViewModel`, `VideoTopicViewModel`, &c.). \ No newline at end of file From e9c22c8bce2f1570ac6dc92498ba7bd3b17f4e7e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 15:17:31 -0800 Subject: [PATCH 628/778] Reviewed, updated `README` for `OnTopic.Data.Sql` namespace Clarified language, expanded on concepts. As there are no major updates to the public interface of the `OnTopic.Data.Sql` project in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. In addition to changing the version number for the package, I also clarified that the caching applies both to read and write operations. --- OnTopic.Data.Sql/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql/README.md b/OnTopic.Data.Sql/README.md index 780ff3f5..b6a8b452 100644 --- a/OnTopic.Data.Sql/README.md +++ b/OnTopic.Data.Sql/README.md @@ -1,5 +1,5 @@ # OnTopic SQL Repository -The `SqlTopicRepository` provides an implementation of the `ITopicRepository` interface for use with Microsoft SQL Server. All requests are sent to the database, with no effort to cache data. +The `SqlTopicRepository` provides an implementation of the `ITopicRepository` interface for use with Microsoft SQL Server. All requests are sent directly to the database, with no effort to first retrieve the data from, or subsequently store the data in, a local cache. [![OnTopic.Data.Sql package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/15c8a666-efa5-4b23-b08b-1de907478d2d/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=15c8a666-efa5-4b23-b08b-1de907478d2d&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) @@ -17,7 +17,7 @@ Installation can be performed by providing a ` to the `OnTo … - + ``` From caade4cb1854004b472a5e6765ca626d82bfa2c8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 15:27:08 -0800 Subject: [PATCH 629/778] Reviewed, updated `README` for `OnTopic.Data.Sql.Database` namespace Clarified language, expanded on concepts. As the major updates to the public interface of the `OnTopic.Data.Sql.Database` project were previously accounted for in OnTopic 5.0.0, most of these changes are minor, and simply provide emphasis or more consistency in wording. --- OnTopic.Data.Sql.Database/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md index 3a848075..ef86dcb1 100644 --- a/OnTopic.Data.Sql.Database/README.md +++ b/OnTopic.Data.Sql.Database/README.md @@ -1,5 +1,5 @@ # SQL Schema -The `OnTopic.Data.Sql.Database` provides a default schema for supporting the [`SqlTopicRepository`](../OnTopic.Data.Sql). +The `OnTopic.Data.Sql.Database` provides a default schema for supporting the [`SqlTopicRepository`](../OnTopic.Data.Sql/README.md). > *Note:* In addition to the objects below—which are all part of the default `[dbo]` schema—there is also a [`[Utilities]`](Utilities/README.md) schema which provides stored procedures for use by administrators in maintening the database. @@ -31,8 +31,8 @@ The following is a summary of the most relevant stored procedures. ### Updating - **[`CreateTopic`](Stored%20Procedures/CreateTopic.sql)**: Creates a new topic based on a `@ParentId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Returns a new `@TopicId`. -- **[`DeleteTopic`](Stored%20Procedures/DeleteTopic.sql)**: Deletes an existing topic based on a `@TopicId`. -- **[`MoveTopic`](Stored%20Procedures/MoveTopic.sql)**: Moves an existing topic based on a `@TopicId`, `@ParentId`, and `@SiblingId`. +- **[`DeleteTopic`](Stored%20Procedures/DeleteTopic.sql)**: Deletes an existing topic and all descendant based on a `@TopicId`. +- **[`MoveTopic`](Stored%20Procedures/MoveTopic.sql)**: Moves an existing topic based on a `@TopicId`, `@ParentId`, and an optional `@SiblingId`. - **[`UpdateTopic`](Stored%20Procedures/UpdateTopic.sql)**: Updates an existing topic based on a `@TopicId`, an `AttributeValues` list of `@Attributes`, and an XML `@ExtendedAttributes`. Old attributes are persisted as previous versions. - **[`UpdateAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the indexed attributes, optionally removing any whose values aren't matched in the provided `@Attributes` parameter. - **[`UpdateExtendedAttributes`](Stored%20Procedures/UpdateAttributes.sql)**: Updates the extended attributes, assuming the `@ExtendedAttributes` parameter doesn't match the previous value. @@ -46,18 +46,18 @@ The following is a summary of the most relevant stored procedures. - **[`GetAttributes`](functions/GetAttributes.sql)**: Given a `@TopicID`, provides the latest version of each attribute value from both `Attributes` and `ExtendedAttributes`, excluding key attributes (i.e., `Key`, `ContentType`, and `ParentID`). - **[`GetChildTopicIDs`](functions/GetChildTopicIDs.sql)**: Given a `@TopicID`, returns a list of `TopicID`s that are immediate children. - **[`GetExtendedAttribute`](Functions/GetExtendedAttribute.sql)**: Retrieves an individual attribute from a topic's latest `ExtendedAttributes` record. -- **[`FindTopicIDs`](Functions/FindTopicIDs.sql)**: Retrieves all `TopicID`s under a given `@TopicID` that match the `@AttributeKey` and `@AttributeValue`. Accepts `@IsExtendedAttribute` and `@UsePartialMatch`. +- **[`FindTopicIDs`](Functions/FindTopicIDs.sql)**: Retrieves all `TopicID`s under a given `@TopicID` that match the `@AttributeKey` and `@AttributeValue`. Accepts `@IsExtendedAttribute` and `@UsePartialMatch` parameters. ## Views -The majority of the views provide records corresponding to the latest version of records for each topic. These include: +The majority of the views provide records corresponding to the latest version for each topic. These include: - **[`AttributeIndex`](Views/AttributeIndex.sql)**: Includes `TopicId`, `AttributeKey` and nullable `AttributeValue`. - **[`ExtendedAttributesIndex`](Views/ExtendedAttributeIndex.sql)**: Includes `TopicId` and `AttributeXml`. -- **[`RelationshipIndex`](Views/RelationshipIndex.sql)**: Includes the `Source_TopicID`, `RelationshipKey`, `Target_TopicID, and `IsDeleted`. +- **[`RelationshipIndex`](Views/RelationshipIndex.sql)**: Includes the `Source_TopicID`, `RelationshipKey`, `Target_TopicID`, and `IsDeleted`. - **[`ReferenceIndex`](Views/ReferenceIndex.sql)**: Includes `Source_TopicID`, `ReferenceKey`, and nullable `Target_TopicID`. - **[`VersionHistoryIndex`](Views/VersionHistoryIndex.sql)**: Includes up to the last five `Version` records for every `TopicId`. ## Types -User-defined table valued types are used to relay arrays of information to (and between) the stored procedures. These can be mimicked in C# using e.g. a `DataTable`. These include: +User-defined table-valued types are used to relay arrays of information to (and between) the stored procedures. These can be mimicked in C# using e.g. a `DataTable`. These include: - **[`AttributeValues`](Types/AttributeValues.sql)**: Defines a table with an `AttributeKey` `Varchar(128)` and `AttributeValue` `Varchar(255)` columns. - **[`TopicList`](Types/TopicList.sql)**: Defines a table with a single `TopicId` `Int` column for passing lists of topics. - **[`TopicReferences`](Types/TopicReferences.sql)**: Defines a table with a `ReferenceKey` `Varchar(128)` and a `Target_TopicId` `Int` column for passing lists of topic references. \ No newline at end of file From 0dad1b9029aa502d41917f6d8ffa8aa5478e006c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 15:56:31 -0800 Subject: [PATCH 630/778] Reviewed, updated `README` for `OnTopic.TestDoubles` namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There haven't been any real updates to the `TestDoubles` as part of OnTopic 5.0.0. That said, there were gaps in the documentation. Most notably, I documented the full structure of the `StubTopicRepository`, which will make it easier to work with. I also introduced a note that this package isn't intended for customer implementation—though, of course, nothing prevents customers from using it, if it's needed. --- OnTopic.TestDoubles/README.md | 53 +++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/OnTopic.TestDoubles/README.md b/OnTopic.TestDoubles/README.md index 7f173d72..4446a0de 100644 --- a/OnTopic.TestDoubles/README.md +++ b/OnTopic.TestDoubles/README.md @@ -1,5 +1,7 @@ # `TestDoubles` -Provides common test doubles for use in testing the **OnTopic Library**. +Provides common test doubles for use in testing the **OnTopic Library**. + +> _Note:_ This package is primarily intended for use by other OnTopic libraries that benefit from sharing testing infrastructure; most OnTopic implementations should not require this package. [![OnTopic.TestDoubles package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/3a741b7a-7fa1-4bdb-bc55-efbac3f04e6c/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3a741b7a-7fa1-4bdb-bc55-efbac3f04e6c&preferRelease=true) [![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) @@ -17,7 +19,7 @@ Installation can be performed by providing a ` to the `OnTo … - + ``` @@ -35,4 +37,49 @@ Dummies provide no actual functionality and are not expected to function correct ### Stubs Stubs not only satisfy the interface, but will return canned data that tests can operate against, thus allowing unit tests to interact with predetermined scenarios against the service. -- [`StubTopicRepository`](StubTopicRepository.cs) +#### `StubTopicRepository` + +The [`StubTopicRepository`](StubTopicRepository.cs) automatically generates an in-memory topic graph with the following structure: + +- `Root` (`Container`) + - `Configuration` (`Container`) + - `ContentTypes` (`ContentTypeDescriptor`) + - `ContentTypeDescriptor` (`ContentTypeDescriptor`) + - `AttributeDescriptor` (`ContentTypeDescriptor`) + - `BooleanAttributeDescriptor` (`ContentTypeDescriptor`) + - `NestedTopicListAttributeDescriptor` (`ContentTypeDescriptor`) + - `NumberAttributeDescriptor` (`ContentTypeDescriptor`) + - `RelationshipAttributeDescriptor` (`ContentTypeDescriptor`) + - `TextAttributeDescriptor` (`ContentTypeDescriptor`) + - `TopicReferenceAttributeDescriptor` (`ContentTypeDescriptor`) + - `Page` (`ContentTypeDescriptor`) + - `Contact` (`ContentTypeDescriptor`) + - `Metadata` (`Container`) + - `Categories` (`Lookup`) + - `Web` (`Container`) + - `Web_0` (`Page`) + - `Web_0_0` (`Page`) + - `Web_0_0_0` (`Page`) + - `Web_0_0_0_0` (`Page`) + - `Web_0_0_0_1` (`Page`) + - `Web_0_0_1` (`Page`) + - `Web_0_0_1_0` (`Page`) + - `Web_0_0_1_1` (`Page`) + - `Web_1` (`Page`) + - `Web_1_0` (`Page`) + - `Web_1_0_0` (`Page`) + - `Web_1_0_0_0` (`Page`) + - `Web_1_0_0_1` (`Page`) + - `Web_1_0_1` (`Page`) + - `Web_1_0_1_0` (`Page`) + - `Web_1_0_1_1` (`Page`) + - `Web_1_1` (`Page`) + - `Web_1_1_0` (`Page`) + - `Web_1_1_0_0` (`Page`) + - `Web_1_1_0_1` (`Page`) + - `Web_1_1_1` (`Page`) + - `Web_1_1_1_0` (`Page`) + - `Web_1_1_1_1` (`Page`) + - `Web_3` (`PageGroup`) + - `Web_3_0` (`Page`) + - `Web_3_1` (`Page`) \ No newline at end of file From a6db252302852093708c0f5a27d396246539d2a0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 16:20:04 -0800 Subject: [PATCH 631/778] Rename `TrackedItem` to `TrackedRecord` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will facilitate a more intuitive naming convention for derived classes later on. This required updating a lot of XML Docs—which, in turn, required a lot of rewrapping. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 18 +- OnTopic.Tests/TopicReferenceCollectionTest.cs | 2 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 6 +- OnTopic/Associations/TopicReference.cs | 8 +- OnTopic/Attributes/AttributeValue.cs | 16 +- .../Attributes/AttributeValueCollection.cs | 4 +- .../AttributeValueCollectionExtensions.cs | 16 +- ...ckedCollection{TItem,TValue,TAttribute}.cs | 162 +++++++++--------- ...{TrackedItem{T}.cs => TrackedRecord{T}.cs} | 26 +-- .../Reflection/TopicPropertyDispatcher.cs | 4 +- OnTopic/Metadata/AttributeDescriptor.cs | 4 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 4 +- OnTopic/Repositories/TopicRepository.cs | 13 +- OnTopic/Topic.cs | 16 +- OnTopic/TopicFactory.cs | 2 +- 15 files changed, 151 insertions(+), 150 deletions(-) rename OnTopic/Collections/Specialized/{TrackedItem{T}.cs => TrackedRecord{T}.cs} (84%) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 386fa8a1..d0eeb1d5 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -318,7 +318,7 @@ public void Clear_ExistingValues_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets the value of a custom to the existing value and ensures it is not marked as - /// . + /// . /// [TestMethod] public void SetValue_ValueUnchanged_IsNotDirty() { @@ -337,7 +337,7 @@ public void SetValue_ValueUnchanged_IsNotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is marked as . Confirms that returns + /// cref="TrackedRecord{T}.IsDirty"/>. Confirms that returns /// true. /// [TestMethod] @@ -375,7 +375,7 @@ public void IsDirty_DeletedValues_ReturnsTrue() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is not marked as - /// . Confirms that returns + /// . Confirms that returns /// false/ /// [TestMethod] @@ -394,7 +394,7 @@ public void IsDirty_NoDirtyValues_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a that is not marked as - /// as well as a LastModified that is. Confirms + /// as well as a LastModified that is. Confirms /// that returns false. /// [TestMethod] @@ -415,7 +415,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a and then deletes it. Confirms - /// that the returns the new version after calling returns the new version after calling . /// [TestMethod] @@ -481,8 +481,8 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates a associated with an - /// with a that is not marked as and then confirms that - /// returns true. + /// with a that is not marked as and then confirms + /// that returns true. /// [TestMethod] public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { @@ -670,8 +670,8 @@ public void Add_InvalidAttributeValue_ThrowsException() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new which maps to directly to a and confirms that the original is replaced if the - /// changes. + /// "AttributeValueCollection"/> and confirms that the original is replaced if the + /// changes. /// [TestMethod] public void Add_WithBusinessLogic_MaintainsIsDirty() { diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index 150e9797..e56eae91 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -116,7 +116,7 @@ public void Clear_ExistingReferences_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new and adds a new reference using with set to false + /// KeyedCollection{TKey, TItem}.InsertItem(Int32, TItem)"/> with set to false /// , confirming that remains true since /// the target is unsaved. /// diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 2b3dc731..43284c42 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -279,8 +279,8 @@ public void GetAttributes_ExtendedAttributes_ReturnsExtendedAttributes() { | TEST: GET ATTRIBUTES: EXTENDED ATTRIBUTE MISMATCH: RETURNS EXTENDED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a list of attributes from a topic, filtering by . Expects an to be returned even if it's not but its + /// Retrieves a list of attributes from a topic, filtering by . Expects an to be returned even if it's not but its /// disagrees with . /// [TestMethod] @@ -301,7 +301,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() | TEST: GET ATTRIBUTES: EXTENDED ATTRIBUTE MISMATCH: RETURNS NOTHING \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a list of attributes from a topic, filtering by . Expects the . Expects the to not be returned even though its /// disagrees with , since it won't match the 's isExtendedAttribute call. diff --git a/OnTopic/Associations/TopicReference.cs b/OnTopic/Associations/TopicReference.cs index 6ac1f741..ac3aa222 100644 --- a/OnTopic/Associations/TopicReference.cs +++ b/OnTopic/Associations/TopicReference.cs @@ -18,9 +18,9 @@ namespace OnTopic.Associations { /// /// /// - /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. + /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. /// /// /// Typically, the will be exposed as part of a via @@ -37,7 +37,7 @@ namespace OnTopic.Associations { /// />'s method. /// /// - public record TopicReference: TrackedItem { + public record TopicReference: TrackedRecord { /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeValue.cs index f6cf9f86..2dcdee3f 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeValue.cs @@ -18,9 +18,9 @@ namespace OnTopic.Attributes { /// /// /// - /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. + /// Provides values and metadata specific to individual attribute values, such as state (e.g., the property signifies whether the attribute value has changed) and its date. /// /// /// Typically, the will be exposed as part of a via @@ -39,7 +39,7 @@ namespace OnTopic.Attributes { /// method. /// /// - public record AttributeValue: TrackedItem { + public record AttributeValue: TrackedRecord { /*========================================================================================================================== | CONSTRUCTOR @@ -101,10 +101,10 @@ public AttributeValue( /// /// /// This is important because, otherwise, implementations rely primarily on to determine if a value should be saved. If an attribute's value hasn't changed, but - /// the location it should be stored has, that could potentially result in the attribute being deleted, as the attribute - /// won't show up for when is called with isDirty set to true - /// and isExtendedAttribute is set to either true or false. By introducing to determine if a value should be saved. If an attribute's value hasn't changed, + /// but the location it should be stored has, that could potentially result in the attribute being deleted, as the + /// attribute won't show up for when is called with isDirty set to + /// true and isExtendedAttribute is set to either true or false. By introducing , the is able to detect conflicts between the configuration and /// the underlying data store, and ensure data is stored appropriately. /// diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 6338922b..cfa00156 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -89,8 +89,8 @@ public bool IsDirty(bool excludeLastModified) /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index e4490904..3d1a5bcd 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -183,8 +183,8 @@ out var result /// The string identifier for the . /// The boolean value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// @@ -221,8 +221,8 @@ public static void SetBoolean( /// The string identifier for the . /// The integer value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// @@ -263,8 +263,8 @@ public static void SetInteger( /// The string identifier for the . /// The double value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// @@ -305,8 +305,8 @@ public static void SetDouble( /// The string identifier for the . /// The value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs index d4989171..94ea5177 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs @@ -18,11 +18,11 @@ namespace OnTopic.Collections.Specialized { | CLASS: TRACKED COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Represents a collection of records, along with methods "updating" those records and working - /// with their state. + /// Represents a collection of records, along with methods "updating" those records and + /// working with their state. /// /// - /// records represent individual instances of values associated with a particular records represent individual instances of values associated with a particular . The class tracks these through e.g. its property. The class provides a base class with methods for working with these /// records, such as , for determining if a given record has been modified, or public abstract class TrackedCollection : KeyedCollection, ITrackDirtyKeys - where TItem: TrackedItem, new() + where TItem: TrackedRecord, new() where TAttribute: Attribute where TValue : class { @@ -79,18 +79,18 @@ internal TrackedCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnor | PROPERTY: DELETED ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// When a is deleted, keep track of it so that it can be marked for deletion when the is deleted, keep track of it so that it can be marked for deletion when the is saved. /// /// /// As a performance enhancement, implementations will only save topics that are marked as - /// . If a is deleted, then it won't be marked as . If no other instances were modified, then the won't get saved, and that won't be deleted. Further more, methods like the method have no way of detecting the deletion of arbitrary - /// values—i.e., attributes that were deleted which don't correspond to attributes configured on the . By tracking any deleted instances, we ensure both scenarios can - /// be accounted for. + /// . If a is deleted, then it won't be marked as . If no other instances were modified, then the won't get saved, and that won't be deleted. Further more, methods like + /// the method have no way of detecting the deletion of + /// arbitrary values—i.e., attributes that were deleted which don't correspond to attributes configured on the . By tracking any deleted instances, we ensure both + /// scenarios can be accounted for. /// internal List DeletedItems { get; } = new(); @@ -102,20 +102,20 @@ internal TrackedCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnor public virtual bool IsDirty() => DeletedItems.Count > 0 || Items.Any(a => a.IsDirty); /// - /// Determine if a given is marked as . Will return - /// false if the cannot be found in the collection. + /// Determine if a given is marked as . Will return + /// false if the cannot be found in the collection. /// /// /// This method is intended primarily for data storage providers, such as , which may need - /// to determine the state of a prior to saving it to - /// the data storage medium. Because is a state of the current , it does not support inheritFromParent or inheritFromBase (which otherwise default to - /// true). + /// to determine the state of a prior to saving it + /// to the data storage medium. Because is a state of the current , it does not support inheritFromParent or inheritFromBase (which otherwise default + /// to true). /// - /// The string identifier for the . + /// The string identifier for the . /// - /// Returns true if the is marked as ; otherwise - /// false. + /// Returns true if the is marked as ; + /// otherwise false. /// public bool IsDirty(string key) { if (!Contains(key)) { @@ -132,17 +132,17 @@ public bool IsDirty(string key) { public void MarkClean() => MarkClean((DateTime?)null); /// - /// Marks the collection—including all instances—as clean, meaning they have been persisted + /// Marks the collection—including all instances—as clean, meaning they have been persisted /// to the underlying . /// /// /// This method is intended primarily for data storage providers, such as , so that they can - /// mark the collection, and all instances it contains, as clean. After this, method will return false until any instances are added, modified, or - /// removed. + /// mark the collection, and all instances it contains, as clean. After this, method will return false until any instances are added, modified, + /// or removed. /// /// - /// The value that the was last saved. This corresponds to the value that the was last saved. This corresponds to the . /// public void MarkClean(DateTime? version) { @@ -161,16 +161,16 @@ public void MarkClean(DateTime? version) { public void MarkClean(string key) => MarkClean(key, null); /// - /// Marks an individual as clean. + /// Marks an individual as clean. /// /// /// This method is intended primarily for data storage providers, such as , so that they can - /// mark an as clean. After this, will return false for - /// that item until it is modified. + /// mark an as clean. After this, will return false + /// for that item until it is modified. /// - /// The string identifier for the . + /// The string identifier for the . /// - /// The value that the was last saved. This corresponds to the value that the was last saved. This corresponds to the . /// public void MarkClean(string key, DateTime? version) { @@ -190,9 +190,9 @@ public void MarkClean(string key, DateTime? version) { \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets a from the collection based on the . + /// Gets a from the collection based on the . /// - /// The string identifier for the . + /// The string identifier for the . /// /// Boolean indicator nothing whether to recusrively search through s in order to get the value. /// @@ -200,11 +200,11 @@ public void MarkClean(string key, DateTime? version) { public TValue? GetValue(string key, bool inheritFromParent = false) => GetValue(key, null, inheritFromParent); /// - /// Gets a from the collection based on the with a specified - /// , an optional setting to enable , and an optional - /// setting for . + /// Gets a from the collection based on the with a + /// specified , an optional setting to enable , and an + /// optional setting for . /// - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to recusrively search through s in order to get the value. @@ -213,7 +213,7 @@ public void MarkClean(string key, DateTime? version) { /// Boolean indicator nothing whether to search through any of the topic's topics in /// order to get the value. /// - /// The for the . + /// The for the . [return: NotNullIfNotNull("defaultValue")] public TValue? GetValue(string key, TValue? defaultValue, bool inheritFromParent = false, bool inheritFromBase = true) { Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); @@ -221,11 +221,11 @@ public void MarkClean(string key, DateTime? version) { } /// - /// Gets a from the collection based on the with a specified - /// and an optional number of s through whom to crawl to - /// retrieve an inherited value. + /// Gets a from the collection based on the with a + /// specified paramref name="defaultValue"/> and an optional number of s through whom to + /// crawl to retrieve an inherited value. /// - /// The string identifier for the . + /// The string identifier for the . /// /// A to which to fall back in the case the value is not found. /// @@ -331,29 +331,29 @@ ParentCollection is not null | METHOD: SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// /// Working with records can be a bit cumbersome, and especially in determining if a value should be marked as , since that's based on a comparison with the previous value. The method handles this logic for implementers, while simultaneously allowing callers to - /// explicitly set whether the instances should be marked as dirty—via the parameter—and, optionally, what the should be. + /// TrackedRecord{T}.IsDirty"/>, since that's based on a comparison with the previous value. The method handles this logic for implementers, while simultaneously allowing + /// callers to explicitly set whether the instances should be marked as dirty—via the + /// parameter—and, optionally, what the should be. /// - /// The string identifier for the . - /// The text value for the . + /// The string identifier for the . + /// The text value for the . /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// /// - /// The value that the was last modified. This is intended exclusively - /// for use when populating the topic graph from a persistent data store as a means of indicating the current version for - /// each . This is used when e.g. importing values to determine if the existing value is newer - /// than the source value. + /// The value that the was last modified. This is intended + /// exclusively for use when populating the topic graph from a persistent data store as a means of indicating the current + /// version for each . This is used when e.g. importing values to determine if the existing + /// value is newer than the source value. /// /// SetValue(key, value, markDirty, true, version); /// - /// Internal helper method that either adds a new object or updates the value of an existing - /// one, depending on whether that value already exists. + /// Internal helper method that either adds a new object or updates the value of an + /// existing one, depending on whether that value already exists. /// /// /// When the parameter is called, no attempt will be made to route the call /// through the corresponding properties, if available. As such, this is intended specifically to be called by internal /// properties as a means of avoiding the property being called again when a caller uses the property's setter directly. /// - /// The string identifier for the . - /// The text value for the . + /// The string identifier for the . + /// The text value for the . /// /// Instructs the underlying code to call corresponding properties, if available, to ensure business logic is enforced. /// This should be set to false if setting items from internal properties in order to avoid an infinite loop. /// /// - /// Specified whether the value should be marked as . By default, it will be marked as - /// true if the value is new or has changed from a previous value. By setting this parameter, that behavior is + /// Specified whether the value should be marked as . By default, it will be marked + /// as true if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . /// @@ -523,25 +523,25 @@ internal void SetValue( | OVERRIDE: INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to insert a new into the collection, to ensure that local + /// Intercepts all attempts to insert a new into the collection, to ensure that local /// business logic is enforced. /// /// /// - /// If a settable property is available corresponding to the , the call should be routed - /// through that to ensure local business logic is enforced, if it hasn't already been enforced. + /// If a settable property is available corresponding to the , the call should be + /// routed through that to ensure local business logic is enforced, if it hasn't already been enforced. /// /// /// Compared to the base implementation, will throw a specific error if a duplicate key - /// is inserted. This conveniently provides the name of the so it's clear what key is + /// is inserted. This conveniently provides the name of the so it's clear what key is /// being duplicated. /// /// - /// The location that the should be set. - /// The object which is being inserted. + /// The location that the should be set. + /// The object which is being inserted. /// - /// An is thrown if an with the same as the already exists. + /// An is thrown if an with the same as the already exists. /// protected override void InsertItem(int index, TItem item) { Contract.Requires(item, nameof(item)); @@ -572,15 +572,15 @@ protected override void InsertItem(int index, TItem item) { | OVERRIDE: SET ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to update an in the collection, to ensure that local business + /// Intercepts all attempts to update an in the collection, to ensure that local business /// logic is enforced. /// /// - /// If a settable property is available corresponding to the , the call should be routed + /// If a settable property is available corresponding to the , the call should be routed /// through that to ensure local business logic is enforced, if it hasn't already been enforced. /// - /// The location that the should be set. - /// The object which is being inserted. + /// The location that the should be set. + /// The object which is being inserted. protected override void SetItem(int index, TItem item) { Contract.Requires(item, nameof(item)); if (!AllowClean(item)) { @@ -600,12 +600,12 @@ protected override void SetItem(int index, TItem item) { | OVERRIDE: REMOVE ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to remove an from the collection, to ensure that it is + /// Intercepts all attempts to remove an from the collection, to ensure that it is /// appropriately marked as . /// /// - /// When an is removed, will return true—even if no remaining s are marked as . + /// When an is removed, will return true—even if no remaining s are marked as . /// protected override void RemoveItem(int index) { var trackedItem = this[index]; @@ -623,8 +623,8 @@ protected override void RemoveItem(int index) { /// appropriately marked as . /// /// - /// When an is removed, will return true—even if no remaining s are marked as . + /// When an is removed, will return true—even if no remaining s are marked as . /// protected override void ClearItems() { if (!AssociatedTopic.IsNew) { @@ -637,14 +637,14 @@ protected override void ClearItems() { | METHOD: ALLOW CLEAN? \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Determines if a is permitted to be marked as not . + /// Determines if a is permitted to be marked as not . /// /// /// If the is or the is and the is , then + /// Topic"/> and the is , then /// should never be set to false. /// - /// The object which is being inserted. + /// The object which is being inserted. protected bool AllowClean(TItem item) { Contract.Requires(item, nameof(item)); var topic = item.Value as Topic; diff --git a/OnTopic/Collections/Specialized/TrackedItem{T}.cs b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs similarity index 84% rename from OnTopic/Collections/Specialized/TrackedItem{T}.cs rename to OnTopic/Collections/Specialized/TrackedRecord{T}.cs index a510bdf3..e8b5be4f 100644 --- a/OnTopic/Collections/Specialized/TrackedItem{T}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs @@ -19,33 +19,35 @@ namespace OnTopic.Collections.Specialized { /// Provides a base class for tracking versioned records, such as . /// /// - /// The class is comparable to the , in that it tracks the and for an item, but it additionally provides metadata related to the record, including the - /// and whether or not it . This makes it easier for e.g. class is comparable to the , in that it tracks the and for an item, but it additionally provides metadata related to the record, including + /// the and whether or not it . This makes it easier for e.g. implementations to make more informed decisions about whether a record needs to be saved or /// overwritten during a or . /// - public abstract record TrackedItem { + public abstract record TrackedRecord { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Constructs a new instance of a class. + /// Constructs a new instance of a class. /// - protected TrackedItem() { + protected TrackedRecord() { LastModified = DateTime.UtcNow; } /// - /// Constructs a new instance of a class. + /// Constructs a new instance of a class. /// - /// The for the instance. - /// The for the instance. - /// The optional state for the instance. - /// The optional for the instance. - protected TrackedItem(string key, T value, bool isDirty = true, DateTime? lastModified = null) { + /// The for the instance. + /// The for the instance. + /// The optional state for the instance. + /// + /// The optional for the instance. + /// + protected TrackedRecord(string key, T value, bool isDirty = true, DateTime? lastModified = null) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index f54d3947..5494f16d 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -52,7 +52,7 @@ namespace OnTopic.Internal.Reflection { /// optionally be retrieved via . This is useful in case there is data from /// the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as , , and , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we allow /// the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as critical @@ -86,7 +86,7 @@ namespace OnTopic.Internal.Reflection { /// internal class TopicPropertyDispatcher where TAttributeType: Attribute - where TItem: TrackedItem + where TItem: TrackedRecord where TValue: class { diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index cf6af478..c0423203 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -49,9 +49,9 @@ public class AttributeDescriptor : Topic { /// /// /// By default, when creating new attributes, the s for both and will be set to , which is required in order to + /// cref="Topic.ContentType"/> will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the /// persistence store. /// diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 9819c91c..a35337d6 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -51,9 +51,9 @@ public class ContentTypeDescriptor : Topic { /// /// /// By default, when creating new attributes, the s for both and will be set to , which is required in order to + /// cref="Topic.ContentType"/> will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the /// persistence store. /// diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 80e47b85..670219d9 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -686,7 +686,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Given a , returns a list of , optionally filtering based on and . + /// cref="AttributeDescriptor.IsExtendedAttribute"/> and . /// /// The from which to pull the attributes. /// @@ -694,7 +694,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi /// cref="AttributeValue"/>s are returned. /// /// - /// Whether or not to filter by . If null, all s + /// Whether or not to filter by . If null, all s /// are returned. /// /// Exclude any attributes that start with LastModified. @@ -885,15 +885,14 @@ private static bool IsAttributeDescriptor(Topic topic) => /// The determines where an attribute should be stored; the /// determines where an attribute was stored. If these two /// values are in conflict, that suggests the coniguration for has - /// changed since the attribute value was last saved. In that case, it should be treated as even though its value hasn't changed to ensure that its storage location is - /// updated. + /// changed since the attribute value was last saved. In that case, it should be treated as even though its value hasn't changed to ensure that its storage location is updated. /// /// /// If cannot be found then the is arbitrary attribute /// not mapped to the schema. In that case, its storage location is dynamically determined based on its length, and thus - /// it should only change locations when it . Otherwise, its length will remain the - /// same, and thus the storage location should remain unchanged. + /// it should only change locations when it . Otherwise, its length will remain + /// the same, and thus the storage location should remain unchanged. /// /// /// The source , if available. diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index ac4d4050..14ffba51 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -44,11 +44,11 @@ public class Topic: ITrackDirtyKeys { /// optionally, , . /// /// - /// By default, when creating new attributes, the s for both and will be set to , which is required in order to correctly save - /// new topics to the database. When the parameter is set, however, the property is set to falseon and , as - /// it is assumed these are being set to the same values currently used in the persistence store. + /// By default, when creating new attributes, the s for both and will be set to , which is required in order to correctly save new + /// topics to the database. When the parameter is set, however, the property is set to falseon and , as it is assumed these + /// are being set to the same values currently used in the persistence store. /// /// A string representing the key for the new topic instance. /// A string representing the key of the target content type. @@ -725,8 +725,8 @@ public Topic? BaseTopic { /// /// /// Attributes are stored via an class which, in addition to the Attribute Key and Value, - /// also track other metadata for the attribute, such as the version (via the - /// property) and whether it has been persisted to the database or not (via the + /// also track other metadata for the attribute, such as the version (via the + /// property) and whether it has been persisted to the database or not (via the /// property). /// /// The current 's attributes. @@ -810,7 +810,7 @@ public Topic? BaseTopic { /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. /// - /// Specified whether the value should be marked as . By default, it will be marked + /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is /// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being /// persisted to the data store on . diff --git a/OnTopic/TopicFactory.cs b/OnTopic/TopicFactory.cs index 217a76ea..01aab3eb 100644 --- a/OnTopic/TopicFactory.cs +++ b/OnTopic/TopicFactory.cs @@ -39,7 +39,7 @@ public static class TopicFactory { /// by the parameter. /// /// - /// When the parameter is set the property is set to + /// When the parameter is set the property is set to /// false on as well as on , since it is assumed these are /// being set to the same values currently used in the persistence store. /// From 453c9e916004f677b412736d5216d418a36c17d8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 16:32:24 -0800 Subject: [PATCH 632/778] Rename `TrackedCollection<>` to `TrackedRecordCollection<>` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will facilitate a more intuitive naming convention for derived classes later on. This required updating a lot of XML Docs—which, in turn, required a lot of rewrapping. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 16 ++--- OnTopic.Tests/TopicReferenceCollectionTest.cs | 70 +++++++++---------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 +- .../Associations/ReferenceSetterAttribute.cs | 24 +++---- OnTopic/Associations/TopicReference.cs | 3 +- .../Associations/TopicReferenceCollection.cs | 6 +- .../Attributes/AttributeSetterAttribute.cs | 12 ++-- .../Attributes/AttributeValueCollection.cs | 6 +- ...ordCollection{TItem,TValue,TAttribute}.cs} | 28 ++++---- .../Specialized/TrackedRecord{T}.cs | 2 +- .../Reflection/TopicPropertyDispatcher.cs | 18 ++--- .../Annotations/AttributeKeyAttribute.cs | 4 +- .../Mapping/Annotations/InheritAttribute.cs | 12 ++-- .../Mapping/Internal/PropertyConfiguration.cs | 6 +- OnTopic/Mapping/TopicMappingService.cs | 2 +- OnTopic/Topic.cs | 16 ++--- 16 files changed, 115 insertions(+), 114 deletions(-) rename OnTopic/Collections/Specialized/{TrackedCollection{TItem,TValue,TAttribute}.cs => TrackedRecordCollection{TItem,TValue,TAttribute}.cs} (97%) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index d0eeb1d5..2e9b0920 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -416,7 +416,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { /// /// Populates the with a and then deletes it. Confirms /// that the returns the new version after calling . + /// TrackedRecordCollection{TItem, TValue, TAttribute}.MarkClean(DateTime?)"/>. /// [TestMethod] public void IsDirty_MarkClean_UpdatesLastModified() { @@ -438,7 +438,7 @@ public void IsDirty_MarkClean_UpdatesLastModified() { /// /// Populates the with a and then deletes it. Confirms /// that returns false after calling . + /// TrackedRecordCollection{TItem, TValue, TAttribute}.MarkClean(DateTime?)"/>. /// [TestMethod] public void IsDirty_MarkClean_ReturnsFalse() { @@ -461,8 +461,8 @@ public void IsDirty_MarkClean_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates the with a and then confirms that returns false for that attribute after - /// calling . + /// cref="TrackedRecordCollection{TItem, TValue, TAttribute}.IsDirty(String)"/> returns false for that attribute + /// after calling . /// [TestMethod] public void IsDirty_MarkAttributeClean_ReturnsFalse() { @@ -482,7 +482,7 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { /// /// Populates a associated with an /// with a that is not marked as and then confirms - /// that returns true. + /// that returns true. /// [TestMethod] public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { @@ -506,9 +506,9 @@ public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates a associated with an - /// with a and then confirms that returns true for that attribute after calling . + /// with a and then confirms that returns true for that attribute after calling . /// [TestMethod] public void IsDirty_MarkNewTopicAsClean_ReturnsTrue() { diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index e56eae91..af27211b 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -17,10 +17,10 @@ namespace OnTopic.Tests { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the , with a particular emphasis on the custom features - /// such as , , , and the cross-referencing of reciprocal values in the property. + /// such as , , , and the cross-referencing of reciprocal values in the property. /// [TestClass] public class TopicReferenceCollectionTest { @@ -30,7 +30,7 @@ public class TopicReferenceCollectionTest { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// is correctly set. + /// is correctly set. /// [TestMethod] public void Add_NewReference_IsDirty() { @@ -50,8 +50,8 @@ public void Add_NewReference_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , and confirms that is not set. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, and confirms that + /// is not set. /// [TestMethod] public void SetValue_NewReference_NotDirty() { @@ -71,8 +71,8 @@ public void SetValue_NewReference_NotDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a topic reference, removes that reference using , and confirms that is set. + /// "TrackedRecordCollection{TItem, TValue, TAttribute}.RemoveItem(Int32)"/>, and confirms that is set. /// [TestMethod] public void Remove_ExistingReference_IsDirty() { @@ -93,9 +93,9 @@ public void Remove_ExistingReference_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , calls and confirms that is set. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, calls and confirms that is set. /// [TestMethod] public void Clear_ExistingReferences_IsDirty() { @@ -117,8 +117,8 @@ public void Clear_ExistingReferences_IsDirty() { /// /// Assembles a new and adds a new reference using with set to false - /// , confirming that remains true since - /// the target is unsaved. + /// , confirming that remains true + /// since the target is unsaved. /// [TestMethod] public void Add_NewTopic_IsDirty() { @@ -137,8 +137,8 @@ public void Add_NewTopic_IsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , and confirms that reference is correctly set. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, and confirms that + /// reference is correctly set. /// [TestMethod] public void Add_NewReference_IncomingRelationshipSet() { @@ -157,9 +157,9 @@ public void Add_NewReference_IncomingRelationshipSet() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , removes the reference - /// using , and confirms that the reference is correctly removed as well. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, removes the + /// reference using , and confirms that + /// the reference is correctly removed as well. /// [TestMethod] public void Remove_ExistingReference_IncomingRelationshipRemoved() { @@ -179,9 +179,9 @@ public void Remove_ExistingReference_IncomingRelationshipRemoved() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , updates the reference - /// using , and - /// confirms that the reference is correctly updated. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, updates the + /// reference using , and confirms that the reference is correctly updated. /// [TestMethod] public void SetValue_ExistingReference_TopicUpdated() { @@ -202,9 +202,9 @@ public void SetValue_ExistingReference_TopicUpdated() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference using , updates the reference - /// with a null value using , and confirms that the reference is correctly removed. + /// TrackedRecordCollection{TItem, TValue, TAttribute}.SetValue(String, TValue, Boolean?, DateTime?)"/>, updates the + /// reference with a null value using , and confirms that the reference is correctly removed. /// [TestMethod] public void SetValue_NullReference_TopicRemoved() { @@ -245,8 +245,8 @@ public void Add_NewReference_TopicIsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// correctly returns the . + /// correctly returns the . /// [TestMethod] public void GetTopic_ExistingReference_ReturnsTopic() { @@ -265,8 +265,8 @@ public void GetTopic_ExistingReference_ReturnsTopic() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new , adds a new reference, and confirms that - /// correctly returns null if - /// an incorrect referencedKey is entered. + /// correctly returns null + /// if an incorrect referencedKey is entered. /// [TestMethod] public void GetTopic_MissingReference_ReturnsNull() { @@ -285,8 +285,8 @@ public void GetTopic_MissingReference_ReturnsNull() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns the related topic reference. + /// Topic"/> reference to the , and confirms that correctly returns the related topic reference. /// [TestMethod] public void GetTopic_InheritedReference_ReturnsTopic() { @@ -307,8 +307,8 @@ public void GetTopic_InheritedReference_ReturnsTopic() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if an incorrect referencedKey is + /// Topic"/> reference to the , and confirms that correctly returns null if an incorrect referencedKey is /// entered. /// [TestMethod] @@ -330,8 +330,8 @@ public void GetTopic_InheritedReference_ReturnsNull() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Assembles a new with a , adds a new reference to the , and confirms that correctly returns null if inheritFromBase is set to + /// Topic"/> reference to the , and confirms that correctly returns null if inheritFromBase is set to /// false. /// [TestMethod] diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 43284c42..3a05d8c8 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -593,8 +593,8 @@ public void Save_IsRecursive_SavesChild() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Saves a new with an unresolved and confirms that it successfully - /// resolves it by marking the collection as as false. + /// resolves it by marking the collection as as false. /// [TestMethod] public void Save_UnresolvedReference_Resolves() { diff --git a/OnTopic/Associations/ReferenceSetterAttribute.cs b/OnTopic/Associations/ReferenceSetterAttribute.cs index da8b8369..282b6692 100644 --- a/OnTopic/Associations/ReferenceSetterAttribute.cs +++ b/OnTopic/Associations/ReferenceSetterAttribute.cs @@ -12,17 +12,17 @@ namespace OnTopic.Associations { | CLASS: REFERENCE SETTER [ATTRIBUTE] \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Flags that a property should be used when setting a reference via . + /// Flags that a property should be used when setting a reference via . /// /// /// - /// When a call is made to the code will check to see if a property with the same name as the reference key exists, and whether - /// that property is decorated with the (i.e., [ReferenceSetter]). If - /// it is, then the update will be routed through that property. This ensures that business logic is enforced by local - /// properties, instead of allowing business logic to be potentially bypassed by writing directly to the collection. + /// When a call is made to the code will check to see if a property with the same name as the reference key exists, and + /// whether that property is decorated with the (i.e., [ReferenceSetter] + /// ). If is, then the update will be routed through that property. This ensures that business logic is enforced by + /// local properties, instead of allowing business logic to be potentially bypassed by writing directly to the collection. /// /// /// As an example, the property is adorned with the . @@ -31,10 +31,10 @@ namespace OnTopic.Associations { /// /// /// To ensure this logic, it is critical that implementers of ensure that the - /// property setters call the overload with the final parameter set to false to disable the enforcement of business logic. - /// Otherwise, an infinite loop will occur. Calling that overload tells that the - /// business logic has already been enforced by the caller. + /// property setters call the overload with the final parameter set to false to disable the enforcement of business + /// logic. Otherwise, an infinite loop will occur. Calling that overload tells that + /// the business logic has already been enforced by the caller. /// /// [AttributeUsage(AttributeTargets.Property)] diff --git a/OnTopic/Associations/TopicReference.cs b/OnTopic/Associations/TopicReference.cs index ac3aa222..a582b0b7 100644 --- a/OnTopic/Associations/TopicReference.cs +++ b/OnTopic/Associations/TopicReference.cs @@ -34,7 +34,8 @@ namespace OnTopic.Associations { /// /// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either /// create a new instance of the class or, preferably, call the 's method. + /// />'s + /// method. /// /// public record TopicReference: TrackedRecord { diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs index e8d7bc67..88933bc0 100644 --- a/OnTopic/Associations/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -15,7 +15,7 @@ namespace OnTopic.Associations { /// /// Represents a collection of objects associated with particular reference keys. /// - public class TopicReferenceCollection : TrackedCollection { + public class TopicReferenceCollection : TrackedRecordCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -30,14 +30,14 @@ public TopicReferenceCollection(Topic parentTopic) : base(parentTopic) { } | PROPERTY: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedCollection? ParentCollection => + protected override TrackedRecordCollection? ParentCollection => AssociatedTopic.Parent?.References; /*========================================================================================================================== | PROPERTY: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedCollection? BaseCollection => + protected override TrackedRecordCollection? BaseCollection => AssociatedTopic.BaseTopic?.References; /*========================================================================================================================== diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs index 4c6bce50..4f0bdfa3 100644 --- a/OnTopic/Attributes/AttributeSetterAttribute.cs +++ b/OnTopic/Attributes/AttributeSetterAttribute.cs @@ -34,12 +34,12 @@ namespace OnTopic.Attributes { /// /// /// To ensure this logic, it is critical that implementers of ensure that the - /// property setters call overload with the final parameter set to false to disable the enforcement of business - /// logic. Otherwise, an infinite loop will occur. Calling that overload tells that - /// the business logic has already been enforced by the caller. As this is an internal overload, implementers should use - /// the local proxy at , which ensures that final parameter - /// is set to false. + /// property setters call overload with the final parameter set to false to disable the enforcement of business logic. + /// Otherwise, an infinite loop will occur. Calling that overload tells that the + /// business logic has already been enforced by the caller. As this is an internal overload, implementers should use the + /// local proxy at , which ensures that final parameter is + /// set to false. /// /// [AttributeUsage(AttributeTargets.Property)] diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index cfa00156..4018b87e 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -21,7 +21,7 @@ namespace OnTopic.Attributes { /// The class tracks these through its property, which is an instance of /// the class. /// - public class AttributeValueCollection : TrackedCollection { + public class AttributeValueCollection : TrackedRecordCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -41,14 +41,14 @@ internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { | PROPERTY: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedCollection? ParentCollection => + protected override TrackedRecordCollection? ParentCollection => AssociatedTopic.Parent?.Attributes; /*========================================================================================================================== | PROPERTY: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedCollection? BaseCollection => + protected override TrackedRecordCollection? BaseCollection => AssociatedTopic.BaseTopic?.Attributes; /*========================================================================================================================== diff --git a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs similarity index 97% rename from OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs rename to OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs index 94ea5177..c95b8562 100644 --- a/OnTopic/Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs @@ -15,7 +15,7 @@ namespace OnTopic.Collections.Specialized { /*============================================================================================================================ - | CLASS: TRACKED COLLECTION + | CLASS: TRACKED RECORD COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents a collection of records, along with methods "updating" those records and @@ -24,12 +24,12 @@ namespace OnTopic.Collections.Specialized { /// /// records represent individual instances of values associated with a particular . The class tracks these through e.g. its property. The class provides a base class with methods for working with these - /// records, such as , for determining if a given record has been modified, or class provides a base class with methods for working with + /// these records, such as , for determining if a given record has been modified, or for creating or "updating" a record. (Records are /// immutable, so updates actually involve cloning the record with updated values.) /// - public abstract class TrackedCollection : + public abstract class TrackedRecordCollection : KeyedCollection, ITrackDirtyKeys where TItem: TrackedRecord, new() where TAttribute: Attribute @@ -45,10 +45,10 @@ public abstract class TrackedCollection : | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// A reference to the topic that the current collection is bound to. - internal TrackedCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { + internal TrackedRecordCollection(Topic parentTopic) : base(StringComparer.OrdinalIgnoreCase) { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -313,19 +313,19 @@ ParentCollection is not null | METHOD: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a reference to the corresponding on the , if available. + /// Provides a reference to the corresponding on the , if available. /// - protected abstract TrackedCollection? ParentCollection { get; } + protected abstract TrackedRecordCollection? ParentCollection { get; } /*========================================================================================================================== | METHOD: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a reference to the corresponding on the , if available. + /// Provides a reference to the corresponding on the , if available. /// - protected abstract TrackedCollection? BaseCollection { get; } + protected abstract TrackedRecordCollection? BaseCollection { get; } /*========================================================================================================================== | METHOD: SET VALUE @@ -619,8 +619,8 @@ protected override void RemoveItem(int index) { | OVERRIDE: CLEAR ITEMS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Intercepts all attempts to clear the , to ensure that it is - /// appropriately marked as . + /// Intercepts all attempts to clear the , to ensure that + /// it is appropriately marked as . /// /// /// When an is removed, will return true—even if no remaining /// Provides a base class for tracking versioned records, such as . diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 5494f16d..5d147867 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -76,12 +76,12 @@ namespace OnTopic.Internal.Reflection { /// from . In that case, the business logic will already have been enforced, but the method will not have been called. To mitigate the property setter getting called /// twice, collection implementors are advised to offer an internal overload that allows an item to be added to the - /// collection while bypassing the business logic. For instance, this can be done using or ; in each case, the internally - /// accessible enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this - /// is done by calling , thus assuring that - /// the business logic has already occurred. + /// collection while bypassing the business logic. For instance, this can be done using or ; in each + /// case, the internally accessible enforceBusinessLogic parameter allows a property setter to disable business + /// logic. Internally, this is done by calling , thus assuring that the business logic has already occurred. /// /// internal class TopicPropertyDispatcher @@ -165,9 +165,9 @@ internal TopicPropertyDispatcher(Topic associatedTopic) { /// business logic, thus preventing them from being called twice. These methods should be marked internal to prevent /// external actors from bypassing the business logic; the purpose is to confirm that the business logic has already /// been enforced, not to make the business logic optional. Two examples of this are the internal - /// enforceBusinessLogic parameters on and . + /// enforceBusinessLogic parameters on and . /// /// /// It's worth noting that any calls to are invalidated the next time . + /// Flags that a property should be mapped to a specific attributeKey in when calling . /// /// /// By default, implementations will attempt to map the property of the target data diff --git a/OnTopic/Mapping/Annotations/InheritAttribute.cs b/OnTopic/Mapping/Annotations/InheritAttribute.cs index 787415a5..e93d23eb 100644 --- a/OnTopic/Mapping/Annotations/InheritAttribute.cs +++ b/OnTopic/Mapping/Annotations/InheritAttribute.cs @@ -13,14 +13,14 @@ namespace OnTopic.Mapping.Annotations { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Flags that a property should be inherit its value from when calling . + /// cref="TrackedRecordCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)"/>. /// /// - /// By default, implementations will call with the inheritFromParent parameter set to its default of false. - /// This attribute instructs it to instead set that parameter to true, which in turn causes the to crawl up the tree until a value is found. - /// This is useful when an attribute is expected to be inherited by all child topics. + /// By default, implementations will call with the inheritFromParent parameter set to its default of + /// false. This attribute instructs it to instead set that parameter to true, which in turn causes the to crawl up the tree until a value is + /// found. This is useful when an attribute is expected to be inherited by all child topics. /// [System.AttributeUsage(System.AttributeTargets.Property)] public sealed class InheritAttribute : System.Attribute { diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index 9f91abb0..eff5b56c 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -220,9 +220,9 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// /// The configuration is only applicable if the value is pulled from the collection. This is the equivalent to calling the method with an InheritFromParent - /// parameter set to True. + /// cref="Topic.Attributes"/> collection. This is the equivalent to calling the method with an InheritFromParent parameter set to + /// True. /// /// /// The property corresponds to the being set on a given diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 85f331ab..9d3d386f 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -358,7 +358,7 @@ await MapAsync( /// cref="String"/>, , , or , the method will attempt to set the property on the based on, in order, the 's Get{Property}() method, {Property} - /// property, and, finally, its collection (using collection (using ). If the property is not of a settable type, or the source value /// cannot be identified on the , then the property is not set. /// diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 14ffba51..d2708e82 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -679,13 +679,13 @@ public void MarkClean(string key, bool includeCollections) { /// /// Base topics allow attribute values to be inherited from another topic. When a is configured /// as a BaseTopic , values from that are used when the method is unable to find a local - /// value for the attribute. + /// cref="TrackedRecordCollection{TItem, TValue, TAttribute}.GetValue(String, Boolean)" /> method is unable to find a + /// local value for the attribute. /// /// - /// Be aware that while multiple levels of s can be configured, the method defaults to a maximum level of five "hops" in order - /// to help avoid an infinite loop. + /// Be aware that while multiple levels of s can be configured, the method defaults to a maximum level + /// of five "hops" in order to help avoid an infinite loop. /// /// /// The underlying value of the is stored as a topic reference with the . This is intended to enforce local business logic, and prevent /// callers from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the /// that the business logic has already been enforced. To do that, they must either - /// call - /// with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this - /// overload. + /// call with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, + /// call this overload. /// /// The string identifier for the AttributeValue. /// The text value for the AttributeValue. From 1cf813a422fbff2d161754cd0df4d2a1c75b9084 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 16:36:58 -0800 Subject: [PATCH 633/778] Updated text references to `TrackedRecord`, `TrackedRecordCollection` These correspond to the rename of `TrackedCollection<>` to `TrackedRecordCollection<>` (453c9e9) and `TrackedItem<>` to `TrackedRecord<>` (a6db252). --- ...ecordCollection{TItem,TValue,TAttribute}.cs | 18 +++++++++--------- OnTopic/README.md | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs index c95b8562..3c4dfde7 100644 --- a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs @@ -149,9 +149,9 @@ public void MarkClean(DateTime? version) { if (AssociatedTopic.IsNew) { return; } - foreach (var trackedItem in Items.Where(a => a.IsDirty).ToArray()) { - if (AllowClean(trackedItem)) { - SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); + foreach (var trackedRecord in Items.Where(a => a.IsDirty).ToArray()) { + if (AllowClean(trackedRecord)) { + SetValue(trackedRecord.Key, trackedRecord.Value, false, false, version?? DateTime.UtcNow); } } DeletedItems.Clear(); @@ -178,9 +178,9 @@ public void MarkClean(string key, DateTime? version) { return; } else if (Contains(key)) { - var trackedItem = this[key]; - if (trackedItem.IsDirty && AllowClean(trackedItem)) { - SetValue(trackedItem.Key, trackedItem.Value, false, false, version?? DateTime.UtcNow); + var trackedRecord = this[key]; + if (trackedRecord.IsDirty && AllowClean(trackedRecord)) { + SetValue(trackedRecord.Key, trackedRecord.Value, false, false, version?? DateTime.UtcNow); } } } @@ -448,7 +448,7 @@ internal void SetValue( /*------------------------------------------------------------------------------------------------------------------------ | Update existing item >-----------------------------------------------------------------------------------------------------------------------— - | Because TrackedItem is immutable, a new instance must be constructed to replace the previous version. + | Because TrackedRecord is immutable, a new instance must be constructed to replace the previous version. \-----------------------------------------------------------------------------------------------------------------------*/ else if (originalItem is not null) { var markAsDirty = originalItem.IsDirty; @@ -608,9 +608,9 @@ protected override void SetItem(int index, TItem item) { /// cref="TrackedRecord{T}"/>s are marked as . /// protected override void RemoveItem(int index) { - var trackedItem = this[index]; + var trackedRecord = this[index]; if (!AssociatedTopic.IsNew) { - DeletedItems.Add(trackedItem.Key); + DeletedItems.Add(trackedRecord.Key); } base.RemoveItem(index); } diff --git a/OnTopic/README.md b/OnTopic/README.md index c78b2bd3..64bc4a1d 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -69,9 +69,9 @@ The `OnTopic` assembly contains a number of generic, keyed, and/or read-only col ### Specialty Collections The `OnTopic.Collections.Specialized` namespace includes a number of collections that are used by the OnTopic library, but won't generally be used directly by implementors, except as exposed by the core library. These include: -- **[`TrackedCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedItem` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. - - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedCollection` of [`AttributeValue`](Attributes/AttributeValue.cs) instances keyed by `AttributeValue.Key`; exposed by `Topic.Attributes`. - - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedCollection` of [`TopicReference`](Associations/TopicReference.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. +- **[`TrackedRecordCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedRecord` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. + - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedRecordCollection` of [`AttributeValue`](Attributes/AttributeValue.cs) instances keyed by `AttributeValue.Key`; exposed by `Topic.Attributes`. + - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReference`](Associations/TopicReference.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. - **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. - **[`TopicRelationshipMultiMap`](associations/TopicRelationshipMultiMap.cs)**: A `TopicMultiMap` of [`KeyValuesPair`](Collections/Specialized/KeyValuesPair.cs) instances keyed by `KeyValuesPair.Key`; exposed by `Topic.Relationships`. From 696080089b90107e39134f1e409b17bdff818263 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 16:43:34 -0800 Subject: [PATCH 634/778] Renamed `TopicReference` to `TopicReferenceRecord` This aligns with the rename of `TrackedItem` to `TrackedRecord` (a6db252). --- .../Associations/TopicReferenceCollection.cs | 16 ++++----- ...icReference.cs => TopicReferenceRecord.cs} | 34 +++++++++---------- OnTopic/README.md | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) rename OnTopic/Associations/{TopicReference.cs => TopicReferenceRecord.cs} (71%) diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs index 88933bc0..e4413ee6 100644 --- a/OnTopic/Associations/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -15,7 +15,7 @@ namespace OnTopic.Associations { /// /// Represents a collection of objects associated with particular reference keys. /// - public class TopicReferenceCollection : TrackedRecordCollection { + public class TopicReferenceCollection : TrackedRecordCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -30,14 +30,14 @@ public TopicReferenceCollection(Topic parentTopic) : base(parentTopic) { } | PROPERTY: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedRecordCollection? ParentCollection => + protected override TrackedRecordCollection? ParentCollection => AssociatedTopic.Parent?.References; /*========================================================================================================================== | PROPERTY: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedRecordCollection? BaseCollection => + protected override TrackedRecordCollection? BaseCollection => AssociatedTopic.BaseTopic?.References; /*========================================================================================================================== @@ -55,9 +55,9 @@ public TopicReferenceCollection(Topic parentTopic) : base(parentTopic) { } /// ITopicRepository"/> should not deleted unmatched topic references. /// /// - /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped back to - /// a valid reference in memory. + /// The property defaults to true. It should be set to false during the method if any members of the collection cannot be mapped + /// back to a valid reference in memory. /// /// public bool IsFullyLoaded { get; set; } = true; @@ -66,7 +66,7 @@ public TopicReferenceCollection(Topic parentTopic) : base(parentTopic) { } | INSERT ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void InsertItem(int index, TopicReference item) { + protected override void InsertItem(int index, TopicReferenceRecord item) { /*------------------------------------------------------------------------------------------------------------------------ | Provide base logic @@ -84,7 +84,7 @@ protected override void InsertItem(int index, TopicReference item) { | OVERRIDE: SET ITEM \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override void SetItem(int index, TopicReference item) { + protected override void SetItem(int index, TopicReferenceRecord item) { /*------------------------------------------------------------------------------------------------------------------------ | Get existing reference diff --git a/OnTopic/Associations/TopicReference.cs b/OnTopic/Associations/TopicReferenceRecord.cs similarity index 71% rename from OnTopic/Associations/TopicReference.cs rename to OnTopic/Associations/TopicReferenceRecord.cs index a582b0b7..19e0394c 100644 --- a/OnTopic/Associations/TopicReference.cs +++ b/OnTopic/Associations/TopicReferenceRecord.cs @@ -11,7 +11,7 @@ namespace OnTopic.Associations { /*============================================================================================================================ - | CLASS: TOPIC REFERENCE + | CLASS: TOPIC REFERENCE RECORD \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents the immutable value of a particular topic reference on a . @@ -23,41 +23,41 @@ namespace OnTopic.Associations { /// TrackedRecord{T}.LastModified"/> date. /// /// - /// Typically, the will be exposed as part of a via - /// the collection. + /// Typically, the will be exposed as part of a + /// via the collection. /// /// - /// Be aware that while represents the value of a specific topic reference, the metadata for - /// describing the purpose, constraints, and usage of that particular attribute is described by the class. + /// Be aware that while represents the value of a specific topic reference, the + /// metadata for describing the purpose, constraints, and usage of that particular attribute is described by the class. /// /// /// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either - /// create a new instance of the class or, preferably, call the 's - /// method. + /// create a new instance of the class or, preferably, call the 's method. /// /// - public record TopicReference: TrackedRecord { + public record TopicReferenceRecord: TrackedRecord { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public TopicReference(): base() { } + public TopicReferenceRecord(): base() { } /// - /// Initializes a new instance of the class, using the specified key/value pair. + /// Initializes a new instance of the class, using the specified key/value pair. /// /// - /// The string identifier for the collection item key/value pair. + /// The string identifier for the collection item key/value pair. /// /// - /// The string value text for the collection item key/value pair. + /// The string value text for the collection item key/value pair. /// /// - /// An optional boolean indicator noting whether the collection item is a new value, and - /// should thus be saved to the database when is next called. + /// An optional boolean indicator noting whether the collection item is a new value, + /// and should thus be saved to the database when is next called. /// /// /// The value that the attribute was last modified. This is intended primarily for use when @@ -68,7 +68,7 @@ public TopicReference(): base() { } /// description="The key must be specified for the key/value pair." exception="T:System.ArgumentNullException"> /// !String.IsNullOrWhiteSpace(key) /// - public TopicReference( + public TopicReferenceRecord( string key, Topic value, bool isDirty = true, diff --git a/OnTopic/README.md b/OnTopic/README.md index 64bc4a1d..68344985 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -71,7 +71,7 @@ The `OnTopic` assembly contains a number of generic, keyed, and/or read-only col The `OnTopic.Collections.Specialized` namespace includes a number of collections that are used by the OnTopic library, but won't generally be used directly by implementors, except as exposed by the core library. These include: - **[`TrackedRecordCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedRecord` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedRecordCollection` of [`AttributeValue`](Attributes/AttributeValue.cs) instances keyed by `AttributeValue.Key`; exposed by `Topic.Attributes`. - - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReference`](Associations/TopicReference.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. + - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReferenceRecord`](Associations/TopicReferenceRecord.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. - **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. - **[`TopicRelationshipMultiMap`](associations/TopicRelationshipMultiMap.cs)**: A `TopicMultiMap` of [`KeyValuesPair`](Collections/Specialized/KeyValuesPair.cs) instances keyed by `KeyValuesPair.Key`; exposed by `Topic.Relationships`. From f41c551e795cb2f8b54c5ab21c5b5685e960d22b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 16:49:48 -0800 Subject: [PATCH 635/778] Rename `AttributeValue` to `AttributeRecord` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This aligns with the rename of `TrackedItem` to `TrackedRecord` (a6db252). This is a major breaking change, since `AttributeValue` is a long established class. That said, this avoids the awkward convention of e.g. `AttributeValue.Value`, and unifies the naming convention for combining metadata and values as a "Record". (As it so happens, this also _happens_ to be a C# 9.0 record type—though that's not why we're using this naming convention here.) --- .../Models/AttributeValuesDataTable.cs | 8 ++-- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic.Tests/AttributeValueCollectionTest.cs | 32 ++++++------- OnTopic.Tests/Schemas/AttributesDataTable.cs | 4 +- OnTopic.Tests/TopicRepositoryBaseTest.cs | 12 ++--- OnTopic.Tests/TopicTest.cs | 2 +- .../{AttributeValue.cs => AttributeRecord.cs} | 22 ++++----- .../Attributes/AttributeValueCollection.cs | 22 ++++----- .../AttributeValueCollectionExtensions.cs | 48 +++++++++---------- ...cordCollection{TItem,TValue,TAttribute}.cs | 2 +- .../Specialized/TrackedRecord{T}.cs | 2 +- .../Reflection/TopicPropertyDispatcher.cs | 4 +- OnTopic/Metadata/AttributeDescriptor.cs | 6 +-- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- OnTopic/Querying/TopicExtensions.cs | 4 +- OnTopic/README.md | 2 +- OnTopic/Repositories/TopicRepository.cs | 22 ++++----- OnTopic/Topic.cs | 16 +++---- 19 files changed, 107 insertions(+), 107 deletions(-) rename OnTopic/Attributes/{AttributeValue.cs => AttributeRecord.cs} (88%) diff --git a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs index bf116c0d..36003f89 100644 --- a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs +++ b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs @@ -38,7 +38,7 @@ internal AttributeValuesDataTable() { | COLUMN: Attribute Value \-----------------------------------------------------------------------------------------------------------------------*/ Columns.Add( - new DataColumn("AttributeValue") { + new DataColumn("AttributeRecord") { MaxLength = 255 } ); @@ -51,8 +51,8 @@ internal AttributeValuesDataTable() { /// /// Provides a convenience method for adding a new based on the expected column values. /// - /// The . - /// The . + /// The . + /// The . internal DataRow AddRow(string attributeKey, string? attributeValue = null) { /*------------------------------------------------------------------------------------------------------------------------ @@ -60,7 +60,7 @@ internal DataRow AddRow(string attributeKey, string? attributeValue = null) { \-----------------------------------------------------------------------------------------------------------------------*/ var record = NewRow(); record["AttributeKey"] = attributeKey; - record["AttributeValue"] = attributeValue; + record["AttributeRecord"] = attributeValue; /*------------------------------------------------------------------------------------------------------------------------ | Add record diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 93936bd3..6ff58a96 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -237,7 +237,7 @@ private static void SetIndexedAttributes(this IDataReader reader, TopicIndex top \-----------------------------------------------------------------------------------------------------------------------*/ var topicId = reader.GetTopicId(); var attributeKey = reader.GetString("AttributeKey"); - var attributeValue = reader.GetString("AttributeValue"); + var attributeValue = reader.GetString("AttributeRecord"); var version = reader.GetVersion(); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 1797d200..5d8fecb5 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -139,7 +139,7 @@ protected override void DeleteTopic(Topic topic) { } | METHOD: GET ATTRIBUTES (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ /// - public IEnumerable GetAttributesProxy( + public IEnumerable GetAttributesProxy( Topic topic, bool? isExtendedAttribute, bool? isDirty = null, diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 2e9b0920..3793ec7b 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -317,7 +317,7 @@ public void Clear_ExistingValues_IsDirty() { | TEST: SET VALUE: VALUE UNCHANGED: IS NOT DIRTY? \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets the value of a custom to the existing value and ensures it is not marked as + /// Sets the value of a custom to the existing value and ensures it is not marked as /// . /// [TestMethod] @@ -336,7 +336,7 @@ public void SetValue_ValueUnchanged_IsNotDirty() { | TEST: IS DIRTY: DIRTY VALUES: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is marked as with a that is marked as . Confirms that returns /// true. /// @@ -355,7 +355,7 @@ public void IsDirty_DirtyValues_ReturnsTrue() { | TEST: IS DIRTY: DELETED VALUES: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms + /// Populates the with a and then deletes it. Confirms /// that returns true. /// [TestMethod] @@ -374,7 +374,7 @@ public void IsDirty_DeletedValues_ReturnsTrue() { | TEST: IS DIRTY: NO DIRTY VALUES: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is not marked as + /// Populates the with a that is not marked as /// . Confirms that returns /// false/ /// @@ -393,8 +393,8 @@ public void IsDirty_NoDirtyValues_ReturnsFalse() { | TEST: IS DIRTY: EXCLUDE LAST MODIFIED: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is not marked as - /// as well as a LastModified that is. Confirms + /// Populates the with a that is not marked as + /// as well as a LastModified that is. Confirms /// that returns false. /// [TestMethod] @@ -414,7 +414,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { | TEST: IS DIRTY: MARK CLEAN: UPDATES LAST MODIFIED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms + /// Populates the with a and then deletes it. Confirms /// that the returns the new version after calling . /// @@ -436,7 +436,7 @@ public void IsDirty_MarkClean_UpdatesLastModified() { | TEST: IS DIRTY: MARK CLEAN: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms + /// Populates the with a and then deletes it. Confirms /// that returns false after calling . /// @@ -460,7 +460,7 @@ public void IsDirty_MarkClean_ReturnsFalse() { | TEST: IS DIRTY: MARK ATTRIBUTE CLEAN: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then confirms that with a and then confirms that returns false for that attribute /// after calling . /// @@ -481,7 +481,7 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates a associated with an - /// with a that is not marked as and then confirms + /// with a that is not marked as and then confirms /// that returns true. /// [TestMethod] @@ -506,7 +506,7 @@ public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Populates a associated with an - /// with a and then confirms that and then confirms that returns true for that attribute after calling . /// @@ -542,7 +542,7 @@ public void SetValue_InvalidValue_ThrowsException() { | TEST: ADD: VALID ATTRIBUTE VALUE: IS RETURNED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a custom attribute on a topic by directly adding an instance; ensures it can be + /// Sets a custom attribute on a topic by directly adding an instance; ensures it can be /// retrieved. /// [TestMethod] @@ -669,7 +669,7 @@ public void Add_InvalidAttributeValue_ThrowsException() { | TEST: REPLACE VALUE: WITH BUSINESS LOGIC: MAINTAINS ISDIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a new which maps to directly to a which maps to directly to a and confirms that the original is replaced if the /// changes. /// @@ -683,7 +683,7 @@ public void Add_WithBusinessLogic_MaintainsIsDirty() { var index = topic.Attributes.IndexOf(originalValue); - topic.Attributes[index] = new AttributeValue("View", "NewValue", false); + topic.Attributes[index] = new AttributeRecord("View", "NewValue", false); topic.Attributes.TryGetValue("View", out var newAttribute); topic.Attributes.SetValue("View", "NewerValue", false); @@ -699,7 +699,7 @@ public void Add_WithBusinessLogic_MaintainsIsDirty() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new attribute with an empty value, and confirms that it is not added as a new . Empty values are treated as the same as non-existent attributes. They are stored for the sake + /// cref="AttributeRecord"/>. Empty values are treated as the same as non-existent attributes. They are stored for the sake /// of tracking deleted attributes, but should not be stored for new attributes. /// [TestMethod] @@ -718,7 +718,7 @@ public void SetValue_EmptyAttributeValue_Skips() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new attribute with an empty value, and confirms that it is is added as a new assuming the value previously existed. Empty values are treated as the same as non-existent + /// cref="AttributeRecord"/> assuming the value previously existed. Empty values are treated as the same as non-existent /// attributes, but they should be stored for the sake of tracking deleted attributes. /// [TestMethod] diff --git a/OnTopic.Tests/Schemas/AttributesDataTable.cs b/OnTopic.Tests/Schemas/AttributesDataTable.cs index 2571698c..3ad739f7 100644 --- a/OnTopic.Tests/Schemas/AttributesDataTable.cs +++ b/OnTopic.Tests/Schemas/AttributesDataTable.cs @@ -52,7 +52,7 @@ public AttributesDataTable() : base("Attributes") { \-----------------------------------------------------------------------------------------------------------------------*/ Columns.Add(new DataColumn() { DataType = typeof(string), - ColumnName = "AttributeValue", + ColumnName = "AttributeRecord", AllowDBNull = true }); @@ -87,7 +87,7 @@ public void AddRow(int topicId, string attributeKey, string? attributeValue, Dat row["TopicId"] = topicId; row["AttributeKey"] = attributeKey; - row["AttributeValue"] = attributeValue is null? DBNull.Value : attributeValue; + row["AttributeRecord"] = attributeValue is null? DBNull.Value : attributeValue; row["Version"] = version?? DateTime.UtcNow; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 3a05d8c8..a7919df0 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -219,7 +219,7 @@ public void GetAttributes_AnyAttributes_ReturnsAllAttributes() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of attributes from a topic, without any filtering by whether or not the attribute is an . Any s with a null or empty value should + /// cref="AttributeDescriptor.IsExtendedAttribute"/>. Any s with a null or empty value should /// be skipped. /// [TestMethod] @@ -280,8 +280,8 @@ public void GetAttributes_ExtendedAttributes_ReturnsExtendedAttributes() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of attributes from a topic, filtering by . Expects an to be returned even if it's not but its - /// disagrees with . + /// cref="AttributeRecord"/> to be returned even if it's not but its + /// disagrees with . /// [TestMethod] public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() { @@ -302,7 +302,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsExtendedAttributes() \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of attributes from a topic, filtering by . Expects the to not be returned even though its + /// cref="AttributeRecord"/> to not be returned even though its /// disagrees with , since it won't match the 's isExtendedAttribute call. /// @@ -324,7 +324,7 @@ public void GetAttributes_ExtendedAttributeMismatch_ReturnsNothing() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of attributes from a topic, filtering by excludeLastModified. Confirms that s are not returned which start with LastModified. + /// cref="AttributeRecord"/>s are not returned which start with LastModified. /// [TestMethod] public void GetAttributes_ExcludeLastModified_ReturnsOtherAttributes() { @@ -345,7 +345,7 @@ public void GetAttributes_ExcludeLastModified_ReturnsOtherAttributes() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets an arbitrary (unmatched) attribute on a with a value shorter than 255 characters, then - /// ensures that it is returned as an an indexed when calling indexed when calling . /// [TestMethod] diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 56fbd485..00a6b834 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -275,7 +275,7 @@ public void LastModified_UpdateValue_ReturnsExpectedValue() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets a base topic to a topic entity, then replaces the references with a new topic entity. Ensures that both the - /// base topic as well as the underlying correctly reference the new value. + /// base topic as well as the underlying correctly reference the new value. /// [TestMethod] public void BaseTopic_UpdateValue_ReturnsExpectedValue() { diff --git a/OnTopic/Attributes/AttributeValue.cs b/OnTopic/Attributes/AttributeRecord.cs similarity index 88% rename from OnTopic/Attributes/AttributeValue.cs rename to OnTopic/Attributes/AttributeRecord.cs index 2dcdee3f..75797b5b 100644 --- a/OnTopic/Attributes/AttributeValue.cs +++ b/OnTopic/Attributes/AttributeRecord.cs @@ -11,7 +11,7 @@ namespace OnTopic.Attributes { /*============================================================================================================================ - | CLASS: ATTRIBUTE VALUE + | CLASS: ATTRIBUTE RECORD \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents the immutable value of a particular attribute on a . @@ -23,41 +23,41 @@ namespace OnTopic.Attributes { /// TrackedRecord{T}.LastModified"/> date. /// /// - /// Typically, the will be exposed as part of a via + /// Typically, the will be exposed as part of a via /// the collection. /// /// - /// Be aware that while represents the value of a specific attribute, the metadata for + /// Be aware that while represents the value of a specific attribute, the metadata for /// describing the purpose, constraints, and usage of that particular attribute is described by the class. /// /// /// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either - /// create a new instance of the class or, preferably, call the + /// create a new instance of the class or, preferably, call the /// 's /// method. /// /// - public record AttributeValue: TrackedRecord { + public record AttributeRecord: TrackedRecord { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - public AttributeValue(): base() { } + public AttributeRecord(): base() { } /// - /// Initializes a new instance of the class, using the specified key/value pair. + /// Initializes a new instance of the class, using the specified key/value pair. /// /// - /// The string identifier for the collection item key/value pair. + /// The string identifier for the collection item key/value pair. /// /// - /// The string value text for the collection item key/value pair. + /// The string value text for the collection item key/value pair. /// /// - /// An optional boolean indicator noting whether the collection item is a new value, and + /// An optional boolean indicator noting whether the collection item is a new value, and /// should thus be saved to the database when is next called. /// /// @@ -70,7 +70,7 @@ public AttributeValue(): base() { } /// description="The key must be specified for the key/value pair." exception="T:System.ArgumentNullException"> /// !String.IsNullOrWhiteSpace(key) /// - public AttributeValue( + public AttributeRecord( string key, string value, bool isDirty = true, diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeValueCollection.cs index 4018b87e..bf327f19 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeValueCollection.cs @@ -14,14 +14,14 @@ namespace OnTopic.Attributes { | CLASS: ATTRIBUTE VALUE COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Represents a collection of objects. + /// Represents a collection of objects. /// /// - /// objects represent individual instances of attributes associated with particular topics. + /// objects represent individual instances of attributes associated with particular topics. /// The class tracks these through its property, which is an instance of /// the class. /// - public class AttributeValueCollection : TrackedRecordCollection { + public class AttributeValueCollection : TrackedRecordCollection { /*========================================================================================================================== | CONSTRUCTOR @@ -41,14 +41,14 @@ internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { | PROPERTY: PARENT COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedRecordCollection? ParentCollection => + protected override TrackedRecordCollection? ParentCollection => AssociatedTopic.Parent?.Attributes; /*========================================================================================================================== | PROPERTY: BASE COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - protected override TrackedRecordCollection? BaseCollection => + protected override TrackedRecordCollection? BaseCollection => AssociatedTopic.BaseTopic?.Attributes; /*========================================================================================================================== @@ -65,7 +65,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { /// to persist changes to the storage medium. /// /// - /// Optionally excludes s whose keys start with LastModified. This is useful for + /// Optionally excludes s whose keys start with LastModified. This is useful for /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. /// @@ -80,14 +80,14 @@ public bool IsDirty(bool excludeLastModified) | METHOD: SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// /// Minimizes the need for defensive conditions throughout the library. /// - /// The string identifier for the AttributeValue. - /// The text value for the AttributeValue. + /// The string identifier for the . + /// The text value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -101,12 +101,12 @@ public bool IsDirty(bool excludeLastModified) /// /// Determines if the attribute originated from an extended attributes data store. /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) /// diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 3d1a5bcd..9041ec76 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -28,7 +28,7 @@ public static class AttributeValueCollectionExtensions { /// of inheritance, and an optional setting for searching through base topics for values. Return as a boolean. /// /// The instance of the this extension is bound to. - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. @@ -66,7 +66,7 @@ out var result /// of inheritance, and an optional setting for searching through base topics for values. Return as a integer. /// /// The instance of the this extension is bound to. - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. @@ -104,7 +104,7 @@ out var result /// of inheritance, and an optional setting for searching through base topics for values. Return as a double. /// /// The instance of the this extension is bound to. - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. @@ -142,7 +142,7 @@ out var result /// of inheritance, and an optional setting for searching through base topics for values. Return as a DateTime. /// /// The instance of the this extension is bound to. - /// The string identifier for the . + /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// /// Boolean indicator nothing whether to search through the topic's parents in order to get the value. @@ -176,12 +176,12 @@ out var result | METHOD: SET BOOLEAN \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// The instance of the this extension is bound to. - /// The string identifier for the . - /// The boolean value for the . + /// The string identifier for the . + /// The boolean value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -189,12 +189,12 @@ out var result /// persisted to the data store on . /// /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) /// @@ -214,12 +214,12 @@ public static void SetBoolean( | METHOD: SET INTEGER \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// The instance of the this extension is bound to. - /// The string identifier for the . - /// The integer value for the . + /// The string identifier for the . + /// The integer value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -227,12 +227,12 @@ public static void SetBoolean( /// persisted to the data store on . /// /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) /// @@ -256,12 +256,12 @@ public static void SetInteger( | METHOD: SET DOUBLE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// The instance of the this extension is bound to. - /// The string identifier for the . - /// The double value for the . + /// The string identifier for the . + /// The double value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -269,12 +269,12 @@ public static void SetInteger( /// persisted to the data store on . /// /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) /// @@ -298,12 +298,12 @@ public static void SetDouble( | METHOD: SET DATETIME \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Helper method that either adds a new object or updates the value of an existing one, + /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// /// The instance of the this extension is bound to. - /// The string identifier for the . - /// The value for the . + /// The string identifier for the . + /// The value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -311,12 +311,12 @@ public static void SetDouble( /// persisted to the data store on . /// /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) /// diff --git a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs index 3c4dfde7..e957b12d 100644 --- a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs @@ -400,7 +400,7 @@ public virtual void SetValue( /// attribute. This is used when e.g. importing values to determine if the existing value is newer than the source value. /// /// /// !String.IsNullOrWhiteSpace(key) /// diff --git a/OnTopic/Collections/Specialized/TrackedRecord{T}.cs b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs index d90ebbd9..e104d52e 100644 --- a/OnTopic/Collections/Specialized/TrackedRecord{T}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecord{T}.cs @@ -16,7 +16,7 @@ namespace OnTopic.Collections.Specialized { | CLASS: TRACKED RECORD \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a base class for tracking versioned records, such as . + /// Provides a base class for tracking versioned records, such as . /// /// /// The class is comparable to the , in that it tracks the or , and can /// optionally be retrieved via . This is useful in case there is data from /// the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as , , and collection works with instances, which contain metadata such as , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we allow /// the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as critical diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index c0423203..042e4291 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -28,9 +28,9 @@ namespace OnTopic.Metadata { /// /// /// The purpose of the class is only to describe the schema of an attribute. For each - /// individual , the actual values of attributes are stored in objects via + /// individual , the actual values of attributes are stored in objects via /// the property. By contrast to , the - /// class is focused exclusively on representing the attribute's value; it is not aware of + /// class is focused exclusively on representing the attribute's value; it is not aware of /// whether that attribute is required, what data type it represents, or how it should be displayed in the editor. /// /// @@ -48,7 +48,7 @@ public class AttributeDescriptor : Topic { /// cref="Topic.ContentType"/>, and, optionally, , . /// /// - /// By default, when creating new attributes, the s for both and s for both and will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on , and, optionally, , . /// /// - /// By default, when creating new attributes, the s for both and s for both and will be set to , which is required in order to /// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on /// The instance of the to operate against; populated automatically by .NET. - /// The string identifier for the against which to be searched. - /// The text value for the against which to be searched. + /// The string identifier for the against which to be searched. + /// The text value for the against which to be searched. /// A collection of topics matching the input parameters. /// /// !String.IsNullOrWhiteSpace(name) diff --git a/OnTopic/README.md b/OnTopic/README.md index 68344985..ff630d69 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -70,7 +70,7 @@ The `OnTopic` assembly contains a number of generic, keyed, and/or read-only col ### Specialty Collections The `OnTopic.Collections.Specialized` namespace includes a number of collections that are used by the OnTopic library, but won't generally be used directly by implementors, except as exposed by the core library. These include: - **[`TrackedRecordCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedRecord` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. - - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedRecordCollection` of [`AttributeValue`](Attributes/AttributeValue.cs) instances keyed by `AttributeValue.Key`; exposed by `Topic.Attributes`. + - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedRecordCollection` of [`AttributeRecord`](Attributes/AttributeRecord.cs) instances keyed by `AttributeRecord.Key`; exposed by `Topic.Attributes`. - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReferenceRecord`](Associations/TopicReferenceRecord.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. - **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 670219d9..55309180 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -685,20 +685,20 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi | METHOD: GET ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a , returns a list of , optionally filtering based on , returns a list of , optionally filtering based on and . /// /// The from which to pull the attributes. /// /// Whether or not to filter by . If null, all s are returned. + /// cref="AttributeRecord"/>s are returned. /// /// - /// Whether or not to filter by . If null, all s + /// Whether or not to filter by . If null, all s /// are returned. /// /// Exclude any attributes that start with LastModified. - protected IEnumerable GetAttributes( + protected IEnumerable GetAttributes( Topic topic, bool? isExtendedAttribute, bool? isDirty = null, @@ -723,7 +723,7 @@ protected IEnumerable GetAttributes( /*------------------------------------------------------------------------------------------------------------------------ | Get indexed attributes \-----------------------------------------------------------------------------------------------------------------------*/ - var attributes = new List(); + var attributes = new List(); foreach (var attributeValue in topic.Attributes) { @@ -781,7 +781,7 @@ isDirty is null || | METHOD: GET UNMATCHED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a , identifies s that are defined based on the , identifies s that are defined based on the , but aren't defined in the . /// /// The from which to pull the attributes. @@ -878,27 +878,27 @@ private static bool IsAttributeDescriptor(Topic topic) => \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Determines whether or not there's a mismatch between the and the - /// . + /// . /// /// /// /// The determines where an attribute should be stored; the - /// determines where an attribute was stored. If these two + /// determines where an attribute was stored. If these two /// values are in conflict, that suggests the coniguration for has /// changed since the attribute value was last saved. In that case, it should be treated as even though its value hasn't changed to ensure that its storage location is updated. /// /// - /// If cannot be found then the is arbitrary attribute + /// If cannot be found then the is arbitrary attribute /// not mapped to the schema. In that case, its storage location is dynamically determined based on its length, and thus /// it should only change locations when it . Otherwise, its length will remain /// the same, and thus the storage location should remain unchanged. /// /// /// The source , if available. - /// The target . + /// The target . /// - private static bool IsExtendedAttributeMismatch(AttributeDescriptor? attributeDescriptor, AttributeValue attributeValue) => + private static bool IsExtendedAttributeMismatch(AttributeDescriptor? attributeDescriptor, AttributeRecord attributeValue) => attributeDescriptor is not null && attributeValue.IsExtendedAttribute is not null && attributeDescriptor.IsExtendedAttribute != attributeValue.IsExtendedAttribute; diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index d2708e82..7e6250a3 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -44,7 +44,7 @@ public class Topic: ITrackDirtyKeys { /// optionally, , . /// /// - /// By default, when creating new attributes, the s for both and s for both and will be set to , which is required in order to correctly save new /// topics to the database. When the parameter is set, however, the property is set to falseon and , as it is assumed these @@ -567,7 +567,7 @@ public string GetWebPath() { /// Determines if , , and should be checked. /// /// - /// Optionally excludes s whose keys start with LastModified. This is useful for + /// Optionally excludes s whose keys start with LastModified. This is useful for /// excluding the byline (LastModifiedBy) and dateline (LastModified) since these values are automatically /// generated by e.g. the OnTopic Editor and, thus, may be irrelevant updates if no other attribute values have changed. /// @@ -724,7 +724,7 @@ public Topic? BaseTopic { /// significant extensibility. /// /// - /// Attributes are stored via an class which, in addition to the Attribute Key and Value, + /// Attributes are stored via an class which, in addition to the Attribute Key and Value, /// also track other metadata for the attribute, such as the version (via the /// property) and whether it has been persisted to the database or not (via the /// property). @@ -795,7 +795,7 @@ public Topic? BaseTopic { | METHOD: SET ATTRIBUTE VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Protected helper method that either adds a new object or updates the value of an + /// Protected helper method that either adds a new object or updates the value of an /// existing one, depending on whether that value already exists. /// /// @@ -807,8 +807,8 @@ public Topic? BaseTopic { /// DateTime?)"/> with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, /// call this overload. /// - /// The string identifier for the AttributeValue. - /// The text value for the AttributeValue. + /// The string identifier for the . + /// The text value for the . /// /// Specified whether the value should be marked as . By default, it will be marked /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is @@ -816,13 +816,13 @@ public Topic? BaseTopic { /// persisted to the data store on . /// /// /// !String.IsNullOrWhiteSpace(key) /// /// /// !String.IsNullOrWhiteSpace(value) From 400520b9eb1b29db590d332132136144854d9f59 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:03:32 -0800 Subject: [PATCH 636/778] Rename `AttributeValueCollection` to `AttributeCollection` This aligns with the rename of `AttributeValue` to `AttributeRecord` (f41c551). Technically, we could have named this `AttributeRecordCollection`, but that's a lower-level implementation detail that most callers won't need to worry about. This is also consistent with the (preexisting) name for `TopicReferenceCollection`, which also derives from `TrackedRecordCollection<>`. --- OnTopic.Tests/AttributeValueCollectionTest.cs | 67 +++++++++---------- OnTopic.Tests/ITopicRepositoryTest.cs | 2 +- OnTopic.Tests/TopicTest.cs | 2 +- ...ueCollection.cs => AttributeCollection.cs} | 16 ++--- OnTopic/Attributes/AttributeRecord.cs | 14 ++-- .../Attributes/AttributeSetterAttribute.cs | 10 +-- .../AttributeValueCollectionExtensions.cs | 36 +++++----- .../Reflection/TopicPropertyDispatcher.cs | 24 +++---- .../Reverse/ReverseTopicMappingService.cs | 6 +- OnTopic/README.md | 4 +- OnTopic/Repositories/TopicRepository.cs | 4 +- OnTopic/Topic.cs | 13 ++-- 12 files changed, 98 insertions(+), 100 deletions(-) rename OnTopic/Attributes/{AttributeValueCollection.cs => AttributeCollection.cs} (92%) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeValueCollectionTest.cs index 3793ec7b..ecc5207e 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeValueCollectionTest.cs @@ -17,7 +17,7 @@ namespace OnTopic.Tests { | CLASS: ATTRIBUTE VALUE COLLECTION TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the class. + /// Provides unit tests for the class. /// [TestClass] public class AttributeValueCollectionTest { @@ -336,9 +336,8 @@ public void SetValue_ValueUnchanged_IsNotDirty() { | TEST: IS DIRTY: DIRTY VALUES: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is marked as . Confirms that returns - /// true. + /// Populates the with a that is marked as . Confirms that returns true. /// [TestMethod] public void IsDirty_DirtyValues_ReturnsTrue() { @@ -355,8 +354,8 @@ public void IsDirty_DirtyValues_ReturnsTrue() { | TEST: IS DIRTY: DELETED VALUES: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms - /// that returns true. + /// Populates the with a and then deletes it. Confirms + /// that returns true. /// [TestMethod] public void IsDirty_DeletedValues_ReturnsTrue() { @@ -374,9 +373,9 @@ public void IsDirty_DeletedValues_ReturnsTrue() { | TEST: IS DIRTY: NO DIRTY VALUES: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is not marked as - /// . Confirms that returns - /// false/ + /// Populates the with a that is not marked as . Confirms that returns + /// false. /// [TestMethod] public void IsDirty_NoDirtyValues_ReturnsFalse() { @@ -393,9 +392,9 @@ public void IsDirty_NoDirtyValues_ReturnsFalse() { | TEST: IS DIRTY: EXCLUDE LAST MODIFIED: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a that is not marked as - /// as well as a LastModified that is. Confirms - /// that returns false. + /// Populates the with a that is not marked as as well as a LastModified that is. Confirms + /// that returns false. /// [TestMethod] public void IsDirty_ExcludeLastModified_ReturnsFalse() { @@ -414,7 +413,7 @@ public void IsDirty_ExcludeLastModified_ReturnsFalse() { | TEST: IS DIRTY: MARK CLEAN: UPDATES LAST MODIFIED \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms + /// Populates the with a and then deletes it. Confirms /// that the returns the new version after calling . /// @@ -436,8 +435,8 @@ public void IsDirty_MarkClean_UpdatesLastModified() { | TEST: IS DIRTY: MARK CLEAN: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then deletes it. Confirms - /// that returns false after calling with a and then deletes it. Confirms + /// that returns false after calling . /// [TestMethod] @@ -460,9 +459,9 @@ public void IsDirty_MarkClean_ReturnsFalse() { | TEST: IS DIRTY: MARK ATTRIBUTE CLEAN: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates the with a and then confirms that returns false for that attribute - /// after calling . + /// Populates the with a and then confirms that returns false for that attribute after + /// calling . /// [TestMethod] public void IsDirty_MarkAttributeClean_ReturnsFalse() { @@ -480,9 +479,9 @@ public void IsDirty_MarkAttributeClean_ReturnsFalse() { | TEST: IS DIRTY: ADD CLEAN ATTRIBUTE TO NEW TOPIC: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates a associated with an - /// with a that is not marked as and then confirms - /// that returns true. + /// Populates a associated with an with a + /// that is not marked as and then confirms that returns true. /// [TestMethod] public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { @@ -505,10 +504,10 @@ public void IsDirty_AddCleanAttributeToNewTopic_ReturnsTrue() { | TEST: IS DIRTY: MARK NEW TOPIC AS CLEAN: RETURNS TRUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Populates a associated with an - /// with a and then confirms that returns true for that attribute after calling . + /// Populates a associated with an with a + /// and then confirms that returns true for that attribute after calling . /// [TestMethod] public void IsDirty_MarkNewTopicAsClean_ReturnsTrue() { @@ -634,7 +633,7 @@ public void Add_DateTimeValueWithBusinessLogic_IsReturned() { | TEST: ADD: DATE/TIME VALUE WITH BUSINESS LOGIC: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets a Date/Time attribute on a topic instance with an invalid value; ensures an exception is thrown. + /// Sets a attribute on a topic instance with an invalid value; ensures an exception is thrown. /// [TestMethod] [ExpectedException( @@ -670,8 +669,8 @@ public void Add_InvalidAttributeValue_ThrowsException() { \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new which maps to directly to a and confirms that the original is replaced if the - /// changes. + /// "AttributeCollection"/> and confirms that the original is replaced if the changes. /// [TestMethod] public void Add_WithBusinessLogic_MaintainsIsDirty() { @@ -698,9 +697,9 @@ public void Add_WithBusinessLogic_MaintainsIsDirty() { | TEST: SET VALUE: EMPTY ATTRIBUTE VALUE: SKIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a new attribute with an empty value, and confirms that it is not added as a new . Empty values are treated as the same as non-existent attributes. They are stored for the sake - /// of tracking deleted attributes, but should not be stored for new attributes. + /// Adds a new attribute with an empty value, and confirms that it is not added as a new . Empty values are treated as the same as non-existent attributes. They are stored for the sake of tracking + /// deleted attributes, but should not be stored for new attributes. /// [TestMethod] public void SetValue_EmptyAttributeValue_Skips() { @@ -717,9 +716,9 @@ public void SetValue_EmptyAttributeValue_Skips() { | TEST: SET VALUE: UPDATE EMPTY ATTRIBUTE VALUE: REPLACES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Adds a new attribute with an empty value, and confirms that it is is added as a new assuming the value previously existed. Empty values are treated as the same as non-existent - /// attributes, but they should be stored for the sake of tracking deleted attributes. + /// Adds a new attribute with an empty value, and confirms that it is is added as a new assuming the value previously existed. Empty values are treated as the same as non-existent attributes, but they + /// should be stored for the sake of tracking deleted attributes. /// [TestMethod] public void SetValue_EmptyAttributeValue_Replaces() { diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs index ee6f6dea..27224957 100644 --- a/OnTopic.Tests/ITopicRepositoryTest.cs +++ b/OnTopic.Tests/ITopicRepositoryTest.cs @@ -17,7 +17,7 @@ namespace OnTopic.Tests { | CLASS: TOPIC REPOSITORY TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides unit tests for the class. + /// Provides unit tests for the class. /// /// /// These tests not only validate that the is functioning as expected, but also that the diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 00a6b834..07cdb54b 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -255,7 +255,7 @@ public void LastModified_UpdateVersionHistory_ReturnsExpectedValue() { | TEST: LAST MODIFIED: UPDATE ATTRIBUTE: RETURNS EXPECTED VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Returns the last modified date via , and ensures it's returned correctly. + /// Returns the last modified date via , and ensures it's returned correctly. /// [TestMethod] public void LastModified_UpdateValue_ReturnsExpectedValue() { diff --git a/OnTopic/Attributes/AttributeValueCollection.cs b/OnTopic/Attributes/AttributeCollection.cs similarity index 92% rename from OnTopic/Attributes/AttributeValueCollection.cs rename to OnTopic/Attributes/AttributeCollection.cs index bf327f19..2b94afb7 100644 --- a/OnTopic/Attributes/AttributeValueCollection.cs +++ b/OnTopic/Attributes/AttributeCollection.cs @@ -11,7 +11,7 @@ namespace OnTopic.Attributes { /*============================================================================================================================ - | CLASS: ATTRIBUTE VALUE COLLECTION + | CLASS: ATTRIBUTE COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Represents a collection of objects. @@ -19,22 +19,22 @@ namespace OnTopic.Attributes { /// /// objects represent individual instances of attributes associated with particular topics. /// The class tracks these through its property, which is an instance of - /// the class. + /// the class. /// - public class AttributeValueCollection : TrackedRecordCollection { + public class AttributeCollection : TrackedRecordCollection { /*========================================================================================================================== | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - /// The is intended exclusively for providing access to attributes via the - /// property. For this reason, the constructor is marked as internal. + /// The is intended exclusively for providing access to attributes via the property. For this reason, the constructor is marked as internal. /// /// A reference to the topic that the current attribute collection is bound to. - internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { + internal AttributeCollection(Topic parentTopic) : base(parentTopic) { } /*========================================================================================================================== @@ -56,7 +56,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(parentTopic) { \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Determine if any attributes in the are dirty. + /// Determine if any attributes in the are dirty. /// /// /// This method is intended primarily for data storage providers, such as , which may need diff --git a/OnTopic/Attributes/AttributeRecord.cs b/OnTopic/Attributes/AttributeRecord.cs index 75797b5b..cc18f4a1 100644 --- a/OnTopic/Attributes/AttributeRecord.cs +++ b/OnTopic/Attributes/AttributeRecord.cs @@ -23,20 +23,18 @@ namespace OnTopic.Attributes { /// TrackedRecord{T}.LastModified"/> date. /// /// - /// Typically, the will be exposed as part of a via - /// the collection. + /// Typically, the will be exposed as part of a via the + /// collection. /// /// /// Be aware that while represents the value of a specific attribute, the metadata for - /// describing the purpose, constraints, and usage of that particular attribute is described by the class. + /// describing the purpose, constraints, and usage of that particular attribute is described by the class. /// /// /// This class is immutable: once it is constructed, the values cannot be changed. To change a value, callers must either - /// create a new instance of the class or, preferably, call the - /// 's - /// method. + /// create a new instance of the class or, preferably, call the 's method. /// /// public record AttributeRecord: TrackedRecord { diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs index 4f0bdfa3..8e7bcb89 100644 --- a/OnTopic/Attributes/AttributeSetterAttribute.cs +++ b/OnTopic/Attributes/AttributeSetterAttribute.cs @@ -13,13 +13,13 @@ namespace OnTopic.Attributes { | CLASS: ATTRIBUTE SETTER [ATTRIBUTE] \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Flags that a property should be used when setting an attribute via - /// . + /// Flags that a property should be used when setting an attribute via . /// /// /// - /// When a call is made to , - /// the code will check to see if a property with the same name as the attribute key exists, and whether that property is + /// When a call is made to , the + /// code will check to see if a property with the same name as the attribute key exists, and whether that property is /// decorated with the (i.e., [AttributeSetter]). If it is, then the /// update will be routed through that property. This ensures that business logic is enforced by local properties, instead /// of allowing business logic to be potentially bypassed by writing directly to the @@ -36,7 +36,7 @@ namespace OnTopic.Attributes { /// To ensure this logic, it is critical that implementers of ensure that the /// property setters call overload with the final parameter set to false to disable the enforcement of business logic. - /// Otherwise, an infinite loop will occur. Calling that overload tells that the + /// Otherwise, an infinite loop will occur. Calling that overload tells that the /// business logic has already been enforced by the caller. As this is an internal overload, implementers should use the /// local proxy at , which ensures that final parameter is /// set to false. diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs index 9041ec76..664e0899 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeValueCollectionExtensions.cs @@ -15,8 +15,8 @@ namespace OnTopic.Attributes { | CLASS: ATTRIBUTE VALUE COLLECTION (EXTENSIONS) \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides extensions for setting and retrieving values from the using strongly - /// typed values. + /// Provides extensions for setting and retrieving values from the using strongly typed + /// values. /// public static class AttributeValueCollectionExtensions { @@ -27,7 +27,7 @@ public static class AttributeValueCollectionExtensions { /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling /// of inheritance, and an optional setting for searching through base topics for values. Return as a boolean. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// @@ -39,7 +39,7 @@ public static class AttributeValueCollectionExtensions { /// /// The value for the attribute as a boolean. public static bool GetBoolean( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string name, bool defaultValue = default, bool inheritFromParent = false, @@ -65,7 +65,7 @@ out var result /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling /// of inheritance, and an optional setting for searching through base topics for values. Return as a integer. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// @@ -77,7 +77,7 @@ out var result /// /// The value for the attribute as an integer. public static int GetInteger( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string name, int defaultValue = default, bool inheritFromParent = false, @@ -103,7 +103,7 @@ out var result /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling /// of inheritance, and an optional setting for searching through base topics for values. Return as a double. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// @@ -115,7 +115,7 @@ out var result /// /// The value for the attribute as a double. public static double GetDouble( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string name, double defaultValue = default, bool inheritFromParent = false, @@ -141,7 +141,7 @@ out var result /// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling /// of inheritance, and an optional setting for searching through base topics for values. Return as a DateTime. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// A string value to which to fall back in the case the value is not found. /// @@ -153,7 +153,7 @@ out var result /// /// The value for the attribute as a DateTime object. public static DateTime GetDateTime( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string name, DateTime defaultValue = default, bool inheritFromParent = false, @@ -179,7 +179,7 @@ out var result /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// The boolean value for the . /// @@ -204,7 +204,7 @@ out var result /// !value.Contains(" ") /// public static void SetBoolean( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string key, bool value, bool? isDirty = null @@ -217,7 +217,7 @@ public static void SetBoolean( /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// The integer value for the . /// @@ -242,7 +242,7 @@ public static void SetBoolean( /// !value.Contains(" ") /// public static void SetInteger( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string key, int value, bool? isDirty = null @@ -259,7 +259,7 @@ public static void SetInteger( /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// The double value for the . /// @@ -284,7 +284,7 @@ public static void SetInteger( /// !value.Contains(" ") /// public static void SetDouble( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string key, double value, bool? isDirty = null @@ -301,7 +301,7 @@ public static void SetDouble( /// Helper method that either adds a new object or updates the value of an existing one, /// depending on whether that value already exists. /// - /// The instance of the this extension is bound to. + /// The instance of the this extension is bound to. /// The string identifier for the . /// The value for the . /// @@ -326,7 +326,7 @@ public static void SetDouble( /// !value.Contains(" ") /// public static void SetDateTime( - this AttributeValueCollection attributes, + this AttributeCollection attributes, string key, DateTime value, bool? isDirty = null diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs index 1d7ae806..75d6d514 100644 --- a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs +++ b/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs @@ -51,8 +51,8 @@ namespace OnTopic.Internal.Reflection { /// saved as part of either or , and can /// optionally be retrieved via . This is useful in case there is data from /// the original request that might be lost when routed through the property setter. For example, the collection works with instances, which contain metadata such as , , and collection works with instances, which contain metadata such as , , and , which are not passed to the corresponding property decorated with . As such, by saving a reference to those as part of the process, we allow /// the source collection to retrieve the original request in order to ensure that data isn't lost. This isn't as critical @@ -72,16 +72,16 @@ namespace OnTopic.Internal.Reflection { /// /// One caveat to this are cases where the caller attempts to set the value via the property directly, /// instead of adding the item directly to the corresponding collection—e.g., they call instead - /// of e.g. the method - /// from . In that case, the business logic will already have been enforced, but the method will not have been called. To mitigate the property setter getting called - /// twice, collection implementors are advised to offer an internal overload that allows an item to be added to the - /// collection while bypassing the business logic. For instance, this can be done using or ; in each - /// case, the internally accessible enforceBusinessLogic parameter allows a property setter to disable business - /// logic. Internally, this is done by calling , thus assuring that the business logic has already occurred. + /// of e.g. the method from + /// . In that case, the business logic will already have been enforced, but the method will not have been called. To mitigate the property setter getting called twice, + /// collection implementors are advised to offer an internal overload that allows an item to be added to the collection + /// while bypassing the business logic. For instance, this can be done using or ; in each case, the internally + /// accessible enforceBusinessLogic parameter allows a property setter to disable business logic. Internally, this + /// is done by calling , thus assuring that + /// the business logic has already occurred. /// /// internal class TopicPropertyDispatcher diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index a851aed6..7fa44c52 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -336,13 +336,15 @@ await MapAsync( /// . If the value is not set on the then the will be evaluated as a fallback. If the property is not of a settable type then the /// property is not set. If the value is empty, then it will be treated as null in the 's - /// . + /// . /// /// /// The binding model from which to derive the data. Must inherit from . /// /// The entity to map the data to. - /// The with details about the property's attributes. + /// + /// The with details about the property's attributes. + /// /// private static void SetScalarValue( object source, diff --git a/OnTopic/README.md b/OnTopic/README.md index ff630d69..71d9184e 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -49,7 +49,7 @@ Out of the box, the OnTopic library contains two specially derived topics for su ## Extension Methods - **[`Querying`](Querying/TopicExtensions.cs)**: The [`TopicExtensions`](Querying/TopicExtensions.cs) class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate. There are also specialty extensions for querying [`IEnumerable`](Querying/TopicCollectionExtensions.cs). -- **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeValueCollection`](Attributes/AttributeValueCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. +- **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeCollection`](Attributes/AttributeCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. ## Collections The `OnTopic` assembly contains a number of generic, keyed, and/or read-only collections for working with topics. These include: @@ -70,7 +70,7 @@ The `OnTopic` assembly contains a number of generic, keyed, and/or read-only col ### Specialty Collections The `OnTopic.Collections.Specialized` namespace includes a number of collections that are used by the OnTopic library, but won't generally be used directly by implementors, except as exposed by the core library. These include: - **[`TrackedRecordCollection{TItem, TValue, TAttribute}`](Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs)**: A `KeyedCollection` of `TrackedRecord` instances which tracks the `IsDirty` status and `DeletedItems`, while also enforcing business logic against corresponding properties on the associated `Topic`. - - **[`AttributeValueCollection`](attributes/AttributeValueCollection.cs)**: A `TrackedRecordCollection` of [`AttributeRecord`](Attributes/AttributeRecord.cs) instances keyed by `AttributeRecord.Key`; exposed by `Topic.Attributes`. + - **[`AttributeCollection`](attributes/AttributeCollection.cs)**: A `TrackedRecordCollection` of [`AttributeRecord`](Attributes/AttributeRecord.cs) instances keyed by `AttributeRecord.Key`; exposed by `Topic.Attributes`. - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReferenceRecord`](Associations/TopicReferenceRecord.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. - **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index 55309180..f7304504 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -781,8 +781,8 @@ isDirty is null || | METHOD: GET UNMATCHED ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Given a , identifies s that are defined based on the , but aren't defined in the . + /// Given a , identifies s that are defined based on the , but aren't defined in the . /// /// The from which to pull the attributes. protected IEnumerable GetUnmatchedAttributes(Topic topic) { diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 7e6250a3..dc76f28b 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -730,7 +730,7 @@ public Topic? BaseTopic { /// property). /// /// The current 's attributes. - public AttributeValueCollection Attributes { get; } + public AttributeCollection Attributes { get; } /*========================================================================================================================== | PROPERTY: RELATIONSHIPS @@ -800,12 +800,11 @@ public Topic? BaseTopic { /// /// /// When an attribute value is set and a corresponding, writable property exists on the topic, that property will be - /// called by the . This is intended to enforce local business logic, and prevent - /// callers from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the - /// that the business logic has already been enforced. To do that, they must either - /// call with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, - /// call this overload. + /// called by the . This is intended to enforce local business logic, and prevent callers + /// from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the that the business logic has already been enforced. To do that, they must either call with the + /// enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this overload. /// /// The string identifier for the . /// The text value for the . From d18d0eb5144dd811cc431fa52a516a462294672d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:05:47 -0800 Subject: [PATCH 637/778] Renamed `AttributeValueCollectionTest` to `AttributeCollectionTest` This corresponds to the rename of `AttributeValueCollection` to `AttributeCollection` (400520b). --- ...ibuteValueCollectionTest.cs => AttributeCollectionTest.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename OnTopic.Tests/{AttributeValueCollectionTest.cs => AttributeCollectionTest.cs} (99%) diff --git a/OnTopic.Tests/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeCollectionTest.cs similarity index 99% rename from OnTopic.Tests/AttributeValueCollectionTest.cs rename to OnTopic.Tests/AttributeCollectionTest.cs index ecc5207e..1eabba67 100644 --- a/OnTopic.Tests/AttributeValueCollectionTest.cs +++ b/OnTopic.Tests/AttributeCollectionTest.cs @@ -14,13 +14,13 @@ namespace OnTopic.Tests { /*============================================================================================================================ - | CLASS: ATTRIBUTE VALUE COLLECTION TEST + | CLASS: ATTRIBUTE COLLECTION TEST \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the class. /// [TestClass] - public class AttributeValueCollectionTest { + public class AttributeCollectionTest { /*========================================================================================================================== | TEST: GET VALUE: CORRECT VALUE: IS RETURNED From 4c99dbcb74888c4244ba81be9ae6aeea92eccfce Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:08:48 -0800 Subject: [PATCH 638/778] Rename to `AttributeCollectionExtensions` This corresponds to the rename of `AttributeValueCollection` to `AttributeCollection` (400520b). --- ...llectionExtensions.cs => AttributeCollectionExtensions.cs} | 4 ++-- OnTopic/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename OnTopic/Attributes/{AttributeValueCollectionExtensions.cs => AttributeCollectionExtensions.cs} (99%) diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeCollectionExtensions.cs similarity index 99% rename from OnTopic/Attributes/AttributeValueCollectionExtensions.cs rename to OnTopic/Attributes/AttributeCollectionExtensions.cs index 664e0899..b581bbf2 100644 --- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs +++ b/OnTopic/Attributes/AttributeCollectionExtensions.cs @@ -12,13 +12,13 @@ namespace OnTopic.Attributes { /*============================================================================================================================ - | CLASS: ATTRIBUTE VALUE COLLECTION (EXTENSIONS) + | CLASS: ATTRIBUTE COLLECTION (EXTENSIONS) \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides extensions for setting and retrieving values from the using strongly typed /// values. /// - public static class AttributeValueCollectionExtensions { + public static class AttributeCollectionExtensions { /*========================================================================================================================== | METHOD: GET BOOLEAN VALUE diff --git a/OnTopic/README.md b/OnTopic/README.md index 71d9184e..81de778a 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -49,7 +49,7 @@ Out of the box, the OnTopic library contains two specially derived topics for su ## Extension Methods - **[`Querying`](Querying/TopicExtensions.cs)**: The [`TopicExtensions`](Querying/TopicExtensions.cs) class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate. There are also specialty extensions for querying [`IEnumerable`](Querying/TopicCollectionExtensions.cs). -- **[`Attributes`](Attributes/AttributeValueCollectionExtensions.cs)**: The `AttributeValueCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeCollection`](Attributes/AttributeCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string`value type. +- **[`Attributes`](Attributes/AttributeCollectionExtensions.cs)**: The `AttributeCollectionExtensions` class exposes optional extension methods for strongly typed access to the [`AttributeCollection`](Attributes/AttributeCollection.cs). This includes e.g., `GetBooleanValue()` and `SetBooleanValue()`, which takes care of the conversion to and from the underlying `string` value type. ## Collections The `OnTopic` assembly contains a number of generic, keyed, and/or read-only collections for working with topics. These include: From 98c73f8b403f4c1ac7c6890658939172934e5477 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:14:36 -0800 Subject: [PATCH 639/778] Renamed unit tests involving `AttributeRecord` This aligns with the rename of `AttributeValue` to `AttributeRecord` (f41c551). --- OnTopic.Tests/AttributeCollectionTest.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/OnTopic.Tests/AttributeCollectionTest.cs b/OnTopic.Tests/AttributeCollectionTest.cs index 1eabba67..e52a6195 100644 --- a/OnTopic.Tests/AttributeCollectionTest.cs +++ b/OnTopic.Tests/AttributeCollectionTest.cs @@ -538,14 +538,14 @@ public void SetValue_InvalidValue_ThrowsException() { } /*========================================================================================================================== - | TEST: ADD: VALID ATTRIBUTE VALUE: IS RETURNED + | TEST: ADD: VALID ATTRIBUTE RECORD: IS RETURNED \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets a custom attribute on a topic by directly adding an instance; ensures it can be /// retrieved. /// [TestMethod] - public void Add_ValidAttributeValue_IsReturned() { + public void Add_ValidAttributeRecord_IsReturned() { var topic = TopicFactory.Create("Test", "Container"); @@ -649,17 +649,18 @@ public void Add_DateTimeValueWithBusinessLogic_ThrowsException() { } /*========================================================================================================================== - | TEST: SET VALUE: INSERT INVALID ATTRIBUTE VALUE: THROWS EXCEPTION + | TEST: SET VALUE: INSERT INVALID ATTRIBUTE RECORD: THROWS EXCEPTION \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Attempts to violate the business logic by bypassing SetValue() entirely; ensures that business logic is enforced. + /// Attempts to violate the business logic by bypassing entirely; ensures that business logic is enforced. /// [TestMethod] [ExpectedException( typeof(InvalidKeyException), "The topic allowed a key to be set via a back door, without routing it through the View property." )] - public void Add_InvalidAttributeValue_ThrowsException() { + public void Add_InvalidAttributeRecord_ThrowsException() { var topic = TopicFactory.Create("Test", "Container"); topic.Attributes.Add(new("View", "# ?")); } @@ -694,7 +695,7 @@ public void Add_WithBusinessLogic_MaintainsIsDirty() { } /*========================================================================================================================== - | TEST: SET VALUE: EMPTY ATTRIBUTE VALUE: SKIPS + | TEST: SET VALUE: EMPTY ATTRIBUTE RECORD: SKIPS \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Adds a new attribute with an empty value, and confirms that it is not added as a new /// Adds a new attribute with an empty value, and confirms that it is is added as a new Date: Tue, 9 Feb 2021 17:19:29 -0800 Subject: [PATCH 640/778] Fixed off-target rename of `AttributeValue` column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rename of `AttributeValue` to `AttributeRecord` (f41c551) inadvertently resulted in the `AttributeDataTable`'s `AttributeValue` column being renamed to `AttributeRecord`. But this actually represents the `AttributeRecord.Value` properties—or the `Attributes.AttributeValue` column in the database—and should not be renamed. Oops! --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.Tests/Schemas/AttributesDataTable.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 6ff58a96..93936bd3 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -237,7 +237,7 @@ private static void SetIndexedAttributes(this IDataReader reader, TopicIndex top \-----------------------------------------------------------------------------------------------------------------------*/ var topicId = reader.GetTopicId(); var attributeKey = reader.GetString("AttributeKey"); - var attributeValue = reader.GetString("AttributeRecord"); + var attributeValue = reader.GetString("AttributeValue"); var version = reader.GetVersion(); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Tests/Schemas/AttributesDataTable.cs b/OnTopic.Tests/Schemas/AttributesDataTable.cs index 3ad739f7..2571698c 100644 --- a/OnTopic.Tests/Schemas/AttributesDataTable.cs +++ b/OnTopic.Tests/Schemas/AttributesDataTable.cs @@ -52,7 +52,7 @@ public AttributesDataTable() : base("Attributes") { \-----------------------------------------------------------------------------------------------------------------------*/ Columns.Add(new DataColumn() { DataType = typeof(string), - ColumnName = "AttributeRecord", + ColumnName = "AttributeValue", AllowDBNull = true }); @@ -87,7 +87,7 @@ public void AddRow(int topicId, string attributeKey, string? attributeValue, Dat row["TopicId"] = topicId; row["AttributeKey"] = attributeKey; - row["AttributeRecord"] = attributeValue is null? DBNull.Value : attributeValue; + row["AttributeValue"] = attributeValue is null? DBNull.Value : attributeValue; row["Version"] = version?? DateTime.UtcNow; /*------------------------------------------------------------------------------------------------------------------------ From a09cd00fb01c50c9e8280f3aaaade67fc9ad1b9d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:33:42 -0800 Subject: [PATCH 641/778] Migrated `PropertyConfiguration` to `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `PropertyConfiguration` class has always been intended for `internal` use—as indicate by its placement in the `OnTopic.Mapping.Internal` namespace!—but it maintained a `public` access modifier because it was referenced as a parameter in `protected` methods off of the `TopicMappingService`. Those methods have recently been moved to `private` (f74e2c9b) in order to acknowledge that they don't offer a fully thought out opportunity for extensibility. Given that, we can now migrate `PropertyConfiguration` to a _truly_ `internal` class. --- .../Mapping/Internal/PropertyConfiguration.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/OnTopic/Mapping/Internal/PropertyConfiguration.cs b/OnTopic/Mapping/Internal/PropertyConfiguration.cs index eff5b56c..78de90d2 100644 --- a/OnTopic/Mapping/Internal/PropertyConfiguration.cs +++ b/OnTopic/Mapping/Internal/PropertyConfiguration.cs @@ -36,7 +36,7 @@ namespace OnTopic.Mapping.Internal { /// property on the DTO to be aliased to a different property or attribute name on the source . /// /// - public class PropertyConfiguration { + internal class PropertyConfiguration { /*========================================================================================================================== | CONSTRUCTOR @@ -47,7 +47,7 @@ public class PropertyConfiguration { /// /// The instance to check for values. /// The prefix to apply to the attributes. - public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "") { + internal PropertyConfiguration(PropertyInfo property, string? attributePrefix = "") { /*------------------------------------------------------------------------------------------------------------------------ | Validate parameters @@ -121,7 +121,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// The that the current is associated with. /// - public PropertyInfo Property { get; } + internal PropertyInfo Property { get; } /*========================================================================================================================== | PROPERTY: ATTRIBUTE KEY @@ -142,7 +142,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// can be assigned by decorating a DTO property with e.g. [AttributeKey("AlternateAttributeKey")]. /// /// - public string AttributeKey { get; set; } + internal string AttributeKey { get; set; } /*========================================================================================================================== | PROPERTY: MAP TO PARENT? @@ -162,7 +162,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// provided; see for details. /// /// - public bool MapToParent { get; set; } + internal bool MapToParent { get; set; } /*========================================================================================================================== | PROPERTY: ATTRIBUTE PREFIX @@ -196,7 +196,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// those two prefixes. This allows potentially very deep object models. /// /// - public string? AttributePrefix { get; set; } + internal string? AttributePrefix { get; set; } /*========================================================================================================================== | PROPERTY: DEFAULT VALUE @@ -208,7 +208,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// The property corresponds to the property. It can /// be assigned by decorating a DTO property with e.g. [DefaultValue("DefaultValue")]. /// - public object? DefaultValue { get; set; } + internal object? DefaultValue { get; set; } /*========================================================================================================================== | PROPERTY: INHERIT VALUE @@ -229,7 +229,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// property. It can be assigned by decorating a model property with e.g. [Inherit]. /// /// - public bool InheritValue { get; set; } + internal bool InheritValue { get; set; } /*========================================================================================================================== | PROPERTY: COLLECTION KEY @@ -251,7 +251,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// can be assigned by decorating a model property with e.g. [Collection("AlternateCollectionKey")]. /// /// - public string CollectionKey { get; set; } + internal string CollectionKey { get; set; } /*========================================================================================================================== | PROPERTY: COLLECTION TYPE @@ -273,7 +273,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// Children)]. /// /// - public CollectionType CollectionType { get; set; } + internal CollectionType CollectionType { get; set; } /*========================================================================================================================== | PROPERTY: INCLUDE ASSOCIATIONS @@ -295,7 +295,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// property. It can be assigned by decorating a model property with e.g. [Include(Relationships.Children)]. /// /// - public AssociationTypes IncludeAssociations { get; set; } + internal AssociationTypes IncludeAssociations { get; set; } /*========================================================================================================================== | PROPERTY: METADATA KEY @@ -317,7 +317,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// assigned by decorating a DTO property with e.g. [Metadata("States")]. /// /// - public string? MetadataKey { get; set; } + internal string? MetadataKey { get; set; } /*========================================================================================================================== | PROPERTY: FLATTEN CHILDREN @@ -348,7 +348,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// property. It can be assigned by decorating a DTO property with e.g. [Flatten]. /// /// - public bool FlattenChildren { get; set; } + internal bool FlattenChildren { get; set; } /*========================================================================================================================== | PROPERTY: DISABLE MAPPING @@ -362,7 +362,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// given property. It can be assigned by decorating a DTO property with e.g. [DisableMapping]. /// /// - public bool DisableMapping { get; set; } + internal bool DisableMapping { get; set; } /*========================================================================================================================== | PROPERTY: CONTENT TYPE FILTER @@ -383,7 +383,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// . /// /// - public string? ContentTypeFilter { get; set; } + internal string? ContentTypeFilter { get; set; } /*========================================================================================================================== | PROPERTY: ATTRIBUTE FILTERS @@ -406,7 +406,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// to a single property, thus allowing each item in a collection to be filtered by multiple values. /// /// - public Dictionary AttributeFilters { get; } + internal Dictionary AttributeFilters { get; } /*========================================================================================================================== | METHOD: SATISFIES ATTRIBUTE FILTERS @@ -417,7 +417,7 @@ public PropertyConfiguration(PropertyInfo property, string? attributePrefix = "" /// /// /// - public bool SatisfiesAttributeFilters(Topic source) => + internal bool SatisfiesAttributeFilters(Topic source) => AttributeFilters.All(f => source?.Attributes?.GetValue(f.Key, "")?.Equals(f.Value, StringComparison.OrdinalIgnoreCase)?? false ); @@ -430,7 +430,7 @@ public bool SatisfiesAttributeFilters(Topic source) => /// ensure that their conditions are satisfied. /// /// The target DTO to validate the current property on. - public void Validate(object target) { + internal void Validate(object target) { foreach (ValidationAttribute validator in Property.GetCustomAttributes(typeof(ValidationAttribute))) { validator.Validate(Property.GetValue(target), Property.Name); } From 198e086a75690ac42439b16e64640a3b97b87da6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:34:21 -0800 Subject: [PATCH 642/778] Migrated `MappedTopicCache` to `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `MappedTopicCache` class has always been intended for `internal` use—as indicate by its placement in the `OnTopic.Mapping.Internal` namespace!—but it maintained a `public` access modifier because it was referenced as a parameter in `protected` methods off of the `TopicMappingService`. Those methods have recently been moved to `private` (f74e2c9b) in order to acknowledge that they don't offer a fully thought out opportunity for extensibility. Given that, we can now migrate `MappedTopicCache` to a _truly_ `internal` class. --- OnTopic/Mapping/Internal/MappedTopicCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Mapping/Internal/MappedTopicCache.cs b/OnTopic/Mapping/Internal/MappedTopicCache.cs index 1af9ab76..807d1460 100644 --- a/OnTopic/Mapping/Internal/MappedTopicCache.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCache.cs @@ -13,7 +13,7 @@ namespace OnTopic.Mapping.Internal { /// /// Provides a collection intended to track local caching of objects mapped using the . /// - public class MappedTopicCache: ConcurrentDictionary { + internal class MappedTopicCache: ConcurrentDictionary { } //Class From 59a18453967f133ab5863d6c6def21998cb07a1f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:35:09 -0800 Subject: [PATCH 643/778] Migrated `MappedTopicCacheEntry` to `internal` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `MappedTopicCacheEntry` class has always been intended for `internal` use—as indicate by its placement in the `OnTopic.Mapping.Internal` namespace!—but it maintained a `public` access modifier because it was referenced as a parameter in `protected` methods off of the `TopicMappingService`. Those methods have recently been moved to `private` (f74e2c9b) in order to acknowledge that they don't offer a fully thought out opportunity for extensibility. Given that, we can now migrate `MappedTopicCacheEntry` to a _truly_ `internal` class, alongside its parent collection (198e086). --- OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs index 42475118..879e82d6 100644 --- a/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs +++ b/OnTopic/Mapping/Internal/MappedTopicCacheEntry.cs @@ -21,7 +21,7 @@ namespace OnTopic.Mapping.Internal { /// associations by using . This ensures that even if a topic has /// already been mapped, its scope can be expanded without duplicating effort. /// - public class MappedTopicCacheEntry { + internal class MappedTopicCacheEntry { /*========================================================================================================================== | PROPERTY: MAPPED TOPIC @@ -29,7 +29,7 @@ public class MappedTopicCacheEntry { /// /// Provides a reference to the mapped object. /// - public object MappedTopic { get; set; } = null!; + internal object MappedTopic { get; set; } = null!; /*========================================================================================================================== | PROPERTY: ASSOCIATIONS @@ -37,7 +37,7 @@ public class MappedTopicCacheEntry { /// /// Provides a reference to the associations that the was mapped with. /// - public AssociationTypes Associations { get; set; } = AssociationTypes.None; + internal AssociationTypes Associations { get; set; } = AssociationTypes.None; /*========================================================================================================================== | METHOD: GET MISSING ASSOCIATIONS @@ -46,7 +46,7 @@ public class MappedTopicCacheEntry { /// Given a target , identifies any associations not covered by /// and returns them as a new instance. /// - public AssociationTypes GetMissingAssociations(AssociationTypes associations) => Associations ^ (associations | Associations); + internal AssociationTypes GetMissingAssociations(AssociationTypes associations) => Associations ^ (associations | Associations); /*========================================================================================================================== | METHOD: ADD MISSING ASSOCIATIONS @@ -55,7 +55,7 @@ public class MappedTopicCacheEntry { /// Given a target , adds any missing to the property. /// - public void AddMissingAssociations(AssociationTypes associations) => Associations = associations | Associations; + internal void AddMissingAssociations(AssociationTypes associations) => Associations = associations | Associations; } //Class } //Namespace \ No newline at end of file From 9c3cbd1af18f5ef043180dd706fb7898bc49281c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:49:57 -0800 Subject: [PATCH 644/778] Update to latest version of `GitVersion` This is an internal development dependency, and has no impact on downstream implementations. --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 2 +- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 2 +- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 2 +- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 2 +- OnTopic/OnTopic.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index b1fc5269..27a468b7 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 85bc01b8..65f0f8f4 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 14acd865..8e4ddf69 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -36,7 +36,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 63e4820f..aa965858 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -38,7 +38,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 0de3fa47..453389d9 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -39,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From ab945dff2953283cdf948d6449578be8b819c0de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 17:57:09 -0800 Subject: [PATCH 645/778] Fixed typo in `cref` in XML Doc --- .../TrackedRecordCollection{TItem,TValue,TAttribute}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs index e957b12d..d082e721 100644 --- a/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs +++ b/OnTopic/Collections/Specialized/TrackedRecordCollection{TItem,TValue,TAttribute}.cs @@ -87,7 +87,7 @@ internal TrackedRecordCollection(Topic parentTopic) : base(StringComparer.Ordina /// . If a is deleted, then it won't be marked as . If no other instances were modified, then the won't get saved, and that won't be deleted. Further more, methods like - /// the method have no way of detecting the deletion of + /// the method have no way of detecting the deletion of /// arbitrary values—i.e., attributes that were deleted which don't correspond to attributes configured on the . By tracking any deleted instances, we ensure both /// scenarios can be accounted for. From 9e9bd7ce53c229bb48b0dfa08b5d19544f318c0b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 18:01:57 -0800 Subject: [PATCH 646/778] Fixed references to `SetPropertyValue()` In a previous commit, I merged two overloads of `SetPropertyValue()` into one which accepts a final parameter of `object?`, not `string`). To accommodate this, the XML Doc references to it needed to be updated. While I was at it, I rewrapped the XML Docs within the `TypeMemberInfoCollection` unit test class. --- OnTopic.Tests/TypeMemberInfoCollectionTest.cs | 40 +++++++++---------- .../Internal/Reflection/MemberDispatcher.cs | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs b/OnTopic.Tests/TypeMemberInfoCollectionTest.cs index 0ef2f221..78f2b12f 100644 --- a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs +++ b/OnTopic.Tests/TypeMemberInfoCollectionTest.cs @@ -78,8 +78,8 @@ public void Constructor_ValidType_IdentifiesMethod() { | TEST: GET MEMBERS: PROPERTY INFO: RETURNS PROPERTIES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that functions. + /// Establishes a and confirms that + /// functions. /// [TestMethod] public void GetMembers_PropertyInfo_ReturnsProperties() { @@ -99,8 +99,8 @@ public void GetMembers_PropertyInfo_ReturnsProperties() { | TEST: GET MEMBER: PROPERTY INFO BY KEY: RETURNS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that correctly returns the expected properties. + /// Establishes a and confirms that correctly returns the expected properties. /// [TestMethod] public void GetMember_PropertyInfoByKey_ReturnsValue() { @@ -134,8 +134,8 @@ public void GetMember_MethodInfoByKey_ReturnsValue() { | TEST: GET MEMBER: GENERIC TYPE MISMATCH: RETURNS NULL \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that does not return values if the + /// Establishes a and confirms that does not return values if the types mismatch. /// [TestMethod] public void GetMember_GenericTypeMismatch_ReturnsNull() { @@ -151,8 +151,8 @@ public void GetMember_GenericTypeMismatch_ReturnsNull() { | TEST: SET PROPERTY VALUE: KEY: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a key value can be properly set using the - /// method. + /// Establishes a and confirms that a key value can be properly set using the method. /// [TestMethod] public void SetPropertyValue_Key_SetsValue() { @@ -175,8 +175,8 @@ public void SetPropertyValue_Key_SetsValue() { | TEST: SET PROPERTY VALUE: BOOLEAN: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a boolean value can be properly set using the - /// method. + /// Establishes a and confirms that a boolean value can be properly set using the method. /// [TestMethod] public void SetPropertyValue_Boolean_SetsValue() { @@ -194,8 +194,8 @@ public void SetPropertyValue_Boolean_SetsValue() { | TEST: SET PROPERTY VALUE: DATE/TIME: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a date/time value can be properly set using the - /// method. + /// Establishes a and confirms that a date/time value can be properly set using the method. /// [TestMethod] public void SetPropertyValue_DateTime_SetsValue() { @@ -223,8 +223,8 @@ public void SetPropertyValue_DateTime_SetsValue() { | TEST: SET PROPERTY VALUE: INVALID PROPERTY: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that an invalid property being set via the - /// method returns false. + /// Establishes a and confirms that an invalid property being set via the method returns false. /// [TestMethod] public void SetPropertyValue_InvalidProperty_ReturnsFalse() { @@ -242,8 +242,8 @@ public void SetPropertyValue_InvalidProperty_ReturnsFalse() { | TEST: SET METHOD: VALID VALUE: SETS VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a value can be properly set using the - /// method. + /// Establishes a and confirms that a value can be properly set using the method. /// [TestMethod] public void SetMethod_ValidValue_SetsValue() { @@ -264,8 +264,8 @@ public void SetMethod_ValidValue_SetsValue() { | TEST: SET METHOD: INVALID VALUE: DOESN'T SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that a value set with an invalid value using the - /// method returns an exception. + /// Establishes a and confirms that a value set with an invalid value using the method returns an exception. /// [TestMethod] public void SetMethod_InvalidValue_DoesNotSetValue() { @@ -287,8 +287,8 @@ public void SetMethod_InvalidValue_DoesNotSetValue() { | TEST: SET METHOD: INVALID MEMBER: RETURNS FALSE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a and confirms that setting an invalid property name using the - /// method returns false. + /// Establishes a and confirms that setting an invalid property name using the method returns false. /// [TestMethod] public void SetMethod_Integer_SetsValue() { diff --git a/OnTopic/Internal/Reflection/MemberDispatcher.cs b/OnTopic/Internal/Reflection/MemberDispatcher.cs index 2ac9c548..89dd797a 100644 --- a/OnTopic/Internal/Reflection/MemberDispatcher.cs +++ b/OnTopic/Internal/Reflection/MemberDispatcher.cs @@ -472,7 +472,7 @@ private static bool IsSettableType(Type sourceType, Type? targetType = null) { | PROPERTY: SETTABLE TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// A list of types that are allowed to be set using . + /// A list of types that are allowed to be set using . /// internal static Collection SettableTypes { get; } From d26dc8c7256e102546dcc1e839657d13d62972f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 9 Feb 2021 18:03:18 -0800 Subject: [PATCH 647/778] Suppressed `IDE0060` in generated SSDT unit test code --- OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs b/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs index b29d80bf..d43ba700 100644 --- a/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs +++ b/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs @@ -11,12 +11,14 @@ namespace OnTopic.Data.Sql.Database.Tests { public class SqlDatabaseSetup { [AssemblyInitialize()] + #pragma warning disable IDE0060 // Remove unused parameter public static void InitializeAssembly(TestContext ctx) { // Setup the test database based on setting in the // configuration file SqlDatabaseTestClass.TestService.DeployDatabaseProject(); SqlDatabaseTestClass.TestService.GenerateData(); } + #pragma warning restore IDE0060 // Remove unused parameter } } From ad3e5ddaf8039344b85d1959a86d912221fdf863 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 12:25:48 -0800 Subject: [PATCH 648/778] Rename `GetTopics()` to `GetValues()` This is more consistent with `TrackedRecordCollection<>` as well as the `KeyValuesPair.Values` nomenclature. This includes the version on both `TopicMultiMap` as well as `ReadOnlyTopicMultiMap`. As part of this, maintained a pass-through version of `GetTopics()`, but marked it as deprecated. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.Tests/SqlTopicRepositoryTest.cs | 4 ++-- OnTopic.Tests/TopicReferenceCollectionTest.cs | 4 ++-- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 10 +++++----- OnTopic.Tests/TopicRepositoryBaseTest.cs | 4 ++-- OnTopic/Associations/TopicRelationshipMultiMap.cs | 4 ++-- .../Collections/Specialized/ReadOnlyTopicMultiMap.cs | 8 ++++++-- OnTopic/Collections/Specialized/TopicMultiMap.cs | 12 ++++++++---- OnTopic/Mapping/TopicMappingService.cs | 4 ++-- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- 10 files changed, 31 insertions(+), 23 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 85f59851..0ad91d26 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -641,7 +641,7 @@ private static void PersistRelationships(Topic topic, DateTime version, SqlConne CommandType = CommandType.StoredProcedure }; - foreach (var targetTopic in topic.Relationships.GetTopics(key)) { + foreach (var targetTopic in topic.Relationships.GetValues(key)) { if (!targetTopic.IsNew) { targetIds.AddRow(targetTopic.Id); } diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 844cf342..93ffb73a 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -96,7 +96,7 @@ public void LoadTopicGraph_WithRelationship_ReturnsRelationship() { Assert.IsNotNull(topic); Assert.AreEqual(1, topic.Id); - Assert.AreEqual(2, topic.Relationships.GetTopics("Test").FirstOrDefault()?.Id); + Assert.AreEqual(2, topic.Relationships.GetValues("Test").FirstOrDefault()?.Id); } @@ -214,7 +214,7 @@ public void LoadTopicGraph_WithDeletedRelationship_RemovesRelationship() { tableReader.LoadTopicGraph(related); - Assert.AreEqual(0, topic.Relationships.GetTopics("Test").Count); + Assert.AreEqual(0, topic.Relationships.GetValues("Test").Count); } diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs index af27211b..b80250ee 100644 --- a/OnTopic.Tests/TopicReferenceCollectionTest.cs +++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs @@ -148,7 +148,7 @@ public void Add_NewReference_IncomingRelationshipSet() { topic.References.SetValue("Reference", reference); - Assert.AreEqual(1, reference.IncomingRelationships.GetTopics("Reference").Count); + Assert.AreEqual(1, reference.IncomingRelationships.GetValues("Reference").Count); } @@ -170,7 +170,7 @@ public void Remove_ExistingReference_IncomingRelationshipRemoved() { topic.References.SetValue("Reference", reference); topic.References.Remove("Reference"); - Assert.AreEqual(0, reference.IncomingRelationships.GetTopics("Reference").Count); + Assert.AreEqual(0, reference.IncomingRelationships.GetValues("Reference").Count); } diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 32c99904..27b347ab 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -34,7 +34,7 @@ public void SetTopic_CreatesRelationship() { parent.Relationships.SetTopic("Friends", related); - Assert.ReferenceEquals(parent.Relationships.GetTopics("Friends").First(), related); + Assert.ReferenceEquals(parent.Relationships.GetValues("Friends").First(), related); } @@ -53,7 +53,7 @@ public void RemoveTopic_RemovesRelationship() { parent.Relationships.SetTopic("Friends", related); parent.Relationships.RemoveTopic("Friends", related); - Assert.IsNull(parent.Relationships.GetTopics("Friends").FirstOrDefault()); + Assert.IsNull(parent.Relationships.GetValues("Friends").FirstOrDefault()); } @@ -74,7 +74,7 @@ public void RemoveTopic_RemovesIncomingRelationship() { relationships.SetTopic("Friends", related); relationships.RemoveTopic("Friends", related); - Assert.IsNull(related.IncomingRelationships.GetTopics("Friends").FirstOrDefault()); + Assert.IsNull(related.IncomingRelationships.GetValues("Friends").FirstOrDefault()); } @@ -93,7 +93,7 @@ public void SetTopic_CreatesIncomingRelationship() { relationships.SetTopic("Friends", related); - Assert.ReferenceEquals(related.IncomingRelationships.GetTopics("Friends").First(), parent); + Assert.ReferenceEquals(related.IncomingRelationships.GetValues("Friends").First(), parent); } @@ -135,7 +135,7 @@ public void GetAllTopics_ReturnsAllTopics() { } Assert.AreEqual(5, relationships.Count); - Assert.AreEqual("Related3", relationships.GetTopics("Relationship3").First().Key); + Assert.AreEqual("Related3", relationships.GetValues("Relationship3").First().Key); Assert.AreEqual(5, relationships.GetAllTopics().Count); } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index a7919df0..3ffcf793 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -167,7 +167,7 @@ public void Delete_Relationships_DeleteRelationships() { _topicRepository.Delete(topic, true); - Assert.AreEqual(0, related.IncomingRelationships.GetTopics("Related").Count); + Assert.AreEqual(0, related.IncomingRelationships.GetValues("Related").Count); } @@ -190,7 +190,7 @@ public void Delete_IncomingRelationships_DeleteRelationships() { _topicRepository.Delete(topic, true); - Assert.AreEqual(0, related.Relationships.GetTopics("Related").Count); + Assert.AreEqual(0, related.Relationships.GetValues("Related").Count); } diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index bb2d5464..2cd6986b 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -66,7 +66,7 @@ public TopicRelationshipMultiMap(Topic parent, bool isIncoming = false): base() public void ClearTopics(string relationshipKey) { Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); if (_storage.Contains(relationshipKey)) { - var relationship = _storage.GetTopics(relationshipKey); + var relationship = _storage.GetValues(relationshipKey); if (relationship.Count > 0) { _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew); } @@ -184,7 +184,7 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo /*------------------------------------------------------------------------------------------------------------------------ | Add relationship \-----------------------------------------------------------------------------------------------------------------------*/ - var topics = _storage.GetTopics(relationshipKey); + var topics = _storage.GetValues(relationshipKey); var wasDirty = _dirtyKeys.IsDirty(relationshipKey); if (!topics.Contains(topic)) { _storage.Add(relationshipKey, topic); diff --git a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index 5a92a94e..5fe16f7d 100644 --- a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -105,7 +105,7 @@ protected ReadOnlyTopicMultiMap() {} public bool Contains(string key, Topic topic) => Source.Contains(key, topic); /*========================================================================================================================== - | METHOD: GET TOPICS + | METHOD: GET VALUES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of objects grouped by a specific . @@ -114,7 +114,7 @@ protected ReadOnlyTopicMultiMap() {} /// Returns a reference to the underlying collection. /// /// The key of the collection to be returned. - public ReadOnlyTopicCollection GetTopics(string key) { + public ReadOnlyTopicCollection GetValues(string key) { Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); if (Contains(key)) { return new(Source[key].Values); @@ -122,6 +122,10 @@ public ReadOnlyTopicCollection GetTopics(string key) { return new(new List()); } + /// + [Obsolete("The GetTopics() method has been renamed to GetValues().", false)] + public ReadOnlyTopicCollection GetTopics(string key) => GetValues(key); + /*========================================================================================================================== | METHOD: GET ALL TOPICS \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Collections/Specialized/TopicMultiMap.cs b/OnTopic/Collections/Specialized/TopicMultiMap.cs index 9c8eb431..aecc2465 100644 --- a/OnTopic/Collections/Specialized/TopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/TopicMultiMap.cs @@ -41,7 +41,7 @@ public TopicMultiMap() { public bool Contains(string key, Topic topic) => Contains(key) && this[key].Values.Contains(topic); /*========================================================================================================================== - | METHOD: GET TOPICS + | METHOD: GET VALUES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of objects grouped by a specific . @@ -50,7 +50,7 @@ public TopicMultiMap() { /// Returns a reference to the underlying collection. /// /// The key of the collection to be returned. - public TopicCollection GetTopics(string key) { + public TopicCollection GetValues(string key) { Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key)); if (Contains(key)) { return this[key].Values; @@ -58,6 +58,10 @@ public TopicCollection GetTopics(string key) { return new(); } + /// + [Obsolete("The GetTopics() method has been renamed to GetValues().", false)] + public TopicCollection GetTopics(string key) => GetValues(key); + /*========================================================================================================================== | METHOD: ADD \-------------------------------------------------------------------------------------------------------------------------*/ @@ -112,7 +116,7 @@ public bool Remove(string key, Topic topic) { /*------------------------------------------------------------------------------------------------------------------------ | Validate key \-----------------------------------------------------------------------------------------------------------------------*/ - var topics = GetTopics(key); + var topics = GetValues(key); if (topics is null || !topics.Contains(topic)) { return false; @@ -137,7 +141,7 @@ public bool Remove(string key, Topic topic) { /// Removes all objects grouped by a specific . /// /// The key of the collection to be cleared. - public void Clear(string key) => GetTopics(key).Clear(); + public void Clear(string key) => GetValues(key).Clear(); /*========================================================================================================================== | OVERRIDE: GET KEY FOR ITEM diff --git a/OnTopic/Mapping/TopicMappingService.cs b/OnTopic/Mapping/TopicMappingService.cs index 9d3d386f..b8f713da 100644 --- a/OnTopic/Mapping/TopicMappingService.cs +++ b/OnTopic/Mapping/TopicMappingService.cs @@ -539,7 +539,7 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat listSource = getCollection( CollectionType.Relationship, source.Relationships.Contains, - () => source.Relationships.GetTopics(collectionKey) + () => source.Relationships.GetValues(collectionKey) ); /*------------------------------------------------------------------------------------------------------------------------ @@ -557,7 +557,7 @@ private IList GetSourceCollection(Topic source, AssociationTypes associat listSource = getCollection( CollectionType.IncomingRelationship, source.IncomingRelationships.Contains, - () => source.IncomingRelationships.GetTopics(collectionKey) + () => source.IncomingRelationships.GetValues(collectionKey) ); /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index b5dd935c..0f0186bd 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -138,7 +138,7 @@ public ReadOnlyKeyedTopicCollection PermittedContentTypes \---------------------------------------------------------------------------------------------------------------------*/ if (_permittedContentTypes is null) { var permittedContentTypes = new KeyedTopicCollection(); - var contentTypes = Relationships.GetTopics("ContentTypes"); + var contentTypes = Relationships.GetValues("ContentTypes"); foreach (ContentTypeDescriptor contentType in contentTypes) { permittedContentTypes.Add(contentType); } From 2f015915eb9e7c135c84a8bb9d716199d8af9763 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 12:31:56 -0800 Subject: [PATCH 649/778] Rename `GetAllTopics()` to `GetAllValues()` This is more consistent with `TrackedRecordCollection<>` as well as the `KeyValuesPair.Values` nomenclature, and the recent change from `GetTopics()` to `GetValues()` (ad3e5dd). --- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 17 +++++++++-------- .../Specialized/ReadOnlyTopicMultiMap.cs | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index 27b347ab..e4c042f2 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -119,13 +119,14 @@ public void SetTopic_UpdatesKeyCount() { } /*========================================================================================================================== - | TEST: GET ALL TOPICS: RETURNS ALL TOPICS + | TEST: GET ALL VALUES: RETURNS ALL TOPICS \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Sets relationships in multiple namespaces, and ensures they are all returned via GetAllTopics(). + /// Sets relationships in multiple namespaces, and ensures they are all returned via . /// [TestMethod] - public void GetAllTopics_ReturnsAllTopics() { + public void GetAllValues_ReturnsAllTopics() { var parent = TopicFactory.Create("Parent", "Page"); var relationships = new TopicRelationshipMultiMap(parent); @@ -136,19 +137,19 @@ public void GetAllTopics_ReturnsAllTopics() { Assert.AreEqual(5, relationships.Count); Assert.AreEqual("Related3", relationships.GetValues("Relationship3").First().Key); - Assert.AreEqual(5, relationships.GetAllTopics().Count); + Assert.AreEqual(5, relationships.GetAllValues().Count); } /*========================================================================================================================== - | TEST: GET ALL TOPICS: CONTENT TYPES: RETURNS ALL CONTENT TYPES + | TEST: GET ALL VALUES: CONTENT TYPES: RETURNS ALL CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Sets relationships in multiple namespaces, with different ContentTypes, then filters the results of - /// by content type. + /// by content type. /// [TestMethod] - public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { + public void GetAllValues_ContentTypes_ReturnsAllContentTypes() { var parent = TopicFactory.Create("Parent", "Page"); var relationships = new TopicRelationshipMultiMap(parent); @@ -158,7 +159,7 @@ public void GetAllTopics_ContentTypes_ReturnsAllContentTypes() { } Assert.AreEqual(5, relationships.Keys.Count); - Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count); + Assert.AreEqual(1, relationships.GetAllValues("ContentType3").Count); } diff --git a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index 5fe16f7d..e924dd2f 100644 --- a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -127,7 +127,7 @@ public ReadOnlyTopicCollection GetValues(string key) { public ReadOnlyTopicCollection GetTopics(string key) => GetValues(key); /*========================================================================================================================== - | METHOD: GET ALL TOPICS + | METHOD: GET ALL VALUES \-------------------------------------------------------------------------------------------------------------------------*/ /// /// Retrieves a list of all related objects, independent of collection key. @@ -135,7 +135,7 @@ public ReadOnlyTopicCollection GetValues(string key) { /// /// Returns an enumerable list of objects. /// - public ReadOnlyTopicCollection GetAllTopics() => + public ReadOnlyTopicCollection GetAllValues() => new(Source.SelectMany(list => list.Values).Distinct().ToList()); /// @@ -145,8 +145,16 @@ public ReadOnlyTopicCollection GetAllTopics() => /// /// Returns an enumerable list of objects. /// - public ReadOnlyTopicCollection GetAllTopics(string contentType) => - new(GetAllTopics().Where(t => t.ContentType == contentType).ToList()); + public ReadOnlyTopicCollection GetAllValues(string contentType) => + new(GetAllValues().Where(t => t.ContentType == contentType).ToList()); + + /// + [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", false)] + public ReadOnlyTopicCollection GetAllTopics(string key) => GetAllValues(key); + + /// + [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", false)] + public ReadOnlyTopicCollection GetAllTopics() => GetAllValues(); /*========================================================================================================================== | GET ENUMERATOR From 9f6527152d525797d85edebd0ac15ad3e4c4316e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 12:39:00 -0800 Subject: [PATCH 650/778] Rename `ClearTopics()` to `Clear()` This is more consistent with `TrackedRecordCollection<>`, as well as that of the similar `KeyedCollection<>`. As part of this, maintained a pass-through version of `ClearTopics()`, but marked it as deprecated. --- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.Tests/TopicRelationshipMultiMapTest.cs | 16 ++++++++-------- .../Associations/TopicRelationshipMultiMap.cs | 10 +++++++--- .../Reverse/ReverseTopicMappingService.cs | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 0ad91d26..2d21f072 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -221,7 +221,7 @@ public override Topic Load(int topicId, DateTime version, Topic? referenceTopic if (topic is not null) { foreach (var relationship in topic.Relationships) { - topic.Relationships.ClearTopics(relationship.Key); + topic.Relationships.Clear(relationship.Key); } topic.References.Clear(); } diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index e4c042f2..d7cbbc4d 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -297,14 +297,14 @@ public void RemoveTopic_MissingTopic_StaysDirty() { } /*========================================================================================================================== - | TEST: CLEAR TOPICS: EXISTING TOPICS: IS DIRTY + | TEST: CLEAR: EXISTING TOPICS: IS DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Call and confirms that value of and confirms that value of is true. /// [TestMethod] - public void ClearTopics_ExistingTopics_IsDirty() { + public void Clear_ExistingTopics_IsDirty() { var topic = TopicFactory.Create("Test", "Page", 1); var relationships = new TopicRelationshipMultiMap(topic); @@ -312,26 +312,26 @@ public void ClearTopics_ExistingTopics_IsDirty() { relationships.SetTopic("Related", related); relationships.MarkClean(); - relationships.ClearTopics("Related"); + relationships.Clear("Related"); Assert.IsTrue(relationships.IsDirty()); } /*========================================================================================================================== - | TEST: CLEAR TOPICS: NO TOPICS: IS NOT DIRTY + | TEST: CLEAR: NO TOPICS: IS NOT DIRTY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Call with no existing s and confirms that + /// Call with no existing s and confirms that /// the value of is set to false. /// [TestMethod] - public void ClearTopics_NoTopics_IsNotDirty() { + public void Clear_NoTopics_IsNotDirty() { var topic = TopicFactory.Create("Test", "Page"); var relationships = new TopicRelationshipMultiMap(topic); - relationships.ClearTopics("Related"); + relationships.Clear("Related"); Assert.IsFalse(relationships.IsDirty()); diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index 2cd6986b..f773ce2b 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -53,17 +53,17 @@ public TopicRelationshipMultiMap(Topic parent, bool isIncoming = false): base() } /*========================================================================================================================== - | METHOD: CLEAR TOPICS + | METHOD: CLEAR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Removes all objects grouped by a specific relationship key. + /// Removes all objects grouped by a specific . /// /// /// If there are any objects in the specified , then the will be marked as . /// /// The key of the relationship to be cleared. - public void ClearTopics(string relationshipKey) { + public void Clear(string relationshipKey) { Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey)); if (_storage.Contains(relationshipKey)) { var relationship = _storage.GetValues(relationshipKey); @@ -74,6 +74,10 @@ public void ClearTopics(string relationshipKey) { } } + /// + [Obsolete("The ClearTopics(relationshipKey) method has been renamed to Clear(relationshipKey).", false)] + public void ClearTopics(string relationshipKey) => Clear(relationshipKey); + /*========================================================================================================================== | METHOD: REMOVE TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 7fa44c52..c33062ff 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -430,7 +430,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Clear existing relationships \-----------------------------------------------------------------------------------------------------------------------*/ - target.Relationships.ClearTopics(configuration.AttributeKey); + target.Relationships.Clear(configuration.AttributeKey); /*------------------------------------------------------------------------------------------------------------------------ | Set relationships for each From 53f7244048212b2283d8bc56ac898d1d81cb834c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 12:47:30 -0800 Subject: [PATCH 651/778] Rename `RemoveTopic()` to `Remove()` This is more consistent with `TrackedRecordCollection<>`, as well as that of the similar `KeyedCollection<>`. As part of this, maintained a pass-through versions of `RemoveTopic()`, but marked it as deprecated. --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- .../TopicRelationshipMultiMapTest.cs | 10 ++++---- .../Associations/TopicReferenceCollection.cs | 4 ++-- .../Associations/TopicRelationshipMultiMap.cs | 23 +++++++++++++------ OnTopic/Repositories/TopicRepository.cs | 4 ++-- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 93936bd3..f9ef7f40 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -382,7 +382,7 @@ private static void SetRelationships(this IDataReader reader, TopicIndex topics, current.Relationships.SetTopic(relationshipKey, related, isDirty); } else if (current.Relationships.Contains(relationshipKey, related)) { - current.Relationships.RemoveTopic(relationshipKey, related); + current.Relationships.Remove(relationshipKey, related); } } diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index d7cbbc4d..b84a7c20 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -51,7 +51,7 @@ public void RemoveTopic_RemovesRelationship() { var related = TopicFactory.Create("Related", "Page"); parent.Relationships.SetTopic("Friends", related); - parent.Relationships.RemoveTopic("Friends", related); + parent.Relationships.Remove("Friends", related); Assert.IsNull(parent.Relationships.GetValues("Friends").FirstOrDefault()); @@ -72,7 +72,7 @@ public void RemoveTopic_RemovesIncomingRelationship() { var relationships = new TopicRelationshipMultiMap(parent); relationships.SetTopic("Friends", related); - relationships.RemoveTopic("Friends", related); + relationships.Remove("Friends", related); Assert.IsNull(related.IncomingRelationships.GetValues("Friends").FirstOrDefault()); @@ -245,7 +245,7 @@ public void RemoveTopic_IsDirty() { relationships.SetTopic("Related", related); relationships.MarkClean(); - relationships.RemoveTopic("Related", related); + relationships.Remove("Related", related); Assert.IsTrue(relationships.IsDirty()); @@ -265,7 +265,7 @@ public void RemoveTopic_MissingTopic_IsNotDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); - var isSuccessful = relationships.RemoveTopic("Related", related); + var isSuccessful = relationships.Remove("Related", related); Assert.IsFalse(isSuccessful); Assert.IsFalse(relationships.IsDirty()); @@ -289,7 +289,7 @@ public void RemoveTopic_MissingTopic_StaysDirty() { relationships.SetTopic("Related", related); - var isSuccessful = relationships.RemoveTopic("Related", missing); + var isSuccessful = relationships.Remove("Related", missing); Assert.IsFalse(isSuccessful); Assert.IsTrue(relationships.IsDirty()); diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs index e4413ee6..49aabf98 100644 --- a/OnTopic/Associations/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -115,7 +115,7 @@ protected override sealed void RemoveItem(int index) { \-----------------------------------------------------------------------------------------------------------------------*/ var existing = this[index]; - existing.Value?.IncomingRelationships.RemoveTopic(existing.Key, AssociatedTopic, true); + existing.Value?.IncomingRelationships.Remove(existing.Key, AssociatedTopic, true); /*------------------------------------------------------------------------------------------------------------------------ | Provide base logic @@ -140,7 +140,7 @@ protected override sealed void ClearItems() { | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var item in Items) { - item.Value?.IncomingRelationships.RemoveTopic(item.Key, AssociatedTopic, true); + item.Value?.IncomingRelationships.Remove(item.Key, AssociatedTopic, true); } } diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index f773ce2b..e817566d 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -79,18 +79,18 @@ public void Clear(string relationshipKey) { public void ClearTopics(string relationshipKey) => Clear(relationshipKey); /*========================================================================================================================== - | METHOD: REMOVE TOPIC + | METHOD: REMOVE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Removes a specific object associated with a specific relationship key. + /// Removes a specific object associated with a specific . /// /// The key of the relationship. /// The to be removed. /// - /// Returns true if the is removed; returns false if either the relationship key or the - /// cannot be found. + /// Returns true if the is removed; returns false if either the specified or the cannot be found. /// - public bool RemoveTopic(string relationshipKey, Topic topic) => RemoveTopic(relationshipKey, topic, false); + public bool Remove(string relationshipKey, Topic topic) => Remove(relationshipKey, topic, false); /// /// Removes a specific object associated with a specific relationship key. @@ -104,7 +104,7 @@ public void Clear(string relationshipKey) { /// Returns true if the is removed; returns false if either the relationship key or the /// cannot be found. /// - internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) { + internal bool Remove(string relationshipKey, Topic topic, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -122,7 +122,7 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) nameof(isIncoming) ); } - topic.IncomingRelationships.RemoveTopic(relationshipKey, _parent, true); + topic.IncomingRelationships.Remove(relationshipKey, _parent, true); } /*------------------------------------------------------------------------------------------------------------------------ @@ -145,6 +145,15 @@ internal bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) } + /// + [Obsolete("The RemoveTopic() method has been renamed to Remove().", false)] + public bool RemoveTopic(string relationshipKey, Topic topic) => Remove(relationshipKey, topic); + + /// + [Obsolete("The RemoveTopic() method has been renamed to Remove().", false)] + public bool RemoveTopic(string relationshipKey, Topic topic, Boolean isIncoming) => + Remove(relationshipKey, topic, isIncoming); + /*========================================================================================================================== | METHOD: SET TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index f7304504..b0164ac5 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -610,7 +610,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi foreach (var relationship in descendantTopic.Relationships) { foreach (var relatedTopic in relationship.Values.ToList()) { if (!descendantTopics.Contains(relatedTopic)) { - descendantTopic.Relationships.RemoveTopic(relationship.Key, relatedTopic); + descendantTopic.Relationships.Remove(relationship.Key, relatedTopic); } } } @@ -635,7 +635,7 @@ public override sealed void Delete([ValidatedNotNull]Topic topic, bool isRecursi foreach (var relatedTopic in relationship.Values.ToList()) { if (!descendantTopics.Contains(relatedTopic)) { if (relatedTopic.Relationships.Contains(relationship.Key, descendantTopic)) { - relatedTopic.Relationships.RemoveTopic(relationship.Key, descendantTopic); + relatedTopic.Relationships.Remove(relationship.Key, descendantTopic); } else if (relatedTopic.References.Contains(relationship.Key)) { relatedTopic.References.Remove(relationship.Key); From b99a04e14c3729e0fff06979fb39fbf7eaecc0f5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 13:00:54 -0800 Subject: [PATCH 652/778] Rename `SetTopic()` to `SetValue()` This is more consistent with `TrackedRecordCollection<>` as well as the `KeyValuesPair.Values` nomenclature, and the recent change from `GetTopics()` to `GetValues()` (ad3e5dd). --- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 4 +- .../ReverseTopicMappingServiceTest.cs | 2 +- OnTopic.Tests/SqlTopicRepositoryTest.cs | 2 +- OnTopic.Tests/TopicMappingServiceTest.cs | 32 ++++++++-------- .../TopicRelationshipMultiMapTest.cs | 38 +++++++++---------- OnTopic.Tests/TopicRepositoryBaseTest.cs | 6 +-- OnTopic.Tests/TopicTest.cs | 4 +- .../Associations/ReferenceSetterAttribute.cs | 2 +- .../Associations/TopicReferenceCollection.cs | 6 +-- .../Associations/TopicRelationshipMultiMap.cs | 33 ++++++++++------ OnTopic/Mapping/README.md | 2 +- .../Reverse/ReverseTopicMappingService.cs | 2 +- OnTopic/Metadata/ContentTypeDescriptor.cs | 2 +- 14 files changed, 74 insertions(+), 63 deletions(-) diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index f9ef7f40..a32911a7 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -379,7 +379,7 @@ private static void SetRelationships(this IDataReader reader, TopicIndex topics, | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ if (!isDeleted) { - current.Relationships.SetTopic(relationshipKey, related, isDirty); + current.Relationships.SetValue(relationshipKey, related, isDirty); } else if (current.Relationships.Contains(relationshipKey, related)) { current.Relationships.Remove(relationshipKey, related); diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 5d8fecb5..2014d793 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -228,8 +228,8 @@ private Topic CreateFakeData() { addAttribute(pageContentType, "IsHidden", "TextAttributeDescriptor", false); addAttribute(pageContentType, "TopicReference", "TopicReferenceAttributeDescriptor", false); - pageContentType.Relationships.SetTopic("ContentTypes", pageContentType); - pageContentType.Relationships.SetTopic("ContentTypes", contentTypeDescriptor); + pageContentType.Relationships.SetValue("ContentTypes", pageContentType); + pageContentType.Relationships.SetValue("ContentTypes", contentTypeDescriptor); var contactContentType = TopicFactory.Create("Contact", "ContentTypeDescriptor", contentTypes); diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index f9618585..3e4c2042 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -223,7 +223,7 @@ public async Task Map_Relationships_ReturnsMappedTopic() { var contentTypes = _topicRepository.GetContentTypeDescriptors(); var topic = (ContentTypeDescriptor)TopicFactory.Create("Test", "ContentTypeDescriptor"); - topic.Relationships.SetTopic("ContentTypes", contentTypes[4]); + topic.Relationships.SetValue("ContentTypes", contentTypes[4]); for (var i = 0; i < 3; i++) { bindingModel.ContentTypes.Add( diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs index 93ffb73a..555d91e6 100644 --- a/OnTopic.Tests/SqlTopicRepositoryTest.cs +++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs @@ -203,7 +203,7 @@ public void LoadTopicGraph_WithDeletedRelationship_RemovesRelationship() { var child = TopicFactory.Create("Child", "Container", topic, 2); var related = TopicFactory.Create("Related", "Container", topic, 3); - child.Relationships.SetTopic("Test", related); + child.Relationships.SetValue("Test", related); using var empty = new AttributesDataTable(); using var relationships = new RelationshipsDataTable(); diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 2e000b5d..94b4c1de 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -339,9 +339,9 @@ public async Task Map_Relationships_ReturnsMappedModel() { var relatedTopic3 = TopicFactory.Create("Sibling", "Relation"); var topic = TopicFactory.Create("Test", "Relation"); - topic.Relationships.SetTopic("Cousins", relatedTopic1); - topic.Relationships.SetTopic("Cousins", relatedTopic2); - topic.Relationships.SetTopic("Siblings", relatedTopic3); + topic.Relationships.SetValue("Cousins", relatedTopic1); + topic.Relationships.SetValue("Cousins", relatedTopic2); + topic.Relationships.SetValue("Siblings", relatedTopic3); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); @@ -365,8 +365,8 @@ public async Task Map_Relationships_SkipsDisabled() { var relatedTopic2 = TopicFactory.Create("Cousin2", "Relation"); var topic = TopicFactory.Create("Test", "Relation"); - topic.Relationships.SetTopic("Cousins", relatedTopic1); - topic.Relationships.SetTopic("Cousins", relatedTopic2); + topic.Relationships.SetValue("Cousins", relatedTopic1); + topic.Relationships.SetValue("Cousins", relatedTopic2); topic.IsDisabled = true; relatedTopic2.IsDisabled = true; @@ -403,12 +403,12 @@ public async Task Map_AlternateRelationship_ReturnsCorrectRelationship() { var topic = TopicFactory.Create("Test", "AmbiguousRelation"); //Set outgoing relationships - topic.Relationships.SetTopic("RelationshipAlias", ambiguousRelation); - topic.Relationships.SetTopic("AmbiguousRelationship", outgoingRelation); + topic.Relationships.SetValue("RelationshipAlias", ambiguousRelation); + topic.Relationships.SetValue("AmbiguousRelationship", outgoingRelation); //Set incoming relationships - ambiguousRelation.Relationships.SetTopic("RelationshipAlias", topic); - incomingRelation.Relationships.SetTopic("AmbiguousRelationship", topic); + ambiguousRelation.Relationships.SetValue("RelationshipAlias", topic); + incomingRelation.Relationships.SetValue("AmbiguousRelationship", topic); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); @@ -653,12 +653,12 @@ public async Task Map_RecursiveRelationships_ReturnsGraph() { var cousinOnceRemoved = TopicFactory.Create("CousinOnceRemoved", "Relation", childTopic3); //Set first cousins - topic.Relationships.SetTopic("Cousins", cousinTopic1); - topic.Relationships.SetTopic("Cousins", cousinTopic2); - topic.Relationships.SetTopic("Cousins", cousinTopic3); + topic.Relationships.SetValue("Cousins", cousinTopic1); + topic.Relationships.SetValue("Cousins", cousinTopic2); + topic.Relationships.SetValue("Cousins", cousinTopic3); //Set ancillary relationships - cousinTopic3.Relationships.SetTopic("Cousins", secondCousin); + cousinTopic3.Relationships.SetValue("Cousins", secondCousin); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); @@ -719,9 +719,9 @@ public async Task Map_TopicEntities_ReturnsTopics() { var relatedTopic3 = TopicFactory.Create("RelatedTopic3", "KeyOnly"); var topic = TopicFactory.Create("Test", "RelatedEntity"); - topic.Relationships.SetTopic("RelatedTopics", relatedTopic1); - topic.Relationships.SetTopic("RelatedTopics", relatedTopic2); - topic.Relationships.SetTopic("RelatedTopics", relatedTopic3); + topic.Relationships.SetValue("RelatedTopics", relatedTopic1); + topic.Relationships.SetValue("RelatedTopics", relatedTopic2); + topic.Relationships.SetValue("RelatedTopics", relatedTopic3); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); var relatedTopic3copy = (getRelatedTopic(target, "RelatedTopic3")); diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs index b84a7c20..24e33448 100644 --- a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs +++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs @@ -32,7 +32,7 @@ public void SetTopic_CreatesRelationship() { var parent = TopicFactory.Create("Parent", "Page"); var related = TopicFactory.Create("Related", "Page"); - parent.Relationships.SetTopic("Friends", related); + parent.Relationships.SetValue("Friends", related); Assert.ReferenceEquals(parent.Relationships.GetValues("Friends").First(), related); @@ -50,7 +50,7 @@ public void RemoveTopic_RemovesRelationship() { var parent = TopicFactory.Create("Parent", "Page"); var related = TopicFactory.Create("Related", "Page"); - parent.Relationships.SetTopic("Friends", related); + parent.Relationships.SetValue("Friends", related); parent.Relationships.Remove("Friends", related); Assert.IsNull(parent.Relationships.GetValues("Friends").FirstOrDefault()); @@ -71,7 +71,7 @@ public void RemoveTopic_RemovesIncomingRelationship() { var related = TopicFactory.Create("Related", "Page"); var relationships = new TopicRelationshipMultiMap(parent); - relationships.SetTopic("Friends", related); + relationships.SetValue("Friends", related); relationships.Remove("Friends", related); Assert.IsNull(related.IncomingRelationships.GetValues("Friends").FirstOrDefault()); @@ -91,7 +91,7 @@ public void SetTopic_CreatesIncomingRelationship() { var related = TopicFactory.Create("Related", "Page"); var relationships = new TopicRelationshipMultiMap(parent); - relationships.SetTopic("Friends", related); + relationships.SetValue("Friends", related); Assert.ReferenceEquals(related.IncomingRelationships.GetValues("Friends").First(), parent); @@ -110,7 +110,7 @@ public void SetTopic_UpdatesKeyCount() { var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { - relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); + relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); } Assert.AreEqual(5, relationships.Keys.Count); @@ -132,7 +132,7 @@ public void GetAllValues_ReturnsAllTopics() { var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { - relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); + relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "Page")); } Assert.AreEqual(5, relationships.Count); @@ -155,7 +155,7 @@ public void GetAllValues_ContentTypes_ReturnsAllContentTypes() { var relationships = new TopicRelationshipMultiMap(parent); for (var i = 0; i < 5; i++) { - relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i)); + relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i)); } Assert.AreEqual(5, relationships.Keys.Count); @@ -177,7 +177,7 @@ public void SetTopic_IsDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); Assert.IsTrue(relationships.IsDirty()); @@ -197,10 +197,10 @@ public void SetTopic_IsDuplicate_IsNotDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page", 2); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); relationships.MarkClean(); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); Assert.IsFalse(relationships.IsDirty()); @@ -221,8 +221,8 @@ public void SetTopic_IsDuplicate_StaysDirty() { var related1 = TopicFactory.Create("Topic", "Page"); var related2 = TopicFactory.Create("Topic", "Page"); - relationships.SetTopic("Related", related1); - relationships.SetTopic("Related", related2); + relationships.SetValue("Related", related1); + relationships.SetValue("Related", related2); Assert.IsTrue(relationships.IsDirty()); @@ -243,7 +243,7 @@ public void RemoveTopic_IsDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); relationships.MarkClean(); relationships.Remove("Related", related); @@ -287,7 +287,7 @@ public void RemoveTopic_MissingTopic_StaysDirty() { var related = TopicFactory.Create("Topic1", "Page"); var missing = TopicFactory.Create("Topic2", "Page"); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); var isSuccessful = relationships.Remove("Related", missing); @@ -310,7 +310,7 @@ public void Clear_ExistingTopics_IsDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); - relationships.SetTopic("Related", related); + relationships.SetValue("Related", related); relationships.MarkClean(); relationships.Clear("Related"); @@ -343,7 +343,7 @@ public void Clear_NoTopics_IsNotDirty() { /// /// Adds an existing to a associated with a and confirms that returns true - /// even if is called with the + /// even if is called with the /// markDirty parameter set to false. /// [TestMethod] @@ -353,7 +353,7 @@ public void SetTopic_NewParent_IsDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page", 1); - relationships.SetTopic("Related", related, false); + relationships.SetValue("Related", related, false); Assert.IsTrue(relationships.IsDirty()); @@ -365,7 +365,7 @@ public void SetTopic_NewParent_IsDirty() { /// /// Adds a new to a associated with an existing and confirms that returns true even if is called with the markDirty parameter + /// TopicRelationshipMultiMap.SetValue(String, Topic, Boolean?, Boolean)"/> is called with the markDirty parameter /// set to false. /// [TestMethod] @@ -375,7 +375,7 @@ public void SetTopic_NewTopic_IsDirty() { var relationships = new TopicRelationshipMultiMap(topic); var related = TopicFactory.Create("Topic", "Page"); - relationships.SetTopic("Related", related, false); + relationships.SetValue("Related", related, false); Assert.IsTrue(relationships.IsDirty()); diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 3ffcf793..4b8b4b0e 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -163,7 +163,7 @@ public void Delete_Relationships_DeleteRelationships() { var child = TopicFactory.Create("Child", "Page", topic); var related = TopicFactory.Create("Related", "Page", root); - child.Relationships.SetTopic("Related", related); + child.Relationships.SetValue("Related", related); _topicRepository.Delete(topic, true); @@ -186,7 +186,7 @@ public void Delete_IncomingRelationships_DeleteRelationships() { var child = TopicFactory.Create("Child", "Page", topic); var related = TopicFactory.Create("Related", "Page", root); - related.Relationships.SetTopic("Related", child); + related.Relationships.SetValue("Related", child); _topicRepository.Delete(topic, true); @@ -541,7 +541,7 @@ public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { var lookupContentType = contentTypes.GetTopic("Lookup"); var initialCount = pageContentType.PermittedContentTypes.Count; - pageContentType.Relationships.SetTopic("ContentTypes", lookupContentType); + pageContentType.Relationships.SetValue("ContentTypes", lookupContentType); _topicRepository.Save(contentTypesRoot, true); diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs index 07cdb54b..3e613324 100644 --- a/OnTopic.Tests/TopicTest.cs +++ b/OnTopic.Tests/TopicTest.cs @@ -380,7 +380,7 @@ public void IsDirty_ChangeCollections_ReturnsTrue() { topic.Attributes.SetValue("Related", related.Key); topic.References.SetValue("Related", related); - topic.Relationships.SetTopic("Related", related); + topic.Relationships.SetValue("Related", related); Assert.IsTrue(topic.IsDirty(true)); @@ -402,7 +402,7 @@ public void MarkClean_ChangeCollection_ResetIsDirty() { topic.Attributes.SetValue("Related", related.Key); topic.References.SetValue("Related", related); - topic.Relationships.SetTopic("Related", related); + topic.Relationships.SetValue("Related", related); topic.MarkClean(true); diff --git a/OnTopic/Associations/ReferenceSetterAttribute.cs b/OnTopic/Associations/ReferenceSetterAttribute.cs index 282b6692..9f5446f4 100644 --- a/OnTopic/Associations/ReferenceSetterAttribute.cs +++ b/OnTopic/Associations/ReferenceSetterAttribute.cs @@ -26,7 +26,7 @@ namespace OnTopic.Associations { /// /// /// As an example, the property is adorned with the . - /// As a result, if a client calls topic.References.SetTopic("BaseTopic", topic), then that update will be + /// As a result, if a client calls topic.References.SetValue("BaseTopic", topic), then that update will be /// routed through , thus enforcing any validation. /// /// diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs index 49aabf98..d6221bd5 100644 --- a/OnTopic/Associations/TopicReferenceCollection.cs +++ b/OnTopic/Associations/TopicReferenceCollection.cs @@ -76,7 +76,7 @@ protected override void InsertItem(int index, TopicReferenceRecord item) { /*------------------------------------------------------------------------------------------------------------------------ | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ - item?.Value?.IncomingRelationships.SetTopic(item.Key, AssociatedTopic, null, true); + item?.Value?.IncomingRelationships.SetValue(item.Key, AssociatedTopic, null, true); } @@ -99,8 +99,8 @@ protected override void SetItem(int index, TopicReferenceRecord item) { /*------------------------------------------------------------------------------------------------------------------------ | Handle recipricol references \-----------------------------------------------------------------------------------------------------------------------*/ - item?.Value?.IncomingRelationships.SetTopic(item.Key, AssociatedTopic, null, true); - existingItem?.Value?.IncomingRelationships.SetTopic(existingItem.Key, AssociatedTopic, null, true); + item?.Value?.IncomingRelationships.SetValue(item.Key, AssociatedTopic, null, true); + existingItem?.Value?.IncomingRelationships.SetValue(existingItem.Key, AssociatedTopic, null, true); } diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index e817566d..b15bc356 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -44,7 +44,7 @@ public class TopicRelationshipMultiMap : ReadOnlyTopicMultiMap, ITrackDirtyKeys /// with. This will be used when setting incoming relationships. In addition, a /// may be set as if it is specifically intended to track incoming relationships; if this is /// not set, then it will not allow incoming relationships to be set via the internal overload. + /// "SetValue(String, Topic, Boolean?, Boolean)"/> overload. /// public TopicRelationshipMultiMap(Topic parent, bool isIncoming = false): base() { _parent = parent; @@ -151,31 +151,33 @@ internal bool Remove(string relationshipKey, Topic topic, bool isIncoming) { /// [Obsolete("The RemoveTopic() method has been renamed to Remove().", false)] - public bool RemoveTopic(string relationshipKey, Topic topic, Boolean isIncoming) => + public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) => Remove(relationshipKey, topic, isIncoming); /*========================================================================================================================== - | METHOD: SET TOPIC + | METHOD: SET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Ensures that a is associated with the specified relationship key. + /// Ensures that a is associated with the specified . /// /// - /// If a relationship by a given key is not currently established, it will automatically be created. + /// If a relationship by a given is not currently established, it will automatically be + /// created. /// /// The key of the relationship. /// The topic to be added, if it doesn't already exist. /// /// Optionally forces the collection to an state, assuming the topic was set. /// - public void SetTopic(string relationshipKey, Topic topic, bool? markDirty = null) - => SetTopic(relationshipKey, topic, markDirty, false); + public void SetValue(string relationshipKey, Topic topic, bool? markDirty = null) + => SetValue(relationshipKey, topic, markDirty, false); /// - /// Ensures that an incoming is associated with the specified relationship key. + /// Ensures that an incoming is associated with the specified . /// /// - /// If a relationship by a given key is not currently established, it will automatically be c. + /// If a relationship by a given is not currently established, it will automatically be + /// created. /// /// The key of the relationship. /// The topic to be added, if it doesn't already exist. @@ -185,7 +187,7 @@ public void SetTopic(string relationshipKey, Topic topic, bool? markDirty = null /// /// Optionally forces the collection to an state, assuming the topic was set. /// - internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, bool isIncoming) { + internal void SetValue(string relationshipKey, Topic topic, bool? markDirty, bool isIncoming) { /*------------------------------------------------------------------------------------------------------------------------ | Validate contracts @@ -219,11 +221,20 @@ internal void SetTopic(string relationshipKey, Topic topic, bool? markDirty, boo nameof(isIncoming) ); } - topic.IncomingRelationships.SetTopic(relationshipKey, _parent, markDirty, true); + topic.IncomingRelationships.SetValue(relationshipKey, _parent, markDirty, true); } } + /// + [Obsolete("The SetTopic() method has been renamed to SetValue().", false)] + public void SetTopic(string relationshipKey, Topic topic, bool? isDirty) => SetValue(relationshipKey, topic, isDirty); + + /// + [Obsolete("The SetTopic() method has been renamed to SetValue().", false)] + public void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) => + SetValue(relationshipKey, topic, isDirty, isIncoming); + /*========================================================================================================================== | IS FULLY LOADED? \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index d00c459b..0447b85a 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -53,7 +53,7 @@ If a property implements `IList` (e.g., `List<>`, `Collection<>`, `TopicViewMode - It will pull the value from a collection with the same name as the property. - If the property is explicitly named `Children`, then it will load the `topic.Children`. - It will search, in order, `topic.Relationships`, `topic.IncomingRelationships`, and finally `topic.Children`. -- E.g., If a `List<>` property is named `Cousins` then it might match `topic.Relationships.GetTopics("Cousins")`. +- E.g., If a `List<>` property is named `Cousins` then it might match `topic.Relationships.GetValues("Cousins")`. #### References Topic references relate a single topic to another topic by key. If a property corresponds to the key of a topic reference, and that `Topic` maps to an object that is assignable to the original property, then the `Topic` will be loaded, mapped, and assigned to that property. For instance, the following property: diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index c33062ff..807585be 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -443,7 +443,7 @@ PropertyConfiguration configuration $"be located in the repository." ); } - target.Relationships.SetTopic(configuration.AttributeKey, targetTopic); + target.Relationships.SetValue(configuration.AttributeKey, targetTopic); } } diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 0f0186bd..5b8405cf 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -127,7 +127,7 @@ public bool DisableChildTopics { /// /// /// To add content types to the collection, use . + /// cref="TopicRelationshipMultiMap.SetValue(String, Topic, Boolean?)"/>. /// /// public ReadOnlyKeyedTopicCollection PermittedContentTypes { From 6674c7da6c7010919d9c4aa18988163d5b46fd67 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 13:36:30 -0800 Subject: [PATCH 653/778] Rename `GetTopic()` to `GetValue()` This is more consistent with `KeyedCollection<>`'s `TryVetValue()` as well as `TrackedRecordCollection<>`'s `GetValue()`. As part of this, maintained a pass-through versions of `RemoveTopic()`, but marked it as deprecated. --- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- OnTopic.Tests/ReverseTopicMappingServiceTest.cs | 10 +++++----- OnTopic.Tests/TopicRepositoryBaseTest.cs | 12 ++++++------ OnTopic/Collections/KeyedTopicCollection{T}.cs | 10 +++++++--- .../Collections/ReadOnlyKeyedTopicCollection{T}.cs | 10 +++++++--- OnTopic/Mapping/Reverse/BindingModelValidator.cs | 2 +- .../Mapping/Reverse/ReverseTopicMappingService.cs | 10 +++++----- OnTopic/Querying/TopicExtensions.cs | 2 +- OnTopic/Repositories/TopicRepository.cs | 2 +- 9 files changed, 34 insertions(+), 26 deletions(-) diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index 2014d793..aa528ec4 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -247,7 +247,7 @@ AttributeDescriptor addAttribute( bool isExtended = true, bool isRequired = false ) { - var container = contentType.Children.GetTopic("Attributes"); + var container = contentType.Children.GetValue("Attributes"); if (container is null) { container = TopicFactory.Create("Attributes", "List", contentType); container.Attributes.SetBoolean("IsHidden", true); diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 3e4c2042..4e07ac22 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -270,11 +270,11 @@ public async Task Map_NestedTopics_ReturnsMappedTopic() { var target = (ContentTypeDescriptor?)await mappingService.MapAsync(bindingModel, topic).ConfigureAwait(false); Assert.AreEqual(3, target.AttributeDescriptors.Count); - Assert.IsNotNull(target.AttributeDescriptors.GetTopic("Attribute1")); - Assert.IsNotNull(target.AttributeDescriptors.GetTopic("Attribute2")); - Assert.IsNotNull(target.AttributeDescriptors.GetTopic("Attribute3")); - Assert.AreEqual("New Value", target.AttributeDescriptors.GetTopic("Attribute3").DefaultValue); - Assert.IsNull(target.AttributeDescriptors.GetTopic("Attribute4")); + Assert.IsNotNull(target.AttributeDescriptors.GetValue("Attribute1")); + Assert.IsNotNull(target.AttributeDescriptors.GetValue("Attribute2")); + Assert.IsNotNull(target.AttributeDescriptors.GetValue("Attribute3")); + Assert.AreEqual("New Value", target.AttributeDescriptors.GetValue("Attribute3").DefaultValue); + Assert.IsNull(target.AttributeDescriptors.GetValue("Attribute4")); } diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs index 4b8b4b0e..1551abc5 100644 --- a/OnTopic.Tests/TopicRepositoryBaseTest.cs +++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs @@ -444,9 +444,9 @@ public void GetContentTypeDescriptors_ReturnsContentTypes() { var contentTypes = _topicRepository.GetContentTypeDescriptors(); Assert.AreEqual(15, contentTypes.Count); - Assert.IsNotNull(contentTypes.GetTopic("ContentTypeDescriptor")); - Assert.IsNotNull(contentTypes.GetTopic("Page")); - Assert.IsNotNull(contentTypes.GetTopic("LookupListItem")); + Assert.IsNotNull(contentTypes.GetValue("ContentTypeDescriptor")); + Assert.IsNotNull(contentTypes.GetValue("Page")); + Assert.IsNotNull(contentTypes.GetValue("LookupListItem")); } @@ -536,9 +536,9 @@ public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() { public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() { var contentTypes = _topicRepository.GetContentTypeDescriptors(); - var contentTypesRoot = contentTypes.GetTopic("ContentTypes"); - var pageContentType = contentTypes.GetTopic("Page"); - var lookupContentType = contentTypes.GetTopic("Lookup"); + var contentTypesRoot = contentTypes.GetValue("ContentTypes"); + var pageContentType = contentTypes.GetValue("Page"); + var lookupContentType = contentTypes.GetValue("Lookup"); var initialCount = pageContentType.PermittedContentTypes.Count; pageContentType.Relationships.SetValue("ContentTypes", lookupContentType); diff --git a/OnTopic/Collections/KeyedTopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs index cee38824..2ecb1df0 100644 --- a/OnTopic/Collections/KeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs @@ -34,12 +34,12 @@ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer } /*========================================================================================================================== - | METHOD: GET TOPIC + | METHOD: GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a by key. + /// Retrieves a by . /// - public T? GetTopic(string key) { + public T? GetValue(string key) { TopicFactory.ValidateKey(key); if (Contains(key)) { return this[key]; @@ -47,6 +47,10 @@ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer return null; } + /// + [Obsolete("The GetTopic() method has been renamed to GetValue().", false)] + public T? GetTopic(string key) => GetValue(key); + /*========================================================================================================================== | METHOD: AS READ ONLY \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index be052b4b..f73cdb3e 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -36,12 +36,12 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T } /*========================================================================================================================== - | METHOD: GET TOPIC + | METHOD: GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Retrieves a by key. + /// Retrieves a by . /// - public T? GetTopic(string key) { + public T? GetValue(string key) { TopicFactory.ValidateKey(key); if (_innerCollection.Contains(key)) { return _innerCollection[key]; @@ -49,6 +49,10 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T return null; } + /// + [Obsolete("The GetTopic() method has been renamed to GetValue().", false)] + public T? GetTopic(string key) => GetValue(key); + /*========================================================================================================================== | INDEXER \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index b58bc71f..9c09f3bc 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -161,7 +161,7 @@ static internal void ValidateProperty( var propertyType = property.PropertyType; var configuration = new PropertyConfiguration(property, attributePrefix); var compositeAttributeKey = configuration.AttributeKey; - var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey); + var attributeDescriptor = contentTypeDescriptor.AttributeDescriptors.GetValue(compositeAttributeKey); var childCollections = new[] { CollectionType.Children, CollectionType.NestedTopics }; var relationships = new[] { CollectionType.Relationship, CollectionType.IncomingRelationship }; var listType = (Type?)null; diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 807585be..10c44ca8 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -211,7 +211,7 @@ public ReverseTopicMappingService(ITopicRepository topicRepository) { | Validate model \-----------------------------------------------------------------------------------------------------------------------*/ var properties = _typeCache.GetMembers(source.GetType()); - var contentTypeDescriptor = _contentTypeDescriptors.GetTopic(target.ContentType); + var contentTypeDescriptor = _contentTypeDescriptors.GetValue(target.ContentType); BindingModelValidator.ValidateModel(source.GetType(), properties, contentTypeDescriptor, attributePrefix); @@ -263,7 +263,7 @@ private async Task SetPropertyAsync( | Establish per-property variables \-----------------------------------------------------------------------------------------------------------------------*/ var configuration = new PropertyConfiguration(property, attributePrefix); - var contentTypeDescriptor = _contentTypeDescriptors.GetTopic(target.ContentType); + var contentTypeDescriptor = _contentTypeDescriptors.GetValue(target.ContentType); var compositeAttributeKey = configuration.AttributeKey; Contract.Assume(contentTypeDescriptor, nameof(contentTypeDescriptor)); @@ -290,7 +290,7 @@ await MapAsync( /*------------------------------------------------------------------------------------------------------------------------ | Retrieve attribute descriptor \-----------------------------------------------------------------------------------------------------------------------*/ - var attributeType = contentTypeDescriptor.AttributeDescriptors.GetTopic(compositeAttributeKey); + var attributeType = contentTypeDescriptor.AttributeDescriptors.GetValue(compositeAttributeKey); if (attributeType is null) { throw new MappingModelValidationException( @@ -483,7 +483,7 @@ PropertyConfiguration configuration /*------------------------------------------------------------------------------------------------------------------------ | Establish target collection to store mapped topics \-----------------------------------------------------------------------------------------------------------------------*/ - var container = target.Children.GetTopic(configuration.AttributeKey); + var container = target.Children.GetValue(configuration.AttributeKey); if (container is null) { container = TopicFactory.Create(configuration.AttributeKey, "List", target); container.IsHidden = true; @@ -590,7 +590,7 @@ KeyedTopicCollection targetList foreach (ITopicBindingModel childBindingModel in sourceList) { Contract.Assume(childBindingModel.Key); if (targetList.Contains(childBindingModel.Key)) { - taskQueue.Add(MapAsync(childBindingModel, targetList.GetTopic(childBindingModel.Key)!)); + taskQueue.Add(MapAsync(childBindingModel, targetList.GetValue(childBindingModel.Key)!)); } else { taskQueue.Add(MapAsync(childBindingModel)); diff --git a/OnTopic/Querying/TopicExtensions.cs b/OnTopic/Querying/TopicExtensions.cs index 2600d625..2b5d33b8 100644 --- a/OnTopic/Querying/TopicExtensions.cs +++ b/OnTopic/Querying/TopicExtensions.cs @@ -254,7 +254,7 @@ public static ReadOnlyTopicCollection FindAllByAttribute(this Topic topic, strin | Navigate to the specific path \-----------------------------------------------------------------------------------------------------------------------*/ foreach (var key in keys) { - currentTopic = currentTopic?.Children?.GetTopic(key); + currentTopic = currentTopic?.Children?.GetValue(key); } /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic/Repositories/TopicRepository.cs b/OnTopic/Repositories/TopicRepository.cs index b0164ac5..7264fb46 100644 --- a/OnTopic/Repositories/TopicRepository.cs +++ b/OnTopic/Repositories/TopicRepository.cs @@ -72,7 +72,7 @@ public override ContentTypeDescriptorCollection GetContentTypeDescriptors() { /*---------------------------------------------------------------------------------------------------------------------- | Load root content type \---------------------------------------------------------------------------------------------------------------------*/ - var contentTypes = configuration?.Children.GetTopic("ContentTypes") as ContentTypeDescriptor; + var contentTypes = configuration?.Children.GetValue("ContentTypes") as ContentTypeDescriptor; /*---------------------------------------------------------------------------------------------------------------------- | Add available Content Types to the collection From f28fab2725c30d61d5d2f272fc993a93ea6866c7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 10 Feb 2021 13:47:55 -0800 Subject: [PATCH 654/778] Marked all deprecations as obsolete As this is a major version with a lot of breaking changes, we're going to tear off the bandaid and force adopters to utilize the new methods. That will also allow us to remove these deprecated methods in e.g. OnTopic 5.1.0, instead of carrying them around until e.g. OnTopic 6.0.0, which will likely be another year or two away. --- OnTopic/Associations/TopicRelationshipMultiMap.cs | 10 +++++----- OnTopic/Collections/KeyedTopicCollection{T}.cs | 2 +- OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs | 2 +- .../Collections/Specialized/ReadOnlyTopicMultiMap.cs | 6 +++--- OnTopic/Collections/Specialized/TopicMultiMap.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index b15bc356..43c8b5ac 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -75,7 +75,7 @@ public void Clear(string relationshipKey) { } /// - [Obsolete("The ClearTopics(relationshipKey) method has been renamed to Clear(relationshipKey).", false)] + [Obsolete("The ClearTopics(relationshipKey) method has been renamed to Clear(relationshipKey).", true)] public void ClearTopics(string relationshipKey) => Clear(relationshipKey); /*========================================================================================================================== @@ -146,11 +146,11 @@ internal bool Remove(string relationshipKey, Topic topic, bool isIncoming) { } /// - [Obsolete("The RemoveTopic() method has been renamed to Remove().", false)] + [Obsolete("The RemoveTopic() method has been renamed to Remove().", true)] public bool RemoveTopic(string relationshipKey, Topic topic) => Remove(relationshipKey, topic); /// - [Obsolete("The RemoveTopic() method has been renamed to Remove().", false)] + [Obsolete("The RemoveTopic() method has been renamed to Remove().", true)] public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) => Remove(relationshipKey, topic, isIncoming); @@ -227,11 +227,11 @@ internal void SetValue(string relationshipKey, Topic topic, bool? markDirty, boo } /// - [Obsolete("The SetTopic() method has been renamed to SetValue().", false)] + [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] public void SetTopic(string relationshipKey, Topic topic, bool? isDirty) => SetValue(relationshipKey, topic, isDirty); /// - [Obsolete("The SetTopic() method has been renamed to SetValue().", false)] + [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] public void SetTopic(string relationshipKey, Topic topic, bool? isDirty, bool isIncoming) => SetValue(relationshipKey, topic, isDirty, isIncoming); diff --git a/OnTopic/Collections/KeyedTopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs index 2ecb1df0..516cb0a0 100644 --- a/OnTopic/Collections/KeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs @@ -48,7 +48,7 @@ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer } /// - [Obsolete("The GetTopic() method has been renamed to GetValue().", false)] + [Obsolete("The GetTopic() method has been renamed to GetValue().", true)] public T? GetTopic(string key) => GetValue(key); /*========================================================================================================================== diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index f73cdb3e..df354ec8 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -50,7 +50,7 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T } /// - [Obsolete("The GetTopic() method has been renamed to GetValue().", false)] + [Obsolete("The GetTopic() method has been renamed to GetValue().", true)] public T? GetTopic(string key) => GetValue(key); /*========================================================================================================================== diff --git a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs index e924dd2f..c5f3b9c7 100644 --- a/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs @@ -123,7 +123,7 @@ public ReadOnlyTopicCollection GetValues(string key) { } /// - [Obsolete("The GetTopics() method has been renamed to GetValues().", false)] + [Obsolete("The GetTopics() method has been renamed to GetValues().", true)] public ReadOnlyTopicCollection GetTopics(string key) => GetValues(key); /*========================================================================================================================== @@ -149,11 +149,11 @@ public ReadOnlyTopicCollection GetAllValues(string contentType) => new(GetAllValues().Where(t => t.ContentType == contentType).ToList()); /// - [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", false)] + [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", true)] public ReadOnlyTopicCollection GetAllTopics(string key) => GetAllValues(key); /// - [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", false)] + [Obsolete("The GetAllTopics() method has been renamed to GetAllValues().", true)] public ReadOnlyTopicCollection GetAllTopics() => GetAllValues(); /*========================================================================================================================== diff --git a/OnTopic/Collections/Specialized/TopicMultiMap.cs b/OnTopic/Collections/Specialized/TopicMultiMap.cs index aecc2465..d3b12cd7 100644 --- a/OnTopic/Collections/Specialized/TopicMultiMap.cs +++ b/OnTopic/Collections/Specialized/TopicMultiMap.cs @@ -59,7 +59,7 @@ public TopicCollection GetValues(string key) { } /// - [Obsolete("The GetTopics() method has been renamed to GetValues().", false)] + [Obsolete("The GetTopics() method has been renamed to GetValues().", true)] public TopicCollection GetTopics(string key) => GetValues(key); /*========================================================================================================================== From 532c49d008514d6818897416cee0fc515ed8fc6b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 16:31:02 -0800 Subject: [PATCH 655/778] Remove `virtual` from read-only properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of `virtual` properties, prefer `sealed` properties with a `protected init` accessor. This way, derived classes—and _only_ derived classes—can still override the _value_, but without needing to `override` the _property_. In practice, this ends up not being that much less code, so the gain is small. Nevertheless, overriding properties introduces potential issues with polymorphism (e.g., if a value is set to a local backing field, not relayed through the `base` implementation). And since the only purpose for marking real-only fields as `virtual` is to modify the default value, there isn't a strong case for doing this now that we have `init` accessors. (When these were introduced, we didn't have `init` accessors, though we could have used `protected set` instead to get a similar functionality.) --- OnTopic/Metadata/AttributeDescriptor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 042e4291..fbfe4f8a 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -74,6 +74,7 @@ public AttributeDescriptor( parent, id ) { + } /*========================================================================================================================== @@ -89,7 +90,7 @@ public AttributeDescriptor( /// reduces these down into a single type based on how they're exposed in the Topic Library, not based on how they're /// exposed in the editor. /// - public virtual ModelType ModelType => ModelType.ScalarValue; + public ModelType ModelType { get; protected init; } = ModelType.ScalarValue; /*========================================================================================================================== | PROPERTY: EDITOR TYPE @@ -109,8 +110,7 @@ public AttributeDescriptor( /// /// !value.Contains(" ") && !value.Contains("/") /// - [AttributeSetter] - public virtual string EditorType => GetType().Name.Replace("AttributeDescriptor", "", StringComparison.OrdinalIgnoreCase); + public string EditorType => GetType().Name.Replace("AttributeDescriptor", "", StringComparison.OrdinalIgnoreCase); /*========================================================================================================================== | PROPERTY: DISPLAY GROUP @@ -193,7 +193,7 @@ public string? DefaultValue { /// /// [AttributeSetter] - public virtual bool IsExtendedAttribute { + public bool IsExtendedAttribute { get => Attributes.GetBoolean("IsExtendedAttribute", Attributes.GetBoolean("StoreInBlob")); set => SetAttributeValue("IsExtendedAttribute", value ? "1" : "0"); } From ec2d875e111cdd008046050983f6c9ffdecdbf8a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 16:36:59 -0800 Subject: [PATCH 656/778] Updated `AttributeDescriptor`s to use new `protected init` accessor In the previous commit, `ModelType` was changed from `virtual` to instead having a `protected init` accessor (532c49d). This is a breaking change for derived types. As such, the derived types used in the `TestDoubles` are updated to reflect this change. --- .../Metadata/NestedTopicListAttributeDescriptor.cs | 12 ++++++------ .../Metadata/RelationshipAttributeDescriptor.cs | 12 ++++++------ .../Metadata/TopicReferenceAttributeDescriptor.cs | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs index de21ed9f..9d1ade75 100644 --- a/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs @@ -35,13 +35,13 @@ public NestedTopicListAttributeDescriptor( parent, id ) { - } - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType => ModelType.NestedTopic; + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize values + \-----------------------------------------------------------------------------------------------------------------------*/ + ModelType = ModelType.NestedTopic; + + } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs index d87c6f15..92db8132 100644 --- a/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs @@ -35,13 +35,13 @@ public RelationshipAttributeDescriptor( parent, id ) { - } - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType => ModelType.Relationship; + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize values + \-----------------------------------------------------------------------------------------------------------------------*/ + ModelType = ModelType.Relationship; + + } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs index e8381267..088d7dbb 100644 --- a/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs +++ b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs @@ -35,13 +35,13 @@ public TopicReferenceAttributeDescriptor( parent, id ) { - } - /*========================================================================================================================== - | PROPERTY: MODEL TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public override ModelType ModelType => ModelType.Reference; + /*------------------------------------------------------------------------------------------------------------------------ + | Initialize values + \-----------------------------------------------------------------------------------------------------------------------*/ + ModelType = ModelType.Reference; + + } } //Class } //Namespace \ No newline at end of file From 59936df83a763647d9471bc9673fcdfa09432257 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 16:50:27 -0800 Subject: [PATCH 657/778] Removed unnecessary `[AttributeSetter]` attributes The `[AttributeSetter]` attribute is used to ensure that business logic is enforced when setting attributes that have associated properties. This introduces overhead, and reflection is used to call the property. While this is pretty well optimized for comparatively rare write scenarios, it nonetheless slows down the operation. That's fine if it's needed, but these properties are effectively just convenience pass-throughs that don't provide any data validation or state management, but simply provide a strongly typed property for accessing the underlying attribute. Given that, removing the `[AttributeSetter]` doesn't risk a "backdoor" around enforcing the business logic, but speeds up the constructions of new `Topic`s. Obviously, in the unlikely event that business logic is introduced into these properties, the `[AttributeSetter]` attribute will need to be reintroduced. --- OnTopic/Metadata/AttributeDescriptor.cs | 3 --- OnTopic/Metadata/ContentTypeDescriptor.cs | 1 - 2 files changed, 4 deletions(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index fbfe4f8a..1678ccf7 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -127,7 +127,6 @@ public AttributeDescriptor( /// /// !String.IsNullOrWhiteSpace(value) /// - [AttributeSetter] public string? DisplayGroup { get => Attributes.GetValue("DisplayGroup", ""); set { @@ -146,7 +145,6 @@ public string? DisplayGroup { /// This is used to establish a required field validator in the editor interface. This should be used by the form /// validation in the editor to ensure the field contains a value. /// - [AttributeSetter] public bool IsRequired { get => Attributes.GetBoolean("IsRequired"); set => SetAttributeValue("IsRequired", value ? "1" : "0"); @@ -192,7 +190,6 @@ public string? DefaultValue { /// This property and its corresponding attribute was named StoreInBlob in versions of OnTopic prior to 4.0. /// /// - [AttributeSetter] public bool IsExtendedAttribute { get => Attributes.GetBoolean("IsExtendedAttribute", Attributes.GetBoolean("StoreInBlob")); set => SetAttributeValue("IsExtendedAttribute", value ? "1" : "0"); diff --git a/OnTopic/Metadata/ContentTypeDescriptor.cs b/OnTopic/Metadata/ContentTypeDescriptor.cs index 5b8405cf..8c69e403 100644 --- a/OnTopic/Metadata/ContentTypeDescriptor.cs +++ b/OnTopic/Metadata/ContentTypeDescriptor.cs @@ -98,7 +98,6 @@ public ContentTypeDescriptor( /// cref="ContentTypeDescriptor"/> is set to . /// /// - [AttributeSetter] public bool DisableChildTopics { get => Attributes.GetBoolean("DisableChildTopics"); set => SetAttributeValue("DisableChildTopics", value ? "1" : "0"); From 6541560ea8a7086dbfb130dfd227b48d8e10f2ff Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 16:55:04 -0800 Subject: [PATCH 658/778] Ensured `isDirty` is optional on deprecated `SetTopic()` method The `SetTopic()` method is deprecated, and should not be called. That said, the original version had an optional `isDirty` parameter, which many callers took advantage of. By reverting this parameter to be optional, that ensures that those callers received the `[Obsolete()]` message, whereas otherwise they get an unintuitive message that the overload doesn't exist. This is simply intended to aid in the migration process. --- OnTopic/Associations/TopicRelationshipMultiMap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs index 43c8b5ac..391a9995 100644 --- a/OnTopic/Associations/TopicRelationshipMultiMap.cs +++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs @@ -228,7 +228,7 @@ internal void SetValue(string relationshipKey, Topic topic, bool? markDirty, boo /// [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] - public void SetTopic(string relationshipKey, Topic topic, bool? isDirty) => SetValue(relationshipKey, topic, isDirty); + public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) => SetValue(relationshipKey, topic, isDirty); /// [Obsolete("The SetTopic() method has been renamed to SetValue().", true)] From 3cd060e7b45ed460e87495c8e738edb74bbc9838 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:17:10 -0800 Subject: [PATCH 659/778] Implemented `DerivedTopic` as `[Obsolete()]` for warning messages While `DerivedTopic` is obsolete, it had been removed entirely, thus preventing callers from receiving clear guidance from code analysis or the compiler as to how to resolve this issue. Temporarily reintroducing it as an `[Obsolete()]` member to provide direction. These obsolete members will likely be removed as part of OnTopic 5.1.0. --- OnTopic/Topic.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index dc76f28b..5b0c34f3 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -716,6 +716,13 @@ public Topic? BaseTopic { } } + /// + [Obsolete("The DerivedTopic property has been renamed to BaseTopic. Please update references.", true)] + public Topic? DerivedTopic { + get => BaseTopic; + set => BaseTopic = value; + } + /*========================================================================================================================== | PROPERTY: ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ From 0929fb4691e20ef7d98fc8a18a98b6683d499f4b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:25:01 -0800 Subject: [PATCH 660/778] Corrected rename of `DerivedTopic` While the C# property was named `DerivedTopic`, and the `AttributeKey` was named `TopicID`, the attribute name itself had a key of `InheritedTopic`. Confusing? That's part of why we've unified around `BaseTopic` as a consistent naming convention. Regardless, this upgrade script failed to work due to that. In addition, the `ContentType` at this point is still `TopicReferenceAttribute`. The change to `TopicReferenceAttributeDescriptor` will occur in the subsequent script. This could be placed after that, but it doesn't really matter so long as it acknowledges the current state of the data. --- .../Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 88095fd6..ed404dc7 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -133,8 +133,8 @@ WHERE ReferenceKey = 'Topic' UPDATE Topics SET TopicKey = 'BaseTopic' -WHERE TopicKey = 'DerivedTopic' -AND ContentType = 'TopicReferenceAttributeDescriptor' +WHERE TopicKey = 'InheritedTopic' +AND ContentType = 'TopicReferenceAttribute' -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE ATTRIBUTE KEYS From ade1f90ebdac2909e1092f569ac1af3050514ba2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:29:10 -0800 Subject: [PATCH 661/778] Fixed bug in adding root topics In a previous update, I set the `CreateTopic` stored procedure to look for an existing root topic if `@ParentID` is `null`, and set the `RangeRight` to be its `RangeRight` + 1 (500ca094). That way, if a second root topic is added, it won't overlap with the existing root topic. Unfortunately, in doing so, I broke the scenario where a root topic doesn't already exist. This, too, is a rare scenario, but occurs at the critical point of initializing a new database (e.g., by importing a reference database). This fix ensures that both scenarios continue to be supported. --- OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql index d963b075..e6bf0df2 100644 --- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql +++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql @@ -90,7 +90,7 @@ IF (@ParentID IS NOT NULL) END ELSE BEGIN - SELECT @RangeRight = MAX(RangeRight) + 1 + SELECT @RangeRight = ISNULL(MAX(RangeRight), 0) + 1 FROM Topics WITH ( TABLOCK ) From 87d7570f44a14bd7d06260d29761936112c460e2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:45:19 -0800 Subject: [PATCH 662/778] Established metapackage for `OnTopic` The metapackage will allow consumers to establish a dependency on just `OnTopic.All` instead of needing to establish a dependency on each of the core libraries associated with OnTopic. --- OnTopic.All/OnTopic.All.csproj | 20 ++++++++++++++++++++ OnTopic.sln | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 OnTopic.All/OnTopic.All.csproj diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj new file mode 100644 index 00000000..262f0d82 --- /dev/null +++ b/OnTopic.All/OnTopic.All.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.1 + + + + OnTopic Library Metapackage + Ignia + OnTopic + + Includes all core packages associated with the OnTopic Library, excluding the OnTopic Editor. Reference this package as a + shorthand for establishing a reference to each of the individual packages. + + ©2021 Ignia, LLC + bin\$(Configuration)\ + Ignia + + + diff --git a/OnTopic.sln b/OnTopic.sln index b29473fa..7dfc4d9b 100644 --- a/OnTopic.sln +++ b/OnTopic.sln @@ -32,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.TestDoubles", "OnTo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnTopic.Data.Sql.Database.Tests", "OnTopic.Data.Sql.Database.Tests\OnTopic.Data.Sql.Database.Tests.csproj", "{D7FE876D-A75F-4493-8283-B316271FD5AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnTopic.All", "OnTopic.All\OnTopic.All.csproj", "{5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,6 +83,10 @@ Global {D7FE876D-A75F-4493-8283-B316271FD5AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D7FE876D-A75F-4493-8283-B316271FD5AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7FE876D-A75F-4493-8283-B316271FD5AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 028e4aedc4225f1101b8a8578eeed28b3c83392d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:47:49 -0800 Subject: [PATCH 663/778] Introduced dependencies to core packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In addition to `OnTopic`, this also includes `OnTopic.AspNetCore.Mvc`, `OnTopic.Data.Caching`, and `OnTopic.ViewModels`. These are expected to be used by _most_ implementations of OnTopic. Obviously, implementers can prefer relying on individual packages if they need a custom configuration—e.g., if they don't want to use our prepackaged view models, for instance. --- OnTopic.All/OnTopic.All.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index 262f0d82..fb86e43f 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -17,4 +17,11 @@ Ignia + + + + + + + From ab81af4ffa537877ef24abdebb3c16c972692990 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:49:50 -0800 Subject: [PATCH 664/778] Introduced dependency on `OnTopic.Data.Sql` This is a more controversial decision as it assumes that implementors will be using a SQL Server backend. As that's the only production-ready `ITopicRepository` currently available, however, that is a safe assumption. Further, while there are non-production proof-of-concepts for e.g. MongoDB and Cosmos DB, they introduce significant limitations for e.g versioning, and aren't expected to be the primary client. --- OnTopic.All/OnTopic.All.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index fb86e43f..dfa6eeb5 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -20,6 +20,7 @@ + From 3740b99dff1299e3c974bce07aa038650d0d6239 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:52:55 -0800 Subject: [PATCH 665/778] Ensured use of `netcoreapp3.1` moniker The `OnTopic.All` metapackage must target .NET Core 3.1, since it relies in turn on `OnTopic.AspNetCore.Mvc`, which relies on .NET Core 3.1. --- OnTopic.All/OnTopic.All.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index dfa6eeb5..6b95169c 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -1,7 +1,7 @@ - netstandard2.1 + netcoreapp3.1 From fd295a7526ea734e85a1213a13b39729105127f3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:54:41 -0800 Subject: [PATCH 666/778] Introduced `GitVersion` for the `OnTopic.All` metapackage The `OnTopic.All` metapackage should maintain the same versioning as the OnTopic libraries that it depends upon. To help ensure that, a dependency on the `GitVersion.MsBuild` package is introduced. --- OnTopic.All/OnTopic.All.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index 6b95169c..cb56de22 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -17,6 +17,13 @@ Ignia + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + From e1144ecdb63356dc9be8905cefdfb677c77619b5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 17:59:17 -0800 Subject: [PATCH 667/778] Updated `Host` project to rely on the new `OnTopic.All` metapackage The `Host` project isn't just a way of doing quick testing of the OnTopic Library; it's also a barebones reference for how an OnTopic implementation is expected to be configured. Given that, it should use the new `OnTopic.All` metapackage, which is what we expect most customers will use. --- .../OnTopic.AspNetCore.Mvc.Host.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index c50a72ae..c25426af 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -15,11 +15,7 @@ - - - - - + From 9da53fa9451d19458fdf0ada008cc8d4a90939a5 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 16 Feb 2021 18:19:28 -0800 Subject: [PATCH 668/778] Introduced basic documentation for the metapackage --- OnTopic.All/README.md | 31 +++++++++++++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 35 insertions(+) create mode 100644 OnTopic.All/README.md diff --git a/OnTopic.All/README.md b/OnTopic.All/README.md new file mode 100644 index 00000000..307ea93c --- /dev/null +++ b/OnTopic.All/README.md @@ -0,0 +1,31 @@ +# OnTopic Metapackage +The `OnTopic.All` metapackage includes a reference to the core OnTopic libraries that most implementations will require. It is recommended that implementers reference this package instead of referencing each of the OnTopic packages individually, unless they have a specific need to customize which packages are referenced. + +[![OnTopic.Data.Caching package in Internal feed in Azure Artifacts](https://igniasoftware.feeds.visualstudio.com/_apis/public/Packaging/Feeds/46d5f49c-5e1e-47bb-8b14-43be6c719ba8/Packages/3dfb3a0a-c049-407d-959e-546f714dcd0f/Badge)](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3dfb3a0a-c049-407d-959e-546f714dcd0f&preferRelease=true) +[![Build Status](https://igniasoftware.visualstudio.com/OnTopic/_apis/build/status/OnTopic-CI-V3?branchName=master)](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master) +![NuGet Deployment Status](https://rmsprodscussu1.vsrm.visualstudio.com/A09668467-721c-4517-8d2e-aedbe2a7d67f/_apis/public/Release/badge/bd7f03e0-6fcf-4ec6-939d-4e995668d40f/2/2) + +### Contents +- [Scope](#scope) +- [Installation](#installation) + +## Scope +The `OnTopic.All` metapackage maintains a reference to the following packages: +- [`OnTopic`](../OnTopic/README.md): The core OnTopic library. +- [`OnTopic.AspNetCore.Mvc`](../OnTopic.AspNetCore.Mvc/README.md): The ASP.NET Core implementation, with support for both ASP.NET Core 3.x and ASP.NET Core 5.x. +- [`OnTopic.Data.Caching`](../OnTopic.Data.Caching/README.md): An `ITopicRepository` decorator for caching the topic graph in memory. +- [`OnTopic.Data.Sql`](../OnTopic.Data.Sql/README.md): An `ITopicRepository` implementation for persisting topic data in a SQL Server database. +- [`OnTopic.ViewModels`](../OnTopic.ViewModels/README.md): A set of reference view models and binding models mapping to the out-of-the-box schema for the standard content types. + +## Installation +Installation can be performed by providing a ` to the `OnTopic.All` **NuGet** package. +```xml + + … + + + + +``` + +> *Note:* This package is currently only available on Ignia's private **NuGet** repository. For access, please contact [Ignia](http://www.ignia.com/). \ No newline at end of file diff --git a/README.md b/README.md index 1d101170..0f3049cd 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,12 @@ In addition, OnTopic is optimized for multi-client/multi-device scenarios since ### Extensible Fundamentally, OnTopic is based on structured schemas ("Content Types") which can be modified via the editor itself. This allows new data structures to be introduced without needing to modify the database or creating extensive plugins. So, for example, if a site includes job postings, it might create a `JobPosting` content type that describes the structure of a job posting, such as _job title_, _job description_, _job requirements_, &c. By contrast, some CMSs—such as WordPress—try to fit all items into a single data model—such as a _blog post_—or require extensive customizations of database objects and intermediate queries in order to extend the data model. OnTopic is designed with extensibility in mind, so updates to the data model are comparatively trivial to implement. + ## Library +### Metapackage +- **[`OnTopic.All`](OnTopic.All/README.md)** The metapackage includes a reference to all of the core libraries discussed below under [Domain Layer](#domain-layer), [Data Access Layer](#data-access-layer), and [Presentation Layer](#presentation-layer). It is recommended that most implementations rely on this, instead of including package references for individual libraries. + ### Domain Layer - **[`OnTopic.Topics`](OnTopic/README.md)**: Core domain model including the `Topic` entity and service abstractions such as `ITopicRepository`. From 01ad3c916eb143ff59a2fc4d1067d03b8631945b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 14:37:41 -0800 Subject: [PATCH 669/778] Introduced polyfill for `[MemberNotNull]` attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `[MemberNotNull]` attribute is introduced in .NET 5, and not available in .NET Standard 2.1, which OnTopic targets. We could multi-target .NET 5 to gain access to it. But as it's one of the few .NET 5 capabilities we have an immediate need for, and it can easily be introduced via a polyfill, we're doing that. This allows us to annotate e.g. properties as a means of validating that local members—including private fields—will not be null after the annotated member is called. --- .../Diagnostics/MemberNotNullAttribute.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 OnTopic/Internal/Diagnostics/MemberNotNullAttribute.cs diff --git a/OnTopic/Internal/Diagnostics/MemberNotNullAttribute.cs b/OnTopic/Internal/Diagnostics/MemberNotNullAttribute.cs new file mode 100644 index 00000000..5246d7a3 --- /dev/null +++ b/OnTopic/Internal/Diagnostics/MemberNotNullAttribute.cs @@ -0,0 +1,45 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +namespace System.Diagnostics.CodeAnalysis { + + /*============================================================================================================================ + | CLASS: MEMBER NOT NULL (ATTRIBUTE) + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + #pragma warning disable CA1019 // Define accessors for attribute arguments + public MemberNotNullAttribute(string member) { + Members = new[] { member }; + } + #pragma warning restore CA1019 // Define accessors for attribute arguments + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) { + Members = members; + } + + /*========================================================================================================================== + | PROPERTY: MEMBERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// Gets field or property member names. + public string[] Members { get; } + + } +} \ No newline at end of file From 8d1bcbd443e563182d47591d2665871609becd0a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 14:39:06 -0800 Subject: [PATCH 670/778] Introduced polyfill for `[MemberNotNullWhen]` attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `[MemberNotNullWhen]` attribute is introduced in .NET 5, and not available in .NET Standard 2.1, which OnTopic targets. We could multi-target .NET 5 to gain access to it. But as it's one of the few .NET 5 capabilities we have an immediate need for, and it can easily be introduced via a polyfill, we're doing that. This allows us to annotate e.g. properties as a means of validating that local members—including private fields—will not be null after the annotated member is called, assuming a given return value. --- .../Diagnostics/MemberNotNullWhenAttribute.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 OnTopic/Internal/Diagnostics/MemberNotNullWhenAttribute.cs diff --git a/OnTopic/Internal/Diagnostics/MemberNotNullWhenAttribute.cs b/OnTopic/Internal/Diagnostics/MemberNotNullWhenAttribute.cs new file mode 100644 index 00000000..c6baab71 --- /dev/null +++ b/OnTopic/Internal/Diagnostics/MemberNotNullWhenAttribute.cs @@ -0,0 +1,62 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +namespace System.Diagnostics.CodeAnalysis { + + /*============================================================================================================================ + | CLASS: MEMBER NOT NULL (ATTRIBUTE) + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when + /// returning with the specified return value condition. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute: Attribute { + + /*========================================================================================================================== + | CONSTRUCTOR + \-------------------------------------------------------------------------------------------------------------------------*/ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + #pragma warning disable CA1019 // Define accessors for attribute arguments + public MemberNotNullWhenAttribute(bool returnValue, string member) { + ReturnValue = returnValue; + Members = new[] { member }; + } + #pragma warning restore CA1019 // Define accessors for attribute arguments + + /// + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) { + ReturnValue = returnValue; + Members = members; + } + + /*========================================================================================================================== + | PROPERTY: RETURN VALUE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// Gets the return value condition. + public bool ReturnValue { get; } + + /*========================================================================================================================== + | PROPERTY: MEMBERS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// Gets field or property member names. + public string[] Members { get; } + + } +} \ No newline at end of file From 62e6d2f012bf29fb6f916a202af11ec46dbfe9d0 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 14:40:29 -0800 Subject: [PATCH 671/778] Applied `[MemberNotNull()]` attribute to `ContentType`, `Key` setters This allows us to remove the ugly hack of setting the local `_key` and `_contentType` variables to themselves at the end of the constructor to assure Roslyn code analysis that those were, in fact, set. --- OnTopic/Topic.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index 5b0c34f3..fc3adde9 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -87,15 +87,6 @@ public Topic(string key, string contentType, Topic? parent = null, int id = -1) Parent = parent; } - /*------------------------------------------------------------------------------------------------------------------------ - | Initialize key fields - \-----------------------------------------------------------------------------------------------------------------------*/ - //###HACK JJC20190924: The local backing fields _key and _contentType are always initialized at this point. But Roslyn's - //flow analysis isn't smart enough to detect this. As such, the following effectively sets _key and _contentType to - //themselves. - _key = Key; - _contentType = ContentType; - } #region Core Properties @@ -183,6 +174,7 @@ public Topic? Parent { /// public string ContentType { get => _contentType; + [MemberNotNull(nameof(_contentType))] set { TopicFactory.ValidateKey(value); if (_contentType == value) { @@ -215,6 +207,7 @@ public string ContentType { /// public string Key { get => _key; + [MemberNotNull(nameof(_key))] set { TopicFactory.ValidateKey(value); if (_key == value) { From 39e314737ffed6d33be88f4f0ec71ae7a8d1dc06 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 14:45:55 -0800 Subject: [PATCH 672/778] Set `VersionHistory` to `init` only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With mapped collections, items are added to the collection after they are mapped. With compatible properties, however, the property value is simply set as a reference to the original object. If the property is a collection, there's no evaluation or mapping of the individual items in the collection. As a result, the property must be settable—even though it's a collection, and we don't normally want collection-valued properties to be settable. Previously, that necessitated the suppression of `CA2227`. With the introduction of `init`, however, we can replace the `set` with `init` to workaround this. Once we target .NET 5, this won't work and we'll need to reintroduce the suppression. But even then, `init` better communicates the expectations for this property. And, in fact, most new view models are actually implemented as `record` types to enforce this. --- OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs index b467c73c..d301008f 100644 --- a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs @@ -20,12 +20,11 @@ namespace OnTopic.Tests.ViewModels { /// /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. /// - [SuppressMessage("Usage", "CA2227", Justification = "This is intended to be initialized by the mapping service.")] public class CompatiblePropertyTopicViewModel { public ModelType ModelType { get; set; } - public Collection? VersionHistory { get; set; } + public Collection? VersionHistory { get; init; } } //Class } //Namespace \ No newline at end of file From 416da77c9e720d6aa4c786fd564ccd8c5df3fea1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 16:07:42 -0800 Subject: [PATCH 673/778] Updated tests to use .NET 5 Currently, none of the core OnTopic libraries rely on .NET 5 specific capabilities and, therefore, none of them multi-target .NET 5. That said, we _expect_ most implementors to be using .NET 5 and, therefore, it makes sense that the test hosts will be running in .NET 5 themselves. That should also provide them with some minor performance benefits. For reasons that aren't entirely clear, migrating the `OnTopic.Tests` project to .NET 5 required introducing its own copy of `IsExternalInit`. This is surprising since a) this is available in `OnTopic`, which makes its internals visible to `OnTopic.Tests`, and b) this class is available in .NET 5. Regardless, an easy fix and one we don't expect to affect most scenarios, since internals aren't usually made available outside of unit tests. --- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 3 +-- OnTopic.Tests/Internal/IsExternalInit.cs | 19 +++++++++++++++++++ OnTopic.Tests/OnTopic.Tests.csproj | 3 +-- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 OnTopic.Tests/Internal/IsExternalInit.cs diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index c1b60105..b83e17f5 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -1,8 +1,7 @@  - netcoreapp3.1 - 9.0 + net5.0 false diff --git a/OnTopic.Tests/Internal/IsExternalInit.cs b/OnTopic.Tests/Internal/IsExternalInit.cs new file mode 100644 index 00000000..2bd828ad --- /dev/null +++ b/OnTopic.Tests/Internal/IsExternalInit.cs @@ -0,0 +1,19 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ + +namespace System.Runtime.CompilerServices { + + /*============================================================================================================================ + | CLASS: IS EXTERNAL INIT + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The class is made available as part of the .NET 5.0 CLR in order to enable init accessors. + /// As this is not available in .NET Standard, however, we must maintain this separate copy until we migrate to .NET 5.0. + /// + internal static class IsExternalInit { + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index a580804e..7836fbed 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -1,8 +1,7 @@  - netcoreapp3.1 - 9.0 + net5.0 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 enable From fc7d79510789e1e51a54ede491f297109c9aec70 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 16:08:37 -0800 Subject: [PATCH 674/778] Updated `Host` to use .NET 5 Currently, none of the core OnTopic libraries rely on .NET 5 specific capabilities and, therefore, none of them multi-target .NET 5. That said, we _expect_ most implementors to be using .NET 5 and, therefore, it makes sense that the host example will be running in .NET 5 itself. That should also provide it with some performance benefits. --- .../OnTopic.AspNetCore.Mvc.Host.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj index c25426af..097a454b 100644 --- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj +++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj @@ -1,8 +1,7 @@  - netcoreapp3.1 - 9.0 + net5.0 62eb85bf-f802-4afd-8bec-3d344e1cfc79 false @@ -18,4 +17,4 @@ - + \ No newline at end of file From b9accc0ee27f9e44cab1159e7c6af9bae7c38f5b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 16:10:56 -0800 Subject: [PATCH 675/778] Consolidated project references within `OnTopic.AspNetCore.Mvc.Tests` The `OnTopic.AspNetCore.Mvc.Tests` project relies upon the ``OnTopic.AspNetCore.Mvc.Host` project, which in turn relies upon the `OnTopic.All` project. As such, there's no need or benefit to explicitly referencing each of the individual projects. --- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index b83e17f5..4b09ab60 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -17,11 +17,7 @@ - - - - - + \ No newline at end of file From c87182566d86ec6a41c086a255212372450b829b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 16:26:01 -0800 Subject: [PATCH 676/778] Sort and remove usings A small bit of housekeeping --- OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs | 1 - OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs | 1 - .../Components/PageLevelNavigationViewComponentBase{T}.cs | 1 - OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 1 - OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs | 1 - OnTopic.Tests/ITypeLookupServiceTest.cs | 1 - OnTopic.Tests/KeyedTopicCollectionTest.cs | 1 - OnTopic.ViewModels/SectionTopicViewModel.cs | 1 - OnTopic.ViewModels/VideoTopicViewModel.cs | 1 - 9 files changed, 9 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index db298799..3902420b 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs index 3be2c115..99e0dc69 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Routing; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index ca3f2fba..977000ec 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Models; diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index a32911a7..9f1d0732 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Net; using Microsoft.Data.SqlClient; -using OnTopic.Collections; using OnTopic.Collections.Specialized; using OnTopic.Internal.Diagnostics; using OnTopic.Querying; diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs index 91419232..26a770c5 100644 --- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.Mapping.Annotations; using OnTopic.ViewModels.BindingModels; namespace OnTopic.Tests.BindingModels { diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs index d21bc291..3b816b9d 100644 --- a/OnTopic.Tests/ITypeLookupServiceTest.cs +++ b/OnTopic.Tests/ITypeLookupServiceTest.cs @@ -6,7 +6,6 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using OnTopic.Lookup; -using OnTopic.Metadata; using OnTopic.Tests.TestDoubles; using OnTopic.Tests.ViewModels; using OnTopic.ViewModels; diff --git a/OnTopic.Tests/KeyedTopicCollectionTest.cs b/OnTopic.Tests/KeyedTopicCollectionTest.cs index 6342c503..3dd8db39 100644 --- a/OnTopic.Tests/KeyedTopicCollectionTest.cs +++ b/OnTopic.Tests/KeyedTopicCollectionTest.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OnTopic.Attributes; using OnTopic.Collections; namespace OnTopic.Tests { diff --git a/OnTopic.ViewModels/SectionTopicViewModel.cs b/OnTopic.ViewModels/SectionTopicViewModel.cs index 7f39c501..5aca691d 100644 --- a/OnTopic.ViewModels/SectionTopicViewModel.cs +++ b/OnTopic.ViewModels/SectionTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ - using System; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/VideoTopicViewModel.cs b/OnTopic.ViewModels/VideoTopicViewModel.cs index 842a946f..f34120b4 100644 --- a/OnTopic.ViewModels/VideoTopicViewModel.cs +++ b/OnTopic.ViewModels/VideoTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ - using System; namespace OnTopic.ViewModels { From ce2067bf4b78863a0e74634d1081bc7bd493061e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 17:00:03 -0800 Subject: [PATCH 677/778] Configure `release` branches to auto-increment --- GitVersion.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GitVersion.yml b/GitVersion.yml index b84f148d..952eda08 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -3,5 +3,7 @@ mode: ContinuousDeployment branches: master: mode: ContinuousDelivery + release: + mode: ContinuousDelivery ignore: sha: [] \ No newline at end of file From a827e8d3485d1aa6b54521ca9e94b8d7e6e1eb1c Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 18:14:55 -0800 Subject: [PATCH 678/778] Reverted `ModelType` property to be `virtual` While the recent update to avoid read-only `virtual` properties (532c49d) generally makes sense, and should be maintained for most types, it doesn't make sense for `ModelType` as some implementations of this actually need to be dynamic, as they conditionally rely on the _current_ value from `Attributes`. As the `Attributes` aren't populated until after the constructor has initialized the object, that results in the `ModelType` never being updated based on these values. Oops! --- OnTopic/Metadata/AttributeDescriptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic/Metadata/AttributeDescriptor.cs b/OnTopic/Metadata/AttributeDescriptor.cs index 1678ccf7..8dd957c5 100644 --- a/OnTopic/Metadata/AttributeDescriptor.cs +++ b/OnTopic/Metadata/AttributeDescriptor.cs @@ -90,7 +90,7 @@ public AttributeDescriptor( /// reduces these down into a single type based on how they're exposed in the Topic Library, not based on how they're /// exposed in the editor. /// - public ModelType ModelType { get; protected init; } = ModelType.ScalarValue; + public virtual ModelType ModelType { get; protected init; } = ModelType.ScalarValue; /*========================================================================================================================== | PROPERTY: EDITOR TYPE From 3665fcc3904dee0f41bc7d62e304a1265d9bf7e9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Wed, 17 Feb 2021 19:07:54 -0800 Subject: [PATCH 679/778] Revert `release` branches to auto-increment This doesn't work as expected. Reverting to the original configuration. --- GitVersion.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index 952eda08..b84f148d 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -3,7 +3,5 @@ mode: ContinuousDeployment branches: master: mode: ContinuousDelivery - release: - mode: ContinuousDelivery ignore: sha: [] \ No newline at end of file From 3b0ec7e9615d23b622f20294011b1eb8d49dbe02 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:01:10 -0800 Subject: [PATCH 680/778] Remove configuration conditions from `csproj` files --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 9 --------- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 10 ---------- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 9 --------- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 9 --------- OnTopic/OnTopic.csproj | 5 ----- 5 files changed, 42 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 27a468b7..9ffea357 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -28,15 +28,6 @@ true - - full - false - latest - - - pdbonly - - diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 65f0f8f4..abfb4602 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -28,16 +28,6 @@ true - - full - false - latest - - - pdbonly - false - - all diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 8e4ddf69..68c7eb45 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -26,15 +26,6 @@ true - - full - false - latest - - - pdbonly - - all diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index aa965858..67da4ccf 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -28,15 +28,6 @@ true - - full - false - latest - - - pdbonly - - all diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 453389d9..2f735968 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -31,11 +31,6 @@ full bin\$(Configuration)\OnTopic.XML - false - - - pdbonly - false From bf23fe8e9147ee7b5eaa9923369afc110d67eefa Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:02:57 -0800 Subject: [PATCH 681/778] Remove exclusions for `README.md` files It's not clear why these are in place, but as they aren't implemented for _all_ `README.md` files, it doesn't seem they're still needed. This might have been due to an early incompatibility with Visual Studio attempting to compile them as code? --- OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj | 4 ---- OnTopic.Data.Caching/OnTopic.Data.Caching.csproj | 4 ---- OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 4 ---- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 5 +---- OnTopic/OnTopic.csproj | 5 ----- 5 files changed, 1 insertion(+), 21 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index 9ffea357..edad7bfe 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -44,8 +44,4 @@ - - - - \ No newline at end of file diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index abfb4602..74c2d5b8 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -43,8 +43,4 @@ - - - - \ No newline at end of file diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index 68c7eb45..d6d3d645 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -42,8 +42,4 @@ - - - - \ No newline at end of file diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index 67da4ccf..e6421020 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -38,12 +38,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + \ No newline at end of file diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 2f735968..556c5c3c 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -48,9 +48,4 @@ - - - - - \ No newline at end of file From b851900545670f5cea0d544ad639280d903dd171 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:14:34 -0800 Subject: [PATCH 682/778] Centralized common properties into `Directory.Build.props` --- Directory.Build.props | 19 ++++++++++++++ OnTopic.All/OnTopic.All.csproj | 4 --- .../OnTopic.AspNetCore.Mvc.csproj | 17 +------------ .../OnTopic.Data.Caching.csproj | 17 +------------ OnTopic.Data.Sql/OnTopic.Data.Sql.csproj | 15 +---------- .../OnTopic.TestDoubles.csproj | 13 ---------- OnTopic.Tests/OnTopic.Tests.csproj | 25 ------------------- OnTopic.ViewModels/OnTopic.ViewModels.csproj | 17 +------------ OnTopic.sln | 3 ++- OnTopic/OnTopic.csproj | 17 +------------ 10 files changed, 26 insertions(+), 121 deletions(-) create mode 100644 Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..08bc4689 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + + 9.0 + enable + latest + AllEnabledByDefault + + + + Ignia + OnTopic + ©2021 Ignia, LLC + Ignia + https://github.com/Ignia/Topics-Library + true + + + \ No newline at end of file diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index cb56de22..832872f4 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -6,15 +6,11 @@ OnTopic Library Metapackage - Ignia - OnTopic Includes all core packages associated with the OnTopic Library, excluding the OnTopic Editor. Reference this package as a shorthand for establishing a reference to each of the individual packages. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj index edad7bfe..7c50e7df 100644 --- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj +++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj @@ -2,30 +2,15 @@ {B7F136A1-C86D-4A74-AC4F-3693CD1358A4} - OnTopic.AspNetCore.Mvc netcoreapp3.1 - 9.0 - True - False - enable - latest - AllEnabledByDefault + OnTopic.AspNetCore.Mvc OnTopic ASP.NET Core Library - Ignia - OnTopic Provides presentation-layer support for the ASP.NET Core Framework. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET CMS Presentation Web MVC ASP.NET Core Controller - true diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj index 74c2d5b8..97987cb2 100644 --- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj +++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj @@ -2,30 +2,15 @@ {206B7F91-CA25-4E9D-9576-60D2E54A2C0A} - OnTopic.Data.Caching netstandard2.1 - 9.0 - True - False - enable - latest - AllEnabledByDefault + OnTopic.Data.Caching OnTopic Cached Repository - Ignia - OnTopic Provides a caching decorator for ITopicRepository implementations. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET CMS Caching Data Repository - true diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj index d6d3d645..08ac2a72 100644 --- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj +++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj @@ -2,28 +2,15 @@ {1DE1F923-C7C2-435B-B49A-975ACBCB5FF0} - OnTopic.Data.Sql netstandard2.1 - 9.0 - enable - latest - AllEnabledByDefault + OnTopic.Data.Sql OnTopic SQL Server Repository - Ignia - OnTopic Provides Microsoft SQL Server support for persisting the OnTopic graph to a database. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET CMS SQL Data Repository - true diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj index 220bef6f..8b36dfd2 100644 --- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj +++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj @@ -2,26 +2,13 @@ netstandard2.1 - 9.0 - enable - latest - AllEnabledByDefault OnTopic Test Doubles - Ignia - OnTopic Test doubles, such as dummies and stubs, useful in setting up unit and integration tests for OnTopic. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET AspDotNet Unit-Tests CMS Test-Doubles - true diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 7836fbed..22e34c57 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -4,27 +4,6 @@ net5.0 false CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059 - enable - latest - AllEnabledByDefault - - - - Ignia OnTopic Unit Tests - Ignia - Ignia OnTopic Library - Provides unit tests for the OnTopic library. - ©2021 Ignia, LLC - bin\$(Configuration)\ - - - - full - bin\$(Configuration)\OnTopic.Tests.XML - latest - - - pdbonly @@ -45,8 +24,4 @@ - - - - \ No newline at end of file diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj index e6421020..3ad34af9 100644 --- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj +++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj @@ -2,30 +2,15 @@ {E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7} - OnTopic.ViewModels netstandard2.1 - 9.0 - True - False - enable - latest - AllEnabledByDefault + OnTopic.ViewModels OnTopic View Models - Ignia - OnTopic Provides view models that map to the factory default content type schemas. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET CMS Presentation View Models POCO - true diff --git a/OnTopic.sln b/OnTopic.sln index 7dfc4d9b..076935f4 100644 --- a/OnTopic.sln +++ b/OnTopic.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore + Directory.Build.props = Directory.Build.props GitVersion.yml = GitVersion.yml README.md = README.md EndProjectSection @@ -32,7 +33,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.TestDoubles", "OnTo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnTopic.Data.Sql.Database.Tests", "OnTopic.Data.Sql.Database.Tests\OnTopic.Data.Sql.Database.Tests.csproj", "{D7FE876D-A75F-4493-8283-B316271FD5AE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OnTopic.All", "OnTopic.All\OnTopic.All.csproj", "{5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.All", "OnTopic.All\OnTopic.All.csproj", "{5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 556c5c3c..9bf9d43d 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -2,30 +2,15 @@ {B8D5B290-4451-4C3B-AE9E-0FF075958A74} - OnTopic netstandard2.1 - 9.0 - True - False - enable - latest - AllEnabledByDefault + OnTopic OnTopic Library - Ignia - OnTopic Libraries for supporting Ignia Topics, a content management system (CMS) based on structured, hierarchical data. - ©2021 Ignia, LLC bin\$(Configuration)\ - Ignia - - - - https://github.com/Ignia/Topics-Library C# .NET CMS Domain - true From 7033842a3c1a655a5a6db31e51a1ae9043bb6221 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:19:46 -0800 Subject: [PATCH 683/778] Resolve assigning `null` to non-nullable fields This resolves CS8625. --- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 8 ++++---- OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs | 2 +- .../ValidateTopicAttributeTest.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 60b9228f..942e23de 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -31,15 +31,15 @@ public class SampleActivator : IControllerActivator, IViewComponentActivator { /*========================================================================================================================== | PRIVATE INSTANCES \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly ITypeLookupService _typeLookupService = null; - private readonly ITopicMappingService _topicMappingService = null; - private readonly ITopicRepository _topicRepository = null; + private readonly ITypeLookupService _typeLookupService; + private readonly ITopicMappingService _topicMappingService; + private readonly ITopicRepository _topicRepository; private DateTime _cacheLastUpdated = DateTime.UtcNow; /*========================================================================================================================== | HIERARCHICAL TOPIC MAPPING SERVICE \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly IHierarchicalTopicMappingService _hierarchicalMappingService = null; + private readonly IHierarchicalTopicMappingService _hierarchicalMappingService; /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index 3902420b..b415046f 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -41,7 +41,7 @@ public class TopicViewComponentTest { /*========================================================================================================================== | HIERARCHICAL TOPIC MAPPING SERVICE \-------------------------------------------------------------------------------------------------------------------------*/ - private readonly IHierarchicalTopicMappingService _hierarchicalMappingService = null; + private readonly IHierarchicalTopicMappingService _hierarchicalMappingService; /*========================================================================================================================== | CONSTRUCTOR diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs index 99e0dc69..c2b07620 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs @@ -76,7 +76,7 @@ public static ControllerContext GetControllerContext() => /// /// Generates a barebones for testing a controller. /// - public static TopicController GetTopicController(Topic topic) => + public static TopicController GetTopicController(Topic? topic) => new( new DummyTopicRepository(), new DummyTopicMappingService() From cec7b37e8e2b8e658cfa1dcea51469b809d77d0f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:24:11 -0800 Subject: [PATCH 684/778] Resolve possible null references for parameters This mostly involved changing parameters to accept expected nulls. This resolves CS8604. --- .../SampleActivator.cs | 2 +- .../TopicControllerTest.cs | 2 +- .../TopicViewComponentTest.cs | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 942e23de..b506f880 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -100,7 +100,7 @@ public object Create(ControllerContext context) { \-----------------------------------------------------------------------------------------------------------------------*/ if (DateTime.UtcNow > _cacheLastUpdated.AddMinutes(1)) { var currentUpdate = DateTime.UtcNow; - _topicRepository.Refresh(_topicRepository.Load(), _cacheLastUpdated); + _topicRepository.Refresh(_topicRepository.Load()!, _cacheLastUpdated); _cacheLastUpdated = currentUpdate; } diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index 2fbf4741..fa7fa640 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -96,7 +96,7 @@ public async Task TopicController_IndexAsync_ReturnsTopicViewResult() { controller.Dispose(); Assert.IsNotNull(model); - Assert.AreEqual("Web_0_1_1", model.Title); + Assert.AreEqual("Web_0_1_1", model?.Title); } diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index b415046f..01a41ab5 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -110,13 +110,13 @@ public async Task Menu_Invoke_ReturnsNavigationViewModel() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model.CurrentKey); - Assert.AreEqual("Root:Web", model.NavigationRoot.UniqueKey); - Assert.AreEqual(3, model.NavigationRoot.Children.Count); - Assert.IsTrue(model.NavigationRoot.IsSelected(_topic.GetUniqueKey())); + Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentKey); + Assert.AreEqual("Root:Web", model?.NavigationRoot?.UniqueKey); + Assert.AreEqual(3, model?.NavigationRoot?.Children.Count); + Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetUniqueKey())?? false); } @@ -135,13 +135,13 @@ public async Task PageLevelNavigation_Invoke_ReturnsNavigationViewModel() { var result = await viewComponent.InvokeAsync().ConfigureAwait(false); var concreteResult = result as ViewViewComponentResult; - var model = concreteResult.ViewData.Model as NavigationViewModel; + var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model.CurrentKey); - Assert.AreEqual("Root:Web:Web_3", model.NavigationRoot.UniqueKey); - Assert.AreEqual(2, model.NavigationRoot.Children.Count); - Assert.IsTrue(model.NavigationRoot.IsSelected(_topic.GetUniqueKey())); + Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentKey); + Assert.AreEqual("Root:Web:Web_3", model?.NavigationRoot?.UniqueKey); + Assert.AreEqual(2, model?.NavigationRoot?.Children.Count); + Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetUniqueKey())?? false); } From bda031b84434978119decbf612a4c2454a0b5a38 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:26:34 -0800 Subject: [PATCH 685/778] Suppress warning regarding underscores While underscores should normally not be used in public member identifiers, popular unit test conventions contradict this. As such, we suppress the otherwise-appropriate CA1707 warning in the unit test projects. --- OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index 4b09ab60..4de28039 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -3,6 +3,7 @@ net5.0 false + CA1707 From b01b8a402847a2393d8655a5714124e4b5c0dcbc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:33:12 -0800 Subject: [PATCH 686/778] Resolved dereferencing of possibly null references This is typically fixed by using the ternary conditional operator (`.?`), and then providing either a fallback, or making sure that the target variable or parameter type accepts nulls. It can also be resolved, where appropriate, by using the null-forgiving operator (`!`). This resolves CS8602. --- .../TopicControllerTest.cs | 42 +++++++++---------- .../TopicRepositoryExtensionsTest.cs | 4 +- .../ValidateTopicAttributeTest.cs | 10 ++--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index fa7fa640..c4c5f963 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -91,7 +91,7 @@ public async Task TopicController_IndexAsync_ReturnsTopicViewResult() { }; var result = await controller.IndexAsync(_topic.GetWebPath()).ConfigureAwait(false) as TopicViewResult; - var model = result.Model as PageTopicViewModel; + var model = result?.Model as PageTopicViewModel; controller.Dispose(); @@ -115,8 +115,8 @@ public void RedirectController_TopicRedirect_ReturnsRedirectResult() { controller.Dispose(); Assert.IsNotNull(result); - Assert.IsTrue(result.Permanent); - Assert.AreEqual("/Web/Web_1/Web_1_1/Web_1_1_1/", result.Url); + Assert.IsTrue(result?.Permanent?? false); + Assert.AreEqual("/Web/Web_1/Web_1_1/Web_1_1_1/", result?.Url); } @@ -138,13 +138,13 @@ public void SitemapController_Index_ReturnsSitemapXml() { ControllerContext = new(actionContext) }; var result = controller.Index() as ContentResult; - var model = result.Content as string; + var model = result?.Content as string; controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model.StartsWith("")); - Assert.IsTrue(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); + Assert.IsTrue(model!.StartsWith("")); + Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); } @@ -177,20 +177,20 @@ public void SitemapController_Index_ExcludesContentTypes() { ControllerContext = new(actionContext) }; var result = controller.Index(false, true) as ContentResult; - var model = result.Content as string; + var model = result?.Content as string; controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model.Contains("")); - Assert.IsFalse(model.Contains("")); - Assert.IsFalse(model.Contains("")); - Assert.IsFalse(model.Contains("")); - Assert.IsTrue(model.Contains("/Web/Web_0/Web_0_0/Web_0_0_1/")); - Assert.IsTrue(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/")); - Assert.IsFalse(model.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/")); - Assert.IsFalse(model.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); - Assert.IsFalse(model.Contains("/Web/Web_0/Web_0_0/")); + Assert.IsTrue(model!.Contains("")); + Assert.IsFalse(model!.Contains("")); + Assert.IsFalse(model!.Contains("")); + Assert.IsFalse(model!.Contains("")); + Assert.IsTrue(model!.Contains("/Web/Web_0/Web_0_0/Web_0_0_1/")); + Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/")); + Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/")); + Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); + Assert.IsFalse(model!.Contains("/Web/Web_0/Web_0_0/")); } @@ -220,15 +220,15 @@ public void SitemapController_Index_ExcludesAttributes() { ControllerContext = new(actionContext) }; var result = controller.Index(false, true) as ContentResult; - var model = result.Content as string; + var model = result?.Content as string; controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model.Contains("")); - Assert.IsTrue(model.Contains("")); - Assert.IsFalse(model.Contains("")); - Assert.IsFalse(model.Contains("")); + Assert.IsTrue(model!.Contains("")); + Assert.IsTrue(model!.Contains("")); + Assert.IsFalse(model!.Contains("")); + Assert.IsFalse(model!.Contains("")); } diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs index d0953bdc..a0cb9fa9 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs @@ -61,7 +61,7 @@ public void Load_ByRoute_ReturnsTopic() { Assert.IsNotNull(currentTopic); Assert.ReferenceEquals(topic, currentTopic); - Assert.AreEqual("Web_0_1_1", currentTopic.Key); + Assert.AreEqual("Web_0_1_1", currentTopic?.Key); } @@ -83,7 +83,7 @@ public void Load_ByRoute_ReturnsRootTopic() { Assert.IsNotNull(currentTopic); Assert.ReferenceEquals(topic, currentTopic); - Assert.AreEqual("Root", currentTopic.Key); + Assert.AreEqual("Root", currentTopic?.Key); } diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs index c2b07620..48ab7d67 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs @@ -127,7 +127,7 @@ public void NullTopic_ReturnsNotFound() { controller.Dispose(); - Assert.AreEqual(typeof(NotFoundObjectResult), context.Result.GetType()); + Assert.AreEqual(typeof(NotFoundObjectResult), context.Result?.GetType()); } @@ -151,7 +151,7 @@ public void DisabledTopic_ReturnsNotFound() { controller.Dispose(); - Assert.AreEqual(typeof(UnauthorizedResult), context.Result.GetType()); + Assert.AreEqual(typeof(UnauthorizedResult), context.Result?.GetType()); } @@ -176,7 +176,7 @@ public void TopicWithUrl_ReturnsRedirect() { controller.Dispose(); - Assert.AreEqual(typeof(RedirectResult), context.Result.GetType()); + Assert.AreEqual(typeof(RedirectResult), context.Result?.GetType()); } @@ -202,7 +202,7 @@ public void NestedTopic_Returns403() { var result = context.Result as StatusCodeResult; Assert.IsNotNull(result); - Assert.AreEqual(403, result.StatusCode); + Assert.AreEqual(403, result?.StatusCode); } @@ -227,7 +227,7 @@ public void PageGroupTopic_ReturnsRedirect() { controller.Dispose(); - Assert.AreEqual(typeof(RedirectResult), context.Result.GetType()); + Assert.AreEqual(typeof(RedirectResult), context.Result?.GetType()); } From c1ec4fdf88599d8baed21598b6000f7a5def7c24 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:34:33 -0800 Subject: [PATCH 687/778] Validate non-nullable reference types, where appropriate Alternatively, make sure they're marked as nullable. This resolves CA1062. --- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index b506f880..1511ddc0 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -11,6 +11,7 @@ using OnTopic.AspNetCore.Mvc.Host.Components; using OnTopic.Data.Caching; using OnTopic.Data.Sql; +using OnTopic.Internal.Diagnostics; using OnTopic.Lookup; using OnTopic.Mapping; using OnTopic.Mapping.Hierarchical; @@ -90,6 +91,11 @@ public SampleActivator(string connectionString) { /// A concrete instance of an . public object Create(ControllerContext context) { + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(context, nameof(context)); + /*------------------------------------------------------------------------------------------------------------------------ | Determine controller type \-----------------------------------------------------------------------------------------------------------------------*/ @@ -125,6 +131,11 @@ public object Create(ControllerContext context) { /// A concrete instance of an . public object Create(ViewComponentContext context) { + /*------------------------------------------------------------------------------------------------------------------------ + | Validate parameters + \-----------------------------------------------------------------------------------------------------------------------*/ + Contract.Requires(context, nameof(context)); + /*------------------------------------------------------------------------------------------------------------------------ | Determine view component type \-----------------------------------------------------------------------------------------------------------------------*/ From 94119ebe32bb5714baee59e87122d7ceb31e8423 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:38:08 -0800 Subject: [PATCH 688/778] Mark assemblies with CLSCompliant As both of these assemblies rely on the ASP.NET Core SDK, they cannot be marked as CLSCompliant. This should be communicated to callers. This resolves CA1014. --- OnTopic.All/Properties/AssemblyInfo.cs | 15 +++++++++++++++ .../Properties/AssemblyInfo.cs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 OnTopic.All/Properties/AssemblyInfo.cs create mode 100644 OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs diff --git a/OnTopic.All/Properties/AssemblyInfo.cs b/OnTopic.All/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bcdd7ce1 --- /dev/null +++ b/OnTopic.All/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.InteropServices; + +/*============================================================================================================================== +| DEFINE ASSEMBLY ATTRIBUTES +>=============================================================================================================================== +| Declare and define attributes used in the compiling of the finished assembly. +\-----------------------------------------------------------------------------------------------------------------------------*/ +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..bcdd7ce1 --- /dev/null +++ b/OnTopic.AspNetCore.Mvc.Host/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; +using System.Runtime.InteropServices; + +/*============================================================================================================================== +| DEFINE ASSEMBLY ATTRIBUTES +>=============================================================================================================================== +| Declare and define attributes used in the compiling of the finished assembly. +\-----------------------------------------------------------------------------------------------------------------------------*/ +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] \ No newline at end of file From 30d1d171596d8498fc2ca7920feea4fc34cfb951 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:44:15 -0800 Subject: [PATCH 689/778] Ensure use of `StringComparison` to avoid variance by local Ensure comparisons such as `Contains()` and `StartsWith()` correctly pass the `StringComparison` enum argument to provide more consistent expectations of how to handle potential variability by locale. This resolves CA1307. --- .../TopicControllerTest.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index c4c5f963..e88e598f 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -143,8 +143,8 @@ public void SitemapController_Index_ReturnsSitemapXml() { controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model!.StartsWith("")); - Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); + Assert.IsTrue(model!.StartsWith("", StringComparison.Ordinal)); + Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/", StringComparison.Ordinal)); } @@ -182,15 +182,15 @@ public void SitemapController_Index_ExcludesContentTypes() { controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model!.Contains("")); - Assert.IsFalse(model!.Contains("")); - Assert.IsFalse(model!.Contains("")); - Assert.IsFalse(model!.Contains("")); - Assert.IsTrue(model!.Contains("/Web/Web_0/Web_0_0/Web_0_0_1/")); - Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/")); - Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/")); - Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/")); - Assert.IsFalse(model!.Contains("/Web/Web_0/Web_0_0/")); + Assert.IsTrue(model!.Contains("", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("", StringComparison.Ordinal)); + Assert.IsTrue(model!.Contains("/Web/Web_0/Web_0_0/Web_0_0_1/", StringComparison.Ordinal)); + Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_0/", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_0/Web_1_0_0/", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("/Web/Web_0/Web_0_0/", StringComparison.Ordinal)); } @@ -225,10 +225,10 @@ public void SitemapController_Index_ExcludesAttributes() { controller.Dispose(); Assert.IsNotNull(model); - Assert.IsTrue(model!.Contains("")); - Assert.IsTrue(model!.Contains("")); - Assert.IsFalse(model!.Contains("")); - Assert.IsFalse(model!.Contains("")); + Assert.IsTrue(model!.Contains("", StringComparison.Ordinal)); + Assert.IsTrue(model!.Contains("", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("", StringComparison.Ordinal)); + Assert.IsFalse(model!.Contains("", StringComparison.Ordinal)); } From 3f10197f67ee77798314180f1849b6c6647209d4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 12:47:52 -0800 Subject: [PATCH 690/778] Address nullability issues in `cshtml` files These aren't picked up by code analysis at design time, but were picked up during compilation. --- .../Views/Shared/Components/Menu/Default.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml index 1bd3a852..69d32b74 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml @@ -3,7 +3,7 @@

Menu

public ReadOnlyTopicCollection AsReadOnly() => new(this); + /*========================================================================================================================== + | METHOD: GET TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + [Obsolete("The GetTopic() method is not implemented on TopicCollection. Use KeyedTopicCollection instead.", true)] + public Topic? GetValue(string key) => throw new NotImplementedException(); + + /*========================================================================================================================== + | INDEXER + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + [Obsolete("Indexing by key is not implemented on the TopicCollection. Use the KeyedTopicCollection instead.",true)] + public Topic this[string key] => throw new ArgumentOutOfRangeException(nameof(key)); + } //Class } //Namespace \ No newline at end of file From 5c260cce52563ee1f298ed1262ef5bb50172e2eb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:05:10 -0800 Subject: [PATCH 713/778] Reintroduced `DeleteEventArgs` class as `[Obsolete]` This was renamed to `TopicDeleteEventArgs`. This provides instructions to callers of the original class name. --- .../Obsolete/Repositories/DeleteEventArgs.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 OnTopic/Obsolete/Repositories/DeleteEventArgs.cs diff --git a/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs b/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs new file mode 100644 index 00000000..bdad9c77 --- /dev/null +++ b/OnTopic/Obsolete/Repositories/DeleteEventArgs.cs @@ -0,0 +1,39 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: DELETE EVENT ARGS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The DeleteEventArgs class defines an event argument type specific to deletion events + /// + [Obsolete("The DeleteEventArgs has been renamed to TopicEventArgs", true)] + public class DeleteEventArgs : EventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR: TAXONOMY DELETE EVENT ARGS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class. + /// + /// The topic. + public DeleteEventArgs(Topic topic) : base() { + Topic = topic; + } + + /*========================================================================================================================== + | PROPERTY: TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Getter that returns the Topic object associated with the event + /// + public Topic Topic { get; set; } + + } //Class +} //Namespace \ No newline at end of file From 7971f761c1499589276956f89608ad2c371cd605 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:05:28 -0800 Subject: [PATCH 714/778] Reintroduced `MoveEventArgs` class as `[Obsolete]` This was renamed to `MoveDeleteEventArgs`. This provides instructions to callers of the original class name. --- .../Obsolete/Repositories/MoveEventArgs.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 OnTopic/Obsolete/Repositories/MoveEventArgs.cs diff --git a/OnTopic/Obsolete/Repositories/MoveEventArgs.cs b/OnTopic/Obsolete/Repositories/MoveEventArgs.cs new file mode 100644 index 00000000..a833e936 --- /dev/null +++ b/OnTopic/Obsolete/Repositories/MoveEventArgs.cs @@ -0,0 +1,68 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia +| Project Topics Library +\=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: MOVE EVENT ARGS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The MoveEventArgs object defines an event argument type specific to move events. + /// + /// + /// Allows tracking of the source and destination topics. + /// + [Obsolete("The MoveEventArgs have been renamed to TopicMoveEventArgs.", true)] + public class MoveEventArgs : EventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR: MOVE EVENT ARGS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class and sets the and + /// properties based on the specified objects. + /// + /// The topic object associated with the move event. + /// The parent topic object targeted by the move event. + /// + /// topic is not null + /// + /// + /// target is not null + /// + /// + /// topic != target + /// + public MoveEventArgs(Topic topic, Topic target) { + Contract.Requires(topic, "topic"); + Contract.Requires(target, "target"); + Contract.Requires(topic != target, "The topic cannot be its own parent."); + Topic = topic; + Target = target; + } + + /*========================================================================================================================== + | PROPERTY: EVENT TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the Topic object associated with the event. + /// + public Topic Topic { get; set; } + + /*========================================================================================================================== + | PROPERTY: TARGET + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the new parent that the topic will be moved to. + /// + public Topic Target { get; set; } + + } //Class +} //Namespace \ No newline at end of file From a50da2a3c5b3974554f120528f6049c8652599f6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:06:44 -0800 Subject: [PATCH 715/778] Reintroduced `RenameEventArgs` class as `[Obsolete]` This was renamed to `TopicRenameEventArgs`. This provides instructions to callers of the original class name. --- .../Obsolete/Repositories/RenameEventArgs.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 OnTopic/Obsolete/Repositories/RenameEventArgs.cs diff --git a/OnTopic/Obsolete/Repositories/RenameEventArgs.cs b/OnTopic/Obsolete/Repositories/RenameEventArgs.cs new file mode 100644 index 00000000..5c1d3f71 --- /dev/null +++ b/OnTopic/Obsolete/Repositories/RenameEventArgs.cs @@ -0,0 +1,43 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System; + +namespace OnTopic.Repositories { + + /*============================================================================================================================ + | CLASS: RENAME EVENT ARGS + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The RenameEventArgs object defines an event argument type specific to rename events. + /// + [Obsolete("The RenameEventArgs have been renamed to TopicEventArgs.", true)] + public class RenameEventArgs : EventArgs { + + /*========================================================================================================================== + | CONSTRUCTOR: TAXONOMY RENAME EVENT ARGS + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Initializes a new instance of the class and sets the property based + /// on the specified object. + /// + /// The topic object associated with the rename event. + public RenameEventArgs(Topic topic) { + Topic = topic; + } + + /*========================================================================================================================== + | PROPERTY: TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets or sets the Topic object associated with the event. + /// + /// + /// The topic. + /// + public Topic Topic { get; } + + } //Class +} //Namespace \ No newline at end of file From bad250bb47dc54cf512b954e6689db3e2b1f5e3a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:13:42 -0800 Subject: [PATCH 716/778] Reintroduced events as `[Obsolete]` The `DeleteEvent`, `MoveEvent`, and `RenameEvent` were renamed to, respectively, `TopicDeleted`, `TopicMoved, and `TopicRenamed`. This provides instructions to callers of the original class name. Unfortunately, an `[Obsolete[]` member of an interface still needs to be implemented on each implementation of that interface. As such, we won't want to persist these for long after the migration to OnTopic 5.0.0. That said, fortunately, we expect all implementations of `ITopicRepository` to implement `ObservableTopicRepository` as a base class and, therefore, can implement these in that one location in order to cover all derived types (e.g., `SqlTopicRepository`, `CachedTopicRepository`, &c.) --- OnTopic/Repositories/ITopicRepository.cs | 12 ++++++++++++ OnTopic/Repositories/ObservableTopicRepository.cs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index 465ed990..b837a884 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -56,6 +56,18 @@ public interface ITopicRepository { ///
event EventHandler TopicRenamed; + /// + [Obsolete("The DeleteEvent has been renamed to TopicDeleted")] + event EventHandler DeleteEvent; + + /// + [Obsolete("The MoveEvent has been renamed to TopicMoved")] + event EventHandler MoveEvent; + + /// + [Obsolete("The RenameEvent has been renamed to TopicRenamed")] + event EventHandler RenameEvent; + /*========================================================================================================================== | GET CONTENT TYPE DESCRIPTORS \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 7d59a114..a802c77b 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -65,6 +65,18 @@ public event EventHandler? TopicRenamed { remove => _topicRenamed -= value; } + /// + [Obsolete("The DeleteEvent has been renamed to TopicDeleted")] + public event EventHandler? DeleteEvent; + + /// + [Obsolete("The MoveEvent has been renamed to TopicMoved")] + public event EventHandler? MoveEvent; + + /// + [Obsolete("The RenameEvent has been renamed to TopicRenamed")] + public event EventHandler? RenameEvent; + /*========================================================================================================================== | ON TOPIC LOADED \-------------------------------------------------------------------------------------------------------------------------*/ From 78c872835859c94a8f1851ad2273b931235dcb2e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:20:51 -0800 Subject: [PATCH 717/778] Reintroduced `Load()` overload as `[Obsolete]` The `Load(uniqueKey, isRecursive)` overload was updated to include the `referenceTopic` parameter. By reintroducing the original overload, we provide instructions to callers of the original method signature. In practice, we don't expect this to be a common scenario, as it is generally expected that loads will be recursive. Unfortunately, an `[Obsolete[]` member of an interface still needs to be implemented on each implementation of that interface. As such, we won't want to persist these for long after the migration to OnTopic 5.0.0. That said, fortunately, we expect all implementations of `ITopicRepository` to implement `ObservableTopicRepository` as a base class and, therefore, can implement these in that one location in order to cover all derived types (e.g., `SqlTopicRepository`, `CachedTopicRepository`, &c.) --- OnTopic/Repositories/ITopicRepository.cs | 4 ++++ OnTopic/Repositories/ObservableTopicRepository.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/OnTopic/Repositories/ITopicRepository.cs b/OnTopic/Repositories/ITopicRepository.cs index b837a884..343a37c0 100644 --- a/OnTopic/Repositories/ITopicRepository.cs +++ b/OnTopic/Repositories/ITopicRepository.cs @@ -106,6 +106,10 @@ public interface ITopicRepository { /// A topic object. Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); + /// + [Obsolete("This overload has been removed in preference for Load(string, Topic, Boolean).")] + Topic? Load(string? uniqueKey, bool isRecursive); + /// /// Loads a specific version of a based on its and . diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index a802c77b..142db9a4 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -208,6 +208,10 @@ public event EventHandler? TopicRenamed { /// public abstract Topic? Load(string? uniqueKey = null, Topic? referenceTopic = null, bool isRecursive = true); + /// + [Obsolete("This overload has been removed in preference for Load(string, Topic, Boolean).")] + public Topic? Load(string? uniqueKey, bool isRecursive) => throw new NotImplementedException(); + /// public abstract Topic? Load(Topic topic, DateTime version); From c0b5d7db92126ef950977201c60ac6bdd5de2be2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:29:21 -0800 Subject: [PATCH 718/778] Reintroduced `Save()` overload as `[Obsolete]` The `Save(topic, isRecursive, isDraft)` signature was updated to remove the `isDraft` parameter. By reintroducing the original overload, we provide instructions to callers of the original method signature. In practice, we don't expect this to be a common scenario, as the `isDraft` functionality was never fully implemented, and is an artifact of OnTopic 2.x. Unfortunately, an `[Obsolete[]` member of an interface still needs to be implemented on each implementation of that interface. As such, we won't want to persist these for long after the migration to OnTopic 5.0.0. That said, fortunately, we expect all implementations of `ITopicRepository` to implement `ObservableTopicRepository` as a base class and, therefore, can implement these in that one location in order to cover all derived types (e.g., `SqlTopicRepository`, `CachedTopicRepository`, &c.) As this introduces ambiguities in the XML Doc references, those also needed to be updated to refer explicitly to the signature of the non-obsolete overload. That's probably a best practice anyway, so will be fine even after we remove the obsolete overload. --- OnTopic/Attributes/AttributeRecord.cs | 6 +++--- OnTopic/Obsolete/Attributes/AttributeValue.cs | 6 +++--- OnTopic/Repositories/ITopicRepository.cs | 4 ++++ OnTopic/Repositories/ObservableTopicRepository.cs | 4 ++++ OnTopic/Topic.cs | 14 +++++++------- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/OnTopic/Attributes/AttributeRecord.cs b/OnTopic/Attributes/AttributeRecord.cs index cc18f4a1..416398bf 100644 --- a/OnTopic/Attributes/AttributeRecord.cs +++ b/OnTopic/Attributes/AttributeRecord.cs @@ -93,9 +93,9 @@ public AttributeRecord( /// /// How an attribute is stored in the underlying repository doesn't impact how the attribute is treated as part of the /// object model. By tracking this, however, OnTopic is able to evaluate configuration mismatches during . This allows the to effective handle scenarios where - /// the configuration for an has changed prior to the last time a - /// was saved, and thus change the location where it is stored. + /// cref="ITopicRepository.Save(Topic, Boolean)"/>. This allows the to effective handle + /// scenarios where the configuration for an has changed prior to the last time a was saved, and thus change the location where it is stored. /// /// /// This is important because, otherwise, implementations rely primarily on /// How an attribute is stored in the underlying repository doesn't impact how the attribute is treated as part of the /// object model. By tracking this, however, OnTopic is able to evaluate configuration mismatches during . This allows the to effective handle scenarios where - /// the configuration for an has changed prior to the last time a - /// was saved, and thus change the location where it is stored. + /// cref="ITopicRepository.Save(Topic, Boolean)"/>. This allows the to effective handle + /// scenarios where the configuration for an has changed prior to the last time a was saved, and thus change the location where it is stored. /// /// /// This is important because, otherwise, implementations rely primarily on topic void Save(Topic topic, bool isRecursive = false); + /// + [Obsolete("The 'isDraft' argument of the Save() method has been removed.")] + int Save(Topic topic, bool isRecursive, bool isDraft); + /*========================================================================================================================== | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index 142db9a4..b2d6ae02 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -236,6 +236,10 @@ public event EventHandler? TopicRenamed { /// public abstract void Save(Topic topic, bool isRecursive = false); + /// + [Obsolete("The 'isDraft' argument of the Save() method has been removed.")] + public int Save(Topic topic, bool isRecursive, bool isDraft) => throw new NotImplementedException(); + /*========================================================================================================================== | METHOD: MOVE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Topic.cs b/OnTopic/Topic.cs index fc3adde9..11d5061a 100644 --- a/OnTopic/Topic.cs +++ b/OnTopic/Topic.cs @@ -684,13 +684,13 @@ public void MarkClean(string key, bool includeCollections) { /// The underlying value of the is stored as a topic reference with the of BaseTopic in . If the hasn't been /// saved, then the reference will be established, but the BaseTopic won't be persisted to the underlying - /// repository upon . That said, when is called, the will be reevaluated and, if it has - /// subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory topic graphs to - /// be constructed, while preventing invalid s from being persisted to the underlying data - /// storage. As a result, however, a referencing an that is unsaved will - /// need to be saved again once the has been saved, assuming it's otherwise outside the scope of - /// the original call. + /// repository upon . That said, when is called, the will be reevaluated + /// and, if it has subsequently been saved, and the BaseTopic will be updated accordingly. This allows in-memory + /// topic graphs to be constructed, while preventing invalid s from being persisted to the + /// underlying data storage. As a result, however, a referencing an that is + /// unsaved will need to be saved again once the has been saved, assuming it's otherwise outside + /// the scope of the original call. /// /// /// The that values should be inherited from, if not otherwise available. From e9de72f84cdafeeb610deb8eb8b26a4ceed608c4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:38:27 -0800 Subject: [PATCH 719/778] Reintroduced `FromList` method as `[Obsolete]` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was removed from `ReadOnlyKeyedTopicCollection` since the use case is better covered by the similar constructor overload, but should have been kept with an obsolete notice to provide instructions to callers. In addition, the `ReadOnlyKeyedTopicCollection` used to be named `ReadOnlyTopicCollection`, from which a `ReadOnlyTopicCollection` was derived. The `ReadOnlyTopicCollection` no longer derives from `ReadOnlyKeyedTopicCollection`. But since callers may confuse it with the previous class of the same name—which was based on a `KeyedCollection`—I've introduced it there as well, in order to offer instruction to implementors. --- .../Collections/ReadOnlyKeyedTopicCollection{T}.cs | 13 +++++++++++++ OnTopic/Collections/ReadOnlyTopicCollection.cs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs index df354ec8..136b5f78 100644 --- a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs +++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs @@ -35,6 +35,19 @@ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : T _innerCollection = innerCollection as KeyedTopicCollection?? new(innerCollection); } + /*========================================================================================================================== + | FACTORY METHOD: FROM LIST + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new based on an existing . + /// + /// + /// The will be converted to a . + /// + /// The underlying . + [Obsolete("This is effectively satisfied by the related overload, and has been removed.", true)] + public ReadOnlyTopicCollection FromList(IList innerCollection) => throw new NotImplementedException(); + /*========================================================================================================================== | METHOD: GET VALUE \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs index 31bcad1e..f9f008b1 100644 --- a/OnTopic/Collections/ReadOnlyTopicCollection.cs +++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs @@ -27,6 +27,19 @@ public class ReadOnlyTopicCollection : ReadOnlyCollection { public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection?? new List()) { } + /*========================================================================================================================== + | FACTORY METHOD: FROM LIST + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a new based on an existing . + /// + /// + /// The will be converted to a . + /// + /// The underlying . + [Obsolete("This is effectively satisfied by the related overload, and has been removed.", true)] + public ReadOnlyTopicCollection FromList(IList innerCollection) => throw new NotImplementedException(); + /*========================================================================================================================== | METHOD: GET TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ From 348a4d1572c7816f4bbd0386b09a5510f63595f4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 16:44:39 -0800 Subject: [PATCH 720/778] Reintroduced `DefaultType` property as `[Obsolete]` The `DefaultType` property was removed, in favor of the new `Lookup(params string[])` overload, which allows fallbacks to be defined as subsequent parameters. By reintroducing the original `DefaultType` property, and its associated constructor, we provide instructions to callers of the members. --- OnTopic/Lookup/StaticTypeLookupService.cs | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/OnTopic/Lookup/StaticTypeLookupService.cs b/OnTopic/Lookup/StaticTypeLookupService.cs index 3d93bc4a..729ac964 100644 --- a/OnTopic/Lookup/StaticTypeLookupService.cs +++ b/OnTopic/Lookup/StaticTypeLookupService.cs @@ -53,6 +53,33 @@ public StaticTypeLookupService( } + /// + /// Establishes a new instance of a . Optionally accepts a list of + /// instances and a default value. + /// + /// + /// Any instances submitted via should be unique by ; if they are not, they will be removed. + /// + /// The list of instances to expose as part of this service. + /// The default type to return if no match can be found. Defaults to object. + [Obsolete("The DefaultType property has been removed. Fallbacks types can now be added to Lookup() directly.", true)] + public StaticTypeLookupService( + IEnumerable? types, + Type? defaultType + ) { + throw new NotImplementedException(); + } + + /*========================================================================================================================== + | PROPERTY: DEFAULT TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The default type to return in case cannot find a match. + /// + [Obsolete("The DefaultType property has been removed. Fallbacks types can now be added to Lookup() directly.", true)] + public Type? DefaultType { get; } + /*========================================================================================================================== | METHOD: LOOKUP \-------------------------------------------------------------------------------------------------------------------------*/ From 384f7307f5864b74c19157efdb3deff2987301a9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 18:07:09 -0800 Subject: [PATCH 721/778] =?UTF-8?q?Throw=20exception=20on=20`[FilterByAttr?= =?UTF-8?q?ibute("ContentType",=20=E2=80=A6)]`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `ContentType` is no longer stored as an attribute, so models attempting to `[FilterByAttribute("ContentType", …)]` will never return results—or, perhaps worse, operate off of potentially stale results from prior to the migration, yielding inconsistent results. To help avoid this situation, this will throw a runtime exception when a user attempts to map a model with this setting. A code analyzer would be a more sophisticated approach, since this is fundamentally a design-time problem. This is a quick and easy alternative that should prevent this scenario from being introduced, while giving clear guidance to implementations that have already implemented this from previous versions. --- .../Annotations/FilterByAttributeAttribute.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs index 7cabab49..6479e58c 100644 --- a/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs +++ b/OnTopic/Mapping/Annotations/FilterByAttributeAttribute.cs @@ -4,6 +4,9 @@ | Project Topics Library \=============================================================================================================================*/ +using System; +using OnTopic.Internal.Diagnostics; + namespace OnTopic.Mapping.Annotations { /*============================================================================================================================ @@ -30,9 +33,22 @@ public sealed class FilterByAttributeAttribute : System.Attribute { /// The key of the attribute to filter by. /// The value of the attribute to filter by. public FilterByAttributeAttribute(string key, string value) { + + /*------------------------------------------------------------------------------------------------------------------------ + | Validate input + \-----------------------------------------------------------------------------------------------------------------------*/ TopicFactory.ValidateKey(key, false); + Contract.Requires( + !key.Equals("ContentType", StringComparison.OrdinalIgnoreCase), + "The ContentType is not stored as an attribute. To filter by ContentType, use [FilterByContentType] instead." + ); + + /*------------------------------------------------------------------------------------------------------------------------ + | Set properties + \-----------------------------------------------------------------------------------------------------------------------*/ Key = key; Value = value; + } /*========================================================================================================================== From 5b5dae00621e2d4d2a627a245311a312122101c1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Fri, 19 Feb 2021 18:08:48 -0800 Subject: [PATCH 722/778] Introduced unit test for validation of `[FilterByAttribute()]` This validates that the validation within the `[FilterByAttribute()]` constructor (384f730) works as expected by raising an `ArgumentException` if the `Key` is set to `ContentType`. In that case, the implementation should instead use `[FilterByContentType()]`. --- OnTopic.Tests/TopicMappingServiceTest.cs | 19 +++++++++++++ .../FilteredInvalidTopicViewModel.cs | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index 94b4c1de..c0ff7c2f 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -967,6 +967,25 @@ public async Task Map_FilterByAttribute_ReturnsFilteredCollection() { } + /*========================================================================================================================== + | TEST: MAP: FILTER BY INVALID ATTRIBUTE: THROWS EXCEPTION + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Attempts to map a view model that has an invalid value of ContentType + /// ; throws an . + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public async Task Map_FilterByInvalidAttribute_ThrowsExceptions() { + + var topic = TopicFactory.Create("Test", "FilteredInvalid"); + + var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); + + Assert.AreEqual(2, target.Children.Count); + + } + /*========================================================================================================================== | TEST: MAP: FILTER BY CONTENT TYPE: RETURNS FILTERED COLLECTION \-------------------------------------------------------------------------------------------------------------------------*/ diff --git a/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs new file mode 100644 index 00000000..d5f7d26e --- /dev/null +++ b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs @@ -0,0 +1,28 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Mapping.Annotations; +using OnTopic.ViewModels; +using OnTopic.ViewModels.Collections; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: FILTERED TOPIC (INVALID) + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed data transfer object for testing views properties annotated with the . Includes an invalid . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class FilteredInvalidTopicViewModel { + + [FilterByAttribute("ContentType", "Page")] + public TopicViewModelCollection Children { get; } = new(); + + } //Class +} //Namespace \ No newline at end of file From dc8d9882cd985a6b70c434634e1f66668d1c8fd3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 10:55:59 -0800 Subject: [PATCH 723/778] Account for variability in `DerivedTopic` attribute descriptor name The (legacy) `DerivedTopic` attribute descriptor varied in name. In most databases, it is `InheritedTopic`, but in some newer databases it is `TopicId` or `DerivedTopic`. The variability of the attribute key is permitted by a legacy artifact of the fact that, originally, the `TopicReferenceAttribute` had a hard-coded value of `TopicId` and, therefore, it didn't matter what the key was. As this was later extended to support general topic references, and not _just_ base topic references, this was modified, and thus implementations of e.g. OnTopic 4.6 use the `TopicId` key. It's worth noting that one of the objectives of the `BaseTopic` handling in OnTopic 5.0.0 is to apply uniform nomenclature around this concept, with unified attribute descriptor, attribute keys, etc. This helps deliver on that objective. --- .../Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index ed404dc7..32c03de2 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -133,7 +133,7 @@ WHERE ReferenceKey = 'Topic' UPDATE Topics SET TopicKey = 'BaseTopic' -WHERE TopicKey = 'InheritedTopic' +WHERE TopicKey IN ('TopicID', 'InheritedTopic', 'DerivedTopic') AND ContentType = 'TopicReferenceAttribute' -------------------------------------------------------------------------------------------------------------------------------- From 0747cb7825c3e1079db49955d7478936dc998e06 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 11:24:03 -0800 Subject: [PATCH 724/778] Alter new `[dbo].[Topics]` columns to be `NOT NULL` These columns are initially added as nullable since they don't have a default value. But the script will immediately populate those values, and the result should remove any remaining null values. Once that's done, we can change them to not be nullable, as the final schema expects. This will not only validate that the previous query functioned properly, but also avoid issues with the auto-generated schema comparison script, since it isn't able to update individual columns if there are existing records. While I was at it, I cleaned up the formatting of the original script for adding these columns. --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index 32c03de2..d3519e13 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -46,10 +46,10 @@ COLUMN DateModified; -- in a way that's inconsistent with other attributes. By moving them to Topic, we better acknowledge their unique status. -------------------------------------------------------------------------------------------------------------------------------- -ALTER TABLE [dbo].[Topics] - ADD [TopicKey] VARCHAR (128) NULL, - [ContentType] VARCHAR (128) NULL, - [ParentID] INT NULL; +ALTER TABLE [dbo].[Topics] +ADD [TopicKey] VARCHAR(128) NULL, + [ContentType] VARCHAR(128) NULL, + [ParentID] INT NULL; WITH KeyAttributes AS ( SELECT TopicID, @@ -82,6 +82,12 @@ PIVOT ( MIN(AttributeValue) WHERE RowNumber = 1 AND Topics.TopicID = Pvt.TopicID +ALTER TABLE [dbo].[Topics] +ALTER COLUMN TopicKey VARCHAR(128) NOT NULL; + +ALTER TABLE [dbo].[Topics] +ALTER COLUMN ContentType VARCHAR(128) NOT NULL; + DELETE FROM Attributes WHERE AttributeKey From 9f7b54420bba47cc87b18a9bd17e45ddf1fa4c2e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 11:32:41 -0800 Subject: [PATCH 725/778] Introduced messaging to help aid in reading the results of the query --- .../Upgrade from OnTopic 4 to OnTopic 5.sql | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql index d3519e13..b353c581 100644 --- a/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql +++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql @@ -13,6 +13,8 @@ -- migrations. -------------------------------------------------------------------------------------------------------------------------------- +PRINT('Dropping legacy columns...'); + ALTER TABLE Topics DROP @@ -38,6 +40,8 @@ TABLE ExtendedAttributes DROP COLUMN DateModified; +PRINT('Dropped legacy columns'); + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE CORE ATTRIBUTES -------------------------------------------------------------------------------------------------------------------------------- @@ -46,6 +50,8 @@ COLUMN DateModified; -- in a way that's inconsistent with other attributes. By moving them to Topic, we better acknowledge their unique status. -------------------------------------------------------------------------------------------------------------------------------- +PRINT('Migrating core attributes...'); + ALTER TABLE [dbo].[Topics] ADD [TopicKey] VARCHAR(128) NULL, [ContentType] VARCHAR(128) NULL, @@ -96,6 +102,8 @@ IN ( 'Key', 'ParentID' ) +PRINT('Migrated core attributes'); + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE TOPIC REFERENCES -------------------------------------------------------------------------------------------------------------------------------- @@ -105,6 +113,8 @@ IN ( 'Key', -- service to infer which attributes represent relationships in order to translate their values from `TopicID` to `UniqueKey`. -------------------------------------------------------------------------------------------------------------------------------- +PRINT('Migrating topic references...'); + CREATE TABLE [dbo].[TopicReferences] ( [Source_TopicID] INT NOT NULL, @@ -124,6 +134,8 @@ WHERE AttributeKey LIKE '%ID' AND ISNUMERIC(AttributeValue) = 1 AND Topics.TopicID IS NOT NULL +PRINT('Migrated core attributes'); + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE DERIVED TOPICS -------------------------------------------------------------------------------------------------------------------------------- @@ -133,6 +145,8 @@ WHERE AttributeKey LIKE '%ID' -- and how its 'ReferenceKey'. -------------------------------------------------------------------------------------------------------------------------------- +PRINT('Migrating base topics...'); + UPDATE TopicReferences SET ReferenceKey = 'BaseTopic' WHERE ReferenceKey = 'Topic' @@ -142,6 +156,8 @@ SET TopicKey = 'BaseTopic' WHERE TopicKey IN ('TopicID', 'InheritedTopic', 'DerivedTopic') AND ContentType = 'TopicReferenceAttribute' +PRINT('Migrated base topics'); + -------------------------------------------------------------------------------------------------------------------------------- -- MIGRATE ATTRIBUTE KEYS -------------------------------------------------------------------------------------------------------------------------------- @@ -150,6 +166,8 @@ AND ContentType = 'TopicReferenceAttribute' -- conflict with .NET's own "*Attribute" convention (which is usually reserved for actual attributes). -------------------------------------------------------------------------------------------------------------------------------- +PRINT('Migrating attribute descriptors...'); + UPDATE Topics SET TopicKey = TopicKey + 'Descriptor' WHERE TopicKey LIKE '%Attribute' @@ -157,4 +175,6 @@ AND ContentType = 'ContentTypeDescriptor' UPDATE Topics SET ContentType = ContentType + 'Descriptor' -WHERE ContentType LIKE '%Attribute' \ No newline at end of file +WHERE ContentType LIKE '%Attribute' + +PRINT('Migrated attribute descriptors'); \ No newline at end of file From 4b68e2becfdd550372078314508850fe5422ce99 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 11:40:38 -0800 Subject: [PATCH 726/778] Bypass validation of the `EqualityContract` property On C# 9.0 `record` types, the `EqualityContract` property is generated by the compiler, but it is not intended to be mapped by `ReverseTopicMappingService`. --- OnTopic/Mapping/Reverse/BindingModelValidator.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic/Mapping/Reverse/BindingModelValidator.cs b/OnTopic/Mapping/Reverse/BindingModelValidator.cs index 9c09f3bc..142244e0 100644 --- a/OnTopic/Mapping/Reverse/BindingModelValidator.cs +++ b/OnTopic/Mapping/Reverse/BindingModelValidator.cs @@ -173,6 +173,13 @@ static internal void ValidateProperty( return; } + /*------------------------------------------------------------------------------------------------------------------------ + | Skip properties injected by the compiler for record types + \-----------------------------------------------------------------------------------------------------------------------*/ + if (configuration.Property.Name is "EqualityContract") { + return; + } + /*------------------------------------------------------------------------------------------------------------------------ | Handle mapping properties from referenced objects \-----------------------------------------------------------------------------------------------------------------------*/ From f3695e940a27cced483d5d40e4fd98a85071b7f7 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 11:41:09 -0800 Subject: [PATCH 727/778] Bypass mapping of the `EqualityContract` property On C# 9.0 `record` types, the `EqualityContract` property is generated by the compiler, but it is not intended to be mapped by `ReverseTopicMappingService`. --- OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs index 10c44ca8..dfa18605 100644 --- a/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs +++ b/OnTopic/Mapping/Reverse/ReverseTopicMappingService.cs @@ -275,6 +275,13 @@ private async Task SetPropertyAsync( return; } + /*------------------------------------------------------------------------------------------------------------------------ + | Skip properties injected by the compiler for record types + \-----------------------------------------------------------------------------------------------------------------------*/ + if (configuration.Property.Name is "EqualityContract") { + return; + } + /*------------------------------------------------------------------------------------------------------------------------ | Handle mapping properties from referenced objects \-----------------------------------------------------------------------------------------------------------------------*/ From bea5a201ab77e740389b3db1eee8767b9e5a4054 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 11:49:08 -0800 Subject: [PATCH 728/778] Introduced unit test for validating the mapping of records --- .../BindingModels/RecordTopicBindingModel.cs | 31 +++++++++++++++++++ .../ReverseTopicMappingServiceTest.cs | 25 +++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs diff --git a/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs new file mode 100644 index 00000000..ae77cbd7 --- /dev/null +++ b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs @@ -0,0 +1,31 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.ComponentModel.DataAnnotations; +using OnTopic.Models; + +namespace OnTopic.Tests.BindingModels { + + /*============================================================================================================================ + | BINDING MODEL: RECORD + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a strongly-typed binding model based on a C# 9.0 record data type to ensure that it can be properly + /// mapped from. + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class RecordTopicBindingModel : ITopicBindingModel { + + public RecordTopicBindingModel() { } + + public string? Key { get; init; } + + [Required] + public string? ContentType { get; init; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs index 4e07ac22..d1a7e1a7 100644 --- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs +++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs @@ -155,6 +155,31 @@ public async Task Map_Existing_ReturnsUpdatedTopic() { } + /*========================================================================================================================== + | TEST: MAP: RECORD: RETURNS NEW TOPIC + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Establishes a and tests mapping a binding model that's based on a C# 9.0 + /// record type. + /// + [TestMethod] + public async Task Map_Record_ReturnsNewTopic() { + + var mappingService = new ReverseTopicMappingService(_topicRepository); + + var bindingModel = new RecordTopicBindingModel() { + Key = "Test", + ContentType = "TextAttributeDescriptor" + }; + + var target = await mappingService.MapAsync(bindingModel).ConfigureAwait(false); + + Assert.IsNotNull(target); + Assert.AreEqual("Test", target.Key); + Assert.AreEqual("TextAttributeDescriptor", target.ContentType); + + } + /*========================================================================================================================== | TEST: MAP: COMPLEX OBJECT: RETURNS FLATTENED TOPIC \-------------------------------------------------------------------------------------------------------------------------*/ From 49196954e9e1959e6fcc4443f3b4ae549110313b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:03:19 -0800 Subject: [PATCH 729/778] Configure solution to auto-generate XML documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are diligent about maintaining XML Docs for all types and members. But we don't currently _do_ anything with that documentation. By auto-generating it on compilation, we at least ensure that it will end up being included in NuGet packages—and thus available to IntelliSense for implementers. In addition, this will allow us to use e.g. DocFX in the future as part of our CI/CD process, should we choose. --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index 1a8bedc5..c8e6e15f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,6 +17,7 @@ en true true + true From 6404d58a225b5453aa209ef3a9286e007165df9f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:06:57 -0800 Subject: [PATCH 730/778] Addressed mismatched parameter documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added documentation for parameters where missing, removed extraneous documentation from since-deleted parameters, and updated documentation—or parameters!—to ensure they shared the same name. This resolves CS1573. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 2 +- OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs | 5 +---- OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs | 1 - OnTopic.Data.Sql/SqlCommandExtensions.cs | 2 -- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 4 ++-- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 ++ 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 3af70521..70981efa 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -168,7 +168,7 @@ public ActionResult Index(bool indent = false, bool includeMetadata = false) { /// /// Given a root topic, generates an XML-formatted sitemap. /// - /// The topic to add to the sitemap. + /// The topic to add to the sitemap. /// Optionally enables extended metadata associated with each topic. /// A Sitemap.org sitemap. private XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) => diff --git a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs index c48206c8..86a820c4 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs @@ -88,10 +88,7 @@ public void PopulateValues(ViewLocationExpanderContext context) { /*========================================================================================================================== | METHOD: EXPAND VIEW LOCATIONS \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Introduces additional routes - /// - /// The that the request is operating within. + /// public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) { /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index 2a155b98..86db58d3 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -35,7 +35,6 @@ public class TopicViewResultExecutor : ViewExecutor, IActionResultExecutorThe . /// The . /// The . - /// The . /// The . public TopicViewResultExecutor( IOptions viewOptions, diff --git a/OnTopic.Data.Sql/SqlCommandExtensions.cs b/OnTopic.Data.Sql/SqlCommandExtensions.cs index 8ee245c5..3fed756a 100644 --- a/OnTopic.Data.Sql/SqlCommandExtensions.cs +++ b/OnTopic.Data.Sql/SqlCommandExtensions.cs @@ -105,7 +105,6 @@ internal static void AddParameter(this SqlCommand command, string sqlParameter, /// The SQL command object. /// The SQL parameter. /// The SQL field value. - /// The SQL field data type. internal static void AddParameter(this SqlCommand command, string sqlParameter, string? fieldValue) => AddParameter(command, sqlParameter, String.IsNullOrEmpty(fieldValue)? null : fieldValue, SqlDbType.VarChar); @@ -117,7 +116,6 @@ internal static void AddParameter(this SqlCommand command, string sqlParameter, /// The SQL field value. /// The SQL field data type. /// The SQL parameter's directional setting (input-only, output-only, etc.). - /// Length limit for the SQL field. /// /// command is not null /// diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index 9f1d0732..d06e00b5 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -347,7 +347,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, TopicIndex /// behavior is overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update /// from being persisted to the data store on . /// - private static void SetRelationships(this IDataReader reader, TopicIndex topics, bool? isDirty = false) { + private static void SetRelationships(this IDataReader reader, TopicIndex topics, bool? markDirty = false) { /*------------------------------------------------------------------------------------------------------------------------ | Identify attributes @@ -378,7 +378,7 @@ private static void SetRelationships(this IDataReader reader, TopicIndex topics, | Set relationship on object \-----------------------------------------------------------------------------------------------------------------------*/ if (!isDeleted) { - current.Relationships.SetValue(relationshipKey, related, isDirty); + current.Relationships.SetValue(relationshipKey, related, markDirty); } else if (current.Relationships.Contains(relationshipKey, related)) { current.Relationships.Remove(relationshipKey, related); diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index 2d21f072..b4bb754c 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -618,6 +618,7 @@ protected override sealed void DeleteTopic(Topic topic) { /// Internal method that saves topic relationships to the n:n mapping table in SQL. /// /// The topic object whose relationships should be persisted. + /// The version that should be associated with the updated value. /// The SQL connection. private static void PersistRelationships(Topic topic, DateTime version, SqlConnection connection) { @@ -684,6 +685,7 @@ private static void PersistRelationships(Topic topic, DateTime version, SqlConne /// Internal method that saves topic references to the 1:n mapping table in SQL. ///
/// The topic object whose references should be persisted. + /// The version that should be associated with the updated value. /// The SQL connection. private static void PersistReferences(Topic topic, DateTime version, SqlConnection connection) { From 6128b6989c84a8fe9ca98bd335a040eaacce34a4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:09:05 -0800 Subject: [PATCH 731/778] Removed mismatched closing tag This resolves CS1570. --- OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs index 70981efa..f7b2e91f 100644 --- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs +++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs @@ -31,7 +31,6 @@ namespace OnTopic.AspNetCore.Mvc.Controllers { /// their content types; in this case, the is excluded, but its descendents are not. What content /// types are excluded or skipped can be configured, respectively, by modifying the static and collections. - /// . /// /// /// The action enables an extended sitemap with Google's custom PageMap schema for From c0830b4f38b0f6b22d042fc85574f03184b28070 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:10:10 -0800 Subject: [PATCH 732/778] Removed unencoded ampersand As the XML Docs are XML, ampersands must be encoded. This resolves CS1570. --- OnTopic.AspNetCore.Mvc/TopicViewResult.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs index 21fab214..2d2faaf5 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs @@ -78,7 +78,7 @@ public TopicViewResult( ///
/// /// The associated will fall back to the if the view isn't - /// set via other sources, such as the HTTP accepts header, the query string, &c. + /// set via other sources, such as the HTTP accepts header, the query string, etc. /// public string TopicView { get; } From 684b47959bd44929c42dda86c3ce91d07637d03d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:30:32 -0800 Subject: [PATCH 733/778] Resolved `cref` references within XML Docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not all `` references resolved. Sometimes this was due to references to old or incorrect identifiers, but more often simply out-of-scope references. In many cases, these can be resolved by simply adding a `using` statement. In other cases, it required updating the references—and, especially, the method signatures. This resolves CS1574. --- .../Components/MenuViewComponent.cs | 3 ++- .../Components/PageLevelNavigationViewComponent.cs | 8 ++++---- OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs | 8 ++++---- .../TopicControllerTest.cs | 14 +++++++------- .../TopicViewComponentTest.cs | 1 + .../ValidateTopicAttributeTest.cs | 1 + .../Components/MenuViewComponentBase{T}.cs | 1 + .../NavigationTopicViewComponentBase{T}.cs | 5 +++-- .../PageLevelNavigationViewComponentBase{T}.cs | 5 +++-- .../Models/NavigationViewModel{T}.cs | 10 ++++++---- .../TopicRepositoryExtensions.cs | 2 +- .../TopicViewLocationExpander.cs | 4 +--- OnTopic.AspNetCore.Mvc/TopicViewResult.cs | 5 +++-- OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs | 2 ++ OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs | 11 ++++++----- .../Models/AttributeValuesDataTable.cs | 5 +++-- OnTopic.Data.Sql/SqlDataReaderExtensions.cs | 2 +- OnTopic.Data.Sql/SqlTopicRepository.cs | 2 +- OnTopic.TestDoubles/DummyTopicMappingService.cs | 2 +- OnTopic.TestDoubles/StubTopicRepository.cs | 2 +- .../Collections/TopicViewModelCollection.cs | 2 +- OnTopic.ViewModels/Items/ListTopicViewModel.cs | 1 + OnTopic.ViewModels/TopicViewModelLookupService.cs | 1 + 23 files changed, 55 insertions(+), 42 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Components/MenuViewComponent.cs b/OnTopic.AspNetCore.Mvc.Host/Components/MenuViewComponent.cs index 4baef619..1ba0cd78 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Components/MenuViewComponent.cs +++ b/OnTopic.AspNetCore.Mvc.Host/Components/MenuViewComponent.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Components; +using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Mapping.Hierarchical; using OnTopic.Repositories; @@ -16,7 +17,7 @@ namespace OnTopic.AspNetCore.Mvc.Host.Components { | CLASS: MENU VIEW COMPONENT \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Defines a which provides access to a menu of + /// Defines a which provides access to a menu of /// instances. /// /// diff --git a/OnTopic.AspNetCore.Mvc.Host/Components/PageLevelNavigationViewComponent.cs b/OnTopic.AspNetCore.Mvc.Host/Components/PageLevelNavigationViewComponent.cs index efa07736..d054347e 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Components/PageLevelNavigationViewComponent.cs +++ b/OnTopic.AspNetCore.Mvc.Host/Components/PageLevelNavigationViewComponent.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Components; +using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Mapping.Hierarchical; using OnTopic.Repositories; @@ -16,15 +17,14 @@ namespace OnTopic.AspNetCore.Mvc.Host.Components { | CLASS: PAGE-LEVEL NAVIGATION VIEW COMPONENT \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Defines a which provides access to a menu of + /// Defines a which provides access to a menu of /// instances representing the nearest page-level navigation. /// /// /// /// As a best practice, global data required by the layout view are requested independent of the current page. This - /// allows each layout element to be provided with its own layout data, in the form of s, instead of needing to add this data to every view model returned by . + /// allows each layout element to be provided with its own layout data, in the form of s, instead of needing to add this data to every view model returned by . /// /// public class PageLevelNavigationViewComponent : PageLevelNavigationViewComponentBase { diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs index 1511ddc0..e53b62fa 100644 --- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs +++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs @@ -46,8 +46,8 @@ public class SampleActivator : IControllerActivator, IViewComponentActivator { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new instance of the , including any shared dependencies to be used - /// across instances of controllers. + /// Establishes a new instance of the , including any shared dependencies to be used across + /// instances of controllers. /// /// /// The constructor is responsible for establishing dependencies with the singleton lifestyle so that they are available @@ -88,7 +88,7 @@ public SampleActivator(string connectionString) { /// /// Registers dependencies, and injects them into new instances of controllers in response to each request. /// - /// A concrete instance of an . + /// A concrete instance of an . public object Create(ControllerContext context) { /*------------------------------------------------------------------------------------------------------------------------ @@ -128,7 +128,7 @@ public object Create(ControllerContext context) { /// /// Registers dependencies, and injects them into new instances of view components in response to each request. /// - /// A concrete instance of an . + /// A concrete instance of an . public object Create(ViewComponentContext context) { /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs index e88e598f..8314965a 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs @@ -25,7 +25,7 @@ namespace OnTopic.Tests { \---------------------------------------------------------------------------------------------------------------------------*/ /// /// Provides unit tests for the , and other classes that are part of - /// the namespace. + /// the namespace. /// [TestClass] public class TopicControllerTest { @@ -104,7 +104,7 @@ public async Task TopicController_IndexAsync_ReturnsTopicViewResult() { | TEST: REDIRECT CONTROLLER: REDIRECT: RETURNS REDIRECT RESULT \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Triggers the action. + /// Triggers the action. /// [TestMethod] public void RedirectController_TopicRedirect_ReturnsRedirectResult() { @@ -124,7 +124,7 @@ public void RedirectController_TopicRedirect_ReturnsRedirectResult() { | TEST: SITEMAP CONTROLLER: INDEX: RETURNS SITEMAP XML \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Triggers the index action of the action. + /// Triggers the index action of the action. /// [TestMethod] public void SitemapController_Index_ReturnsSitemapXml() { @@ -152,8 +152,8 @@ public void SitemapController_Index_ReturnsSitemapXml() { | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES CONTENT TYPES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Triggers the index action of the action and verifies that it properly - /// excludes List content types, and skips over Container and PageGroup. + /// Triggers the index action of the action and verifies that it + /// properly excludes List content types, and skips over Container and PageGroup. /// [TestMethod] public void SitemapController_Index_ExcludesContentTypes() { @@ -198,8 +198,8 @@ public void SitemapController_Index_ExcludesContentTypes() { | TEST: SITEMAP CONTROLLER: INDEX: EXCLUDES ATTRIBUTES \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Triggers the index action of the action and verifies that it properly - /// excludes the Body and IsHidden attributes. + /// Triggers the index action of the action and verifies that it + /// properly excludes the Body and IsHidden attributes. /// [TestMethod] public void SitemapController_Index_ExcludesAttributes() { diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index 01a41ab5..e423ccad 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.AspNetCore.Routing; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OnTopic.AspNetCore.Mvc.Components; using OnTopic.AspNetCore.Mvc.Host.Components; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Data.Caching; diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs index 02998768..87e31cef 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs @@ -14,6 +14,7 @@ using OnTopic.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.Attributes; +using OnTopic.Metadata; using OnTopic.TestDoubles; namespace OnTopic.Tests { diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index add98820..f8ebf53d 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -6,6 +6,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Internal.Diagnostics; using OnTopic.Mapping.Hierarchical; diff --git a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs index c28c0e44..0f4f3d0b 100644 --- a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs @@ -52,7 +52,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService /// on the route data. /// /// - /// The associated with the . + /// The associated with the . /// protected ITopicRepository TopicRepository { get; } @@ -64,7 +64,8 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService /// be mapped. /// /// - /// The associated with the . + /// The associated with the . /// protected IHierarchicalTopicMappingService HierarchicalTopicMappingService { get; } diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index 977000ec..3d570f56 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using OnTopic.AspNetCore.Mvc.Controllers; using OnTopic.AspNetCore.Mvc.Models; using OnTopic.Mapping.Hierarchical; using OnTopic.Models; @@ -16,8 +17,8 @@ namespace OnTopic.AspNetCore.Mvc.Components { | CLASS: PAGE-LEVEL NAVIGATION VIEW COMPONENT \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Defines a which provides access to a menu of - /// instances representing the nearest page-level navigation. + /// Defines a which provides access to a menu of instances representing + /// the nearest page-level navigation. /// /// /// diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs index 034d260a..bb86771c 100644 --- a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs @@ -3,6 +3,8 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; +using OnTopic.AspNetCore.Mvc.Components; using OnTopic.Models; namespace OnTopic.AspNetCore.Mvc.Models { @@ -16,13 +18,13 @@ namespace OnTopic.AspNetCore.Mvc.Models { /// /// /// No topics are expected to have a Navigation content type. Instead, this view model is expected to be manually - /// constructed by the . + /// constructed by the . /// /// - /// The can be any view model that implements , + /// The can be any view model that implements , /// which provides a base level of support for properties associated with the typical Page content type as well as - /// a method for determining if a given instance is the currently-selected topic. - /// Implementations may support additional properties, as appropriate. + /// a method for determining if a given instance is the currently-selected + /// topic. Implementations may support additional properties, as appropriate. /// /// public class NavigationViewModel where T : class, INavigationTopicViewModel { diff --git a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs index 28fb7e71..61c83370 100644 --- a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs +++ b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs @@ -31,7 +31,7 @@ public static class TopicRepositoryExtensions { /// of the box routes, such as controller and action, the defines /// additional topic-specific routes, such as rootTopic and path. These can be combined to identify a topic /// in the repository. By using the extension method, callers needn't assemble their own - /// prior to calling , assuming they are using the standard routing + /// prior to calling , assuming they are using the standard routing /// variables. /// public static Topic? Load( diff --git a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs index 86a820c4..0cab8b79 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs @@ -74,9 +74,7 @@ public TopicViewLocationExpander() { /// /// Initializes a new instance of the class. /// - /// - /// - /// + /// /// The that the request is operating within. public void PopulateValues(ViewLocationExpanderContext context) { Contract.Requires(context, nameof(context)); diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs index 2d2faaf5..01d20459 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResult.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; using OnTopic.Internal.Diagnostics; +using OnTopic.Metadata; namespace OnTopic.AspNetCore.Mvc { @@ -65,8 +66,8 @@ public TopicViewResult( /// /// /// The preferred nomenclature for the name of a is simply ContentType. The - /// base class has an existing property representing the HTTP response - /// value, however. As such, is used to disambiguate the terms. + /// base class has an existing property representing the + /// HTTP response value, however. As such, is used to disambiguate the terms. /// public string TopicContentType { get; } = "Page"; diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs index 86db58d3..a00eab84 100644 --- a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs +++ b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs @@ -8,9 +8,11 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.Logging; diff --git a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs index afd90f5e..1390b301 100644 --- a/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs +++ b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs @@ -23,7 +23,8 @@ namespace OnTopic.AspNetCore.Mvc { /// Not all topics are appropriate to display in a view. If the topic isn't in the repository, the action should return a /// . If the topic is marked as , then the action should return a /// . If the topic contains a Url attribute, then the action should return a . All of this logic can be enforced by adding the to an action. + /// cref="RedirectResult"/>. All of this logic can be enforced by adding the to an + /// action. /// [AttributeUsage(AttributeTargets.Method)] public sealed class ValidateTopicAttribute : ActionFilterAttribute { @@ -48,10 +49,10 @@ public sealed class ValidateTopicAttribute : ActionFilterAttribute { /// /// /// While the event can be used to provide a wide variety of - /// filters, this specific implementation is focused on validating the state of the . Namely, - /// it will provide error handling (if the is null), a redirect (if the 's Url attribute is set, and an unauthorized response (if the 's - /// flag is set. + /// filters, this specific implementation is focused on validating the state of the . Namely, it will provide error handling (if the is null), a + /// redirect (if the 's Url attribute is set, and an unauthorized + /// response (if the 's flag is set. /// /// A view associated with the requested topic's Content Type and view. [NonAction] diff --git a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs index 36003f89..e682582b 100644 --- a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs +++ b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System.Data; using OnTopic.Attributes; +using OnTopic.Collections.Specialized; namespace OnTopic.Data.Sql.Models { @@ -51,8 +52,8 @@ internal AttributeValuesDataTable() { /// /// Provides a convenience method for adding a new based on the expected column values. /// - /// The . - /// The . + /// The . + /// The . internal DataRow AddRow(string attributeKey, string? attributeValue = null) { /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs index d06e00b5..d456ff04 100644 --- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs +++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs @@ -28,7 +28,7 @@ namespace OnTopic.Data.Sql { /// main entry point is . It is supported by a number of private extensions which allow /// it to handle individual records from particular data sets (e.g., the method maps to /// data returned from the ExtendedAttributeIndex view). That said, the Get extensions (e.g., ) are not specific to this format, and remain useful for a variety of database + /// cref="GetString(IDataReader, String)"/>) are not specific to this format, and remain useful for a variety of database /// queries, should they be needed, and thus are marked as internal. /// internal static class SqlDataReaderExtensions { diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs index b4bb754c..79880e57 100644 --- a/OnTopic.Data.Sql/SqlTopicRepository.cs +++ b/OnTopic.Data.Sql/SqlTopicRepository.cs @@ -24,7 +24,7 @@ namespace OnTopic.Data.Sql { /// Provides data access to topics stored in Microsoft SQL Server. /// /// - /// Concrete implementation of the class. + /// Concrete implementation of the class. /// public class SqlTopicRepository : TopicRepository, ITopicRepository { diff --git a/OnTopic.TestDoubles/DummyTopicMappingService.cs b/OnTopic.TestDoubles/DummyTopicMappingService.cs index 684aa463..db226a50 100644 --- a/OnTopic.TestDoubles/DummyTopicMappingService.cs +++ b/OnTopic.TestDoubles/DummyTopicMappingService.cs @@ -26,7 +26,7 @@ public class DummyTopicMappingService : ITopicMappingService { | CONSTRUCTOR \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Establishes a new instance of a with required dependencies. + /// Establishes a new instance of a with required dependencies. /// public DummyTopicMappingService() { } diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs index aa528ec4..2fd6da1a 100644 --- a/OnTopic.TestDoubles/StubTopicRepository.cs +++ b/OnTopic.TestDoubles/StubTopicRepository.cs @@ -138,7 +138,7 @@ protected override void DeleteTopic(Topic topic) { } /*========================================================================================================================== | METHOD: GET ATTRIBUTES (PROXY) \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// public IEnumerable GetAttributesProxy( Topic topic, bool? isExtendedAttribute, diff --git a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs index 54df8b51..eafebd74 100644 --- a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs @@ -16,7 +16,7 @@ namespace OnTopic.ViewModels.Collections { | VIEW MODEL: TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a basic collection interface for use with data transfer objects implementing , + /// Provides a basic collection interface for use with data transfer objects implementing , /// including and derivatives. /// /// diff --git a/OnTopic.ViewModels/Items/ListTopicViewModel.cs b/OnTopic.ViewModels/Items/ListTopicViewModel.cs index c8b5a4e4..dc8a6ffe 100644 --- a/OnTopic.ViewModels/Items/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ListTopicViewModel.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System.Collections; namespace OnTopic.ViewModels.Items { diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index b7a8cf0f..f8188c8b 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -5,6 +5,7 @@ \=============================================================================================================================*/ using System; using System.Collections.Generic; +using System.Reflection; using OnTopic.Lookup; using OnTopic.ViewModels.Items; From 726ba445e8a1747a640a8eba2b54bb8a1993231a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:32:23 -0800 Subject: [PATCH 734/778] Remove explicit generation of documentation in `Debug` configuration We now generate the documentation in _all_ configurations, as part of the `Directory.Build.props` (4919695). There's also no need to generate the legacy debug symbols, so I'm removing the entire condition. (This was already removed for other projects, but left in for `OnTopic` until it could be reevaluated.) --- OnTopic/OnTopic.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/OnTopic/OnTopic.csproj b/OnTopic/OnTopic.csproj index 37194f9d..b3bc3dd9 100644 --- a/OnTopic/OnTopic.csproj +++ b/OnTopic/OnTopic.csproj @@ -13,11 +13,6 @@ C# .NET CMS Domain - - full - bin\$(Configuration)\OnTopic.XML - - all From 44b86499189c2a8ec9034df5b17396077f0b203a Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:47:33 -0800 Subject: [PATCH 735/778] Reduced scope of `INavigationTopicViewModel` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `INavigationTopicViewModel` previously inherited from `ITopicViewModel`. The `ITopicViewModel` has grown since its inception, and also includes a lot of properties that aren't relevant to the `INavigationTopicViewModel`. Given this, I've removed the dependency on `ITopicViewModel` from `INavigationTopicViewModel`, and updated it to include only the most critical properties previously inherited from `ITopicViewModel`—namely `Title` and `WebPath`. Since the `INavigationTopicViewModel` will no longer have a `UniqueKey` property to compare against, I've updated the `IsSelected()` method to accept a `webPath` property instead. This makes more sense in context of how it will be used. --- .../Models/INavigationTopicViewModel{T}.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/OnTopic/Models/INavigationTopicViewModel{T}.cs b/OnTopic/Models/INavigationTopicViewModel{T}.cs index 140b5ceb..bd82c93a 100644 --- a/OnTopic/Models/INavigationTopicViewModel{T}.cs +++ b/OnTopic/Models/INavigationTopicViewModel{T}.cs @@ -16,10 +16,13 @@ namespace OnTopic.Models { /// No topics are expected to have a Navigation content type. Instead, implementers of this view model are expected /// to manually construct instances. /// - public interface INavigationTopicViewModel : - ITopicViewModel, IHierarchicalTopicViewModel - where T: INavigationTopicViewModel - { + public interface INavigationTopicViewModel : IHierarchicalTopicViewModel where T: INavigationTopicViewModel { + + /*========================================================================================================================== + | PROPERTY: TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + string? Title { get; init; } /*========================================================================================================================== | PROPERTY: SHORT TITLE @@ -31,15 +34,21 @@ public interface INavigationTopicViewModel : string? ShortTitle { get; init; } /*========================================================================================================================== - | METHOD: ISSELECTED + | PROPERTY: WEB PATH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + string? WebPath { get; init; } + + /*========================================================================================================================== + | METHOD: IS SELECTED? \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Determines if the current item is selected based on the provided . + /// Determines if the current item is selected based on the provided . /// - /// - /// The unique key to compare against the current + /// + /// The path to compare against the current /// - bool IsSelected(string uniqueKey); + bool IsSelected(string webPath); } //Class } //Namespace \ No newline at end of file From 4f535a50f24af36331308a30a57c2b25328cafa4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:48:45 -0800 Subject: [PATCH 736/778] Updated `NavigationTopicViewModel` to reflect updates to interface This mirrors the changes implemented to `INavigationTopicViewModel` (44b8649). Most notably, it results in `IsSelected()` from operating off of `webPath` instead of `uniqueKey`, which will be a breaking change for implementers. --- OnTopic.ViewModels/NavigationTopicViewModel.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 1448bbd6..48208880 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -28,7 +28,13 @@ namespace OnTopic.ViewModels { /// cref="NavigationTopicViewModel"/> class is marked as sealed. /// /// - public sealed record NavigationTopicViewModel : TopicViewModel, INavigationTopicViewModel { + public sealed record NavigationTopicViewModel : INavigationTopicViewModel { + + /*========================================================================================================================== + | TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public string? Title { get; init; } /*========================================================================================================================== | SHORT TITLE @@ -46,6 +52,12 @@ public sealed record NavigationTopicViewModel : TopicViewModel, INavigationTopic /// public Collection Children { get; } = new(); + /*========================================================================================================================== + | WEB PATH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public string? WebPath { get; init; } + /*========================================================================================================================== | IS SELECTED? \-------------------------------------------------------------------------------------------------------------------------*/ @@ -53,8 +65,8 @@ public sealed record NavigationTopicViewModel : TopicViewModel, INavigationTopic /// Determines whether or not the node represented by this is currently selected, /// typically meaning the user is on the page this object is pointing to. /// - public bool IsSelected(string uniqueKey) => - $"{uniqueKey}:".StartsWith($"{UniqueKey}:", StringComparison.OrdinalIgnoreCase); + public bool IsSelected(string webPath) => + $"{webPath}:".StartsWith($"{WebPath}:", StringComparison.OrdinalIgnoreCase); } //Class } //Namespace \ No newline at end of file From 44f66bfe2b467106f3a9f5d9f4ec6870f55a2574 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 12:55:30 -0800 Subject: [PATCH 737/778] Removed fallback to `NavigationTopicViewModel.Key` in `Menu` view In practice, this wouldn't have been necessary anyway, as the `Topic.Title` which `NavigationTopicViewModel.Title` inherits from already falls back to `Key`, and thus we'd never expect a `null` title. --- .../Views/Shared/Components/Menu/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml index 69d32b74..6c01ca42 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/Components/Menu/Default.cshtml @@ -17,7 +17,7 @@ IHtmlContent WriteMenu(NavigationTopicViewModel topic, int indentLevel = 1) => Body( @
  • - @(topic.ShortTitle?? topic.Title?? topic.Key) + @(topic.ShortTitle?? topic.Title)
      @foreach (var childTopic in topic.Children) { @WriteMenu(childTopic, indentLevel+1); From 8fd4d80fe862d89909741d076c5360ee84d55b7f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 13:07:50 -0800 Subject: [PATCH 738/778] Replace `CurrentKey` with `CurrentWebPath` This is a more logical comparison since, in practice, navigation fundamentally operates against web paths. This includes updating the unit tests to compare against `GetWebPath()` instead of `GetUniqueKey()`. --- .../TopicViewComponentTest.cs | 12 ++++++------ .../Components/MenuViewComponentBase{T}.cs | 2 +- .../PageLevelNavigationViewComponentBase{T}.cs | 2 +- .../Models/NavigationViewModel{T}.cs | 9 +++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index e423ccad..797b4909 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -114,10 +114,10 @@ public async Task Menu_Invoke_ReturnsNavigationViewModel() { var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentKey); - Assert.AreEqual("Root:Web", model?.NavigationRoot?.UniqueKey); + Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentWebPath); + Assert.AreEqual("/Web/", model?.NavigationRoot?.WebPath); Assert.AreEqual(3, model?.NavigationRoot?.Children.Count); - Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetUniqueKey())?? false); + Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false); } @@ -139,10 +139,10 @@ public async Task PageLevelNavigation_Invoke_ReturnsNavigationViewModel() { var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentKey); - Assert.AreEqual("Root:Web:Web_3", model?.NavigationRoot?.UniqueKey); + Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentWebPath); + Assert.AreEqual("/Web/Web_3/", model?.NavigationRoot?.WebPath); Assert.AreEqual(2, model?.NavigationRoot?.Children.Count); - Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetUniqueKey())?? false); + Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false); } diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index f8ebf53d..86dd9927 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -124,7 +124,7 @@ public async Task InvokeAsync() { \-----------------------------------------------------------------------------------------------------------------------*/ var navigationViewModel = new NavigationViewModel() { NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true), - CurrentKey = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path + CurrentWebPath = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path }; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index 3d570f56..64b501df 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -119,7 +119,7 @@ public async Task InvokeAsync() { \-----------------------------------------------------------------------------------------------------------------------*/ var navigationViewModel = new NavigationViewModel() { NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true), - CurrentKey = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path + CurrentWebPath = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path }; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs index bb86771c..dfb2c890 100644 --- a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs @@ -45,16 +45,16 @@ public class NavigationViewModel where T : class, INavigationTopicViewModel - /// The representing the path to the current . + /// The representing the path to the current . /// /// /// /// In order to determine whether any given , the views - /// will need to know where in the hierarchy the user currently is. By storing this on the used as the root view model for every navigation component, we ensure that the views + /// will need to know where in the hierarchy the user currently is. By storing this on the used as the root view model for every navigation component, we ensure that the views /// always have access to this information. /// /// @@ -63,6 +63,7 @@ public class NavigationViewModel where T : class, INavigationTopicViewModel itself. /// /// + public string CurrentWebPath { get; set; } = default!; public string CurrentKey { get; set; } = default!; } //Class From 7a6e7efb170919c51adc4ca6b3d3ec2b61f51281 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 13:10:50 -0800 Subject: [PATCH 739/778] Fixed bug in `IsSelected()` implementation The `IsSelected()` implementation still suffixed the `webPath` and `WebPath` with a `:`, which was needed with `UniqueKey` to ensure it wasn't a partial match against an individual key (e.g., we didn't want `Root:Web:Pro` to match `Root:Web:Products`). With `WebPath`, that shouldn't be necessary, since it always ends in `/`. That said, as web servers can be forgiving in trailing slashes, we still append a `/` at the end of `webPath` to be safe; this may result in two `//` at the end, but that won't impact the logic. --- OnTopic.ViewModels/NavigationTopicViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 48208880..5df1926e 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -66,7 +66,7 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel public bool IsSelected(string webPath) => - $"{webPath}:".StartsWith($"{WebPath}:", StringComparison.OrdinalIgnoreCase); + $"{webPath}/".StartsWith($"{WebPath}", StringComparison.OrdinalIgnoreCase); } //Class } //Namespace \ No newline at end of file From 31475d21bc4dbe416b65ab45925cb4f4bf61788d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 13:11:38 -0800 Subject: [PATCH 740/778] Reintroduced `CurrentKey` as `[Obsolete()]` This will help ensure that implementers get friendlier guidance if they're calling into this property. --- OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs index dfb2c890..5915be7f 100644 --- a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs @@ -64,6 +64,15 @@ public class NavigationViewModel where T : class, INavigationTopicViewModel /// public string CurrentWebPath { get; set; } = default!; + + /*========================================================================================================================== + | CURRENT KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// The representing the path to the current . + /// + /// + [Obsolete("The CurrentKey property has been replaced in favor of CurrentWebPath.", true)] public string CurrentKey { get; set; } = default!; } //Class From fd1e5693f7fc55f13e48884fbb4022a97d559791 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 13:28:44 -0800 Subject: [PATCH 741/778] Enforced `[Obsolete()]` on events This was missed in a previous branch. --- OnTopic/Repositories/ObservableTopicRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OnTopic/Repositories/ObservableTopicRepository.cs b/OnTopic/Repositories/ObservableTopicRepository.cs index b2d6ae02..e0293fc4 100644 --- a/OnTopic/Repositories/ObservableTopicRepository.cs +++ b/OnTopic/Repositories/ObservableTopicRepository.cs @@ -66,15 +66,15 @@ public event EventHandler? TopicRenamed { } /// - [Obsolete("The DeleteEvent has been renamed to TopicDeleted")] + [Obsolete("The DeleteEvent has been renamed to TopicDeleted", true)] public event EventHandler? DeleteEvent; /// - [Obsolete("The MoveEvent has been renamed to TopicMoved")] + [Obsolete("The MoveEvent has been renamed to TopicMoved", true)] public event EventHandler? MoveEvent; /// - [Obsolete("The RenameEvent has been renamed to TopicRenamed")] + [Obsolete("The RenameEvent has been renamed to TopicRenamed", true)] public event EventHandler? RenameEvent; /*========================================================================================================================== From 51d53217f6eddfe24b38dd20afc7176c04e50bbc Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 14:20:14 -0800 Subject: [PATCH 742/778] Abstract navigation components from `INavigationTopicViewModel` The `INavigationTopicViewModel` defines properties that are expected to be used by views. The `NavigationTopicViewComponentBase`, and its derivatives, don't need to know about most of that information. Instead, they simply need to know about the `IHiearchicalTopicViewModel`. As such, by relying on this interface instead, we offer more flexibility to navigation component implementations over what types of view they return. That said, in practice, we anticipate that _most_ implementors _will_ want to implement the members associated with `INavigationTopicViewModel`, and so we continue to support that interface as a convenience for standardizing how navigation view models are implemented, and especially with regards to determining if the current node is currently selected (via the `IsSelected()` method). --- .../Components/MenuViewComponentBase{T}.cs | 21 +++++++++++------ .../NavigationTopicViewComponentBase{T}.cs | 13 ++++++++++- ...PageLevelNavigationViewComponentBase{T}.cs | 13 +++++++++-- .../Models/NavigationViewModel{T}.cs | 23 +++++++++++-------- .../NavigationTopicViewModel.cs | 10 ++++---- .../Models/INavigationTopicViewModel{T}.cs | 2 +- 6 files changed, 56 insertions(+), 26 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index 86dd9927..5502dfde 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -23,22 +23,29 @@ namespace OnTopic.AspNetCore.Mvc.Components { /// /// /// - /// As a best practice, global data required by the layout view are requested independent of the current page. This - /// allows each layout element to be provided with its own layout data, in the form of s, instead of needing to add this data to every view model returned by . + /// As a best practice, global data required by the layout view are requested independent of the current page. This allows + /// each layout element to be provided with its own layout data, in the form of a s, + /// instead of needing to add this data to every view model returned by e.g. a . /// /// /// In order to remain view model agnostic, the does not assume that a particular /// view model will be used, and instead accepts a generic argument for any view model that implements the interface . Since generic view components cannot be effectively routed to, however, that - /// means implementors must, at minimum, provide a local instance of which sets the + /// cref="IHierarchicalTopicViewModel{T}"/>. Since generic view components cannot be effectively routed to, however, that + /// means implementors must, at minimum, provide a local instance of , which sets the /// generic value to the desired view model. To help enforce this, while avoiding ambiguity, this class is marked as /// abstract and suffixed with Base. /// + /// + /// While the only requires that the implement , views will require additional properties. These can be determined on a per-case + /// basis, as required by the implementation. Implementaters, however, should consider implementing the interface, which provides the standard properties that most views will likely need, as + /// well as a method for determining if the navigation item + /// is currently selected. + /// /// public abstract class MenuViewComponentBase : - NavigationTopicViewComponentBase where T : class, INavigationTopicViewModel, new() + NavigationTopicViewComponentBase where T : class, IHierarchicalTopicViewModel, new() { /*========================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs index 0f4f3d0b..f4246d9e 100644 --- a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; using Microsoft.AspNetCore.Mvc; using OnTopic.Mapping.Hierarchical; using OnTopic.Models; @@ -19,10 +20,20 @@ namespace OnTopic.AspNetCore.Mvc.Components { /// model. /// /// + /// /// This class is intended to provide a foundation for concrete implementations. It is not a fully formed implementation /// itself. As a result, it is marked as abstract. + /// + /// + /// While the only requires that the implement + /// , views will require additional properties. These can be determined on a + /// per-case basis, as required by the implementation. Implementaters, however, should consider implementing the interface, which provides the standard properties that most views will likely need, + /// as well as a method for determining if the navigation + /// item is currently selected. + /// /// - public abstract class NavigationTopicViewComponentBase : ViewComponent where T : class, INavigationTopicViewModel, new() { + public abstract class NavigationTopicViewComponentBase : ViewComponent where T : class, IHierarchicalTopicViewModel, new() { /*========================================================================================================================== | PRIVATE VARIABLES diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index 64b501df..b0410032 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using OnTopic.AspNetCore.Mvc.Controllers; @@ -30,14 +31,22 @@ namespace OnTopic.AspNetCore.Mvc.Components { /// /// In order to remain view model agnostic, the does not assume that /// a particular view model will be used, and instead accepts a generic argument for any view model that implements the - /// interface . Since generic view components cannot be effectively routed to, + /// interface . Since generic view components cannot be effectively routed to, /// however, that means implementors must, at minimum, provide a local instance of which sets the generic value to the desired view model. To help /// enforce this, while avoiding ambiguity, this class is marked as abstract and suffixed with Base. /// + /// + /// While the only requires that the + /// implement , views will require additional properties. These can be + /// determined on a per-case basis, as required by the implementation. Implementaters, however, should consider + /// implementing the interface, which provides the standard properties that + /// most views will likely need, as well as a method for + /// determining if the navigation item is currently selected. + /// /// public abstract class PageLevelNavigationViewComponentBase : - NavigationTopicViewComponentBase where T : class, INavigationTopicViewModel, new() + NavigationTopicViewComponentBase where T : class, IHierarchicalTopicViewModel, new() { /*========================================================================================================================== diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs index 5915be7f..641d50af 100644 --- a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs @@ -13,7 +13,8 @@ namespace OnTopic.AspNetCore.Mvc.Models { | VIEW MODEL: NAVIGATION TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a tier of navigation. + /// Provides a strongly-typed view model for feeding views with information expected to be required for each node in of + /// navigation. /// /// /// @@ -21,13 +22,15 @@ namespace OnTopic.AspNetCore.Mvc.Models { /// constructed by the . /// /// - /// The can be any view model that implements , - /// which provides a base level of support for properties associated with the typical Page content type as well as - /// a method for determining if a given instance is the currently-selected - /// topic. Implementations may support additional properties, as appropriate. + /// The can be any view model that implements , + /// which ensures support for hierarchical coverage. In practice, we expect most implementers will choose to implement + /// instead, which addresses not only , but also provides a base level of support for properties that most navigation views will need, as well as a + /// method for determining if a given instance is the currently-selected + /// topic. Derived implementations may introduce additional properties, as appropriate. /// /// - public class NavigationViewModel where T : class, INavigationTopicViewModel { + public class NavigationViewModel where T : class, IHierarchicalTopicViewModel { /*========================================================================================================================== | NAVIGATION ROOT @@ -37,9 +40,9 @@ public class NavigationViewModel where T : class, INavigationTopicViewModel /// /// Since this implements , it may include multiple levels of children. By - /// implementing it as a generic, each site or application can provide its own - /// implementation, thus potentially extending the schema with properties relevant to that site's navigation. For example, - /// a site may optionally add an IconUrl property if it wishes to assign unique icons to each link in the + /// implementing it as a generic, each site or application can provide its own implementation, thus potentially extending the schema with properties relevant to that site's navigation. For + /// example, a site may optionally add an IconUrl property if it wishes to assign unique icons to each link in the /// navigation. /// public T? NavigationRoot { get; set; } @@ -58,7 +61,7 @@ public class NavigationViewModel where T : class, INavigationTopicViewModel /// - /// It's worth noting that while this could be stored on the itself, + /// It's worth noting that while this could be stored on the itself, /// that would prevent those values from being cached. As such, it's preferrable to keep the navigation nodes stateless, /// and maintaining state exclusively in the itself. /// diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 5df1926e..c4144944 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -18,13 +18,13 @@ namespace OnTopic.ViewModels { /// /// /// No topics are expected to have a Navigation content type. Instead, this view model is expected to be manually - /// constructed by e.g. a LayoutController. + /// constructed by e.g. a MenuViewComponent. /// /// - /// Since C# doesn't support return-type covariance, this class can't be derived in a meaningful way (i.e., if it were to - /// be, the property would still return a of - /// instances). Instead, the preferred way to extend the functionality is to create - /// a new implementation of . To help communicate this, the property would still return a of instances). Instead, the preferred way to extend the functionality is + /// to create a new implementation of . To help communicate this, the class is marked as sealed. /// /// diff --git a/OnTopic/Models/INavigationTopicViewModel{T}.cs b/OnTopic/Models/INavigationTopicViewModel{T}.cs index bd82c93a..9ecf16f0 100644 --- a/OnTopic/Models/INavigationTopicViewModel{T}.cs +++ b/OnTopic/Models/INavigationTopicViewModel{T}.cs @@ -10,7 +10,7 @@ namespace OnTopic.Models { | INTERFACE: NAVIGATION TOPIC VIEW MODEL \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for feeding views information about a navigation entry. + /// Provides a generic model for feeding views information about a navigation entry. /// /// /// No topics are expected to have a Navigation content type. Instead, implementers of this view model are expected From 045100b3b37b9a3aebee0287014fc11c8d9452e6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 14:20:42 -0800 Subject: [PATCH 743/778] Move location of `WebPath` property for consistency We typically put collection properties after scalar properties. --- OnTopic.ViewModels/NavigationTopicViewModel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index c4144944..7f50cd18 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -44,6 +44,12 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel public string? ShortTitle { get; init; } + /*========================================================================================================================== + | WEB PATH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + public string? WebPath { get; init; } + /*========================================================================================================================== | CHILDREN \-------------------------------------------------------------------------------------------------------------------------*/ @@ -52,12 +58,6 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel public Collection Children { get; } = new(); - /*========================================================================================================================== - | WEB PATH - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - public string? WebPath { get; init; } - /*========================================================================================================================== | IS SELECTED? \-------------------------------------------------------------------------------------------------------------------------*/ From ebdb6a67e899915fdfb9e8683a7ce02168e7643e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 14:54:24 -0800 Subject: [PATCH 744/778] Introduced icon for the NuGet packages By placing the icon in the root, explicitly setting it to be packed, and then referencing it as the `` in the `Directory.Build.props`, we can ensure that every project ends up with the same icon. For now, we're using the Ignia logo as the icon, and have set it to 128x128 per NuGet recommendations. Note that in order to pack the file, the `` reference needed to crawl back one directory, since the `` will be evaluated in context of each individual `csproj`. The `` doesn't require this, however, since the `` reference will ensure it's in the root of the `*.nupkg` file. --- Directory.Build.props | 5 +++++ Icon.png | Bin 0 -> 5460 bytes OnTopic.sln | 1 + 3 files changed, 6 insertions(+) create mode 100644 Icon.png diff --git a/Directory.Build.props b/Directory.Build.props index c8e6e15f..3e4cb733 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,8 +18,13 @@ true true true + Icon.png + + + + true diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..63cd745af81263c82802b40a9955b80d0309aa06 GIT binary patch literal 5460 zcmch5dpwi<|NpgN4&_V{F*8NXHs-K}usP*?D&4ueH#;z=u}wzNkV8ZiIV4f(zLlav zAz2BPLk^v!)j}mDStGWZxnta;i1ZMqb3B{w z8}H#qjSr*ZX^2fWa4Q}GOb|imQsBIZ@JJ4UXN~xlmjGTb-$oIBe)611dGOC(I)07tQi4^C758~KVArMIyQ|# z@F6+=m<;@6jR@g#qX=kpY;3G?tf?`J9gN1}@pv@G1Z`r10ud-qTqKvmLq&3Qe`X-j zIaD??ipyk0!k05rf><$JYXnI3yM~CUUuh#bKYRimL-Qz6Xsj`2S<_FTyZiqPjfnV# z=5T%JfAjsfi8+38QFOEqox_S@Q|TaF_vcg`uES1{;t#R^L9oM4dL;K(IF0%%HY$c4 z{>>&D6-^JPM}RO6s0#awXH*D_%i@Hv{=tM_!@sPEA`sbh3YW$9W3j@2F0y3`f9PqA->O4CXu3oke3Z z;(kI+uqX@&HSxpZ2&Om!=KqC)HAkaxDgO!B-JReP$>CBWsdN{TH3B4M%w*CCG&&WJ zF{R*8SUTMtWlF_iP?k7`DaynQN1>Q8EX;#w7TCX4%2z`0sU3V^P5zzf2-n z1krJ(I15u0-prDM!kFP0s30tkjtat<({MOTj0Fv6`ZJp+n+a|`O8CEKU1miCGvZC` z94sAiL`OW)4vTfb*_&X@94$zAEY==_vv4p$z-d$hgT;=ZfMvprpaj#=QIWw2_}@&3 ztZ)_^v=a0iobm7bE<~aio5f&;g998NCwsVy1JN9dH#bL_7-N^W3`k*FDxJ;TNvAoo zSrPDWrA%P{8}ok9#Q!tR@3Ejwzl{6;B-;IBEczE2XC?=%x40i2!IvKW<0_m9aszh? zfkIv0-Jl=}$QkkD=D#CsYJsN*nVDiyR2&|I!UW-Ps34q~1&U^2f;Xe#EzK;C8+#!}dHyI`;~{`-C~!5W+V>t^^KL=B-t2GhZIi$*NVTy9JM zdoll6>HeRKK`*QMzDVfr#q{_3eH;6ih74-|b_?wC%a^~NFW`q?PbhjM=o1@!{^Z^0 z3FUz3t*$QUjiNiMv)*qWT67+Z?`nX?Ry`bIqxHlj60>pQ=eYE*Q%^v*FKh7=y7e#aoy=nk7Ig<-nM@BmT-mM zjd2(kgBp590KMY9V#dHJv6vTUlzD3cQsrrOUwhNtwM-i#s!-6AmU7w>#!wrHIH$|tC%@BTcj1qcrhqd^t34?**!2GJ z%*U=8wU*9$wzAKZ1!o1>f_yBi8yKfFeqhgRxL|c?Gw}ZWN*=(IUi@IW>W}iC30i<+ zCA(xZe`YjZB**FXw1VM_HYB|rkrbeH6FW8o3XW z-s`J^AGl0$Jdln;B|ce{P0Uk*Z&-RDHn+wFL#PI>(wKXXGWMptQdGD%{2q#zn1sxg z-F;@VdS+lK>9__@vZo^-@V?b*hm^xK74sgCj1c*-G7>gd3*uezSKpLj-vljF)Gp9F zTyWQ_#G`zpo~>WV-P*wvqH}2^Z8r+Gg8tE9e4PQLV=s(+EcvX=saMJ_Nm*2k?}G|o zD5-VOr2Of8`5sbOd3)31>bt{UDL!t4wQVU8T5j4~WV-9mtF@6Eyd{014>6MS8k>=P zcE@ieu)s1UK?k<4+5!4zf%fQ3r66ebbH?+$d1x@nK*9&=SJv>mOpC`px>xq+7a#q) zPCSwvJH9IMkOWHbl7A*I{3x_mm8TGp0%L>(OkHzW^Qit!>=0smcTjh|{vkQ_kF8Su zLr|CXeEGl?fs(tgy%9|cwy7R#l$Cz{q(`OPw_<#sxVh2I4ciOOJfH(QJX+is>IjGhX2aqY_`Y8Ty2*Hxz&meDJ0a*(A>aZgG=_n70JT1J^JWEwhV?IbIzi;9v{WF~OtH>RZyVuOkH zopyW!fwf#onJ<4(DmV4mmgiET=dBLAcg)>$K?*i^Y~QFY+r*y_l7P>&zIzpR1Zd7v z6Z9h$%A+&X=iMKZUo~C0UUbJuQlJ&$DIWH0$azW3uR=t@{Y3{~)RmVUB?eR`EvbThT zy{>%T0hew^>4UbwF?RZyOG&cMl(b=`au3&l@gkYR#7naDYUu6bw46D+8lT?b=5FV` z)^f_WAENK;sM>XI-qm^_V|%@hXXb46XE|l^=7Ke+PMhDd&&nL%&GB2% zyCpwe5?4{(CVn%Y%kNV}5(ME{Bdrp*#2$uqUn41$Khejf=S~D}_1;RDAv)zgl*oSA zbL3&l7=){r3P9-Z87Z^kYKb`?&X3-haAX6&p{(y@78-_?UCigbd+n4ZU%)^H%Nc1) zjbmksvv~s8#n~g_Ep@^BfELw>f?5&eBjqt^Fo?@&gNw2BE&`z;q3G_ zQLkPw=(@SQ$JXma@x?Z9**l81P;#IDH7XvmfoU|)n6qo2nk*MuHIiQ^c~1bJUxb}5 zzTJ|eOs?fRr%LObrj-m`S3Durur3MExxTV_ZH}^tTp;vuJkpN6lEV{weTc*_lQ}A& z&uw9%>D}k&KG}$#GA?q5;+Yy-Y}KA7K8S|m_MY(XX<{?N>(gfxYp9T{7^#LFp!V4I zJ*NZwj)(ac^ar2ub~viT_kYNa4mC=(=zLY9;CGs+P}RWV54wz1Z4%G`0RUDC_U!utVM z6Z<8va1?Y{TDRU;8JwNu<4~P3Y1&;MfGBhEteV2 z(kTr5TwG*Y8KFdmEo`p#NO+&#F9?nGRC&qPW;+OvI5*Ul8HopEJcf-RcAD;9b6;7x z&h~YZ2EPU_VdB~NZp7R==}7ji(V(P|gjaW`DmCTqYpSPb*vNc(R675@G5+lkQUFY& z#~7Xuat$j}EK0WJCA3!CLrY~8antf556$$+#ob!_F3A%9lvi6}iyELj8L&dGeS;O1 zcz54hMSWo-UTs2fur5G__(DbM!?E)H!mr@ZrWZZ_=Yzv;p4lRjL0Z?*Z(7ByL7*|4 zSmIxTRGRAVs)Z|yj76b=VXG=<4ERkLepOS!m5ENOPLxyX5)aF1jrGn`XS zcYgbERp+{zETdXGJCC)xuE)*~zE_)_$5C61U|(c*8%k$sr;A&g>m=b?qrVHg%zwx&lJ;fwLK1X6f4}gBG^*rtLU$Bf`H>iF_O@ zW+{A=Sm<_*?eCS9c2+C=UB$L$q39}1y7rD0M=R2QMXZ01GZ24BNjg`rA6e_pw?p>a zuHT8JCm{2o!z$J9PloZO_Yprbd8s(t8($@5J|9^bUytqxJF%3|u%KtJIsd^E=*rV*4`ixg-+BAt zU|}t3^;+>~BsujYx#7CK4e`;<(?TqiUzep$(j*oVYCn1g9zJVu>57ev=3HORuIW6W z{sMfPSbD+m(cHBI5MYPhhpEO|*7digrTO9&i^nS3{jc03J}t~kgiijJBhlKBlX@8` zfRCuKD-u3E)bxqT_%kXc{YjO6*sexE9f8DkI;c5lAdtn*rbWH3i*Ed>_w296so%z_&YlM^GuPeCyVOapC zL8by&)E#kYn*9A^h&q_Y!1NUy~)Gu7pnH<~#zlFdf z*_T%_8!yN-?yz>aF4ec6E4zeJGWewEWA!MTBrAWiE8hsZck9X6;$zKe;1{xdG29Y& z-$5t-&Xa>38)jz>-7*P}^MQik#LCH5>)+EB6`U+B4}4th;<-luYz$#VmAEr@+sq!F X){-k0F8f`U|Mzrp@F1PDCnx_GF=FiH literal 0 HcmV?d00001 diff --git a/OnTopic.sln b/OnTopic.sln index 076935f4..aeb7d4e1 100644 --- a/OnTopic.sln +++ b/OnTopic.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore Directory.Build.props = Directory.Build.props GitVersion.yml = GitVersion.yml + Icon.png = Icon.png README.md = README.md EndProjectSection EndProject From 23e1d274822a075f1ee67d263628e216d1b62a81 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 14:55:43 -0800 Subject: [PATCH 745/778] Ensure that the git repository is published We could hard-code this, but SourceLink can instead automatically generate the reference. This is needed for SourceLink to provide a reference to the exact code referenced by the symbols package. --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index 3e4cb733..4cdabdb3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -17,6 +17,7 @@ en true true + true true Icon.png From 9878d2a37480ebb8800d25f17b923937642428fb Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 15:14:10 -0800 Subject: [PATCH 746/778] Avoid the "gets or sets" verbiage for records Records are immutable, so while their properties can be set during initialization, they'll be in a read-only state for most developers. Given that, prefer just "gets" over "gets or sets". (This could, alternatively, use the more accurate "gets or initializes".) --- OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs index 55a9c4e0..04a3bdc8 100644 --- a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs @@ -27,7 +27,7 @@ public record AssociatedTopicBindingModel : IAssociatedTopicBindingModel { | PROPERTY: UNIQUE KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets or sets the topic's attribute, the unique text identifier for the topic. + /// Gets the topic's attribute, the unique text identifier for the topic. /// /// /// value is not null From b8d9036c1e37c4f0496609e34275e697424c54fd Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 22 Feb 2021 15:26:05 -0800 Subject: [PATCH 747/778] Prefer term "model" over "data transfer object" for view models The term "data transfer object" has a specific meaning which doesn't necessarily apply to view models. We can quibble over the semantics, but to avoid confusion, we can just use the more general term, "model". --- .../BindingModels/AssociatedTopicBindingModel.cs | 4 ++-- OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs | 4 ++-- OnTopic.ViewModels/Collections/TopicViewModelCollection.cs | 4 ++-- OnTopic.ViewModels/ContentListTopicViewModel.cs | 2 +- OnTopic.ViewModels/IndexTopicViewModel.cs | 2 +- OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs | 3 ++- OnTopic.ViewModels/Items/ItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/Items/ListTopicViewModel.cs | 2 +- OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/Items/SlideTopicViewModel.cs | 3 ++- OnTopic.ViewModels/NavigationTopicViewModel.cs | 2 +- OnTopic.ViewModels/PageGroupTopicViewModel.cs | 2 +- OnTopic.ViewModels/PageTopicViewModel.cs | 2 +- OnTopic.ViewModels/SectionTopicViewModel.cs | 2 +- OnTopic.ViewModels/SlideshowTopicViewModel.cs | 2 +- OnTopic.ViewModels/TopicViewModel.cs | 2 +- OnTopic.ViewModels/VideoTopicViewModel.cs | 2 +- 17 files changed, 22 insertions(+), 20 deletions(-) diff --git a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs index 04a3bdc8..752fc4b1 100644 --- a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs @@ -10,10 +10,10 @@ namespace OnTopic.ViewModels.BindingModels { /*============================================================================================================================ - | CLASS: ASSOCIATED TOPIC BINDING MODEL + | BINDING MODEL: ASSOCIATED TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for binding an association of a binding model to an existing . + /// Provides a model for binding an association of a to another . /// /// /// While implementors may choose to create a custom implementation, the out-of- diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs index ae6baf18..7cd56b3c 100644 --- a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs @@ -10,10 +10,10 @@ namespace OnTopic.ViewModels.BindingModels { /*============================================================================================================================ - | CLASS: RELATED TOPIC BINDING MODEL + | BINDING MODEL: RELATED TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for binding a relationship of a binding model to an existing . + /// Provides a model for binding a relationship of a to an existing . /// /// /// While implementors may choose to create a custom implementation, the out-of- diff --git a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs index eafebd74..c00fb198 100644 --- a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs @@ -16,8 +16,8 @@ namespace OnTopic.ViewModels.Collections { | VIEW MODEL: TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a basic collection interface for use with data transfer objects implementing , - /// including and derivatives. + /// Provides a basic collection interface for use with models implementing , including and derivatives. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/ContentListTopicViewModel.cs index 6849325d..c8e4aee2 100644 --- a/OnTopic.ViewModels/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentListTopicViewModel.cs @@ -12,7 +12,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: CONTENT LIST TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a content list topic. + /// Provides a strongly-typed model for feeding views with information about a ContentList topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/IndexTopicViewModel.cs b/OnTopic.ViewModels/IndexTopicViewModel.cs index 9c1cde35..215adeb0 100644 --- a/OnTopic.ViewModels/IndexTopicViewModel.cs +++ b/OnTopic.ViewModels/IndexTopicViewModel.cs @@ -10,7 +10,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: INDEX TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about an index topic. + /// Provides a strongly-typed model for feeding views with information about an Index topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs index b7185e34..f12126ec 100644 --- a/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs @@ -11,7 +11,8 @@ namespace OnTopic.ViewModels.Items { | VIEW MODEL: CONTENT ITEM TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a content item topic. + /// Provides a strongly-typed model for feeding views with information about a ContentItem topic, as used in the model, and its derivatives. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/Items/ItemTopicViewModel.cs b/OnTopic.ViewModels/Items/ItemTopicViewModel.cs index 723d7fcc..6a92b013 100644 --- a/OnTopic.ViewModels/Items/ItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ItemTopicViewModel.cs @@ -10,7 +10,7 @@ namespace OnTopic.ViewModels.Items { | VIEW MODEL: ITEM TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about an item topic. + /// Provides a strongly-typed model for feeding views with information about an Item topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/Items/ListTopicViewModel.cs b/OnTopic.ViewModels/Items/ListTopicViewModel.cs index dc8a6ffe..7f4d5268 100644 --- a/OnTopic.ViewModels/Items/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/ListTopicViewModel.cs @@ -11,7 +11,7 @@ namespace OnTopic.ViewModels.Items { | VIEW MODEL: LIST \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for modeling a nested topic list. + /// Provides a strongly-typed model for modeling a List topic, as used for nested topics. /// /// /// diff --git a/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs b/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs index 7004c32e..d11b5c5e 100644 --- a/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs @@ -10,7 +10,7 @@ namespace OnTopic.ViewModels.Items { | VIEW MODEL: LOOKUP LIST ITEM TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about lookup list item topic. + /// Provides a strongly-typed model for feeding views with information about LookupListItem topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/Items/SlideTopicViewModel.cs b/OnTopic.ViewModels/Items/SlideTopicViewModel.cs index 77927f07..1b23b82b 100644 --- a/OnTopic.ViewModels/Items/SlideTopicViewModel.cs +++ b/OnTopic.ViewModels/Items/SlideTopicViewModel.cs @@ -10,7 +10,8 @@ namespace OnTopic.ViewModels.Items { | VIEW MODEL: SLIDE TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a slide topic. + /// Provides a strongly-typed model for feeding views with information about a Slide topic, as used in e.g. . /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index 7f50cd18..bbce1d51 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -13,7 +13,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: NAVIGATION TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about the navigation. + /// Provides a strongly-typed model for feeding views with information about a node in the navigation. /// /// /// diff --git a/OnTopic.ViewModels/PageGroupTopicViewModel.cs b/OnTopic.ViewModels/PageGroupTopicViewModel.cs index 4b5435b7..175c32c3 100644 --- a/OnTopic.ViewModels/PageGroupTopicViewModel.cs +++ b/OnTopic.ViewModels/PageGroupTopicViewModel.cs @@ -10,7 +10,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: PAGE GROUP TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a page group topic. + /// Provides a strongly-typed model for feeding views with information about a PageGroup topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/PageTopicViewModel.cs b/OnTopic.ViewModels/PageTopicViewModel.cs index 2e56d20c..16a4378f 100644 --- a/OnTopic.ViewModels/PageTopicViewModel.cs +++ b/OnTopic.ViewModels/PageTopicViewModel.cs @@ -11,7 +11,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: PAGE TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a page topic. + /// Provides a strongly-typed model for feeding views with information about a Page topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/SectionTopicViewModel.cs b/OnTopic.ViewModels/SectionTopicViewModel.cs index 5aca691d..8edc18a8 100644 --- a/OnTopic.ViewModels/SectionTopicViewModel.cs +++ b/OnTopic.ViewModels/SectionTopicViewModel.cs @@ -11,7 +11,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: SECTION TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a section topic. + /// Provides a strongly-typed model for feeding views with information about a Section topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/SlideshowTopicViewModel.cs b/OnTopic.ViewModels/SlideshowTopicViewModel.cs index 7ec11449..5d2574fd 100644 --- a/OnTopic.ViewModels/SlideshowTopicViewModel.cs +++ b/OnTopic.ViewModels/SlideshowTopicViewModel.cs @@ -10,7 +10,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: SLIDESHOW TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a content list topic. + /// Provides a strongly-typed model for feeding views with information about a Slideshow item. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index e4be7670..651679de 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -13,7 +13,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a generic data transfer topic for feeding views. + /// Provides a model for feeding views general information about a . /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains diff --git a/OnTopic.ViewModels/VideoTopicViewModel.cs b/OnTopic.ViewModels/VideoTopicViewModel.cs index f34120b4..83c3f738 100644 --- a/OnTopic.ViewModels/VideoTopicViewModel.cs +++ b/OnTopic.ViewModels/VideoTopicViewModel.cs @@ -11,7 +11,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: VIDEO TOPIC \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a strongly-typed data transfer object for feeding views with information about a video topic. + /// Provides a strongly-typed model for feeding views with information about a Video topic. /// /// /// Typically, view models should be created as part of the presentation layer. The namespace contains From 91ea5fcfa6de8b0240e5ae3b7b0fb9a6cbe16b9f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 10:37:21 -0800 Subject: [PATCH 748/778] Collapsed `OnTopic.ViewModels.Items` back into `OnTopic.ViewModels` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Originally, `OnTopic.ViewModels.Items` was in the `OnTopic.ViewModels` namespace. To help organize the growing library, I moved the item types into the new `Items` folder—and, with that, namespace. But these don't really make sense in their own namespace, even if it makes sense organizationally. Given that, I've renamed the folder from `Items` to `_items` to help differentiate it from namespaces, and moved each of the items back into the `OnTopic.ViewModels` namespace. --- OnTopic.Tests/TopicMappingServiceTest.cs | 1 - OnTopic.ViewModels/ContentListTopicViewModel.cs | 1 - OnTopic.ViewModels/TopicViewModelLookupService.cs | 1 - .../{Items => _items}/ContentItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/{Items => _items}/ItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/{Items => _items}/ListTopicViewModel.cs | 2 +- .../{Items => _items}/LookupListItemTopicViewModel.cs | 2 +- OnTopic.ViewModels/{Items => _items}/SlideTopicViewModel.cs | 2 +- 8 files changed, 5 insertions(+), 8 deletions(-) rename OnTopic.ViewModels/{Items => _items}/ContentItemTopicViewModel.cs (98%) rename OnTopic.ViewModels/{Items => _items}/ItemTopicViewModel.cs (97%) rename OnTopic.ViewModels/{Items => _items}/ListTopicViewModel.cs (97%) rename OnTopic.ViewModels/{Items => _items}/LookupListItemTopicViewModel.cs (97%) rename OnTopic.ViewModels/{Items => _items}/SlideTopicViewModel.cs (97%) diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index c0ff7c2f..dd2a8b8b 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -23,7 +23,6 @@ using OnTopic.Tests.ViewModels; using OnTopic.Tests.ViewModels.Metadata; using OnTopic.ViewModels; -using OnTopic.ViewModels.Items; namespace OnTopic.Tests { diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/ContentListTopicViewModel.cs index c8e4aee2..138c4a8d 100644 --- a/OnTopic.ViewModels/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentListTopicViewModel.cs @@ -4,7 +4,6 @@ | Project Topics Library \=============================================================================================================================*/ using OnTopic.ViewModels.Collections; -using OnTopic.ViewModels.Items; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs index f8188c8b..341550dd 100644 --- a/OnTopic.ViewModels/TopicViewModelLookupService.cs +++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.Reflection; using OnTopic.Lookup; -using OnTopic.ViewModels.Items; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs similarity index 98% rename from OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs rename to OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs index f12126ec..587163e6 100644 --- a/OnTopic.ViewModels/Items/ContentItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System; -namespace OnTopic.ViewModels.Items { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: CONTENT ITEM TOPIC diff --git a/OnTopic.ViewModels/Items/ItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/Items/ItemTopicViewModel.cs rename to OnTopic.ViewModels/_items/ItemTopicViewModel.cs index 6a92b013..5c0dd63c 100644 --- a/OnTopic.ViewModels/Items/ItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels.Items { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: ITEM TOPIC diff --git a/OnTopic.ViewModels/Items/ListTopicViewModel.cs b/OnTopic.ViewModels/_items/ListTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/Items/ListTopicViewModel.cs rename to OnTopic.ViewModels/_items/ListTopicViewModel.cs index 7f4d5268..cdf96d50 100644 --- a/OnTopic.ViewModels/Items/ListTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/ListTopicViewModel.cs @@ -5,7 +5,7 @@ \=============================================================================================================================*/ using System.Collections; -namespace OnTopic.ViewModels.Items { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: LIST diff --git a/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs b/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs rename to OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs index d11b5c5e..20845298 100644 --- a/OnTopic.ViewModels/Items/LookupListItemTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels.Items { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: LOOKUP LIST ITEM TOPIC diff --git a/OnTopic.ViewModels/Items/SlideTopicViewModel.cs b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs similarity index 97% rename from OnTopic.ViewModels/Items/SlideTopicViewModel.cs rename to OnTopic.ViewModels/_items/SlideTopicViewModel.cs index 1b23b82b..be402f25 100644 --- a/OnTopic.ViewModels/Items/SlideTopicViewModel.cs +++ b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs @@ -4,7 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ -namespace OnTopic.ViewModels.Items { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: SLIDE TOPIC From fa6c13defb6aa6f8373785b8ae812346bb330d02 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 10:40:12 -0800 Subject: [PATCH 749/778] Collapsed `OnTopic.ViewModels.Collections` back into `OnTopic.ViewModels` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Originally, `OnTopic.ViewModels.Collections` was in the `OnTopic.ViewModels` namespace. To help organize the growing library, I moved the `TopicViewModelCollection` into the new `Collections` folder—and, with that, namespace. But this doesn't really make sense in its own namespace, even if it makes sense organizationally. Given that, I've renamed the folder from `Collections` to `_collections` to help differentiate it from namespaces, and moved the `TopicViewModelCollection` type back into the `OnTopic.ViewModels` namespace. --- OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs | 1 - OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs | 1 - OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs | 1 - OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs | 1 - OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs | 1 - OnTopic.ViewModels/ContentListTopicViewModel.cs | 1 - .../{Collections => _collections}/TopicViewModelCollection.cs | 2 +- 7 files changed, 1 insertion(+), 7 deletions(-) rename OnTopic.ViewModels/{Collections => _collections}/TopicViewModelCollection.cs (98%) diff --git a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs index 05b4997a..0b29920d 100644 --- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; -using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs index 355eeb8e..0c613841 100644 --- a/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredContentTypeTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; -using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs index d5f7d26e..9a2d2049 100644 --- a/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; -using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs index 9fb9bb11..4b87fc24 100644 --- a/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/FilteredTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; -using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs index eb3867aa..cce94c39 100644 --- a/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs +++ b/OnTopic.Tests/ViewModels/MetadataLookupTopicViewModel.cs @@ -5,7 +5,6 @@ \=============================================================================================================================*/ using OnTopic.Mapping.Annotations; using OnTopic.ViewModels; -using OnTopic.ViewModels.Collections; namespace OnTopic.Tests.ViewModels { diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/ContentListTopicViewModel.cs index 138c4a8d..89d8112e 100644 --- a/OnTopic.ViewModels/ContentListTopicViewModel.cs +++ b/OnTopic.ViewModels/ContentListTopicViewModel.cs @@ -3,7 +3,6 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using OnTopic.ViewModels.Collections; namespace OnTopic.ViewModels { diff --git a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs similarity index 98% rename from OnTopic.ViewModels/Collections/TopicViewModelCollection.cs rename to OnTopic.ViewModels/_collections/TopicViewModelCollection.cs index c00fb198..137301f7 100644 --- a/OnTopic.ViewModels/Collections/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs @@ -10,7 +10,7 @@ using OnTopic.Internal.Diagnostics; using OnTopic.Models; -namespace OnTopic.ViewModels.Collections { +namespace OnTopic.ViewModels { /*============================================================================================================================ | VIEW MODEL: TOPIC COLLECTION From 302cc50f21aac8cc4b774e3311f09613dde727e2 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 11:07:31 -0800 Subject: [PATCH 750/778] Moved exceptions into a new `_exceptions` folder This doesn't change the namespace, just helps organize the file structure a bit. The `_camelCase` folders are being used to differentiate namespaces from organizational folders. --- OnTopic/Mapping/README.md | 6 +++--- OnTopic/Mapping/{ => _exceptions}/InvalidTypeException.cs | 0 .../{ => _exceptions}/MappingModelValidationException.cs | 0 OnTopic/Mapping/{ => _exceptions}/TopicMappingException.cs | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename OnTopic/Mapping/{ => _exceptions}/InvalidTypeException.cs (100%) rename OnTopic/Mapping/{ => _exceptions}/MappingModelValidationException.cs (100%) rename OnTopic/Mapping/{ => _exceptions}/TopicMappingException.cs (100%) diff --git a/OnTopic/Mapping/README.md b/OnTopic/Mapping/README.md index 0447b85a..4090ac46 100644 --- a/OnTopic/Mapping/README.md +++ b/OnTopic/Mapping/README.md @@ -193,6 +193,6 @@ While the `CachedTopicMappingService` can be useful for particular scenarios, it 3. If a graph is manually constructed (by e.g. programmatically mapping `Children`) then the first instance of a topic will be cached independent of the parent graph, thus potentially allowing it to be shared between multiple graphs. This can introduce concerns if edge maintenance is important (e.g., one instance should include children, while another does not). ## Exceptions -The topic mapping services will throw a [`TopicMappingException`](TopicMappingException.cs) if a foreseeable exception occurs. Specifically, the exceptions expected will be: -- **[`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`. -- **[`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. \ No newline at end of file +The topic mapping services will throw a [`TopicMappingException`](_exceptions/TopicMappingException.cs) if a foreseeable exception occurs. Specifically, the exceptions expected will be: +- **[`InvalidTypeException`](_exceptions/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`. +- **[`MappingModelValidationException`](_exceptions/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. \ No newline at end of file diff --git a/OnTopic/Mapping/InvalidTypeException.cs b/OnTopic/Mapping/_exceptions/InvalidTypeException.cs similarity index 100% rename from OnTopic/Mapping/InvalidTypeException.cs rename to OnTopic/Mapping/_exceptions/InvalidTypeException.cs diff --git a/OnTopic/Mapping/MappingModelValidationException.cs b/OnTopic/Mapping/_exceptions/MappingModelValidationException.cs similarity index 100% rename from OnTopic/Mapping/MappingModelValidationException.cs rename to OnTopic/Mapping/_exceptions/MappingModelValidationException.cs diff --git a/OnTopic/Mapping/TopicMappingException.cs b/OnTopic/Mapping/_exceptions/TopicMappingException.cs similarity index 100% rename from OnTopic/Mapping/TopicMappingException.cs rename to OnTopic/Mapping/_exceptions/TopicMappingException.cs From d9453667ae6f5862dbcbd27fd98e26133dd16a91 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 11:09:56 -0800 Subject: [PATCH 751/778] Moved event arguments into a new `_eventArgs` folder This doesn't change the namespace, just helps organize the file structure a bit. The `_camelCase` folders are being used to differentiate namespaces from organizational folders. --- OnTopic/Repositories/{ => _eventArgs}/TopicEventArgs.cs | 0 OnTopic/Repositories/{ => _eventArgs}/TopicLoadEventArgs.cs | 0 OnTopic/Repositories/{ => _eventArgs}/TopicMoveEventArgs.cs | 0 OnTopic/Repositories/{ => _eventArgs}/TopicRenameEventArgs.cs | 0 OnTopic/Repositories/{ => _eventArgs}/TopicSaveEventArgs.cs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename OnTopic/Repositories/{ => _eventArgs}/TopicEventArgs.cs (100%) rename OnTopic/Repositories/{ => _eventArgs}/TopicLoadEventArgs.cs (100%) rename OnTopic/Repositories/{ => _eventArgs}/TopicMoveEventArgs.cs (100%) rename OnTopic/Repositories/{ => _eventArgs}/TopicRenameEventArgs.cs (100%) rename OnTopic/Repositories/{ => _eventArgs}/TopicSaveEventArgs.cs (100%) diff --git a/OnTopic/Repositories/TopicEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicEventArgs.cs similarity index 100% rename from OnTopic/Repositories/TopicEventArgs.cs rename to OnTopic/Repositories/_eventArgs/TopicEventArgs.cs diff --git a/OnTopic/Repositories/TopicLoadEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs similarity index 100% rename from OnTopic/Repositories/TopicLoadEventArgs.cs rename to OnTopic/Repositories/_eventArgs/TopicLoadEventArgs.cs diff --git a/OnTopic/Repositories/TopicMoveEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs similarity index 100% rename from OnTopic/Repositories/TopicMoveEventArgs.cs rename to OnTopic/Repositories/_eventArgs/TopicMoveEventArgs.cs diff --git a/OnTopic/Repositories/TopicRenameEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs similarity index 100% rename from OnTopic/Repositories/TopicRenameEventArgs.cs rename to OnTopic/Repositories/_eventArgs/TopicRenameEventArgs.cs diff --git a/OnTopic/Repositories/TopicSaveEventArgs.cs b/OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs similarity index 100% rename from OnTopic/Repositories/TopicSaveEventArgs.cs rename to OnTopic/Repositories/_eventArgs/TopicSaveEventArgs.cs From a165f74d59aff0c55f98dbb4078fa16c518c6a7b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 11:11:30 -0800 Subject: [PATCH 752/778] Moved exceptions into a new `_exceptions` folder This doesn't change the namespace, just helps organize the file structure a bit. The `_camelCase` folders are being used to differentiate namespaces from organizational folders. --- .../{ => _exceptions}/ReferentialIntegrityException.cs | 0 OnTopic/Repositories/{ => _exceptions}/TopicNotFoundException.cs | 0 .../Repositories/{ => _exceptions}/TopicRepositoryException.cs | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename OnTopic/Repositories/{ => _exceptions}/ReferentialIntegrityException.cs (100%) rename OnTopic/Repositories/{ => _exceptions}/TopicNotFoundException.cs (100%) rename OnTopic/Repositories/{ => _exceptions}/TopicRepositoryException.cs (100%) diff --git a/OnTopic/Repositories/ReferentialIntegrityException.cs b/OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs similarity index 100% rename from OnTopic/Repositories/ReferentialIntegrityException.cs rename to OnTopic/Repositories/_exceptions/ReferentialIntegrityException.cs diff --git a/OnTopic/Repositories/TopicNotFoundException.cs b/OnTopic/Repositories/_exceptions/TopicNotFoundException.cs similarity index 100% rename from OnTopic/Repositories/TopicNotFoundException.cs rename to OnTopic/Repositories/_exceptions/TopicNotFoundException.cs diff --git a/OnTopic/Repositories/TopicRepositoryException.cs b/OnTopic/Repositories/_exceptions/TopicRepositoryException.cs similarity index 100% rename from OnTopic/Repositories/TopicRepositoryException.cs rename to OnTopic/Repositories/_exceptions/TopicRepositoryException.cs From ddabde10deaacf4aa34c2278fe3045c496274fc6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 11:27:50 -0800 Subject: [PATCH 753/778] Moved content type view models into a `_contentTypes` folder This doesn't change the namespace, just helps organize the file structure a bit. The `_camelCase` folders are being used to differentiate namespaces from organizational folders. --- OnTopic.ViewModels/README.md | 26 +++++++++---------- .../ContentListTopicViewModel.cs | 0 .../IndexTopicViewModel.cs | 0 .../PageGroupTopicViewModel.cs | 0 .../{ => _contentTypes}/PageTopicViewModel.cs | 0 .../SectionTopicViewModel.cs | 0 .../SlideshowTopicViewModel.cs | 0 .../VideoTopicViewModel.cs | 0 8 files changed, 13 insertions(+), 13 deletions(-) rename OnTopic.ViewModels/{ => _contentTypes}/ContentListTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/IndexTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/PageGroupTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/PageTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/SectionTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/SlideshowTopicViewModel.cs (100%) rename OnTopic.ViewModels/{ => _contentTypes}/VideoTopicViewModel.cs (100%) diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md index 7da678b7..545a142a 100644 --- a/OnTopic.ViewModels/README.md +++ b/OnTopic.ViewModels/README.md @@ -31,21 +31,21 @@ Installation can be performed by providing a ` to the `OnTo ## Inventory - [`TopicViewModel`](TopicViewModel.cs) - - [`PageTopicViewModel`](PageTopicViewModel.cs) - - [`ContentListTopicViewModel`](ContentListTopicViewModel.cs) ([`ContentItemTopicViewModel`](Items/ContentItemTopicViewModel.cs)) - - [`IndexTopicViewModel`](IndexTopicViewModel.cs) - - [`SlideshowTopicViewModel`](SlideshowTopicViewModel.cs) ([`SlideTopicViewModel`](Items/SlideTopicViewModel.cs)) - - [`VideoTopicViewModel`](VideoTopicViewModel.cs) - - [`SectionTopicViewModel`](SectionTopicViewModel.cs) - - [`PageGroupTopicViewModel`](PageGroupTopicViewModel.cs) + - [`PageTopicViewModel`](_contentTypes/PageTopicViewModel.cs) + - [`ContentListTopicViewModel`](_contentTypes/ContentListTopicViewModel.cs) ([`ContentItemTopicViewModel`](_items/ContentItemTopicViewModel.cs)) + - [`IndexTopicViewModel`](_contentTypes/IndexTopicViewModel.cs) + - [`SlideshowTopicViewModel`](_contentTypes/SlideshowTopicViewModel.cs) ([`SlideTopicViewModel`](_items/SlideTopicViewModel.cs)) + - [`VideoTopicViewModel`](_contentTypes/VideoTopicViewModel.cs) + - [`SectionTopicViewModel`](_contentTypes/SectionTopicViewModel.cs) + - [`PageGroupTopicViewModel`](_contentTypes/PageGroupTopicViewModel.cs) - [`NavigationTopicViewModel`](NavigationTopicViewModel.cs) - - [`ItemTopicViewModel`](Items/ItemTopicViewModel.cs) - - [`ContentItemTopicViewModel`](Items/ContentItemTopicViewModel.cs) - - [`LookupListItemTopicViewModel`](Items/LookupListItemTopicViewModel.cs) - - [`SlideTopicViewModel`](Items/SlideTopicViewModel.cs) + - [`ItemTopicViewModel`](_items/ItemTopicViewModel.cs) + - [`ContentItemTopicViewModel`](_items/ContentItemTopicViewModel.cs) + - [`LookupListItemTopicViewModel`](_items/LookupListItemTopicViewModel.cs) + - [`SlideTopicViewModel`](_items/SlideTopicViewModel.cs) - [`AssociatedTopicBindingModel`](BindingModels/AssociatedTopicBindingModel.cs) - [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) -- [`TopicViewModelCollection<>`](Collections/TopicViewModelCollection.cs) +- [`TopicViewModelCollection<>`](_collections/TopicViewModelCollection.cs) ## Usage By default, the [`OnTopic.AspNetCore.Mvc`](../OnTopic.AspNetCore.Mvc/README.md)'s [`TopicController`](../OnTopic.AspNetCore.Mvc/Controllers/TopicController.cs) uses the out-of-the-box [`TopicMappingService`](../OnTopic/Mapping) to map topics to view models. For applications primarily relying on the out-of-the-box view models, it is recommended that the [`TopicViewModelLookupService`](TopicViewModelLookupService.cs) be used; this includes all of the out-of-the-box view models, and can be derived to add application-specific view models. @@ -62,6 +62,6 @@ As view models, not all attributes and associations are exposed. The properties All of the view models assume a parameterless constructor (e.g., `new TopicViewModel()`), which can optionally be the default constructor if no other constructors are required. This is necessary to provide compatibility with the `TopicMappingService`, which will attempt to create new instances of view models based on the the topic's `ContentType`, using the view models parameterless constructor. ### Inheritance -The view models map to the hierarchy of the content types in OnTopic, with each view model only including properties that are _specific_ to that content type. So, for example, [`PageTopicViewModel`](PageTopicViewModel.cs) includes a `Body` property, which is introduced by the `Page` content type, but doesn't include e.g. `Key`, `ContentType`, or `Title`; these are all inherited from the base [`TopicViewModel`](TopicViewModel.cs). +The view models map to the hierarchy of the content types in OnTopic, with each view model only including properties that are _specific_ to that content type. So, for example, [`PageTopicViewModel`](_contentTypes/PageTopicViewModel.cs) includes a `Body` property, which is introduced by the `Page` content type, but doesn't include e.g. `Key`, `ContentType`, or `Title`; these are all inherited from the base [`TopicViewModel`](TopicViewModel.cs). This is advantageous not only because it effectively models the familiar content type hierarchy, but also because it allows for polymorphism in the mapping library. So, for example, if a property accepts a `Collection`, then this can also contain any view models that derive from the `PageTopicViewModel` (e.g., `SlideshowTopicViewModel`, `VideoTopicViewModel`, &c.). \ No newline at end of file diff --git a/OnTopic.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/ContentListTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs diff --git a/OnTopic.ViewModels/IndexTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/IndexTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs diff --git a/OnTopic.ViewModels/PageGroupTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/PageGroupTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs diff --git a/OnTopic.ViewModels/PageTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/PageTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs diff --git a/OnTopic.ViewModels/SectionTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/SectionTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs diff --git a/OnTopic.ViewModels/SlideshowTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/SlideshowTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs diff --git a/OnTopic.ViewModels/VideoTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs similarity index 100% rename from OnTopic.ViewModels/VideoTopicViewModel.cs rename to OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs From ec4886c93334f7581293583afa943d209bb24738 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 14:40:40 -0800 Subject: [PATCH 754/778] Fixed assignment of `CurrentWebPath` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a previous update, we replaced `NavigationViewModel`'s `CurrentKey` property with `CurrentWebPath`, in anticipation of it operating off of the URL, instead of the `UniqueKey` (8fd4d80f). Unfortunately, in doing so, I missed that the _assignment_ of the `CurrentWebPath` was still pulling data from `CurrentTopic.GetUniqueKey()`—and, thus, calls to e.g. `NavigationTopicViewModel.IsSelected()` were failing, since it is now comparing against `webPath`. --- OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs | 2 +- .../Components/PageLevelNavigationViewComponentBase{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs index 5502dfde..77773cfe 100644 --- a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs @@ -131,7 +131,7 @@ public async Task InvokeAsync() { \-----------------------------------------------------------------------------------------------------------------------*/ var navigationViewModel = new NavigationViewModel() { NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true), - CurrentWebPath = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path + CurrentWebPath = CurrentTopic?.GetWebPath()?? HttpContext.Request.Path }; /*------------------------------------------------------------------------------------------------------------------------ diff --git a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs index b0410032..5b82579a 100644 --- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs +++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs @@ -128,7 +128,7 @@ public async Task InvokeAsync() { \-----------------------------------------------------------------------------------------------------------------------*/ var navigationViewModel = new NavigationViewModel() { NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true), - CurrentWebPath = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path + CurrentWebPath = CurrentTopic?.GetWebPath()?? HttpContext.Request.Path }; /*------------------------------------------------------------------------------------------------------------------------ From 619ba626e2748f76aaf832cd44eccc5b3751e53f Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 23 Feb 2021 14:43:09 -0800 Subject: [PATCH 755/778] Fixed bug in unit tests regarding `CurrentWebPath` The previous bug with the assignment of the `CurrentWebPath` was allowed to go live because the `TopicViewComponentTest` was still comparing it to the `GetUniqueKey()`. This is now resolved as well, so this should be easier to detect in the future. --- OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs index 797b4909..3d7c5a59 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs +++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs @@ -114,7 +114,7 @@ public async Task Menu_Invoke_ReturnsNavigationViewModel() { var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentWebPath); + Assert.AreEqual(_topic.GetWebPath(), model?.CurrentWebPath); Assert.AreEqual("/Web/", model?.NavigationRoot?.WebPath); Assert.AreEqual(3, model?.NavigationRoot?.Children.Count); Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false); @@ -139,7 +139,7 @@ public async Task PageLevelNavigation_Invoke_ReturnsNavigationViewModel() { var model = concreteResult?.ViewData.Model as NavigationViewModel; Assert.IsNotNull(model); - Assert.AreEqual(_topic.GetUniqueKey(), model?.CurrentWebPath); + Assert.AreEqual(_topic.GetWebPath(), model?.CurrentWebPath); Assert.AreEqual("/Web/Web_3/", model?.NavigationRoot?.WebPath); Assert.AreEqual(2, model?.NavigationRoot?.Children.Count); Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false); From 565273fa93217ca507c611a8b4f578384a1dc667 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 11:46:16 -0800 Subject: [PATCH 756/778] Introduced new `IKeyedTopicViewModel` interface One of the places where we rely on `ITopicViewModel` is for the `KeyedTopicViewModelCollection`. But that only necessitates a `Key`, so that each instance can be indexed by the collection. Obviously, the generic types will almost certainly have more properties, but as those are strongly typed once the generic has been constructed, that's not an issue. Given that, requiring the (overly) comprehensive `ITopicViewModel` is excessive. This provides a (very) lightweight, base interface for keyed (view) models. --- OnTopic/Models/IKeyedTopicViewModel.cs | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 OnTopic/Models/IKeyedTopicViewModel.cs diff --git a/OnTopic/Models/IKeyedTopicViewModel.cs b/OnTopic/Models/IKeyedTopicViewModel.cs new file mode 100644 index 00000000..58b070fa --- /dev/null +++ b/OnTopic/Models/IKeyedTopicViewModel.cs @@ -0,0 +1,40 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using OnTopic.Mapping; + +namespace OnTopic.Models { + + /*============================================================================================================================ + | INTERFACE: KEYED TOPIC VIEW MODEL + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures that a model maintains, at minimum, a property, necessary to support e.g. a . + /// + /// + /// It is not required that topic view models implement the interface for the to correctly identify and map s to topic view models. That said, the interface + /// does ensure that those view models can be keyed, which is useful for, especially, child collections. + /// + public interface IKeyedTopicViewModel { + + /*========================================================================================================================== + | PROPERTY: KEY + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the topic's attribute, the primary text identifier for the . + /// + /// + /// value is not null + /// + [Required, NotNull, DisallowNull] + string? Key { get; init; } + + } //Class +} //Namespace \ No newline at end of file From 3b5dda8cae0f598719d47391ada6a4f8f0693dd8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 11:53:38 -0800 Subject: [PATCH 757/778] Applied `IKeyedTopicViewModel` to `ITopicBindingModel` Since `Key` is now defined on both `ITopicBindingModel` and the new `IKeyedTopicViewModel` (565273fa), and there's no other "pollution" from any otherwise unnecessary interface requirements on `IKeyedTopicViewModel`, we can derive `ITopicBindingModel` from `IKeyedTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. --- OnTopic/Models/ITopicBindingModel.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index 83b976ef..08b95150 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -19,24 +19,7 @@ namespace OnTopic.Models { /// It is strictly required that topic binding models implement the interface for the /// default to correctly identify and map a binding model to a . /// - public interface ITopicBindingModel { - - /*========================================================================================================================== - | PROPERTY: KEY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the topic's Key attribute, the primary text identifier for the topic. - /// - /// - /// value is not null - /// - /// - /// !value.Contains(" ") - /// - [Required] - string? Key { get; init; } + public interface ITopicBindingModel: IKeyedTopicViewModel { /*========================================================================================================================== | PROPERTY: CONTENT TYPE From 5ea98c963029d52702dcf44119438da82dd15994 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 11:56:42 -0800 Subject: [PATCH 758/778] Applied `IKeyedTopicViewModel` to `ITopicViewModel` Since `Key` is now defined on both `ITopicBindingModel` and the new `IKeyedTopicViewModel` (565273fa), and there's no other "pollution" from any otherwise unnecessary interface requirements on `IKeyedTopicViewModel`, we can derive `ITopicBindingModel` from `IKeyedTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. --- OnTopic/Models/ITopicViewModel.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index d5e8109d..0829a545 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -28,7 +28,7 @@ namespace OnTopic.Models { /// presentation layer and any supporting libraries. /// /// - public interface ITopicViewModel { + public interface ITopicViewModel: IKeyedTopicViewModel { /*========================================================================================================================== | PROPERTY: ID @@ -38,14 +38,6 @@ public interface ITopicViewModel { /// int Id { get; init; } - /*========================================================================================================================== - | PROPERTY: KEY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the topic's Key attribute, the primary text identifier for the topic. - /// - string? Key { get; init; } - /*========================================================================================================================== | PROPERTY: UNIQUE KEY \-------------------------------------------------------------------------------------------------------------------------*/ @@ -105,7 +97,7 @@ public interface ITopicViewModel { /// Gets or sets the Title attribute, which represents the friendly name of the topic. /// /// - /// While the may not contain, for instance, spaces or symbols, there are no + /// While the may not contain, for instance, spaces or symbols, there are no /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for /// referencing topics. /// From 8ad42b9e497cb76d646f8bc4c5b0da9bdc3c5df1 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:01:21 -0800 Subject: [PATCH 759/778] Applied `IAssociatedTopicBindingModel` to `ITopicViewModel` Since `UniqueKey` is defined on both `ITopicBindingModel` and `IAssociatedTopicBindingModel`, and there's no other "pollution" from any otherwise unnecessary interface requirements on `IAssociatedTopicBindingModel`, we can derive `ITopicBindingModel` from `IAssociatedTopicBindingModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. This can also be applied to `TopicViewModel`. --- OnTopic.ViewModels/TopicViewModel.cs | 2 +- OnTopic/Models/ITopicViewModel.cs | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 651679de..118ae49e 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -20,7 +20,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public record TopicViewModel: ITopicViewModel { + public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociatedTopicBindingModel { /*========================================================================================================================== | ID diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index 0829a545..b58ad17e 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -28,7 +28,7 @@ namespace OnTopic.Models { /// presentation layer and any supporting libraries. /// /// - public interface ITopicViewModel: IKeyedTopicViewModel { + public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingModel { /*========================================================================================================================== | PROPERTY: ID @@ -38,20 +38,12 @@ public interface ITopicViewModel: IKeyedTopicViewModel { /// int Id { get; init; } - /*========================================================================================================================== - | PROPERTY: UNIQUE KEY - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets or sets the topic's attribute, the unique text identifier for the topic. - /// - string? UniqueKey { get; init; } - /*========================================================================================================================== | PROPERTY: WEB PATH \-------------------------------------------------------------------------------------------------------------------------*/ /// - /// Gets or sets the topic's attribute, which represents the in its URL - /// format. + /// Gets or sets the topic's attribute, which represents the in its URL format. /// string? WebPath { get; init; } From ffe775e87789a095c36457961d9c83c5b07e8ff8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:04:05 -0800 Subject: [PATCH 760/778] Applied `ITopicBindingModel` to `ITopicViewModel` Since `ContentType` is defined on both `ITopicBindingModel` and `ITopicBindingModel`, and there's no other "pollution" from any otherwise unnecessary interface requirements on `ITopicBindingModel`, we can derive `ITopicBindingModel` from `ITopicBindingModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. This can also be applied to `TopicViewModel`. --- OnTopic.ViewModels/TopicViewModel.cs | 2 +- OnTopic/Models/ITopicViewModel.cs | 30 +++++++++------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 118ae49e..405adc66 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -20,7 +20,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociatedTopicBindingModel { + public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { /*========================================================================================================================== | ID diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index b58ad17e..275e879d 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -22,13 +22,13 @@ namespace OnTopic.Models { /// provided via the public interface then it will instead need to be defined in some other way. /// /// - /// For instance, in the default MVC library, the TopicViewResult class requires that the and be supplied separately if they're not provided as part of a - /// . The exact details of this will obviously vary based on the implementation of the - /// presentation layer and any supporting libraries. + /// For instance, in the default MVC library, the TopicViewResult class requires that the and be supplied separately if they're not provided as part of a . The exact details of this will obviously vary based on the implementation of the presentation + /// layer and any supporting libraries. /// /// - public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingModel { + public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { /*========================================================================================================================== | PROPERTY: ID @@ -47,18 +47,6 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// string? WebPath { get; init; } - /*========================================================================================================================== - | PROPERTY: CONTENT TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets the key name of the content type that the current topic represents. - /// - /// - /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). - /// - string? ContentType { get; init; } - /*========================================================================================================================== | PROPERTY: VIEW \-------------------------------------------------------------------------------------------------------------------------*/ @@ -67,10 +55,10 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// /// /// This value can be set via the query string (via the TopicViewResultExecutor class), via the Accepts header - /// (also via the TopicViewResultExecutor class), on the topic itself (via this property), or via the - /// . By default, it will be set to the name of the ; e.g., if the - /// Content Type is "Page", then the view will be "Page". This will cause the TopicViewResultExecutor to look - /// for a view at, for instance, /Views/Page/Page.cshtml. + /// (also via the TopicViewResultExecutor class), on the topic itself (via this property), or via the . By default, it will be set to the name of the ; e.g., if the Content Type is Page, then the view will be Page. This will cause the + /// TopicViewResultExecutor to look for a view at, for instance, /Views/Page/Page.cshtml. /// string? View { get; init; } From 3e60dead2a383535d5c620fe183f83108eb09e60 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:10:18 -0800 Subject: [PATCH 761/778] Fixed unit tests which evaluate missing interfaces in binding models In binding models, the types used for topic references must implement `ITopicBindingModel` and the types used for relationships must implement `IAssociatedTopicBindingModel`. If they don't, an exception is thrown. Previously, this was validated via unit tests that used `TopicViewModel` as the type for each of those, thus failing validation. We now derive `ITopicViewModel` from `ITopicBindingModel` (ffe775e8) and `IAssociatedTopicBindingModel` (8ad42b9e), however, and thus that actually satisfies the condition. That makes view models more flexible by allowing them to double as binding models. But it breaks our unit tests. To fix this, I've introduced a new `EmptyViewModel` which implements no interfaces, and used it as the return type for the `InvalidreferenceTypeTopicBindingModel` as well as the `ContentTypes` collection type on the `InvalidRelationshipBaseTypeTopicBindingModel`. This effectively satisfies the expectations of those unit tests, and correctly returns them to throwing the expected exception. --- .../InvalidReferenceTypeTopicBindingModel.cs | 5 ++-- ...idRelationshipBaseTypeTopicBindingModel.cs | 4 ++-- OnTopic.Tests/ViewModels/EmptyViewModel.cs | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 OnTopic.Tests/ViewModels/EmptyViewModel.cs diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs index 26980d74..66662a10 100644 --- a/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs @@ -4,9 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; -using OnTopic.Mapping.Annotations; using OnTopic.Models; -using OnTopic.ViewModels; +using OnTopic.Tests.ViewModels; namespace OnTopic.Tests.BindingModels { @@ -24,7 +23,7 @@ public class InvalidReferenceTypeTopicBindingModel : BasicTopicBindingModel { public InvalidReferenceTypeTopicBindingModel(string? key = null) : base(key, "Page") { } - public TopicViewModel BaseTopic { get; } = new(); + public EmptyViewModel BaseTopic { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs index 14e2e2ce..357cc130 100644 --- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs @@ -6,7 +6,7 @@ using System; using System.Collections.ObjectModel; using OnTopic.Models; -using OnTopic.ViewModels; +using OnTopic.Tests.ViewModels; namespace OnTopic.Tests.BindingModels { @@ -24,7 +24,7 @@ public class InvalidRelationshipBaseTypeTopicBindingModel : BasicTopicBindingMod public InvalidRelationshipBaseTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { } - public Collection ContentTypes { get; } = new(); + public Collection ContentTypes { get; } = new(); } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic.Tests/ViewModels/EmptyViewModel.cs b/OnTopic.Tests/ViewModels/EmptyViewModel.cs new file mode 100644 index 00000000..4636ec9c --- /dev/null +++ b/OnTopic.Tests/ViewModels/EmptyViewModel.cs @@ -0,0 +1,23 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia, LLC +| Project Topics Library +\=============================================================================================================================*/ +using OnTopic.Models; + +namespace OnTopic.Tests.ViewModels { + + /*============================================================================================================================ + | VIEW MODEL: EMPTY + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// A view model that does not implement any properties or interfaces. This will be invalid for mapping models that expect + /// e.g. or . + /// + /// + /// This is a sample class intended for test purposes only; it is not designed for use in a production environment. + /// + public class EmptyViewModel { + + } +} \ No newline at end of file From adf7ff8b2cb939d4e6d9e57acc1eae7ece5669c4 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:19:51 -0800 Subject: [PATCH 762/778] Centralized key navigation properties to `INavigableTopicViewModel` These were already defined on `INavigationTopicViewModel`, but that _also_ implemented `IHierarchicalTopicViewModel`. Sometimes, there are topics that should be evaluated for navigation, but which _aren't_ hierarchical, and which _don't_ necessitate the use of e.g. the `IHierarchicalTopicMappingService` or `NavigationViewComponentBase`. For example, offering a list of child topics on a page. In this case, these _may_ implement `IHierarchicalTopicViewModel`, but they only _need_ `Title`, `ShortTitle`, and `WebPath`. To support this, the `INavigableTopicViewModel` extracts these from the `INavigationTopicViewModel` so they can, optionally, be applied independently. Then, the `INavigationTopicViewModel` effectively becomes a composite of `INavigableTopicViewModel` and `IHierarchicalTopicViewModel`, adding only `IsSelected()`. This is useful since the view models for the `NavigationTopicViewComponent` are likely going to be independent from other view models, since most view models don't want or need a `Children` collection. But many basic view models will satisfy the `INavigableTopicViewModel` interface, and should be usable for that purpose. This allows different view models types from different chains (e.g., `OnTopic.ViewModels` and a separate customer implementation) to be handled by e.g. the same partial control. --- OnTopic/Models/INavigableTopicViewModel.cs | 47 +++++++++++++++++++ .../Models/INavigationTopicViewModel{T}.cs | 26 ++-------- 2 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 OnTopic/Models/INavigableTopicViewModel.cs diff --git a/OnTopic/Models/INavigableTopicViewModel.cs b/OnTopic/Models/INavigableTopicViewModel.cs new file mode 100644 index 00000000..b574fb19 --- /dev/null +++ b/OnTopic/Models/INavigableTopicViewModel.cs @@ -0,0 +1,47 @@ +/*============================================================================================================================== +| Author Ignia, LLC +| Client Ignia +| Project Website +\=============================================================================================================================*/ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace OnTopic.Models { + + /*============================================================================================================================ + | INTERFACE: NAVIGABLE TOPIC VIEW MODEL + \---------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Ensures basic properties needed to treat a view model as a navigable entry. + /// + /// + /// No topics are expected to have a Navigable content type. Instead, implementers of this view model are expected + /// to manually construct instances. + /// + public interface INavigableTopicViewModel { + + /*========================================================================================================================== + | PROPERTY: TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + [Required, NotNull, DisallowNull] + string? Title { get; init; } + + /*========================================================================================================================== + | PROPERTY: SHORT TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// In addition to the Title, a site may opt to define a Short Title used exclusively in the navigation. If present, this + /// value should be used instead of Title. + /// + string? ShortTitle { get; init; } + + /*========================================================================================================================== + | PROPERTY: WEB PATH + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + [Required, NotNull, DisallowNull] + string? WebPath { get; init; } + + } //Class +} //Namespace \ No newline at end of file diff --git a/OnTopic/Models/INavigationTopicViewModel{T}.cs b/OnTopic/Models/INavigationTopicViewModel{T}.cs index 9ecf16f0..5028f471 100644 --- a/OnTopic/Models/INavigationTopicViewModel{T}.cs +++ b/OnTopic/Models/INavigationTopicViewModel{T}.cs @@ -16,28 +16,10 @@ namespace OnTopic.Models { /// No topics are expected to have a Navigation content type. Instead, implementers of this view model are expected /// to manually construct instances. /// - public interface INavigationTopicViewModel : IHierarchicalTopicViewModel where T: INavigationTopicViewModel { - - /*========================================================================================================================== - | PROPERTY: TITLE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - string? Title { get; init; } - - /*========================================================================================================================== - | PROPERTY: SHORT TITLE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// In addition to the Title, a site may opt to define a Short Title used exclusively in the navigation. If present, this - /// value should be used instead of Title. - /// - string? ShortTitle { get; init; } - - /*========================================================================================================================== - | PROPERTY: WEB PATH - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - string? WebPath { get; init; } + public interface INavigationTopicViewModel : + INavigableTopicViewModel, + IHierarchicalTopicViewModel where T: INavigationTopicViewModel + { /*========================================================================================================================== | METHOD: IS SELECTED? From c41d989e37bf1d0c4c5d826cd79351affaf91845 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:21:32 -0800 Subject: [PATCH 763/778] Applied `INavigableTopicViewModel` to `IPageTopicViewModel` Since `Title`, `ShortTitle`, and `WebPath` are now defined on both `IPageTopicViewModel` and the new `INavigableTopicViewModel` (adf7ff8b), and there's no other "pollution" from any otherwise unnecessary interface requirements on `INavigableTopicViewModel`, we can derive `IPageTopicBindingModel` from `INavigableTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. --- OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs | 2 +- OnTopic/Models/IPageTopicViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs index 16a4378f..ba1cbd56 100644 --- a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs @@ -18,7 +18,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public record PageTopicViewModel: TopicViewModel, IPageTopicViewModel { + public record PageTopicViewModel: TopicViewModel, INavigableTopicViewModel { /*========================================================================================================================== | SUBTITLE diff --git a/OnTopic/Models/IPageTopicViewModel.cs b/OnTopic/Models/IPageTopicViewModel.cs index a63f9ab7..3cb315e1 100644 --- a/OnTopic/Models/IPageTopicViewModel.cs +++ b/OnTopic/Models/IPageTopicViewModel.cs @@ -21,7 +21,7 @@ namespace OnTopic.Models { /// provided via the public interface then it will instead need to be defined in some other way. /// /// - public interface IPageTopicViewModel : ITopicViewModel { + public interface IPageTopicViewModel : ITopicViewModel, INavigableTopicViewModel { /*========================================================================================================================== | PROPERTY: META KEYWORDS From 68a4e83d255d797c1e6cc1cf70f91a8ba8845ed9 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:31:13 -0800 Subject: [PATCH 764/778] Removed `IsHidden` from the `ITopicViewModel` definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In addition to cluttering an already overpopulated interface, the `IsHidden` property doesn't make much in the current version of OnTopic, as hidden view models are explicitly excluded from the the `TopicMappingService`. The one exception to this is the top-level topic—i.e., the one sent directly to the `ITopicMappingService`, as opposed to referenced from its properties. But in those cases, we expect callers, such as `TopicController`, to explicitly determine if `IsHidden is appropriate or not. Given that, there isn't much benefit to exposing `IsHidden` to the view model—either the interface or the implementation. As part of this, I marked the interface as obsolete, and marked the implementation as both obsolete and disabled mapping of the property. In addition, I removed references to it from the unit tests. These weren't strictly necessary, and can be safely removed while still satisfying the basic criteria of the unit tests. --- .../Views/Shared/_TopicAttributes.cshtml | 1 - OnTopic.Tests/TopicMappingServiceTest.cs | 4 ---- OnTopic.ViewModels/TopicViewModel.cs | 4 +++- OnTopic/Models/ITopicViewModel.cs | 1 + 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml index 6168b922..0994ed03 100644 --- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml +++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml @@ -7,7 +7,6 @@
    • ContentType: @Model.ContentType
    • UniqueKey: @Model.UniqueKey
    • WebPath: @Model.WebPath
    • -
    • IsHidden? @Model.IsHidden
    • LastModified: @Model.LastModified
    • View: @Model.View
    diff --git a/OnTopic.Tests/TopicMappingServiceTest.cs b/OnTopic.Tests/TopicMappingServiceTest.cs index dd2a8b8b..15c46178 100644 --- a/OnTopic.Tests/TopicMappingServiceTest.cs +++ b/OnTopic.Tests/TopicMappingServiceTest.cs @@ -85,13 +85,11 @@ public async Task Map_Generic_ReturnsNewModel() { topic.Attributes.SetValue("MetaTitle", "ValueA"); topic.Attributes.SetValue("Title", "Value1"); - topic.Attributes.SetValue("IsHidden", "1"); var target = await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.AreEqual("ValueA", target.MetaTitle); Assert.AreEqual("Value1", target.Title); - Assert.AreEqual(true, target.IsHidden); } @@ -127,13 +125,11 @@ public async Task Map_Dynamic_ReturnsNewModel() { topic.Attributes.SetValue("MetaTitle", "ValueA"); topic.Attributes.SetValue("Title", "Value1"); - topic.Attributes.SetValue("IsHidden", "1"); var target = (PageTopicViewModel?)await _mappingService.MapAsync(topic).ConfigureAwait(false); Assert.AreEqual("ValueA", target.MetaTitle); Assert.AreEqual("Value1", target.Title); - Assert.AreEqual(true, target.IsHidden); } diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 405adc66..cf880ed9 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -67,7 +67,9 @@ public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociated /*========================================================================================================================== | IS HIDDEN? \-------------------------------------------------------------------------------------------------------------------------*/ - /// + /// + [Obsolete("The IsHidden property is no longer supported by TopicViewModel.", true)] + [DisableMapping] public bool IsHidden { get; init; } /*========================================================================================================================== diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index 275e879d..1ea4532d 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -68,6 +68,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// /// Gets or sets whether the current topic is hidden. /// + [Obsolete("The IsHidden property is no longer supported by ITopicViewModel.", true)] bool IsHidden { get; init; } /*========================================================================================================================== From 745e562d162d8294592101bf8d70ecb2dcb15bf8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:39:05 -0800 Subject: [PATCH 765/778] Mark `IPageTopicViewModel` as obsolete There's no expected use case for the `IPageTopicViewModel`. We foresee the limited but potential need for e.g. shared view models that will handle navigation, but these can be handled via `INavigableTopicViewModel` and `INavigationTopicViewModel`. Others _may_ need additional details, which are available via `ITopicViewModel`. But we don't expect reusable _layouts_, which is where the additional requirements of e.g. `IPageTopicViewModel` comes in. On an assessment of OnTopic 4.x implementations, it doesn't appear anyone is utilizing `IPageTopicViewModel`, except possibly on model definitions, so it should be safe to remove. Marking it as obsolete. --- OnTopic/Models/IPageTopicViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/OnTopic/Models/IPageTopicViewModel.cs b/OnTopic/Models/IPageTopicViewModel.cs index 3cb315e1..b19beba9 100644 --- a/OnTopic/Models/IPageTopicViewModel.cs +++ b/OnTopic/Models/IPageTopicViewModel.cs @@ -3,6 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; using OnTopic.Mapping; namespace OnTopic.Models { @@ -21,6 +22,7 @@ namespace OnTopic.Models { /// provided via the public interface then it will instead need to be defined in some other way. /// /// + [Obsolete("The IPageTopicViewModel is no longer utilized.", true)] public interface IPageTopicViewModel : ITopicViewModel, INavigableTopicViewModel { /*========================================================================================================================== From 1bf25be959fcb0195f919b2478b725044158673e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:45:35 -0800 Subject: [PATCH 766/778] Marked core view model properties as not-nullable, required In a typical use case, all properties will be set to their defaults when a view model is initialized. Given this, the return type of otherwise required properties must be nullable. The mapping service will then immediately populate these, however, and thus we never expect them to _actually_ be null. As such, we can annotate them with the `[NotNull]` attribute to assure callers that, in practice, they're not null, and the `[DisallowNull]` attribute to dissuade callers from setting them to null. In addition, there are no scenarios where we expect most of these values to even have empty values. As such, we can mark them as `[Required]` to help enforce their use. This is critical for binding models, where we want to ensure that the interface isn't simply satisfied, but also that data is populated. For instance, a `ITopicBindingModel` without `Key` or `ContentType` values defined isn't especially useful. Given that, most required properties have been annotated as `[Required, NotNull, DisallowNull]`. --- OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs | 6 ++++-- OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs | 4 +++- .../BindingModels/AssociatedTopicBindingModel.cs | 3 ++- OnTopic.ViewModels/NavigationTopicViewModel.cs | 4 ++++ OnTopic.ViewModels/TopicViewModel.cs | 7 +++++++ OnTopic/Models/IAssociatedTopicBindingModel.cs | 3 ++- OnTopic/Models/ITopicBindingModel.cs | 3 ++- OnTopic/Models/ITopicViewModel.cs | 6 +++++- 8 files changed, 29 insertions(+), 7 deletions(-) diff --git a/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs b/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs index 35a9cd07..427138db 100644 --- a/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Models; namespace OnTopic.Tests.BindingModels { @@ -21,14 +22,15 @@ public class BasicTopicBindingModel : ITopicBindingModel { public BasicTopicBindingModel() { } - public BasicTopicBindingModel(string? key, string? contentType) { + public BasicTopicBindingModel(string key, string contentType) { Key = key; ContentType = contentType; } + [Required, NotNull, DisallowNull] public string? Key { get; init; } - [Required] + [Required, NotNull, DisallowNull] public string? ContentType { get; init; } } //Class diff --git a/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs index ae77cbd7..6c194123 100644 --- a/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs +++ b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Models; namespace OnTopic.Tests.BindingModels { @@ -22,9 +23,10 @@ public class RecordTopicBindingModel : ITopicBindingModel { public RecordTopicBindingModel() { } + [Required, NotNull, DisallowNull] public string? Key { get; init; } - [Required] + [Required, NotNull, DisallowNull] public string? ContentType { get; init; } } //Class diff --git a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs index 752fc4b1..cdde217d 100644 --- a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs +++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; using OnTopic.Models; @@ -32,7 +33,7 @@ public record AssociatedTopicBindingModel : IAssociatedTopicBindingModel { /// /// value is not null /// - [Required] + [Required, NotNull, DisallowNull] public string? UniqueKey { get; init; } } //Class diff --git a/OnTopic.ViewModels/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs index bbce1d51..aa1c4cea 100644 --- a/OnTopic.ViewModels/NavigationTopicViewModel.cs +++ b/OnTopic.ViewModels/NavigationTopicViewModel.cs @@ -5,6 +5,8 @@ \=============================================================================================================================*/ using System; using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Models; namespace OnTopic.ViewModels { @@ -34,6 +36,7 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel + [Required, NotNull, DisallowNull] public string? Title { get; init; } /*========================================================================================================================== @@ -48,6 +51,7 @@ public sealed record NavigationTopicViewModel : INavigationTopicViewModel + [Required, NotNull, DisallowNull] public string? WebPath { get; init; } /*========================================================================================================================== diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index cf880ed9..9e686a16 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -4,6 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Annotations; using OnTopic.Models; @@ -32,24 +34,28 @@ public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociated | KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// + [Required, NotNull, DisallowNull] public string? Key { get; init; } /*========================================================================================================================== | CONTENT TYPE \-------------------------------------------------------------------------------------------------------------------------*/ /// + [Required, NotNull, DisallowNull] public string? ContentType { get; init; } /*========================================================================================================================== | UNIQUE KEY \-------------------------------------------------------------------------------------------------------------------------*/ /// + [Required, NotNull, DisallowNull] public string? UniqueKey { get; init; } /*========================================================================================================================== | WEB PATH \-------------------------------------------------------------------------------------------------------------------------*/ /// + [Required, NotNull, DisallowNull] public string? WebPath { get; init; } /*========================================================================================================================== @@ -62,6 +68,7 @@ public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociated | TITLE \-------------------------------------------------------------------------------------------------------------------------*/ /// + [Required, NotNull, DisallowNull] public string? Title { get; init; } /*========================================================================================================================== diff --git a/OnTopic/Models/IAssociatedTopicBindingModel.cs b/OnTopic/Models/IAssociatedTopicBindingModel.cs index 68a0d153..32c5a8a6 100644 --- a/OnTopic/Models/IAssociatedTopicBindingModel.cs +++ b/OnTopic/Models/IAssociatedTopicBindingModel.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; namespace OnTopic.Models { @@ -30,7 +31,7 @@ public interface IAssociatedTopicBindingModel { /// /// value is not null /// - [Required] + [Required, NotNull, DisallowNull] string? UniqueKey { get; init; } } //Class diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index 08b95150..f71334c8 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -4,6 +4,7 @@ | Project Topics Library \=============================================================================================================================*/ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; using OnTopic.Metadata; @@ -31,7 +32,7 @@ public interface ITopicBindingModel: IKeyedTopicViewModel { /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics /// Editor (via the property). /// - [Required] + [Required, NotNull, DisallowNull] string? ContentType { get; init; } } //Class diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index 1ea4532d..cff466ad 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -3,8 +3,10 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping; -using OnTopic.Metadata; namespace OnTopic.Models { @@ -45,6 +47,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// Gets or sets the topic's attribute, which represents the in its URL format. /// + [Required, NotNull, DisallowNull] string? WebPath { get; init; } /*========================================================================================================================== @@ -82,6 +85,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for /// referencing topics. /// + [Required, NotNull, DisallowNull] string? Title { get; init; } } //Class From 3b91ed8b3800a1fabb49d625343f880c2be63116 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:48:45 -0800 Subject: [PATCH 767/778] Moved `ShortTitle` to the top of the `PageTopicViewModel` class This is just an organizational change, and has no functional impact. --- .../_contentTypes/PageTopicViewModel.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs index ba1cbd56..d3919208 100644 --- a/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs @@ -20,6 +20,14 @@ namespace OnTopic.ViewModels { /// public record PageTopicViewModel: TopicViewModel, INavigableTopicViewModel { + /*========================================================================================================================== + | SHORT TITLE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Provides a short title to be used in the navigation, for cases where the normal title is too long. + /// + public string? ShortTitle { get; init; } + /*========================================================================================================================== | SUBTITLE \-------------------------------------------------------------------------------------------------------------------------*/ @@ -56,14 +64,6 @@ public record PageTopicViewModel: TopicViewModel, INavigableTopicViewModel { /// public bool? NoIndex { get; init; } - /*========================================================================================================================== - | SHORT TITLE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Provides a short title to be used in the navigation, for cases where the normal title is too long. - /// - public string? ShortTitle { get; init; } - /*========================================================================================================================== | BODY \-------------------------------------------------------------------------------------------------------------------------*/ From bfa64f1bd181b9f5cf7ac151997a1fe8887b6bab Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 12:58:01 -0800 Subject: [PATCH 768/778] Marked `VideoUrl` as non-nullable, required This is similar to the updates made previously to core properties (1bf25be9). `VideoUrl` is a core property of the `VideoTopicViewModel`, and always expected to have a value. If it doesn't, a validation error should be thrown. --- OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs index 83c3f738..b052e0be 100644 --- a/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs +++ b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs @@ -4,6 +4,8 @@ | Project Topics Library \=============================================================================================================================*/ using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace OnTopic.ViewModels { @@ -26,6 +28,7 @@ public record VideoTopicViewModel: PageTopicViewModel { /// /// Provides a URL reference to a video to display on the page. /// + [Required, NotNull, DisallowNull] public Uri? VideoUrl { get; init; } /*========================================================================================================================== From 9bc0d00258626383193af1ab7cc48f0de3b037ca Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:05:25 -0800 Subject: [PATCH 769/778] Moved `ContentType` to `IKeyedTopicViewModel` This will allow e.g. `TopicViewModelCollection` to filter by content type, which is a common feature of topic (view model) collections. --- OnTopic/Models/IKeyedTopicViewModel.cs | 14 ++++++++++++++ OnTopic/Models/ITopicBindingModel.cs | 15 --------------- OnTopic/Models/ITopicViewModel.cs | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/OnTopic/Models/IKeyedTopicViewModel.cs b/OnTopic/Models/IKeyedTopicViewModel.cs index 58b070fa..0d6cb6d2 100644 --- a/OnTopic/Models/IKeyedTopicViewModel.cs +++ b/OnTopic/Models/IKeyedTopicViewModel.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping; +using OnTopic.Metadata; namespace OnTopic.Models { @@ -36,5 +37,18 @@ public interface IKeyedTopicViewModel { [Required, NotNull, DisallowNull] string? Key { get; init; } + /*========================================================================================================================== + | PROPERTY: CONTENT TYPE + \-------------------------------------------------------------------------------------------------------------------------*/ + /// + /// Gets the key name of the content type that the current topic represents. + /// + /// + /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics + /// Editor (via the property). + /// + [Required, NotNull, DisallowNull] + string? ContentType { get; init; } + } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index f71334c8..f33b4d28 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -3,10 +3,7 @@ | Client Ignia, LLC | Project Topics Library \=============================================================================================================================*/ -using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using OnTopic.Mapping.Reverse; -using OnTopic.Metadata; namespace OnTopic.Models { @@ -22,18 +19,6 @@ namespace OnTopic.Models { /// public interface ITopicBindingModel: IKeyedTopicViewModel { - /*========================================================================================================================== - | PROPERTY: CONTENT TYPE - \-------------------------------------------------------------------------------------------------------------------------*/ - /// - /// Gets the key name of the content type that the current topic represents. - /// - /// - /// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics - /// Editor (via the property). - /// - [Required, NotNull, DisallowNull] - string? ContentType { get; init; } } //Class } //Namespace \ No newline at end of file diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index cff466ad..e40e1bc9 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -59,7 +59,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// /// This value can be set via the query string (via the TopicViewResultExecutor class), via the Accepts header /// (also via the TopicViewResultExecutor class), on the topic itself (via this property), or via the . By default, it will be set to the name of the . By default, it will be set to the name of the ; e.g., if the Content Type is Page, then the view will be Page. This will cause the /// TopicViewResultExecutor to look for a view at, for instance, /Views/Page/Page.cshtml. /// From 527dee4f9824ed47f7e2502ba483eb5438af709e Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:11:01 -0800 Subject: [PATCH 770/778] Renamed `IKeyedTopicViewModel` to `ICoreTopicViewModel` With the introduction of `ContentType` (9bc0d002), the new name is more fitting. In addition, this is more consistent with our familiar phrasing of "core attributes" to refer to `Id`, `Key`, `ContentType`, and `Parent`. (Though we don't need `Id` or `Parent` here, as they're rarely needed in view models.) --- OnTopic.ViewModels/TopicViewModel.cs | 2 +- ...picViewModel.cs => ICoreTopicViewModel.cs} | 20 ++++++++++++------- OnTopic/Models/ITopicBindingModel.cs | 2 +- OnTopic/Models/ITopicViewModel.cs | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) rename OnTopic/Models/{IKeyedTopicViewModel.cs => ICoreTopicViewModel.cs} (73%) diff --git a/OnTopic.ViewModels/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs index 9e686a16..68b2213f 100644 --- a/OnTopic.ViewModels/TopicViewModel.cs +++ b/OnTopic.ViewModels/TopicViewModel.cs @@ -22,7 +22,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public record TopicViewModel: ITopicViewModel, IKeyedTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { + public record TopicViewModel: ITopicViewModel, ICoreTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { /*========================================================================================================================== | ID diff --git a/OnTopic/Models/IKeyedTopicViewModel.cs b/OnTopic/Models/ICoreTopicViewModel.cs similarity index 73% rename from OnTopic/Models/IKeyedTopicViewModel.cs rename to OnTopic/Models/ICoreTopicViewModel.cs index 0d6cb6d2..3b3b5a22 100644 --- a/OnTopic/Models/IKeyedTopicViewModel.cs +++ b/OnTopic/Models/ICoreTopicViewModel.cs @@ -12,18 +12,24 @@ namespace OnTopic.Models { /*============================================================================================================================ - | INTERFACE: KEYED TOPIC VIEW MODEL + | INTERFACE: CORE TOPIC VIEW MODEL \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Ensures that a model maintains, at minimum, a property, necessary to support e.g. a . + /// Ensures that a model maintains, at minimum, a and property, which are + /// expected of all s. /// /// - /// It is not required that topic view models implement the interface for the to correctly identify and map s to topic view models. That said, the interface - /// does ensure that those view models can be keyed, which is useful for, especially, child collections. + /// + /// This is necessary to support e.g. a , while also allowing it to filter by + /// —a common requirement for topic (view model) collections. + /// + /// + /// It is not required that topic view models implement the interface for the to correctly identify and map s to topic view models. That said, the + /// interface does ensure that those view models can be keyed, which is useful for, especially, child collections. + /// /// - public interface IKeyedTopicViewModel { + public interface ICoreTopicViewModel { /*========================================================================================================================== | PROPERTY: KEY diff --git a/OnTopic/Models/ITopicBindingModel.cs b/OnTopic/Models/ITopicBindingModel.cs index f33b4d28..fe24aa13 100644 --- a/OnTopic/Models/ITopicBindingModel.cs +++ b/OnTopic/Models/ITopicBindingModel.cs @@ -17,7 +17,7 @@ namespace OnTopic.Models { /// It is strictly required that topic binding models implement the interface for the /// default to correctly identify and map a binding model to a . /// - public interface ITopicBindingModel: IKeyedTopicViewModel { + public interface ITopicBindingModel: ICoreTopicViewModel { } //Class diff --git a/OnTopic/Models/ITopicViewModel.cs b/OnTopic/Models/ITopicViewModel.cs index e40e1bc9..655a7781 100644 --- a/OnTopic/Models/ITopicViewModel.cs +++ b/OnTopic/Models/ITopicViewModel.cs @@ -30,7 +30,7 @@ namespace OnTopic.Models { /// layer and any supporting libraries. /// /// - public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { + public interface ITopicViewModel: ICoreTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel { /*========================================================================================================================== | PROPERTY: ID @@ -59,7 +59,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// /// This value can be set via the query string (via the TopicViewResultExecutor class), via the Accepts header /// (also via the TopicViewResultExecutor class), on the topic itself (via this property), or via the . By default, it will be set to the name of the . By default, it will be set to the name of the ; e.g., if the Content Type is Page, then the view will be Page. This will cause the /// TopicViewResultExecutor to look for a view at, for instance, /Views/Page/Page.cshtml. /// @@ -81,7 +81,7 @@ public interface ITopicViewModel: IKeyedTopicViewModel, IAssociatedTopicBindingM /// Gets or sets the Title attribute, which represents the friendly name of the topic. /// /// - /// While the may not contain, for instance, spaces or symbols, there are no + /// While the may not contain, for instance, spaces or symbols, there are no /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for /// referencing topics. /// From eef391c7e83afbd56103bc8d40dcea71a1e5905d Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:12:28 -0800 Subject: [PATCH 771/778] Updated `TopicViewModelCollection` to use new `ICoreTopicViewModel` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This greatly reduces the interface requirements of the `TItem` generic type argument. In practice, view models will likely have more properties—but they're not needed as far as `TopicViewModelCollection` is concerned. --- OnTopic.ViewModels/_collections/TopicViewModelCollection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs index 137301f7..07271449 100644 --- a/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs +++ b/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs @@ -16,7 +16,7 @@ namespace OnTopic.ViewModels { | VIEW MODEL: TOPIC COLLECTION \---------------------------------------------------------------------------------------------------------------------------*/ /// - /// Provides a basic collection interface for use with models implementing , including , including and derivatives. /// /// @@ -24,7 +24,7 @@ namespace OnTopic.ViewModels { /// default implementations that can be used directly, used as base classes, or overwritten at the presentation level. They /// are supplied for convenience to model factory default settings for out-of-the-box content types. /// - public class TopicViewModelCollection: KeyedCollection where TItem: ITopicViewModel { + public class TopicViewModelCollection: KeyedCollection where TItem: ICoreTopicViewModel { /*========================================================================================================================== | CONSTRUCTOR From 50e76e7e65f28cb6f0eced1d2ab1ac97f16cfe8b Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:25:15 -0800 Subject: [PATCH 772/778] Renamed `KeyValuesPair` file to reflect generics This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version (though the latter is admittedly unlikely in this case). --- .../{KeyValuesPair.cs => KeyValuesPair{TKey,TValue}.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename OnTopic/Collections/Specialized/{KeyValuesPair.cs => KeyValuesPair{TKey,TValue}.cs} (100%) diff --git a/OnTopic/Collections/Specialized/KeyValuesPair.cs b/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs similarity index 100% rename from OnTopic/Collections/Specialized/KeyValuesPair.cs rename to OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs From 36a85c67bee6569ae45cec33910b9086955a2ce6 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:27:17 -0800 Subject: [PATCH 773/778] Renamed `TopicPropertyDispatcher<>` file to reflect generics This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version (though the latter is admittedly unlikely in this case). --- ...cs => TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename OnTopic/Internal/Reflection/{TopicPropertyDispatcher.cs => TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs} (100%) diff --git a/OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs b/OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs similarity index 100% rename from OnTopic/Internal/Reflection/TopicPropertyDispatcher.cs rename to OnTopic/Internal/Reflection/TopicPropertyDispatcher{TItem,TValue,TAttributeType}.cs From c9fac1759c9cbe3d250dae9644a9af4a20edf648 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:30:54 -0800 Subject: [PATCH 774/778] Renamed `TopicViewModelCollection` file to reflect generics This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version. --- ...cViewModelCollection.cs => TopicViewModelCollection{TItem}.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename OnTopic.ViewModels/_collections/{TopicViewModelCollection.cs => TopicViewModelCollection{TItem}.cs} (100%) diff --git a/OnTopic.ViewModels/_collections/TopicViewModelCollection.cs b/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs similarity index 100% rename from OnTopic.ViewModels/_collections/TopicViewModelCollection.cs rename to OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs From 4ffb7c8b35dd6b3c533c9f596c36a34b15e33952 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Mon, 1 Mar 2021 13:41:04 -0800 Subject: [PATCH 775/778] Updated `README` to reflect recent updates This should have been made prior to the previous merge of `improvement/model-interfaces` (2f98c398). Apologies, future readers, for the clumsy git history. --- OnTopic/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/OnTopic/README.md b/OnTopic/README.md index 81de778a..cba0cb7f 100644 --- a/OnTopic/README.md +++ b/OnTopic/README.md @@ -16,6 +16,7 @@ The `OnTopic` assembly represents the core domain layer of the OnTopic library. - [Specialty Collections](#specialty-collections) - [Editor](#editor-1) - [View Models](#view-models) + - [Binding Models](#binding-models) ## Entities - **[`Topic`](Topic.cs)**: This is the core entity in OnTopic, and models all attributes, relationships, and references associated with a topic record. @@ -74,7 +75,7 @@ The `OnTopic.Collections.Specialized` namespace includes a number of collections - **[`TopicReferenceCollection`](associations/TopicReferenceCollection.cs)**: A `TrackedRecordCollection` of [`TopicReferenceRecord`](Associations/TopicReferenceRecord.cs) instances keyed by `TopicReference.Key`; exposed by `Topic.References`. - **[`TopicMultiMap`](Collections/Specialized/TopicMultiMap.cs)**: Provides a multi-map (or collection-of-collections) for topics organized by a collection key. - **[`ReadOnlyTopicMultiMap`](Collections/Specialized/ReadOnlyTopicMultiMap.cs)**: A read-only interface to the `TopicMultiMap`, thus allowing simple enumeration of the collection withouthout exposing any write access. - - **[`TopicRelationshipMultiMap`](associations/TopicRelationshipMultiMap.cs)**: A `TopicMultiMap` of [`KeyValuesPair`](Collections/Specialized/KeyValuesPair.cs) instances keyed by `KeyValuesPair.Key`; exposed by `Topic.Relationships`. + - **[`TopicRelationshipMultiMap`](associations/TopicRelationshipMultiMap.cs)**: A `TopicMultiMap` of [`KeyValuesPair`](Collections/Specialized/KeyValuesPair{TKey,TValue}.cs) instances keyed by `KeyValuesPair.Key`; exposed by `Topic.Relationships`. ### Editor The following are intended to provide support for the Editor domain objects, `ContentTypeDescriptor` and `AttributeDescriptor`. @@ -83,10 +84,14 @@ The following are intended to provide support for the Editor domain objects, `Co ## View Models The core Topic library has been designed to be view model agnostic; i.e., view models should be defined for the specific presentation framework (e.g., ASP.NET Core) and customer. That said, to facilitate reusability of features that work with view models, several interfaces are defined which can be applied as appropriate. These include: -- **[`ITopicViewModel`](Models/ITopicViewModel.cs)**: Includes universal properties such as `Key`, `UniqueKey`, `Id`, `ContentType`, and `Title`. - - **[`IPageTopicViewModel`](Models/IPageTopicViewModel.cs)**: Includes page-specific properties such as `MetaKeywords` and `MetaDescription`. -- **[`INavigationTopicViewModel`](Models/INavigationTopicViewModel{T}.cs)**: Includes `IPageTopicViewModel`, `Children`, and an `IsSelected()` view logic handler, for use with navigation menus. +- **[`ICoreTopicViewModel`](Models/ICoreTopicViewModel.cs)**: Includes core properties `Key` and `ContentType` necessary for every `Topic`. + - **[`ITopicViewModel`](Models/ITopicViewModel.cs)**: Includes universal properties such as `UniqueKey`, `WebPath`, `Id`, and `Title`. +- **[`IHierarchicalTopicViewModel`](Models/IHierarchicalTopicViewModel{T}.cs)**: Includes a generic `Children` property necessary to model a hierarchical graph. +- **[`INavigableTopicViewModel`](Models/INavigableTopicViewModel.cs)**: Includes core properties `Title`, `ShortTitle`, and `WebPath`, necessary treating a topic as a navigable link. + - **[`INavigationTopicViewModel`](Models/INavigationTopicViewModel{T}.cs)**: Includes `IHierarchicalTopicViewModel` and the `IsSelected()` view logic method, for use with navigation menus. + +### Binding Models - **[`ITopicBindingModel`](Models/ITopicBindingModel.cs)**: Includes the bare minimum properties—namely `Key` and `ContentType`—needed to support a binding model that will be consumed by the `IReverseTopicMappingService`. -- **[`IRelatedTopicBindingModel`](Models/IRelatedTopicBindingModel.cs)**: Includes the bare minimum properties—namely `UniqueKey`—needed to reference another topic on a binding model that will be consumed by the `IReverseTopicMappingService`. +- **[`IAssociatedTopicBindingModel`](Models/IAssociatedTopicBindingModel.cs)**: Includes the bare minimum properties—namely `UniqueKey`—needed to associate another topic on a binding model that will be consumed by the `IReverseTopicMappingService`. In addition to these interfaces, a set of concrete implementations of view models corresponding to the default schemas for the out-of-the-box content types can be found in the [`OnTopic.ViewModels`](../OnTopic.ViewModels/README.md) package. \ No newline at end of file From a05691e501c48a40ef09bdbd7e01e72e3d17c5de Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Mar 2021 15:37:55 -0800 Subject: [PATCH 776/778] Moved description to single line NuGet displays line breaks and white space, instead of compressing it. As such, the `` needs to be implemented on a single line. --- OnTopic.All/OnTopic.All.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj index 832872f4..47d362c4 100644 --- a/OnTopic.All/OnTopic.All.csproj +++ b/OnTopic.All/OnTopic.All.csproj @@ -6,10 +6,7 @@ OnTopic Library Metapackage - - Includes all core packages associated with the OnTopic Library, excluding the OnTopic Editor. Reference this package as a - shorthand for establishing a reference to each of the individual packages. - + Includes all core packages associated with the OnTopic Library, excluding the OnTopic Editor. Reference this package as a shorthand for establishing a reference to each of the individual packages. bin\$(Configuration)\ From b21d49cbad0d8d6e2455b44102f01a554272f4c3 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Mar 2021 15:41:11 -0800 Subject: [PATCH 777/778] Updated to latest version of .NET Test SDK --- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 2 +- OnTopic.Tests/OnTopic.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index 4de28039..79e09287 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index 22e34c57..e38bf21d 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 5c0631da9d53d2ea1ee219259a15fd82e761efd8 Mon Sep 17 00:00:00 2001 From: JeremyCaney Date: Tue, 2 Mar 2021 15:43:27 -0800 Subject: [PATCH 778/778] Updated to latest version of Microsoft Test Framework This only affects the unit tests, and has no downstream impact on implementers. Ensured all unit tests continue to operate correctly. --- .../OnTopic.AspNetCore.Mvc.Tests.csproj | 4 ++-- OnTopic.Tests/OnTopic.Tests.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj index 79e09287..5d2910b7 100644 --- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj +++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj @@ -12,8 +12,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/OnTopic.Tests/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj index e38bf21d..0cad5ed3 100644 --- a/OnTopic.Tests/OnTopic.Tests.csproj +++ b/OnTopic.Tests/OnTopic.Tests.csproj @@ -12,8 +12,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + +