Skip to content

Commit 85216c4

Browse files
committed
Merge branch 'feature/TopicRepositoryBase-Improvements' into develop
Improved handling of the GetContentTypes() cache in `TopicRepositoryBase` In a previous update, I made significant updates to how the `GetContentTypeDescriptors()` are cached, in order to help ensure the current configuration is always being assessed when editing or saving topics via e.g. the OnTopic Editor (fb3366e). Unfortunately, while these updates were sufficient for handling most `Import()` scenarios needed for bootstrapping a new database (695664d), they were not enough to allow for live reconfiguration in most real-world scenarios. That is, after a change is made to the Oroborus Configuration (e.g., via `Root:Configuration:ContentTypes`), the application must be restarted before that change will take effect; it doesn't happen immediately. This was largely due to the fact that the `CachedTopicRepository`, which most sites use for performance reasons, operates off of a decorator pattern. This makes it easy for it to operate cache _any_ `ITopicRepository` instance, not just e.g. `SqlTopicRepository`. But, in doing so, it also makes it difficult for it to interact with and share caching data with the underlying `TopicRepositoryBase` used by e.g. `SqlTopicRepository`. That's notable since `TopicRepositoryBase` maintains its own cache of `ContentTypes`. As such, when those are updated via e.g. the OnTopic Editor, they are updated in the `CacheTopicRepository` topic graph, but not necessarily the `SqlTopicRepository`'s underlying `TopicRepositoryBase._contentTypeDescriptors` cache. As such, while the previous updates (fb3366e) added features for importing missing `AttributeDescriptor`s and `ContentTypeDescriptor`s from the topic graph, and even resetting aspects of the local cache, such as the `ContentTypeDescriptor.AttributeDescriptors` collection, when updating a `ContentTypeDescriptor` or `AttributeDescriptor`, this didn't do much good if the versions of the objects in the `TopicRepositoryBase._contentTypeDescriptors` cache were not necessarily the same as those in the `CachedtopicRepository` cache. In those cases, having e.g. the inheritance structure of `AttributeDescriptors` refreshed wouldn't necessarily pick up new results. To mitigate this, significant changes have been made to help ensure that the objects being cached in the `TopicRepositoryBase` and, thus, `SqlTopicRepository` are, in fact, the same ones being referenced by `CachedTopicRepository`. To support this, the following changes were made: - Introduced a new `ContentTypeDescriptorCollection.Refresh()` method (3e3930f) and corresponding constructor overload (60d4d8b) which allows the cache to be updated based on a root `ContentTypeDescriptor` (b206f3e) - Configured `GetContentTypeDescriptors()` (946ddb9) and `GetContentTypeDescriptors(ContentTypeDescriptor)` (afbfa47), and even `Delete()` (afe7b70) to use the new `Refresh()` method, thus simplifying and centralizing their logic - Renamed `GetContentTypeDescriptors(ContentTypeDescriptor)` to `SetContentTypeDescriptors()` (f1ade10, 78a9d3f, c75bbe2) so it's clearer that this will update the cache based on the parameter - Fixed a bug in `TopicRepositoryBase.Move()` which prevented `ResetAttributeDescriptors()` from being called when a topic was moved (5941385, d7a61cd, b2f27e8, 23f4cb1); this additionally depends on a bug fix in `OnTopic.Editor.AspNetCore` - Initialize the `SetContentTypeDescriptors()` cache on the initial call to `TopicRepositoryBase.Load()`; in most scenarios, this will be the warming of the `CachedTopicRepository` cache, thus ensuring the cache is shared between both e.g. `SqlTopicRepository` and `CachedTopicRepository` (dcdbab2) - Introduced a new `ContentTypeDescriptor.ResetPermittedContentTypes()` method (9a7ef01) so that the cached collection of permitted `ContentTypeDescriptor`s is updated whenever a `ContentTypeDescriptor`'s `Relationships` are updated In addition, while I was at it, I performed a number of other cleanup tasks mostly related to the `TopicRepositoryBase` class: - Updated the XML Docs to reflect previous extensions to `AttributeValueCollection.SetValue()` (8aea8e7, 10c7c09) and `RelatedTopicCollection.SetTopic()` (27391f5, 2f1e2bf) - Removed redundant update to cache in `SqlTopicRepository.Save()` (df7e6d2) - Expanded explanation for `ResetAttributeDescriptors()` method (3f952a6) - Implemented preferred pattern-matching syntax (d175f22) - Removed unnecessary `[NotNull]` attribute on base classes to avoid confusion with Roslyn's static flow analysis (2210b29) - Fixed unit test issues which covered up previous bugs (f980ef8, 6c081d8)
2 parents 86df229 + 60d4d8b commit 85216c4

7 files changed

