Skip to content

Commit 6ec3b60

Browse files
committed
Merge branch 'release/4.3.0'
This update provides backend functionality and bug fixes to improve performance and reliability, with a particular emphasis on importing and saving large topic graphs. New Functionality - `ContentTypeDescriptor`s (a9ea634) and `AttributeDescriptor`s (b0c5a5e) are now updated dynamically when saved, and thus configuration changes are now available immediately without requiring an application restart (fb3366e). - The byline (`LastModifiedBy`) and dateline (`LastModified`) are now only persisted to the underlying topic repository if any other attributes (d8d41b7) or relationships (8caa148) have changed (d8d41b7). - _Arbitrary attributes_ (i.e., attributes not defined in the `ContentTypeDescriptor`) can now be programmatically added (c3c01ef), modified, or deleted (2151a08) via the OnTopic Library (8b7dffe). - Empty databases can now be connected to without throwing exceptions, thus allowing a reference schema can be `Import()`ed via the **OnTopic Data Transfer** library using the **OnTopic Editor** (695664d). - `TopicRepository.Save()` will now loop back and save any topics with previously unresolved (i.e., unsaved) references (including relationships or `DerivedTopic`) (913b68d). - `UpdateRelationships` now synchronizes relationships, instead of just inserting them. It accepts an optional `@DeleteUnmatched` argument which can be disabled to append new relationships, instead of also deleting unmatched relationships (cbba817). Bug Fixes - Fixed a bug where topics from the previous relationship were saved to each subsequent relationship for a topic (27d9bfe). - Fixed a bug where attributes that had been reconfigured away from `IsIndexedAttribute`, but not otherwise changed, would be orphaned (7a61005) deleted (d64d242) on `Save()`. - Fixed a bug where an off-target exception would be thrown if `SqlTopicRepository.Load(string topicKey)` wasn't able to identify a matching topic (c97dcb3). - Fixed a bug where unsaved relationships (6da3fa4) and derived topics (a3b6da1) were saving `-1` to the database if they referenced unsaved topics. Performance - Reduced execution time of `UpdateTopic` by 50% through optimization of queries (fae0e8d). - `SqlTopicRepository.Save()` will only save topics which have actually changed; this greatly increases the performance of recursive saves when merging large topic graphs (afa1e9b). API Changes - Introduced `AttributeValuesDataTable` (313fe71) and `TopicListDataTable` (7a37dc9) to encapsulate the corresponding user-defined, table-valued types in SQL, and extended `SqlCommand.AddParameter()` to support them (0cd2506). - Introduced `TopicNotFoundException` (2300e69), which derives from `TopicRepositoryException` and is used in `ITopicRepository.Load()`. - Introduced `TopicRepositoryBase.GetContentTypeDescriptor()` convenience method (408d01a) for loading a `ContentTypeDescriptor`—either from the database, or from the local topic graph. - Introduced `TopicRepositoryBase.GetAttributes()` method for retrieving extended or indexed attributes, optionally filtered by `IsDirty` (120c55e). - Introduced `Topic.GetTopicByUniqueKey()` (867b110) and `Topic.GetRootTopic()` (0bddfb0) extension methods to `OnTopic.Queryable` namespace (d18984d). - Introduced `Topic.IsNew` property for tracking whether a topic is new, or if it has been persisted to the database (1678523, f046179). - Introduced a `NamedTopicCollection.IsDirty` property (4762694), `RelatedTopicCollection.IsDirty()` (2adef58), and `AttributeValueCollection.IsDirty()` (d3b49ff) for tracking if any relationships or attributes have changed (afa1e9b). - Removed legacy `SqlTopicRepository.CreateRelationshipsXml()` private helper function, as that functionality is no longer used (7bea7d3). General - Migrated to C# 8.0's new block-level `using` statement, in favor of placing explicit `Dispose()` calls in `finally` blocks (f85aab2) - Migrated to GitVersion's `ContinuousDeployment` mode for all branches except `master` to simplify version incrementing on feature branches (b4105f3). Maintenance - Various dependency updates (20374cd, dcf4eaf) and code cleanup (718aea5) tasks.
2 parents 732d405 + 9a63855 commit 6ec3b60

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2338
-533
lines changed