Lines changed: 230 additions & 97 deletions

File tree

OnTopic.Data.Sql/SqlTopicRepository.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ public override Topic Load(int topicId, bool isRecursive = true) {
173173
}
174174
}
175175

176+
/*------------------------------------------------------------------------------------------------------------------------
177+
| Establish content type cache
178+
>-------------------------------------------------------------------------------------------------------------------------
179+
| If this load represents the entire topic graph, then relay the content type configuration to the TopicRepositoryBase in
180+
| order to either update or establish the content type cache. Not only does this prevent the need for a separate redundant
181+
| call later but, even more importantly, it helps ensure the same object references are maintained so that any updates to
182+
| subsequently cached content types are available.
183+
\-----------------------------------------------------------------------------------------------------------------------*/
184+
base.SetContentTypeDescriptors(topic);
185+
176186
/*------------------------------------------------------------------------------------------------------------------------
177187
| Return objects
178188
\-----------------------------------------------------------------------------------------------------------------------*/

OnTopic.TestDoubles/StubTopicRepository.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,16 @@ public IEnumerable<AttributeValue> GetAttributesProxy(
180180
| METHOD: GET CONTENT TYPE DESCRIPTORS (PROXY)
181181
\-------------------------------------------------------------------------------------------------------------------------*/
182182
/// <inheritdoc cref="TopicRepositoryBase.GetContentTypeDescriptors(ContentTypeDescriptor)" />
183+
[Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", false)]
183184
public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) =>
184-
base.GetContentTypeDescriptors(topicGraph);
185+
base.SetContentTypeDescriptors(topicGraph);
186+
187+
/*==========================================================================================================================
188+
| METHOD: SET CONTENT TYPE DESCRIPTORS (PROXY)
189+
\-------------------------------------------------------------------------------------------------------------------------*/
190+
/// <inheritdoc cref="TopicRepositoryBase.SetContentTypeDescriptors(ContentTypeDescriptor)" />
191+
public ContentTypeDescriptorCollection SetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) =>
192+
base.SetContentTypeDescriptors(topicGraph);
185193

186194
/*==========================================================================================================================
187195
| METHOD: GET CONTENT TYPE DESCRIPTOR (PROXY)

OnTopic.Tests/TopicRepositoryBaseTest.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ public void GetContentTypeDescriptors_WithTopicGraph_ReturnsMergedContentTypes()
321321
var newContentType = TopicFactory.Create("NewContentType", "ContentTypeDescriptor", rootContentType);
322322
var contentTypeCount = contentTypes.Count;
323323

324-
_topicRepository.GetContentTypeDescriptorsProxy((ContentTypeDescriptor)newContentType);
324+
_topicRepository.SetContentTypeDescriptorsProxy(rootContentType);
325325

326326
Assert.AreNotEqual<int>(contentTypeCount, contentTypes.Count);
327327
Assert.IsNotNull(contentTypes.Contains(newContentType));
@@ -380,6 +380,30 @@ public void Save_ContentTypeDescriptor_UpdatesContentTypeCache() {
380380

381381
}
382382

383+
/*==========================================================================================================================
384+
| TEST: SAVE: CONTENT TYPE DESCRIPTOR: UPDATES PERMITTED CONTENT TYPES
385+
\-------------------------------------------------------------------------------------------------------------------------*/
386+
/// <summary>
387+
/// Loads the <see cref="TopicRepositoryBase.GetContentTypeDescriptors()"/>, then saves an existing <see cref=
388+
/// "ContentTypeDescriptor"/> via <see cref="TopicRepositoryBase.Save(Topic, Boolean, Boolean)"/>, and ensures that
389+
/// it the <see cref="ContentTypeDescriptor.PermittedContentTypes"/> cache is updated.
390+
/// </summary>
391+
[TestMethod]
392+
public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() {
393+
394+
var contentTypes = _topicRepository.GetContentTypeDescriptors();
395+
var pageContentType = contentTypes.GetTopic("Page");
396+
var lookupContentType = contentTypes.GetTopic("Lookup");
397+
var initialCount = pageContentType.PermittedContentTypes.Count;
398+
399+
pageContentType.Relationships.SetTopic("ContentTypes", lookupContentType);
400+
401+
_topicRepository.Save(pageContentType);
402+
403+
Assert.AreNotEqual<int>(initialCount, pageContentType.PermittedContentTypes.Count);
404+
405+
}
406+
383407
/*==========================================================================================================================
384408
| TEST: DELETE: CONTENT TYPE DESCRIPTOR: UPDATES CONTENT TYPE CACHE
385409
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -418,7 +442,7 @@ public void Move_ContentTypeDescriptor_UpdatesContentTypeCache() {
418442

419443
_topicRepository.Move(contactContentType, pageContentType);
420444

421-
Assert.IsFalse(contactContentType?.AttributeDescriptors.Count > contactAttributeCount);
445+
Assert.AreNotEqual<int?>(contactContentType?.AttributeDescriptors.Count, contactAttributeCount);
422446

423447
}
424448

OnTopic/Attributes/AttributeSetterAttribute.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ namespace OnTopic.Attributes {
1313
\---------------------------------------------------------------------------------------------------------------------------*/
1414
/// <summary>
1515
/// Flags that a property should be used when setting an attribute via
16-
/// <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>.
16+
/// <see cref="AttributeValueCollection.SetValue(String, String?, Boolean?, DateTime?, Boolean?)"/>.
1717
/// </summary>
1818
/// <remarks>
1919
/// <para>
20-
/// When a call is made to <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?)"/>, the code
21-
/// will check to see if a property with the same name as the attribute key exists, and whether that property is decorated
22-
/// with the <see cref="AttributeSetterAttribute"/> (i.e., <code>[AttributeSetter]</code>). If it is, then the update will
23-
/// be routed through that property. This ensures that business logic is enforced by local properties, instead of allowing
24-
/// business logic to be potentially bypassed by writing directly to the <see cref="Topic.Attributes"/> collection.
20+
/// When a call is made to <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, DateTime?, Boolean?)"/>,
21+
/// the code will check to see if a property with the same name as the attribute key exists, and whether that property is
22+
/// decorated with the <see cref="AttributeSetterAttribute"/> (i.e., <code>[AttributeSetter]</code>). If it is, then the
23+
/// update will be routed through that property. This ensures that business logic is enforced by local properties, instead
24+
/// of allowing business logic to be potentially bypassed by writing directly to the <see cref="Topic.Attributes"/>
25+
/// collection.
2526
/// </para>
2627
/// <para>
2728
/// As an example, the <see cref="Topic.Key"/> property is adorned with the <see cref="AttributeSetterAttribute"/>. As a
@@ -33,11 +34,12 @@ namespace OnTopic.Attributes {
3334
/// </para>
3435
/// <para>
3536
/// To ensure this logic, it is critical that implementers of <see cref="AttributeSetterAttribute"/> ensure that the
36-
/// property setters call <see cref="AttributeValueCollection.SetValue(String, String, Boolean?, Boolean, DateTime?)"/>
37-
/// overload with the final parameter set to false to disable the enforcement of business logic. Otherwise, an infinite
38-
/// loop will occur. Calling that overload tells <see cref="AttributeValueCollection"/> that the business logic has
39-
/// already been enforced by the caller. As this is an internal overload, implementers should use the local proxy at
40-
/// <see cref="Topic.SetAttributeValue(String, String, Boolean?)"/>, which ensures that final parameter is set to false.
37+
/// property setters call <see cref="AttributeValueCollection.SetValue(String, String?, Boolean?, Boolean, DateTime?,
38+
/// Boolean?)"/> overload with the final parameter set to false to disable the enforcement of business logic. Otherwise,
39+
/// an infinite loop will occur. Calling that overload tells <see cref="AttributeValueCollection"/> that the business
40+
/// logic has already been enforced by the caller. As this is an internal overload, implementers should use the local
41+
/// proxy at <see cref="Topic.SetAttributeValue(String, String, Boolean?)"/>, which ensures that final parameter is set to
42+
/// false.
4143
/// </para>
4244
/// </remarks>
4345
[AttributeUsage(AttributeTargets.Property)]

OnTopic/Metadata/ContentTypeDescriptor.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public bool DisableChildTopics {
127127
/// </para>
128128
/// <para>
129129
/// To add content types to the <see cref="PermittedContentTypes"/> collection, use <see
130-
/// cref="RelatedTopicCollection.SetTopic(String, Topic, Boolean)"/>.
130+
/// cref="RelatedTopicCollection.SetTopic(String, Topic, Boolean, Boolean?)"/>.
131131
/// </para>
132132
/// </remarks>
133133
public ReadOnlyTopicCollection<ContentTypeDescriptor> PermittedContentTypes {
@@ -238,6 +238,21 @@ internal void ResetAttributeDescriptors() {
238238
}
239239
}
240240

241+
/*==========================================================================================================================
242+
| METHOD: RESET PERMITTED CONTENT TYPES
243+
\-------------------------------------------------------------------------------------------------------------------------*/
244+
/// <summary>
245+
/// Resets the list of <see cref="ContentTypeDescriptor"/>s stored in the <see cref="PermittedContentTypes"/> collection
246+
/// on this <see cref="ContentTypeDescriptor"/>.
247+
/// </summary>
248+
/// <remarks>
249+
/// Each <see cref="ContentTypeDescriptor"/> has a <see cref="PermittedContentTypes"/> collection which includes the <see
250+
/// cref="ContentTypeDescriptor"/>s associated with that <see cref="ContentTypeDescriptor"/>. As this is cached, however,
251+
/// the value should be reset whenever a <see cref="ContentTypeDescriptor"/> has been modified, in case those values have
252+
/// changed.
253+
/// </remarks>
254+
internal void ResetPermittedContentTypes() => _permittedContentTypes = null;
255+
241256
/*==========================================================================================================================
242257
| METHOD: IS TYPE OF?
243258
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic/Metadata/ContentTypeDescriptorCollection.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
| Client Ignia, LLC
44
| Project Topics Library
55
\=============================================================================================================================*/
6+
using System;
7+
using System.Diagnostics.Contracts;
8+
using System.Linq;
69
using OnTopic.Collections;
10+
using OnTopic.Querying;
11+
using OnTopic.Repositories;
712

813
namespace OnTopic.Metadata {
914

@@ -13,6 +18,13 @@ namespace OnTopic.Metadata {
1318
/// <summary>
1419
/// Represents a collection of <see cref="ContentTypeDescriptor"/> objects.
1520
/// </summary>
21+
/// <remarks>
22+
/// While the <see cref="ContentTypeDescriptorCollection"/> can be used to store any collection of <see cref=
23+
/// "ContentTypeDescriptor"/> objects, it has additional tooling in the form of the <see cref="Refresh(
24+
/// ContentTypeDescriptor?)"/> as well as the <see cref="ContentTypeDescriptorCollection(ContentTypeDescriptor?)"/>
25+
/// constructor for handling a flattened list of <see cref="ContentTypeDescriptor"/>s used for the <see cref=
26+
/// "ITopicRepository.GetContentTypeDescriptors()"/> method.
27+
/// </remarks>
1628
public class ContentTypeDescriptorCollection : TopicCollection<ContentTypeDescriptor> {
1729

1830
/*==========================================================================================================================
@@ -24,5 +36,54 @@ public class ContentTypeDescriptorCollection : TopicCollection<ContentTypeDescri
2436
public ContentTypeDescriptorCollection() : base(null) {
2537
}
2638

39+
/// <summary>
40+
/// Initializes a new instance of the <see cref="ContentTypeDescriptorCollection"/> class based on a root <see cref=
41+
/// "ContentTypeDescriptor"/>.
42+
/// </summary>
43+
/// <param name="rootContentType">The <see cref="ContentTypeDescriptor"/> from which to initialize the collection.</param>
44+
public ContentTypeDescriptorCollection(ContentTypeDescriptor? rootContentType) : base(null) {
45+
Refresh(rootContentType);
46+
}
47+
48+
/*==========================================================================================================================
49+
| REFRESH
50+
\-------------------------------------------------------------------------------------------------------------------------*/
51+
/// <summary>
52+
/// Updates the current instance of the <see cref="ContentTypeDescriptorCollection"/> based on a new root <see cref=
53+
/// "ContentTypeDescriptor"/>.
54+
/// </summary>
55+
/// <param name="rootContentType">The <see cref="ContentTypeDescriptor"/> from which to initialize the collection.</param>
56+
public void Refresh(ContentTypeDescriptor? rootContentType) {
57+
58+
/*------------------------------------------------------------------------------------------------------------------------
59+
| Validate parameters
60+
\-----------------------------------------------------------------------------------------------------------------------*/
61+
if (rootContentType == null || rootContentType.Children.Count == 0) {
62+
return;
63+
}
64+
65+
Contract.Requires(
66+
rootContentType.Key.Equals("ContentTypes", StringComparison.OrdinalIgnoreCase),
67+
$"The {nameof(rootContentType)} is expected to represent the root of the content type topic graph, with a " +
68+
$"key of 'ContentTypes'. Instead, the supplied ContentTypeDescriptor has a key of '{rootContentType.Key}'."
69+
);
70+
71+
/*------------------------------------------------------------------------------------------------------------------------
72+
| Clear any existing values
73+
\-----------------------------------------------------------------------------------------------------------------------*/
74+
Clear();
75+
76+
/*------------------------------------------------------------------------------------------------------------------------
77+
| Add all ContentTypeDescriptors to collection
78+
\-----------------------------------------------------------------------------------------------------------------------*/
79+
var contentTypeDescriptors = rootContentType
80+
.FindAll(t => typeof(ContentTypeDescriptor).IsAssignableFrom(t.GetType()))
81+
.Cast<ContentTypeDescriptor>();
82+
foreach (var contentType in contentTypeDescriptors) {
83+
Add((ContentTypeDescriptor)contentType);
84+
}
85+
86+
}
87+
2788
} //Class
2889
} //Namespace

0 commit comments

Comments
 (0)