GitVersion.yml

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
assembly-versioning-scheme: MajorMinorPatch
2-
mode: ContinuousDelivery
3-
branches: {}
2+
mode: ContinuousDeployment
3+
branches:
4+
master:
5+
mode: ContinuousDelivery
46
ignore:
5-
sha: []
7+
sha: []

OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs

+14-18
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,13 @@ public object Create(ControllerContext context) {
9797
/*------------------------------------------------------------------------------------------------------------------------
9898
| Configure and return appropriate controller
9999
\-----------------------------------------------------------------------------------------------------------------------*/
100-
if (type == typeof(TopicController)) {
101-
return new TopicController(_topicRepository, _topicMappingService);
102-
}
103-
if (type == typeof(SitemapController)) {
104-
return new SitemapController(_topicRepository);
105-
}
106-
else {
107-
throw new Exception($"Unknown controller {type.Name}");
108-
}
100+
return type.Name switch {
101+
nameof(TopicController) =>
102+
new TopicController(_topicRepository, _topicMappingService),
103+
nameof(SitemapController) =>
104+
new SitemapController(_topicRepository),
105+
_ => throw new Exception($"Unknown controller {type.Name}")
106+
};
109107

110108
}
111109

@@ -123,15 +121,13 @@ public object Create(ViewComponentContext context) {
123121
/*------------------------------------------------------------------------------------------------------------------------
124122
| Configure and return appropriate view component
125123
\-----------------------------------------------------------------------------------------------------------------------*/
126-
if (type == typeof(MenuViewComponent)) {
127-
return new MenuViewComponent(_topicRepository, _hierarchicalMappingService);
128-
}
129-
else if (type == typeof(PageLevelNavigationViewComponent)) {
130-
return new PageLevelNavigationViewComponent(_topicRepository, _hierarchicalMappingService);
131-
}
132-
else {
133-
throw new Exception($"Unknown view component {type.Name}");
134-
}
124+
return type.Name switch {
125+
nameof(MenuViewComponent) =>
126+
new MenuViewComponent(_topicRepository, _hierarchicalMappingService),
127+
nameof(PageLevelNavigationViewComponent) =>
128+
new PageLevelNavigationViewComponent(_topicRepository, _hierarchicalMappingService),
129+
_ => throw new Exception($"Unknown view component {type.Name}")
130+
};
135131

136132
}
137133

OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
1010
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
1111
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
1212
</ItemGroup>

OnTopic.AspNetCore.Mvc.Tests/TopicRepositoryExtensionsTest.cs

+22
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,27 @@ public void Load_ByRoute_ReturnsTopic() {
6565

6666
}
6767

68+
/*==========================================================================================================================
69+
| TEST: LOAD: BY ROUTE: RETURNS ROOT TOPIC
70+
\-------------------------------------------------------------------------------------------------------------------------*/
71+
/// <summary>
72+
/// Establishes route data and ensures that the root topic is correctly identified based on that route.
73+
/// </summary>
74+
[TestMethod]
75+
public void Load_ByRoute_ReturnsRootTopic() {
76+
77+
var routes = new RouteData();
78+
var topic = _topicRepository.Load("Root");
79+
80+
routes.Values.Add("path", "Root/");
81+
82+
var currentTopic = _topicRepository.Load(routes);
83+
84+
Assert.IsNotNull(currentTopic);
85+
Assert.ReferenceEquals(topic, currentTopic);
86+
Assert.AreEqual<string>("Root", currentTopic.Key);
87+
88+
}
89+
6890
} //Class
6991
} //Namespace

OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs

+33
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,40 @@ namespace OnTopic.AspNetCore.Mvc.Models {
2727
/// </remarks>
2828
public class NavigationViewModel<T> where T : class, INavigationTopicViewModel<T> {
2929

30+
/*==========================================================================================================================
31+
| NAVIGATION ROOT
32+
\-------------------------------------------------------------------------------------------------------------------------*/
33+
/// <summary>
34+
/// Represents the root element in the navigation.
35+
/// </summary>
36+
/// <remarks>
37+
/// Since this implements <see cref="IHierarchicalTopicViewModel{T}"/>, it may include multiple levels of children. By
38+
/// implementing it as a generic, each site or application can provide its own <see cref="INavigationTopicViewModel{T}"/>
39+
/// implementation, thus potentially extending the schema with properties relevant to that site's navigation. For example,
40+
/// a site may optionally add an <c>IconUrl</c> property if it wishes to assign unique icons to each link in the
41+
/// navigation.
42+
/// </remarks>
3043
public T? NavigationRoot { get; set; }
44+
45+
/*==========================================================================================================================
46+
| CURRENT KEY
47+
\-------------------------------------------------------------------------------------------------------------------------*/
48+
/// <summary>
49+
/// The <see cref="Topic.GetUniqueKey()"/> representing the path to the current <see cref="Topic"/>.
50+
/// </summary>
51+
/// <remarks>
52+
/// <para>
53+
/// In order to determine whether any given <see cref="INavigationTopicViewModel{T}.IsSelected(String)"/>, the views
54+
/// will need to know where in the hierarchy the user currently is. By storing this on the <see
55+
/// cref="NavigationViewModel{T}"/> used as the root view model for every navigation component, we ensure that the views
56+
/// always have access to this information.
57+
/// </para>
58+
/// <para>
59+
/// It's worth noting that while this <i>could</i> be stored on the <see cref="INavigationTopicViewModel{T}"/> itself,
60+
/// that would prevent those values from being cached. As such, it's preferrable to keep the navigation nodes stateless,
61+
/// and maintaining state exclusively in the <see cref="NavigationViewModel{T}"/> itself.
62+
/// </para>
63+
/// </remarks>
3164
public string CurrentKey { get; set; } = default!;
3265

3366
} //Class

OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939

4040
<ItemGroup>
4141
<FrameworkReference Include="Microsoft.AspNetCore.App" />
42-
<PackageReference Include="GitVersionTask" Version="5.2.4">
42+
<PackageReference Include="GitVersionTask" Version="5.3.3">
4343
<PrivateAssets>all</PrivateAssets>
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>
46-
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
46+
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
4747
<PrivateAssets>all</PrivateAssets>
4848
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
4949
</PackageReference>

OnTopic.Data.Caching/CachedTopicRepository.cs

+1-76
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
102102
| Lookup by TopicKey
103103
\-----------------------------------------------------------------------------------------------------------------------*/
104104
if (topicKey != null && !topicKey.Length.Equals(0)) {
105-
return GetTopic(_cache, topicKey);
105+
return _cache.GetByUniqueKey(topicKey);
106106
}
107107

108108
/*------------------------------------------------------------------------------------------------------------------------
@@ -131,76 +131,6 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
131131

132132
}
133133

134-
/*==========================================================================================================================
135-
| METHOD: GET TOPIC
136-
\-------------------------------------------------------------------------------------------------------------------------*/
137-
/// <summary>
138-
/// Retrieves a topic object based on the specified partial or full (prefixed) topic key.
139-
/// </summary>
140-
/// <param name="sourceTopic">The root topic to search from.</param>
141-
/// <param name="uniqueKey">
142-
/// The partial or full string value representing the key (or <see cref="Topic.GetUniqueKey"/>) for the topic.
143-
/// </param>
144-
/// <returns>The topic or null, if the topic is not found.</returns>
145-
private Topic? GetTopic(Topic? sourceTopic, string uniqueKey) {
146-
147-
/*------------------------------------------------------------------------------------------------------------------------
148-
| Validate input
149-
\-----------------------------------------------------------------------------------------------------------------------*/
150-
if (sourceTopic == null) return null;
151-
if (String.IsNullOrWhiteSpace(uniqueKey)) return null;
152-
153-
/*------------------------------------------------------------------------------------------------------------------------
154-
| Provide shortcut for local calls
155-
\-----------------------------------------------------------------------------------------------------------------------*/
156-
if (uniqueKey.IndexOf(":", StringComparison.InvariantCulture) < 0 && uniqueKey != "Root") {
157-
if (sourceTopic.Children.Contains(uniqueKey)) {
158-
return sourceTopic.Children[uniqueKey];
159-
}
160-
return null;
161-
}
162-
163-
/*------------------------------------------------------------------------------------------------------------------------
164-
| Provide implicit root
165-
>-------------------------------------------------------------------------------------------------------------------------
166-
| ###NOTE JJC080313: While a root topic is required by the data structure, it should be implicit from the perspective of
167-
| the calling application. A developer should be able to call GetTopic("Namespace:TopicPath") to get to a topic, without
168-
| needing to be aware of the root.
169-
\-----------------------------------------------------------------------------------------------------------------------*/
170-
if (
171-
!uniqueKey.StartsWith("Root:", StringComparison.OrdinalIgnoreCase) &&
172-
!uniqueKey.Equals("Root", StringComparison.OrdinalIgnoreCase)
173-
) {
174-
uniqueKey = "Root:" + uniqueKey;
175-
}
176-
177-
/*------------------------------------------------------------------------------------------------------------------------
178-
| Validate parameters
179-
\-----------------------------------------------------------------------------------------------------------------------*/
180-
if (!uniqueKey.StartsWith(sourceTopic.GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) return null;
181-
if (uniqueKey.Equals(sourceTopic.GetUniqueKey(), StringComparison.OrdinalIgnoreCase)) return sourceTopic;
182-
183-
/*------------------------------------------------------------------------------------------------------------------------
184-
| Define variables
185-
\-----------------------------------------------------------------------------------------------------------------------*/
186-
var remainder = uniqueKey.Substring(sourceTopic.GetUniqueKey().Length + 1);
187-
var marker = remainder.IndexOf(":", StringComparison.Ordinal);
188-
var nextChild = (marker < 0) ? remainder : remainder.Substring(0, marker);
189-
190-
/*------------------------------------------------------------------------------------------------------------------------
191-
| Find topic
192-
\-----------------------------------------------------------------------------------------------------------------------*/
193-
if (!sourceTopic.Children.Contains(nextChild)) return null;
194-
195-
if (nextChild == remainder) return sourceTopic.Children[nextChild];
196-
197-
/*------------------------------------------------------------------------------------------------------------------------
198-
| Return the topic
199-
\-----------------------------------------------------------------------------------------------------------------------*/
200-
return GetTopic(sourceTopic.Children[nextChild], uniqueKey);
201-
202-
}
203-
204134
/*==========================================================================================================================
205135
| METHOD: SAVE
206136
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -212,11 +142,6 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f
212142
| METHOD: MOVE
213143
\-------------------------------------------------------------------------------------------------------------------------*/
214144
/// <inheritdoc />
215-
[System.Diagnostics.CodeAnalysis.SuppressMessage(
216-
"Microsoft.Contracts",
217-
"TestAlwaysEvaluatingToAConstant",
218-
Justification = "Sibling may be null from overloaded caller."
219-
)]
220145
public override void Move(Topic topic, Topic target, Topic? sibling) => _dataProvider.Move(topic, target, sibling);
221146

222147
/*==========================================================================================================================

OnTopic.Data.Caching/OnTopic.Data.Caching.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
</PropertyGroup>
4040

4141
<ItemGroup>
42-
<PackageReference Include="GitVersionTask" Version="5.2.4">
42+
<PackageReference Include="GitVersionTask" Version="5.3.3">
4343
<PrivateAssets>all</PrivateAssets>
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>
46-
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
46+
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
4747
<PrivateAssets>all</PrivateAssets>
4848
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
4949
</PackageReference>

OnTopic.Data.Sql.Database/Functions/GetTopicID.sql

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ BEGIN
1919
-- DECLARE AND DEFINE VARIABLES
2020
------------------------------------------------------------------------------------------------------------------------------
2121
DECLARE @TopicID INT
22+
SET @TopicID = -1
2223

2324
------------------------------------------------------------------------------------------------------------------------------
2425
-- GET TOPIC ID BASED ON TOPIC KEY

OnTopic.Data.Sql.Database/Stored Procedures/DeleteTopic.sql

+8-8
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,23 @@ WHERE RangeLeft
8787
--------------------------------------------------------------------------------------------------------------------------------
8888
DELETE Attributes
8989
FROM Attributes Attributes
90-
INNER JOIN @Topics Topics
91-
ON Topics.TopicId = Attributes.TopicID
90+
INNER JOIN @Topics Topics
91+
ON Topics.TopicId = Attributes.TopicID
9292

9393
DELETE ExtendedAttributes
9494
FROM ExtendedAttributes ExtendedAttributes
95-
INNER JOIN @Topics Topics
96-
ON Topics.TopicId = ExtendedAttributes.TopicID
95+
INNER JOIN @Topics Topics
96+
ON Topics.TopicId = ExtendedAttributes.TopicID
9797

9898
DELETE Relationships
9999
FROM Relationships Relationships
100-
INNER JOIN @Topics Topics
101-
ON Topics.TopicId = Relationships.Source_TopicID
100+
INNER JOIN @Topics Topics
101+
ON Topics.TopicId = Relationships.Source_TopicID
102102

103103
DELETE Relationships
104104
FROM Relationships Relationships
105-
INNER JOIN @Topics Topics
106-
ON Topics.TopicId = Relationships.Target_TopicID
105+
INNER JOIN @Topics Topics
106+
ON Topics.TopicId = Relationships.Target_TopicID
107107

108108
--------------------------------------------------------------------------------------------------------------------------------
109109
-- DELETE RANGE

OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql

+21-23
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,10 @@
77
CREATE PROCEDURE [dbo].[UpdateRelationships]
88
@TopicID INT = -1,
99
@RelationshipKey VARCHAR(255) = 'related',
10-
@RelatedTopics TopicList READONLY
10+
@RelatedTopics TopicList READONLY,
11+
@DeleteUnmatched BIT = 1
1112
AS
1213

13-
--------------------------------------------------------------------------------------------------------------------------------
14-
-- DECLARE AND SET VARIABLES
15-
16-
DECLARE @Existing_TopicIDs TopicList
17-
18-
--------------------------------------------------------------------------------------------------------------------------------
19-
-- IDENTIFY EXISTING VALUES
20-
--------------------------------------------------------------------------------------------------------------------------------
21-
INSERT
22-
INTO @Existing_TopicIDs (
23-
TopicID
24-
)
25-
SELECT Target_TopicID
26-
FROM Relationships
27-
WHERE Source_TopicID = @TopicID
28-
AND RelationshipKey = @RelationshipKey
29-
3014
--------------------------------------------------------------------------------------------------------------------------------
3115
-- INSERT NOVEL VALUES
3216
--------------------------------------------------------------------------------------------------------------------------------
@@ -36,13 +20,27 @@ INTO Relationships (
3620
RelationshipKey,
3721
Target_TopicID
3822
)
39-
SELECT @TopicId,
23+
SELECT @TopicID,
4024
@RelationshipKey,
41-
Target.TopicID
25+
TopicID
4226
FROM @RelatedTopics Target
43-
FULL JOIN @Existing_TopicIDs Existing
44-
ON Existing.TopicID = Target.TopicID
45-
WHERE Existing.TopicID is null
27+
LEFT JOIN Relationships Existing
28+
ON Target_TopicID = TopicID
29+
AND Source_TopicID = @TopicID
30+
WHERE Target_TopicID IS NULL
31+
32+
--------------------------------------------------------------------------------------------------------------------------------
33+
-- DELETE UNMATCHED VALUES
34+
--------------------------------------------------------------------------------------------------------------------------------
35+
IF @DeleteUnmatched = 1
36+
BEGIN
37+
DELETE EXISTING
38+
FROM @RelatedTopics Relationships
39+
RIGHT JOIN Relationships Existing
40+
ON Target_TopicID = TopicID
41+
WHERE Source_TopicID = @TopicID
42+
AND ISNULL(TopicID, '') = ''
43+
END
4644

4745
--------------------------------------------------------------------------------------------------------------------------------
4846
-- RETURN TOPIC ID

0 commit comments

Comments
 (0)