diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..4cdabdb3
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,33 @@
+
+
+
+ 9.0
+ enable
+ latest
+ AllEnabledByDefault
+
+
+
+ Ignia
+ OnTopic
+ ©2021 Ignia, LLC
+ Ignia
+ https://github.com/Ignia/Topics-Library
+ true
+ en
+ true
+ true
+ true
+ true
+ Icon.png
+
+
+
+
+
+
+
+ true
+
+
+
\ No newline at end of file
diff --git a/Icon.png b/Icon.png
new file mode 100644
index 00000000..63cd745a
Binary files /dev/null and b/Icon.png differ
diff --git a/OnTopic.All/OnTopic.All.csproj b/OnTopic.All/OnTopic.All.csproj
new file mode 100644
index 00000000..47d362c4
--- /dev/null
+++ b/OnTopic.All/OnTopic.All.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netcoreapp3.1
+
+
+
+ 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.
+ bin\$(Configuration)\
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
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.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.
+
+[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3dfb3a0a-c049-407d-959e-546f714dcd0f&preferRelease=true)
+[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
+
+### 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/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/OnTopic.AspNetCore.Mvc.Host.csproj b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj
index da24af8d..097a454b 100644
--- a/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj
+++ b/OnTopic.AspNetCore.Mvc.Host/OnTopic.AspNetCore.Mvc.Host.csproj
@@ -1,17 +1,20 @@

- netcoreapp3.1
+ net5.0
62eb85bf-f802-4afd-8bec-3d344e1cfc79
false
-
-
-
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
+
+
+
+
+
\ 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
diff --git a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs
index 3126ab9f..e53b62fa 100644
--- a/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs
+++ b/OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs
@@ -11,9 +11,10 @@
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;
-using OnTopic.Reflection;
using OnTopic.Repositories;
using OnTopic.ViewModels;
@@ -31,21 +32,22 @@ 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
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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
@@ -86,14 +88,28 @@ 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) {
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate parameters
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(context, nameof(context));
+
/*------------------------------------------------------------------------------------------------------------------------
| Determine controller type
\-----------------------------------------------------------------------------------------------------------------------*/
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
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -104,7 +120,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}")
};
}
@@ -112,9 +128,14 @@ 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) {
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate parameters
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(context, nameof(context));
+
/*------------------------------------------------------------------------------------------------------------------------
| Determine view component type
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -128,7 +149,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.Host/Views/ContentList/Accordion.cshtml b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml
index ce32ef5b..04b3d1d4 100644
--- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml
+++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/Accordion.cshtml
@@ -1,7 +1,7 @@
-@Html.PartialAsync("~/Views/ContentList/ContentList.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..a0519cf9 100644
--- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml
+++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/IndexedList.cshtml
@@ -1,7 +1,7 @@
-@Html.PartialAsync("~/Views/ContentList/ContentList.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..d37eb323 100644
--- a/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml
+++ b/OnTopic.AspNetCore.Mvc.Host/Views/ContentList/LinkedList.cshtml
@@ -1,7 +1,7 @@
-@Html.PartialAsync("~/Views/ContentList/ContentList.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/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
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..6c01ca42 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
- @foreach (var topic in Model.NavigationRoot.Children) {
+ @foreach (var topic in Model.NavigationRoot!.Children) {
@WriteMenu(topic);
}
@@ -12,12 +12,12 @@
@{
- IHtmlContent Body(Func body) => body(null);
+ IHtmlContent Body(Func body) => body(null);
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);
@@ -30,6 +30,6 @@
}
+-->
\ 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..0994ed03 100644
--- a/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml
+++ b/OnTopic.AspNetCore.Mvc.Host/Views/Shared/_TopicAttributes.cshtml
@@ -4,9 +4,9 @@
Id: @Model.Id
Key: @Model.Key
+ ContentType: @Model.ContentType
UniqueKey: @Model.UniqueKey
WebPath: @Model.WebPath
- IsHidden? @Model.IsHidden
LastModified: @Model.LastModified
View: @Model.View
@@ -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
diff --git a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj
index 890a03c6..5d2910b7 100644
--- a/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj
+++ b/OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj
@@ -1,24 +1,24 @@

- netcoreapp3.1
+ net5.0
false
- 9.0
+ CA1707
-
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
-
-
-
-
+
\ No newline at end of file
diff --git a/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs b/OnTopic.AspNetCore.Mvc.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..bcdd7ce1
--- /dev/null
+++ b/OnTopic.AspNetCore.Mvc.Tests/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.Tests/TopicControllerTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicControllerTest.cs
index 22155839..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 {
@@ -91,12 +91,12 @@ 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();
Assert.IsNotNull(model);
- Assert.AreEqual("Web_0_1_1", model.Title);
+ Assert.AreEqual("Web_0_1_1", model?.Title);
}
@@ -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() {
@@ -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);
}
@@ -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() {
@@ -138,13 +138,97 @@ 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("", StringComparison.Ordinal));
+ Assert.IsTrue(model!.Contains("/Web/Web_1/Web_1_1/Web_1_1_1/", StringComparison.Ordinal));
+
+ }
+
+ /*==========================================================================================================================
+ | 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")!;
+ 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(),
+ 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("", 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));
+
+ }
+
+ /*==========================================================================================================================
+ | 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("", StringComparison.Ordinal));
+ Assert.IsTrue(model!.Contains("", StringComparison.Ordinal));
+ Assert.IsFalse(model!.Contains("", StringComparison.Ordinal));
+ Assert.IsFalse(model!.Contains("", StringComparison.Ordinal));
}
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/TopicViewComponentTest.cs b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs
index 93fb868a..3d7c5a59 100644
--- a/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs
+++ b/OnTopic.AspNetCore.Mvc.Tests/TopicViewComponentTest.cs
@@ -3,13 +3,13 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
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;
@@ -42,7 +42,7 @@ public class TopicViewComponentTest {
/*==========================================================================================================================
| HIERARCHICAL TOPIC MAPPING SERVICE
\-------------------------------------------------------------------------------------------------------------------------*/
- private readonly IHierarchicalTopicMappingService _hierarchicalMappingService = null;
+ private readonly IHierarchicalTopicMappingService _hierarchicalMappingService;
/*==========================================================================================================================
| CONSTRUCTOR
@@ -111,13 +111,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.GetWebPath(), model?.CurrentWebPath);
+ Assert.AreEqual("/Web/", model?.NavigationRoot?.WebPath);
+ Assert.AreEqual(3, model?.NavigationRoot?.Children.Count);
+ Assert.IsTrue(model?.NavigationRoot?.IsSelected(_topic.GetWebPath())?? false);
}
@@ -136,13 +136,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.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);
}
diff --git a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs
index 3be2c115..87e31cef 100644
--- a/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs
+++ b/OnTopic.AspNetCore.Mvc.Tests/ValidateTopicAttributeTest.cs
@@ -10,11 +10,11 @@
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;
using OnTopic.Attributes;
+using OnTopic.Metadata;
using OnTopic.TestDoubles;
namespace OnTopic.Tests {
@@ -62,6 +62,7 @@ public static ActionExecutingContext GetActionExecutingContext(Controller contro
///
/// Generates a barebones for testing a controller.
///
+ #pragma warning disable CA1024 // Use properties where appropriate
public static ControllerContext GetControllerContext() =>
new(
new() {
@@ -70,6 +71,7 @@ public static ControllerContext GetControllerContext() =>
ActionDescriptor = new ControllerActionDescriptor()
}
);
+ #pragma warning restore CA1024 // Use properties where appropriate
/*==========================================================================================================================
| METHOD: GET TOPIC CONTROLLER
@@ -77,7 +79,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()
@@ -128,7 +130,7 @@ public void NullTopic_ReturnsNotFound() {
controller.Dispose();
- Assert.AreEqual(typeof(NotFoundObjectResult), context.Result.GetType());
+ Assert.AreEqual(typeof(NotFoundObjectResult), context.Result?.GetType());
}
@@ -152,7 +154,7 @@ public void DisabledTopic_ReturnsNotFound() {
controller.Dispose();
- Assert.AreEqual(typeof(UnauthorizedResult), context.Result.GetType());
+ Assert.AreEqual(typeof(UnauthorizedResult), context.Result?.GetType());
}
@@ -177,7 +179,7 @@ public void TopicWithUrl_ReturnsRedirect() {
controller.Dispose();
- Assert.AreEqual(typeof(RedirectResult), context.Result.GetType());
+ Assert.AreEqual(typeof(RedirectResult), context.Result?.GetType());
}
@@ -203,7 +205,7 @@ public void NestedTopic_Returns403() {
var result = context.Result as StatusCodeResult;
Assert.IsNotNull(result);
- Assert.AreEqual(403, result.StatusCode);
+ Assert.AreEqual(403, result?.StatusCode);
}
@@ -228,7 +230,7 @@ public void PageGroupTopic_ReturnsRedirect() {
controller.Dispose();
- Assert.AreEqual(typeof(RedirectResult), context.Result.GetType());
+ Assert.AreEqual(typeof(RedirectResult), context.Result?.GetType());
}
diff --git a/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/MenuViewComponentBase{T}.cs
index c7f35f85..77773cfe 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;
@@ -22,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()
{
/*==========================================================================================================================
@@ -79,7 +87,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService
var configuredRoot = CurrentTopic.Attributes.GetValue("NavigationRoot", true);
if (!String.IsNullOrEmpty(configuredRoot)) {
- navigationRootTopic = TopicRepository.Load(configuredRoot);
+ navigationRootTopic = TopicRepository.Load("Root:" + configuredRoot, CurrentTopic);
}
if (navigationRootTopic is null) {
navigationRootTopic = HierarchicalTopicMappingService.GetHierarchicalRoot(CurrentTopic, 2, "Web");
@@ -123,7 +131,7 @@ public async Task InvokeAsync() {
\-----------------------------------------------------------------------------------------------------------------------*/
var navigationViewModel = new NavigationViewModel() {
NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true),
- CurrentKey = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path
+ CurrentWebPath = CurrentTopic?.GetWebPath()?? HttpContext.Request.Path
};
/*------------------------------------------------------------------------------------------------------------------------
diff --git a/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs b/OnTopic.AspNetCore.Mvc/Components/NavigationTopicViewComponentBase{T}.cs
index c28c0e44..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
@@ -52,7 +63,7 @@ IHierarchicalTopicMappingService hierarchicalTopicMappingService
/// on the route data.
///
///
- /// The associated with the .
+ /// The associated with the .
///
protected ITopicRepository TopicRepository { get; }
@@ -64,7 +75,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 a6e45802..5b82579a 100644
--- a/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{T}.cs
+++ b/OnTopic.AspNetCore.Mvc/Components/PageLevelNavigationViewComponentBase{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.Mapping.Hierarchical;
using OnTopic.Models;
@@ -17,8 +18,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.
///
///
///
@@ -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()
{
/*==========================================================================================================================
@@ -63,6 +72,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 +98,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
@@ -117,7 +128,7 @@ public async Task InvokeAsync() {
\-----------------------------------------------------------------------------------------------------------------------*/
var navigationViewModel = new NavigationViewModel() {
NavigationRoot = await MapNavigationTopicViewModels(navigationRootTopic).ConfigureAwait(true),
- CurrentKey = CurrentTopic?.GetUniqueKey()?? HttpContext.Request.Path
+ CurrentWebPath = CurrentTopic?.GetWebPath()?? HttpContext.Request.Path
};
/*------------------------------------------------------------------------------------------------------------------------
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.
diff --git a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs
index fc311cc4..f7b2e91f 100644
--- a/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs
+++ b/OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs
@@ -5,16 +5,16 @@
\=============================================================================================================================*/
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.AspNetCore.Mvc;
+using OnTopic.Attributes;
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 {
/*============================================================================================================================
@@ -24,6 +24,21 @@ 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 {
/*==========================================================================================================================
@@ -38,28 +53,39 @@ 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.
+ ///
+ public static Collection ExcludedContentTypes { get; } = new() {
+ "List"
+ };
+
+ /*==========================================================================================================================
+ | SKIPPED CONTENT TYPES
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Specifies what content types should not be listed in the sitemap.
+ /// Specifies what content types should not be listed in the sitemap—but whose descendents should still be evaluated.
///
- private static string[] ExcludeContentTypes { get; } = { "List" };
+ 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",
- "IsActive",
"IsDisabled",
- "ParentID",
- "TopicID",
+ "ParentID", //Legacy, but exposed for avoid leacking legacy data
+ "TopicID", //Legacy, but exposed for avoid leacking legacy data
"IsHidden",
"NoIndex",
- "URL",
"SortOrder"
};
@@ -93,7 +119,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
@@ -133,7 +159,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
@@ -141,11 +167,10 @@ public virtual ActionResult Index(bool indent = false, bool includeMetadata = fa
///
/// 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.
- [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 XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) =>
new(
new XElement(_sitemapNamespace + "urlset",
from topic in rootTopic?.Children
@@ -161,8 +186,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
@@ -173,9 +197,9 @@ public 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 (ExcludeContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.InvariantCultureIgnoreCase))) return topics;
+ if (topic.Attributes.GetBoolean("NoIndex")) return topics;
+ if (topic.Attributes.GetBoolean("IsDisabled")) return topics;
+ if (ExcludedContentTypes.Any(c => topic.ContentType.Equals(c, StringComparison.OrdinalIgnoreCase))) return topics;
/*------------------------------------------------------------------------------------------------------------------------
| Establish variables
@@ -192,14 +216,15 @@ public 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()
- ),
- getRelationships()
+ getAttributes(),
+ getRelationships(),
+ getReferences()
) : null
);
- if (!topic.ContentType!.Equals("Container", StringComparison.InvariantCultureIgnoreCase)) {
+ if (
+ !SkippedContentTypes.Any(c => topic.ContentType?.Equals(c, StringComparison.OrdinalIgnoreCase)?? false) &&
+ String.IsNullOrWhiteSpace(topic.Attributes.GetValue("Url"))
+ ) {
topics.Add(topicElement);
}
@@ -215,14 +240,21 @@ public IEnumerable AddTopic(Topic topic, bool includeMetadata = false)
/*------------------------------------------------------------------------------------------------------------------------
| Get attributes
\-----------------------------------------------------------------------------------------------------------------------*/
- IEnumerable getAttributes() =>
- from attribute in topic.Attributes
- where !ExcludeAttributes.Contains(attribute.Key, StringComparer.InvariantCultureIgnoreCase)
- 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"),
+ new XElement(_pagemapNamespace + "Attribute",
+ new XAttribute("name", "ContentType"),
+ new XText(topic.ContentType?? "Page")
+ ),
+ from attribute in topic.Attributes
+ 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),
+ new XText(topic.Attributes.GetValue(attribute.Key))
+ )
+ );
/*------------------------------------------------------------------------------------------------------------------------
| Get relationships
@@ -230,17 +262,30 @@ 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))
+ new XText(relatedTopic.GetUniqueKey().Replace("Root:", "", StringComparison.OrdinalIgnoreCase))
)
);
+ /*------------------------------------------------------------------------------------------------------------------------
+ | 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
-} //Namespace
-
-#pragma warning restore CS0618 // Type or member is obsolete
\ No newline at end of file
+} //Namespace
\ No newline at end of file
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
diff --git a/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs b/OnTopic.AspNetCore.Mvc/Models/NavigationViewModel{T}.cs
index 034d260a..641d50af 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 {
@@ -11,21 +13,24 @@ 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.
///
///
///
/// 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 ,
- /// 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
@@ -35,32 +40,42 @@ 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; }
/*==========================================================================================================================
- | CURRENT KEY
+ | CURRENT WEB PATH
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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.
///
///
- /// 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.
///
///
+ 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
diff --git a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj
index 6fd4c861..9aff74c1 100644
--- a/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj
+++ b/OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj
@@ -2,50 +2,30 @@
{B7F136A1-C86D-4A74-AC4F-3693CD1358A4}
+ netcoreapp3.1
OnTopic.AspNetCore.Mvc
- netcoreapp3.1
- True
- False
- 9.0
- enable
OnTopic ASP.NET Core Library
- Ignia
- OnTopic
Provides presentation-layer support for the ASP.NET Core Framework.
- ©2020 Ignia, LLC
bin\$(Configuration)\
- Ignia
-
-
-
- https://github.com/Ignia/Topics-Library
C# .NET CMS Presentation Web MVC ASP.NET Core Controller
- true
-
-
-
- full
- false
- latest
- 1701;1702;CA1303
-
-
- pdbonly
- 1701;1702;CA1303
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
- runtime; build; native; contentfiles; analyzers
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -53,8 +33,4 @@
-
-
-
-
\ No newline at end of file
diff --git a/OnTopic.AspNetCore.Mvc/README.md b/OnTopic.AspNetCore.Mvc/README.md
index daee88d9..38eed59e 100644
--- a/OnTopic.AspNetCore.Mvc/README.md
+++ b/OnTopic.AspNetCore.Mvc/README.md
@@ -1,8 +1,9 @@
-# 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.
[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=4db5e20c-69c6-4134-823a-c3de06d1176e&preferRelease=true)
[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
### Contents
- [Components](#components)
@@ -20,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`.
@@ -46,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`
@@ -57,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:
@@ -88,7 +98,7 @@ Installation can be performed by providing a ` to the `OnTo
…
-
+
```
@@ -97,18 +107,18 @@ 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();
}
}
```
-> *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:
-```c#
-var activator = new OrganizationNameControllerActivator(Configuration.GetConnectionString("OnTopic")
+```csharp
+var activator = new OrganizationNameActivator(Configuration.GetConnectionString("OnTopic"))
services.AddSingleton(activator);
services.AddSingleton(activator);
```
@@ -120,13 +130,20 @@ 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 => {
- 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();
+
});
}
}
@@ -135,7 +152,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();
@@ -145,9 +162,9 @@ 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.
-> *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
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,
diff --git a/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs b/OnTopic.AspNetCore.Mvc/TopicRepositoryExtensions.cs
index e0857ecc..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(
@@ -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/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..0cab8b79 100644
--- a/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs
+++ b/OnTopic.AspNetCore.Mvc/TopicViewLocationExpander.cs
@@ -74,24 +74,19 @@ 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));
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;
}
/*==========================================================================================================================
| METHOD: EXPAND VIEW LOCATIONS
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Introduces additional routes
- ///
- /// The that the request is operating within.
+ ///
public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) {
/*------------------------------------------------------------------------------------------------------------------------
@@ -109,14 +104,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/TopicViewResult.cs b/OnTopic.AspNetCore.Mvc/TopicViewResult.cs
index 21fab214..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";
@@ -78,7 +79,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; }
diff --git a/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs b/OnTopic.AspNetCore.Mvc/TopicViewResultExecutor.cs
index 2aa253aa..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;
@@ -35,7 +37,6 @@ public class TopicViewResultExecutor : ViewExecutor, IActionResultExecutorThe .
/// The .
/// The .
- /// The .
/// The .
public TopicViewResultExecutor(
IOptions viewOptions,
@@ -101,7 +102,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);
@@ -118,12 +119,12 @@ 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.Ordinal)) {
// 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);
@@ -146,7 +147,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.AspNetCore.Mvc/ValidateTopicAttribute.cs b/OnTopic.AspNetCore.Mvc/ValidateTopicAttribute.cs
index ddd1eaae..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,24 +49,24 @@ 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]
- 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 +83,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 +94,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 +102,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 +113,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 +124,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,8 +135,8 @@ 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(
- currentTopic.Children.Where(t => t.IsVisible()).FirstOrDefault().GetWebPath()
+ context.Result = controller.Redirect(
+ currentTopic.Children.Where(t => t.IsVisible()).FirstOrDefault()?.GetWebPath()
);
return;
}
@@ -147,15 +148,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.Data.Caching/CachedTopicRepository.cs b/OnTopic.Data.Caching/CachedTopicRepository.cs
index 7b152e61..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,17 +59,11 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
}
- /*==========================================================================================================================
- | GET CONTENT TYPE DESCRIPTORS
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public override ContentTypeDescriptorCollection GetContentTypeDescriptors() => _dataProvider.GetContentTypeDescriptors();
-
/*==========================================================================================================================
| 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,13 +80,13 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
}
///
- public override Topic? Load(string? topicKey = null, bool isRecursive = true) {
+ public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = 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);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -113,7 +97,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,28 +111,9 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
/*------------------------------------------------------------------------------------------------------------------------
| Return appropriate topic
\-----------------------------------------------------------------------------------------------------------------------*/
- return _dataProvider.Load(topicId, version);
+ return TopicRepository.Load(topicId, version, referenceTopic?? _cache);
}
- /*==========================================================================================================================
- | METHOD: SAVE
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) =>
- _dataProvider.Save(topic, isRecursive, isDraft);
-
- /*==========================================================================================================================
- | 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
diff --git a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj
index 99bf2851..f09b36de 100644
--- a/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj
+++ b/OnTopic.Data.Caching/OnTopic.Data.Caching.csproj
@@ -2,50 +2,29 @@
{206B7F91-CA25-4E9D-9576-60D2E54A2C0A}
+ netstandard2.1
OnTopic.Data.Caching
- netstandard2.0;netstandard2.1
- True
- False
- 9.0
- enable
OnTopic Cached Repository
- Ignia
- OnTopic
Provides a caching decorator for ITopicRepository implementations.
- ©2020 Ignia, LLC
bin\$(Configuration)\
- Ignia
-
-
-
- https://github.com/Ignia/Topics-Library
C# .NET CMS Caching Data Repository
- true
-
-
-
- full
- false
- latest
- 1701;1702;CA1303
-
-
- pdbonly
- false
- 1701;1702;CA1303
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
- runtime; build; native; contentfiles; analyzers
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -53,8 +32,4 @@
-
-
-
-
\ No newline at end of file
diff --git a/OnTopic.Data.Caching/README.md b/OnTopic.Data.Caching/README.md
index 2c6feddc..6c7e3c79 100644
--- a/OnTopic.Data.Caching/README.md
+++ b/OnTopic.Data.Caching/README.md
@@ -1,8 +1,9 @@
# 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.
[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3dfb3a0a-c049-407d-959e-546f714dcd0f&preferRelease=true)
[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
### Contents
- [Functionality](#functionality)
@@ -10,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.
@@ -18,7 +19,7 @@ Installation can be performed by providing a ` to the `OnTo
…
-
+
```
@@ -26,9 +27,8 @@ 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#
-var sqlTopicRepository = new SqlTopicRepository(connectionString);
-var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository);
-
-var rootTopic = cachedTopicRepository.Load();
+```csharp
+var sqlTopicRepository = new SqlTopicRepository(connectionString);
+var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository);
+var rootTopic = cachedTopicRepository.Load();
```
\ No newline at end of file
diff --git a/OnTopic.Data.Sql.Database.Tests/Functions.cs b/OnTopic.Data.Sql.Database.Tests/Functions.cs
new file mode 100644
index 00000000..d2a2f756
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/Functions.cs
@@ -0,0 +1,495 @@
+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.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;
+ 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;
+ 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.SqlDatabaseTestAction dbo_GetExtendedAttributeTest_PretestAction;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preGetExtendedAttributeCount;
+ 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();
+ 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();
+ 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_FindTopicIDsTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ preFindAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ dbo_GetExtendedAttributeTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ preGetExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ //
+ // 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);
+ resources.ApplyResources(dbo_FindTopicIDsTest_TestAction, "dbo_FindTopicIDsTest_TestAction");
+ //
+ // findTopicCount
+ //
+ findTopicCount.Enabled = true;
+ findTopicCount.Name = "findTopicCount";
+ findTopicCount.ResultSet = 1;
+ findTopicCount.RowCount = 2;
+ //
+ // dbo_GetAttributesTest_TestAction
+ //
+ dbo_GetAttributesTest_TestAction.Conditions.Add(getAttributeCount);
+ dbo_GetAttributesTest_TestAction.Conditions.Add(getAttributeValue);
+ resources.ApplyResources(dbo_GetAttributesTest_TestAction, "dbo_GetAttributesTest_TestAction");
+ //
+ // getAttributeCount
+ //
+ 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
+ //
+ 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);
+ resources.ApplyResources(dbo_GetAttributesTest_PretestAction, "dbo_GetAttributesTest_PretestAction");
+ //
+ // preGetAttributeCount
+ //
+ preGetAttributeCount.Enabled = true;
+ preGetAttributeCount.Name = "preGetAttributeCount";
+ 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 = 8;
+ //
+ // testCleanupAction
+ //
+ testCleanupAction.Conditions.Add(postFunctionTopicCount);
+ resources.ApplyResources(testCleanupAction, "testCleanupAction");
+ //
+ // postFunctionTopicCount
+ //
+ postFunctionTopicCount.Enabled = true;
+ postFunctionTopicCount.Name = "postFunctionTopicCount";
+ postFunctionTopicCount.ResultSet = 1;
+ postFunctionTopicCount.RowCount = 1;
+ //
+ // 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_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;
+ this.dbo_GetExtendedAttributeTestData.PretestAction = dbo_GetExtendedAttributeTest_PretestAction;
+ 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 = dbo_FindTopicIDsTest_PretestAction;
+ this.dbo_FindTopicIDsTestData.TestAction = dbo_FindTopicIDsTest_TestAction;
+ //
+ // dbo_GetAttributesTestData
+ //
+ this.dbo_GetAttributesTestData.PosttestAction = dbo_GetAttributesTest_PosttestAction;
+ this.dbo_GetAttributesTestData.PretestAction = dbo_GetAttributesTest_PretestAction;
+ 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;
+ //
+ // Functions
+ //
+ this.TestCleanupAction = testCleanupAction;
+ this.TestInitializeAction = testInitializeAction;
+ }
+
+ #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..335a6812
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/Functions.resx
@@ -0,0 +1,701 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @Key AS VARCHAR(128),
+ @TopicID AS INT,
+ @AttributeKey AS VARCHAR(128),
+ @AttributeValue AS NVARCHAR(2000);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @Key = 'Topic_1_1',
+ @AttributeKey = 'GetExtendedAttributeTest2';
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @Key;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @AttributeValue = [dbo].[GetExtendedAttribute](
+ @TopicID,
+ @AttributeKey
+ );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VALIDATE RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @AttributeValue AS RC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @ExpectedParentID AS INT,
+ @ParentID AS INT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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';
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @ParentID = [dbo].[GetParentID](
+ @TopicID
+ );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EVALUATE RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @ExpectedParentID - @ParentID AS RC;
+
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @ExpectedTopicID AS INT,
+ @UniqueKey AS NVARCHAR(2500);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @UniqueKey = 'Root:FunctionTests:Topic_1:Topic_1_1:Topic_1_1_1:Topic_1_1_1_2';
+
+SELECT @ExpectedTopicID = TopicID
+FROM Topics
+WHERE TopicKey = 'Topic_1_1_1_2';
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @TopicID = [dbo].[GetTopicID](
+ @UniqueKey
+ );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EVALUATE RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @ExpectedTopicID - @TopicID AS RC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @UniqueKey AS NVARCHAR(2500);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = 'Topic_1_1_1_2';
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @UniqueKey = [dbo].[GetUniqueKey](
+ @TopicID
+ );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EVALUATE RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @UniqueKey AS RC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @AttributeKey AS VARCHAR (255),
+ @AttributeValue AS NVARCHAR (255),
+ @IsExtendedAttribute AS BIT,
+ @UsePartialMatch AS BIT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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,
+ @AttributeKey,
+ @AttributeValue,
+ @IsExtendedAttribute,
+ @UsePartialMatch
+ );
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @Key AS VARCHAR(128),
+ @TopicID AS INT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @Key = 'GetAttributesTest'
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @Key;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM [dbo].[GetAttributes](
+ @TopicID
+ );
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = 'Topic_1_1_1';
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE FUNCTION
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM [dbo].[GetChildTopicIDs](
+ @TopicID
+ );
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @RootTopicID AS INT,
+ @Key AS VARCHAR(128),
+ @TopicID AS INT,
+ @AttributesXml AS XML;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+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;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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',
+ @RootTopicID;
+
+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%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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,
+ 1;
+
+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
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- COMPRESS HIERARCHY
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [Utilities].[CompressHierarchy]
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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
+
+
\ No newline at end of file
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
new file mode 100644
index 00000000..d9076326
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/OnTopic.Data.Sql.Database.Tests.csproj
@@ -0,0 +1,128 @@
+
+
+
+ $(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..d43ba700
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/SqlDatabaseSetup.cs
@@ -0,0 +1,24 @@
+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()]
+ #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
+
+ }
+}
diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs
new file mode 100644
index 00000000..9ccfa814
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.cs
@@ -0,0 +1,1263 @@
+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.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.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;
+ 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.ScalarValueCondition moveTopicValue;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateAttributesTest_TestAction;
+ 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;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateRelationshipsTest_TestAction;
+ 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.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;
+ 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;
+ 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.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.SqlDatabaseTestAction dbo_MoveTopicTest_PretestAction;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preMoveTopicCount;
+ 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;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction dbo_UpdateReferencesTest_PretestAction;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateReferenceCount;
+ 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.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.SqlDatabaseTestAction dbo_UpdateExtendedAttributesTest_PretestAction;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition preUpdateExtendedAttributeCount;
+ 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;
+ 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.SqlDatabaseTestAction dbo_GetTopicUpdatesTest_PosttestAction;
+ Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition postGetUpdatesAttributeCount;
+ 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();
+ 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();
+ 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();
+ 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();
+ 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_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();
+ moveTopicValue = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.ScalarValueCondition();
+ dbo_UpdateAttributesTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ 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();
+ dbo_UpdateRelationshipsTest_TestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ 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();
+ 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();
+ 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_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();
+ 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();
+ dbo_MoveTopicTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ preMoveTopicCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ 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_UpdateReferencesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ preUpdateReferenceCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ 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();
+ 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();
+ dbo_UpdateExtendedAttributesTest_PretestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ preUpdateExtendedAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ 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_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();
+ dbo_GetTopicUpdatesTest_PosttestAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ postGetUpdatesAttributeCount = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.Conditions.RowCountCondition();
+ testCleanupAction = new Microsoft.Data.Tools.Schema.Sql.UnitTesting.SqlDatabaseTestAction();
+ //
+ // dbo_CreateTopicTest_TestAction
+ //
+ dbo_CreateTopicTest_TestAction.Conditions.Add(createTopicTotal);
+ resources.ApplyResources(dbo_CreateTopicTest_TestAction, "dbo_CreateTopicTest_TestAction");
+ //
+ // createTopicTotal
+ //
+ createTopicTotal.Enabled = true;
+ createTopicTotal.Name = "createTopicTotal";
+ createTopicTotal.ResultSet = 1;
+ createTopicTotal.RowCount = 1;
+ //
+ // dbo_DeleteTopicTest_TestAction
+ //
+ 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(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);
+ 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;
+ //
+ // 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);
+ 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");
+ //
+ // 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(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(updateAttributeCount);
+ dbo_UpdateAttributesTest_TestAction.Conditions.Add(updateAttributeValue);
+ resources.ApplyResources(dbo_UpdateAttributesTest_TestAction, "dbo_UpdateAttributesTest_TestAction");
+ //
+ // updateAttributeCount
+ //
+ updateAttributeCount.Enabled = true;
+ updateAttributeCount.Name = "updateAttributeCount";
+ updateAttributeCount.ResultSet = 1;
+ updateAttributeCount.RowCount = 3;
+ //
+ // updateAttributeValue
+ //
+ updateAttributeValue.ColumnNumber = 1;
+ updateAttributeValue.Enabled = true;
+ updateAttributeValue.ExpectedValue = "UpdateAttributesTest4";
+ updateAttributeValue.Name = "updateAttributeValue";
+ updateAttributeValue.NullExpected = false;
+ updateAttributeValue.ResultSet = 1;
+ updateAttributeValue.RowNumber = 3;
+ //
+ // dbo_UpdateExtendedAttributesTest_TestAction
+ //
+ dbo_UpdateExtendedAttributesTest_TestAction.Conditions.Add(updateExtendedAttributeCount);
+ 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);
+ 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(updateRelationshipCount);
+ dbo_UpdateRelationshipsTest_TestAction.Conditions.Add(updateRelationshipValue);
+ resources.ApplyResources(dbo_UpdateRelationshipsTest_TestAction, "dbo_UpdateRelationshipsTest_TestAction");
+ //
+ // updateRelationshipCount
+ //
+ updateRelationshipCount.Enabled = true;
+ updateRelationshipCount.Name = "updateRelationshipCount";
+ updateRelationshipCount.ResultSet = 1;
+ updateRelationshipCount.RowCount = 4;
+ //
+ // updateRelationshipValue
+ //
+ 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(updateTopicCount);
+ dbo_UpdateTopicTest_TestAction.Conditions.Add(updateTopicValue);
+ resources.ApplyResources(dbo_UpdateTopicTest_TestAction, "dbo_UpdateTopicTest_TestAction");
+ //
+ // updateTopicCount
+ //
+ updateTopicCount.Enabled = true;
+ updateTopicCount.Name = "updateTopicCount";
+ 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);
+ resources.ApplyResources(dbo_CreateTopicTest_PosttestAction, "dbo_CreateTopicTest_PosttestAction");
+ //
+ // 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
+ //
+ preDeleteReferenceCount.Enabled = true;
+ preDeleteReferenceCount.Name = "preDeleteReferenceCount";
+ preDeleteReferenceCount.ResultSet = 4;
+ preDeleteReferenceCount.RowCount = 1;
+ //
+ // 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");
+ //
+ // 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");
+ //
+ // 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_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");
+ //
+ // postUpdateTopicCount
+ //
+ postUpdateTopicCount.Enabled = true;
+ postUpdateTopicCount.Name = "postUpdateTopicCount";
+ postUpdateTopicCount.ResultSet = 1;
+ postUpdateTopicCount.RowCount = 0;
+ //
+ // 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_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;
+ //
+ // 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");
+ //
+ // 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);
+ 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;
+ //
+ // 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;
+ 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 = dbo_MoveTopicTest_PosttestAction;
+ this.dbo_MoveTopicTestData.PretestAction = dbo_MoveTopicTest_PretestAction;
+ this.dbo_MoveTopicTestData.TestAction = dbo_MoveTopicTest_TestAction;
+ //
+ // dbo_UpdateAttributesTestData
+ //
+ this.dbo_UpdateAttributesTestData.PosttestAction = dbo_UpdateAttributesTest_PosttestAction;
+ this.dbo_UpdateAttributesTestData.PretestAction = dbo_UpdateAttributesTest_PretestAction;
+ this.dbo_UpdateAttributesTestData.TestAction = dbo_UpdateAttributesTest_TestAction;
+ //
+ // dbo_UpdateExtendedAttributesTestData
+ //
+ this.dbo_UpdateExtendedAttributesTestData.PosttestAction = dbo_UpdateExtendedAttributesTest_PosttestAction;
+ this.dbo_UpdateExtendedAttributesTestData.PretestAction = dbo_UpdateExtendedAttributesTest_PretestAction;
+ this.dbo_UpdateExtendedAttributesTestData.TestAction = dbo_UpdateExtendedAttributesTest_TestAction;
+ //
+ // dbo_UpdateReferencesTestData
+ //
+ this.dbo_UpdateReferencesTestData.PosttestAction = dbo_UpdateReferencesTest_PosttestAction;
+ this.dbo_UpdateReferencesTestData.PretestAction = dbo_UpdateReferencesTest_PretestAction;
+ this.dbo_UpdateReferencesTestData.TestAction = dbo_UpdateReferencesTest_TestAction;
+ //
+ // dbo_UpdateRelationshipsTestData
+ //
+ 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 = dbo_UpdateTopicTest_PosttestAction;
+ this.dbo_UpdateTopicTestData.PretestAction = dbo_UpdateTopicTest_PretestAction;
+ this.dbo_UpdateTopicTestData.TestAction = dbo_UpdateTopicTest_TestAction;
+ //
+ // dbo_GetTopicUpdatesTestData
+ //
+ this.dbo_GetTopicUpdatesTestData.PosttestAction = dbo_GetTopicUpdatesTest_PosttestAction;
+ this.dbo_GetTopicUpdatesTestData.PretestAction = dbo_GetTopicUpdatesTest_PretestAction;
+ this.dbo_GetTopicUpdatesTestData.TestAction = dbo_GetTopicUpdatesTest_TestAction;
+ //
+ // testCleanupAction
+ //
+ resources.ApplyResources(testCleanupAction, "testCleanupAction");
+ //
+ // StoredProcedures
+ //
+ this.TestCleanupAction = testCleanupAction;
+ this.TestInitializeAction = testInitializeAction;
+ }
+
+ #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);
+ }
+ }
+ [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;
+ 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;
+ private SqlDatabaseTestActions dbo_GetTopicUpdatesTestData;
+ }
+}
diff --git a/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx
new file mode 100644
index 00000000..b3f95e0f
--- /dev/null
+++ b/OnTopic.Data.Sql.Database.Tests/StoredProcedures.resx
@@ -0,0 +1,1706 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+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;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @RC = 0,
+ @Key = 'CreateTopicTest',
+ @ContentType = 'Test',
+ @ParentID = 1,
+ @ExtendedAttributes = NULL,
+ @Version = GETUTCDATE();
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE @RC = [dbo].[CreateTopic]
+ @Key,
+ @ContentType,
+ @ParentID,
+ @Attributes,
+ @ExtendedAttributes,
+ @References,
+ @Version;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT TopicID
+FROM Topics
+WHERE TopicKey = @Key
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @RC AS INT,
+ @TopicID AS INT,
+ @TopicKey AS VARCHAR(128);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @RC = 0,
+ @TopicKey = 'DeleteTopicTest';
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @TopicKey
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE @RC = [dbo].[DeleteTopic]
+ @TopicID;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+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%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @UniqueKey AS VARCHAR(128),
+ @TopicID AS INT,
+ @Version AS DATETIME,
+ @NewVersion AS DATETIME;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @UniqueKey = 'GetTopicVersionTest',
+ @Version = '2020-01-01 12:00:00:000',
+ @NewVersion = '2021-01-01 12:00:00:000';
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @UniqueKey
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[GetTopicVersion]
+ @TopicID,
+ @Version;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- ESTABLISH VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @DeepLoad AS BIT,
+ @UniqueKey AS NVARCHAR (255);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @DeepLoad = 1,
+ @UniqueKey = 'GetTopicsTest';
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @UniqueKey
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[GetTopics]
+ @TopicID,
+ @DeepLoad,
+ NULL;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @ParentID AS INT,
+ @SiblingID AS INT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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 PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[MoveTopic]
+ @TopicID,
+ @ParentID,
+ @SiblingID;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT TopicKey
+FROM Topics
+WHERE TopicKey LIKE 'MoveTopic%'
+ORDER BY RangeLeft ASC
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @Attributes AS AttributeValues,
+ @Version AS DATETIME;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = 'UpdateAttributesTest';
+
+SELECT @Version = DATEADD(day, 1, GETUTCDATE());
+
+INSERT
+INTO @Attributes
+VALUES ( 'UpdateAttributesTest1', 'New' ),
+ ( 'UpdateAttributesTest2', 'New' ),
+ ( 'UpdateAttributesTest4', 'New' );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[UpdateAttributes]
+ @TopicID,
+ @Attributes,
+ @Version,
+ 1;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT AttributeKey
+FROM AttributeIndex
+WHERE TopicID = @TopicID
+ AND AttributeKey LIKE 'UpdateAttributesTest%'
+ AND AttributeValue != ''
+ORDER BY AttributeKey ASC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @Key AS VARCHAR(128),
+ @TopicID AS INT,
+ @ExtendedAttributes AS XML,
+ @Version AS DATETIME,
+ @DeleteUnmatched AS BIT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @Key = 'UpdateExtendedAttributesTest',
+ @ExtendedAttributes = '<attributes><attribute key=''Body''>New</attribute></attributes>',
+ @Version = DATEADD(day, 1, GETUTCDATE()),
+ @DeleteUnmatched = 0;
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @Key
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[UpdateExtendedAttributes]
+ @TopicID,
+ @ExtendedAttributes,
+ @Version,
+ @DeleteUnmatched;
+
+EXECUTE [dbo].[UpdateExtendedAttributes]
+ @TopicID,
+ @ExtendedAttributes,
+ @Version,
+ @DeleteUnmatched;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT AttributesXml
+FROM ExtendedAttributes
+ORDER BY Version ASC
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @TargetID AS INT,
+ @ReferencedTopics AS TopicReferences,
+ @Version AS DATETIME,
+ @DeleteUnmatched AS BIT;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = 'UpdateReferencesTest';
+
+SELECT @TargetID = TopicID
+FROM Topics
+WHERE TopicKey = 'UpdateReferencesTestTarget2';
+
+SELECT @Version = DATEADD(day, 1, GETUTCDATE()),
+ @DeleteUnmatched = 1;
+
+INSERT
+INTO @ReferencedTopics
+VALUES ( 'UpdateReferencesTest1', @TargetID ),
+ ( 'UpdateReferencesTest2', @TargetID ),
+ ( 'UpdateReferencesTest4', @TargetID );
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[UpdateReferences]
+ @TopicID,
+ @ReferencedTopics,
+ @Version,
+ @DeleteUnmatched;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT ISNULL(Target_TopicID, -1)
+FROM ReferenceIndex
+WHERE Source_TopicID = @TopicID
+ AND ReferenceKey LIKE 'UpdateReferencesTest%'
+ORDER BY ReferenceKey ASC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @RelationshipKey = 'UpdateRelationshipsTest',
+ @Version = DATEADD(day, 1, GETUTCDATE()),
+ @DeleteUnmatched = 1;
+
+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;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT IsDeleted
+FROM RelationshipIndex
+WHERE Source_TopicID = @TopicID
+ AND RelationshipKey = @RelationshipKey
+ORDER BY Target_TopicID ASC;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT,
+ @Key AS VARCHAR (128),
+ @ContentType AS VARCHAR (128),
+ @NewKey AS VARCHAR (128),
+ @NewContentType AS VARCHAR (128);
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @Key = 'UpdateTopicTest',
+ @ContentType = 'Test',
+ @NewKey = 'UpdateTopicTestNew',
+ @NewContentType = 'TestNew';
+
+SELECT @TopicID = TopicID
+FROM Topics
+WHERE TopicKey = @Key;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- EXECUTE PROCEDURE
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [dbo].[UpdateTopic]
+ @TopicID,
+ @NewKey,
+ @NewContentType;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT ContentType
+FROM Topics
+WHERE TopicKey = @NewKey
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DELETE TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+DELETE
+FROM Topics
+WHERE TopicKey = 'CreateTopicTest'
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM Topics
+WHERE TopicKey = 'CreateTopicTest'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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 'DeleteTopicTest%'
+
+DELETE
+FROM Relationships
+WHERE RelationshipKey = 'DeleteTopicTest'
+
+DELETE
+FROM TopicReferences
+WHERE ReferenceKey = 'DeleteTopicTest'
+
+DELETE
+FROM Topics
+WHERE TopicKey LIKE 'DeleteTopic%'
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT @Key = 'DeleteTopicTest',
+ @ContentType = 'Test',
+ @ParentID = 1,
+ @ExtendedAttributes = '<Attributes><Attribute key=''Body''>Test</Attribute></Attributes>',
+ @Version = GETUTCDATE();
+
+INSERT
+INTO @Attributes
+VALUES ( 'DeleteTopicTest1', 'Value' ),
+ ( 'DeleteTopicTest2', 'Value' )
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- ESTABLISH TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+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
+ )
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+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%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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 = 1,
+ @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
+--------------------------------------------------------------------------------------------------------------------------------
+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,
+ @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 = 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';
+
+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'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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 @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,
+ 1;
+
+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%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DELETE TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+DELETE
+FROM Topics
+WHERE TopicKey LIKE 'MoveTopic%'
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM Topics
+WHERE TopicKey LIKE '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',
+ 1;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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%';
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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',
+ 1;
+
+EXECUTE @TargetID = [dbo].[CreateTopic]
+ 'UpdateReferencesTestTarget1',
+ 'Test',
+ 1;
+
+EXECUTE [dbo].[CreateTopic]
+ 'UpdateReferencesTestTarget2',
+ 'Test',
+ 1;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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%';
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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',
+ 1;
+
+EXECUTE @TargetID1 = [dbo].[CreateTopic]
+ 'UpdateRelationshipsTestTarget1',
+ 'Test',
+ 1;
+
+EXECUTE @TargetID2 = [dbo].[CreateTopic]
+ 'UpdateRelationshipsTestTarget2',
+ 'Test',
+ 1;
+
+EXECUTE @TargetID3 = [dbo].[CreateTopic]
+ 'UpdateRelationshipsTestTarget3',
+ 'Test',
+ 1;
+
+EXECUTE [dbo].[CreateTopic]
+ 'UpdateRelationshipsTestTarget4',
+ 'Test',
+ 1;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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%';
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- DECLARE VARIABLES
+--------------------------------------------------------------------------------------------------------------------------------
+DECLARE @TopicID AS INT;
+
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- DELETE TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+DELETE
+FROM Topics
+WHERE TopicKey LIKE 'UpdateTopicTest%'
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM Topics
+WHERE TopicKey LIKE 'UpdateTopicTest%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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 = 1;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- ESTABLISH TEST DATA
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE @TopicID = [dbo].[CreateTopic]
+ @Key,
+ @ContentType,
+ @ParentID;
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- VERIFY RESULTS
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT *
+FROM Topics
+WHERE TopicKey = @Key;
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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 = 1,
+ @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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- 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%'
+
+
+ --------------------------------------------------------------------------------------------------------------------------------
+-- COMPRESS HIERARCHY
+--------------------------------------------------------------------------------------------------------------------------------
+EXECUTE [Utilities].[CompressHierarchy]
+
+
+ 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.Data.Sql.Database/Functions/FindTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/FindTopicIDs.sql
index a5697fff..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
------------------------------------------------------------------------------------------------------------------------------
@@ -40,13 +48,13 @@ BEGIN
INSERT
INTO @Topics
SELECT TopicID
- FROM TopicIndex
+ FROM Topics
WHERE ( @AttributeKey = 'Key'
AND TopicKey = @AttributeValue
OR @AttributeKey = 'ContentType'
AND ContentType = @AttributeValue
OR @AttributeKey = 'ParentID'
- AND ParentID = @AttributeValue
+ AND ISNULL(ParentID, '') = @AttributeValue
)
RETURN
END
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
diff --git a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql
index f8209653..337604d1 100644
--- a/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql
+++ b/OnTopic.Data.Sql.Database/Functions/GetChildTopicIDs.sql
@@ -17,15 +17,22 @@ AS
BEGIN
+ ------------------------------------------------------------------------------------------------------------------------------
+ -- SET DEFAULTS
+ ------------------------------------------------------------------------------------------------------------------------------
+ IF (@TopicID IS NULL)
+ BEGIN
+ SET @TopicID = -10
+ END
+
------------------------------------------------------------------------------------------------------------------------------
-- RETRIEVE VALUES
------------------------------------------------------------------------------------------------------------------------------
INSERT
INTO @Topics
SELECT TopicID
- FROM Attributes
- WHERE AttributeKey = 'ParentID'
- AND AttributeValue = @TopicID
+ FROM Topics
+ WHERE ISNULL(ParentID, -10) = @TopicID
------------------------------------------------------------------------------------------------------------------------------
-- RETURN
diff --git a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql
index dd5a559a..7b3e14da 100644
--- a/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql
+++ b/OnTopic.Data.Sql.Database/Functions/GetTopicID.sql
@@ -1,14 +1,14 @@
--------------------------------------------------------------------------------------------------------------------------------
-- 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.
+-- 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].[GetTopicID] (
- @TopicKey NVARCHAR(255)
+FUNCTION [dbo].[GetTopicID]
+(
+ @UniqueKey NVARCHAR(2500)
)
RETURNS INT
AS
@@ -21,19 +21,33 @@ BEGIN
DECLARE @TopicID INT = -1
------------------------------------------------------------------------------------------------------------------------------
- -- GET TOPIC ID BASED ON TOPIC KEY
+ -- GET TOPIC ID BASED ON UNIQUE 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
+ ;WITH RCTE AS (
+
+ SELECT TopicID,
+ ParentID,
+ CAST('Root' AS NVARCHAR(255)) AS UniqueKey
+ FROM Topics root
+ WHERE root.TopicID = 1
+
+ UNION ALL
+
+ SELECT Topics.TopicID,
+ Topics.ParentID,
+ CAST(recursive.UniqueKey + ':' + TopicKey AS NVARCHAR(255)) AS UniqueKey
+ FROM Topics
+ INNER JOIN RCTE recursive
+ ON Topics.ParentID = recursive.TopicID
+ WHERE @UniqueKey LIKE CAST(recursive.UniqueKey + ':' + TopicKey AS NVARCHAR(255)) + '%'
+ )
+ SELECT @TopicID = TopicID
+ FROM RCTE AS hierarchy
+ WHERE UniqueKey = @UniqueKey
+ ORDER BY UniqueKey ASC
OPTION (
OPTIMIZE
- FOR ( @TopicKey = 'Root'
+ FOR ( @UniqueKey = 'Root'
)
)
diff --git a/OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql b/OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql
deleted file mode 100644
index e753134b..00000000
--- a/OnTopic.Data.Sql.Database/Functions/GetTopicIDByUniqueKey.sql
+++ /dev/null
@@ -1,66 +0,0 @@
---------------------------------------------------------------------------------------------------------------------------------
--- GET TOPIC ID BY UNIQUE KEY
---------------------------------------------------------------------------------------------------------------------------------
--- 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]
-(
- @UniqueKey NVARCHAR(2500)
-)
-RETURNS INT
-AS
-
-BEGIN
-
- ------------------------------------------------------------------------------------------------------------------------------
- -- DECLARE AND DEFINE VARIABLES
- ------------------------------------------------------------------------------------------------------------------------------
- DECLARE @TopicID INT = -1
-
- ------------------------------------------------------------------------------------------------------------------------------
- -- GET TOPIC ID BASED ON UNIQUE TOPIC KEY
- ------------------------------------------------------------------------------------------------------------------------------
- ;WITH RCTE AS (
-
- SELECT TopicID,
- CAST(NULL AS NVARCHAR(255)) AS ParentID,
- CAST('Root' AS NVARCHAR(255)) AS UniqueKey
- FROM Topics root
- WHERE root.TopicID = 1
-
- UNION ALL
-
- SELECT p.TopicID,
- p.AttributeValue AS 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
- 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)) + '%'
- )
- SELECT @TopicID = TopicID
- FROM RCTE AS hierarchy
- WHERE UniqueKey = @UniqueKey
- ORDER BY UniqueKey ASC
- OPTION (
- OPTIMIZE
- FOR ( @UniqueKey = 'Root'
- )
- )
-
- ------------------------------------------------------------------------------------------------------------------------------
- -- RETURN TOPIC ID
- ------------------------------------------------------------------------------------------------------------------------------
- RETURN @TopicID
-
-END
\ No newline at end of file
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/OnTopic.Data.Sql.Database.sqlproj b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj
index 1cab94a5..631bb12e 100644
--- a/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj
+++ b/OnTopic.Data.Sql.Database/OnTopic.Data.Sql.Database.sqlproj
@@ -62,58 +62,72 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -122,11 +136,4 @@
master
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/OnTopic.Data.Sql.Database/README.md b/OnTopic.Data.Sql.Database/README.md
index bc1dc861..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.
@@ -17,41 +17,47 @@ 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.
+> *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.
### 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
- **[`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.
-- **[`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).
+- **[`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.
+- **[`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 `@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`).
+- **[`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:
-- **[`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`.
+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`.
+- **[`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.
\ 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
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..e1552f33
--- /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 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).
+--------------------------------------------------------------------------------------------------------------------------------
+
+INSERT
+INTO topics_TopicAttributes
+SELECT SourceTopicID,
+ 'Type',
+ AttributeTypes.AttributeValue,
+ SYSUTCDATETIME()
+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
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..b353c581
--- /dev/null
+++ b/OnTopic.Data.Sql.Database/Scripts/Upgrade from OnTopic 4 to OnTopic 5.sql
@@ -0,0 +1,180 @@
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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.
+--------------------------------------------------------------------------------------------------------------------------------
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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.
+--------------------------------------------------------------------------------------------------------------------------------
+
+PRINT('Dropping legacy columns...');
+
+ALTER
+TABLE Topics
+DROP
+COLUMN Stack_Top;
+
+ALTER
+TABLE Attributes
+DROP
+CONSTRAINT DF_Attributes_DateModified;
+
+ALTER
+TABLE Attributes
+DROP
+COLUMN DateModified;
+
+ALTER
+TABLE ExtendedAttributes
+DROP
+CONSTRAINT [DF_ExtendedAttributes_DateModified]
+
+ALTER
+TABLE ExtendedAttributes
+DROP
+COLUMN DateModified;
+
+PRINT('Dropped legacy columns');
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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.
+--------------------------------------------------------------------------------------------------------------------------------
+
+PRINT('Migrating core attributes...');
+
+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
+
+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
+IN ( 'Key',
+ 'ContentType',
+ 'ParentID'
+)
+
+PRINT('Migrated core attributes');
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- MIGRATE TOPIC REFERENCES
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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`.
+--------------------------------------------------------------------------------------------------------------------------------
+
+PRINT('Migrating topic references...');
+
+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
+
+PRINT('Migrated core attributes');
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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'.
+--------------------------------------------------------------------------------------------------------------------------------
+
+PRINT('Migrating base topics...');
+
+UPDATE TopicReferences
+SET ReferenceKey = 'BaseTopic'
+WHERE ReferenceKey = 'Topic'
+
+UPDATE Topics
+SET TopicKey = 'BaseTopic'
+WHERE TopicKey IN ('TopicID', 'InheritedTopic', 'DerivedTopic')
+AND ContentType = 'TopicReferenceAttribute'
+
+PRINT('Migrated base topics');
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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).
+--------------------------------------------------------------------------------------------------------------------------------
+
+PRINT('Migrating attribute descriptors...');
+
+UPDATE Topics
+SET TopicKey = TopicKey + 'Descriptor'
+WHERE TopicKey LIKE '%Attribute'
+AND ContentType = 'ContentTypeDescriptor'
+
+UPDATE Topics
+SET ContentType = ContentType + 'Descriptor'
+WHERE ContentType LIKE '%Attribute'
+
+PRINT('Migrated attribute descriptors');
\ No newline at end of file
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql
index 6d836ccd..e6bf0df2 100644
--- a/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/CreateTopic.sql
@@ -5,31 +5,72 @@
--------------------------------------------------------------------------------------------------------------------------------
CREATE PROCEDURE [dbo].[CreateTopic]
- @ParentID int = -1,
+ @Key VARCHAR(128) ,
+ @ContentType VARCHAR(128) ,
+ @ParentID INT = NULL,
@Attributes AttributeValues READONLY,
- @ExtendedAttributes Xml = null,
- @Version datetime = null
+ @ExtendedAttributes XML = NULL,
+ @References TopicReferences READONLY,
+ @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
--------------------------------------------------------------------------------------------------------------------------------
IF @Version IS NULL
-SET @Version = getdate()
+SET @Version = SYSUTCDATETIME()
--------------------------------------------------------------------------------------------------------------------------------
-- DECLARE AND SET VARIABLES
--------------------------------------------------------------------------------------------------------------------------------
-DECLARE @RangeRight Integer --Right Most Sibling
+DECLARE @RangeRight INT --Right Most Sibling
SET @RangeRight = 0
--------------------------------------------------------------------------------------------------------------------------------
-- RESERVE SPACE FOR NEW CHILD.
--------------------------------------------------------------------------------------------------------------------------------
-IF (@ParentID > -1)
+-- ### 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
@@ -47,25 +88,38 @@ IF (@ParentID > -1)
END
WHERE RangeRight >= @RangeRight
END
+ELSE
+ BEGIN
+ SELECT @RangeRight = ISNULL(MAX(RangeRight), 0) + 1
+ FROM Topics
+ WITH ( TABLOCK
+ )
+ END
--------------------------------------------------------------------------------------------------------------------------------
-- CREATE NEW TOPIC
--------------------------------------------------------------------------------------------------------------------------------
INSERT INTO Topics (
RangeLeft,
- RangeRight
+ RangeRight,
+ TopicKey,
+ ContentType,
+ ParentID,
+ LastModified
)
Values (
@RangeRight,
- @RangeRight + 1
+ @RangeRight + 1,
+ @Key,
+ @ContentType,
+ @ParentID,
+ @Version
)
-DECLARE @TopicID INT
-
SELECT @TopicID = SCOPE_IDENTITY()
--------------------------------------------------------------------------------------------------------------------------------
--- CREATE ATTRIBUTES FROM STRING
+-- ADD INDEXED ATTRIBUTES
--------------------------------------------------------------------------------------------------------------------------------
INSERT INTO Attributes (
TopicID ,
@@ -78,13 +132,12 @@ SELECT @TopicID,
AttributeValue,
@Version
FROM @Attributes
-WHERE AttributeKey != 'ParentID'
- AND IsNull(AttributeValue, '') != ''
+WHERE ISNULL(AttributeValue, '') != ''
--------------------------------------------------------------------------------------------------------------------------------
-- ADD EXTENDED ATTRIBUTES (XML)
--------------------------------------------------------------------------------------------------------------------------------
-IF @ExtendedAttributes is not null
+IF @ExtendedAttributes IS NOT NULL
BEGIN
INSERT
INTO ExtendedAttributes (
@@ -99,19 +152,40 @@ IF @ExtendedAttributes is not null
END
--------------------------------------------------------------------------------------------------------------------------------
--- CACHE PARENT ID FOR DATA INTEGRITY PURPOSES
+-- ADD REFERENCES
--------------------------------------------------------------------------------------------------------------------------------
-INSERT INTO Attributes (
- TopicID ,
- AttributeKey ,
- AttributeValue ,
- Version
-)
-VALUES ( @TopicID ,
- 'ParentID' ,
- CONVERT(NVarChar(255), @ParentID),
- @Version
-)
+DECLARE @ReferenceCount INT
+SELECT @ReferenceCount = COUNT(ReferenceKey)
+FROM @References
+
+IF @ReferenceCount > 0
+ BEGIN
+ EXEC UpdateReferences @TopicID,
+ @References,
+ @Version,
+ 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
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
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..98bba426
--- /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 DATETIME2(7)
+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
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopicVersion.sql
index bbc099c3..0b37a0c8 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 DATETIME2(7) = NULL
AS
--------------------------------------------------------------------------------------------------------------------------------
@@ -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
@@ -58,6 +32,7 @@ AS (
SELECT TopicID,
AttributeKey,
AttributeValue,
+ Version,
RowNumber = ROW_NUMBER() OVER (
PARTITION BY TopicID,
AttributeKey
@@ -66,15 +41,11 @@ AS (
FROM Attributes
WHERE TopicID = @TopicID
AND Version <= @Version
- AND AttributeKey
- NOT IN ( 'Key',
- 'ParentID',
- 'ContentType'
- )
)
SELECT TopicID,
AttributeKey,
- AttributeValue
+ AttributeValue,
+ Version
FROM TopicAttributes
WHERE RowNumber = 1
@@ -85,6 +56,7 @@ WHERE RowNumber = 1
AS (
SELECT TopicID,
AttributesXml,
+ Version,
RowNumber = ROW_NUMBER() OVER (
PARTITION BY TopicID
ORDER BY Version DESC
@@ -94,18 +66,60 @@ AS (
AND Version <= @Version
)
SELECT TopicID,
- AttributesXml
+ AttributesXml,
+ Version
FROM TopicExtendedAttributes
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
+--------------------------------------------------------------------------------------------------------------------------------
+;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
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql
index 4846db5e..0d6d9a28 100644
--- a/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/GetTopics.sql
@@ -6,17 +6,17 @@
--------------------------------------------------------------------------------------------------------------------------------
CREATE PROCEDURE [dbo].[GetTopics]
- @TopicID int = -1,
- @DeepLoad bit = 1,
- @TopicKey nvarchar(255) = null
+ @TopicID INT = -1,
+ @DeepLoad BIT = 1,
+ @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.GetTopicID(@UniqueKey)
END
IF @TopicID < 0
@@ -87,24 +87,24 @@ ELSE
--------------------------------------------------------------------------------------------------------------------------------
-- SELECT KEY ATTRIBUTES
--------------------------------------------------------------------------------------------------------------------------------
-SELECT TopicIndex.TopicID,
- TopicIndex.ContentType,
- TopicIndex.ParentID,
- TopicIndex.TopicKey,
- Storage.SortOrder
-FROM TopicIndex AS TopicIndex
+SELECT Topics.TopicID,
+ ContentType,
+ ParentID,
+ TopicKey,
+ SortOrder
+FROM Topics AS Topics
JOIN #Topics AS Storage
- ON Storage.TopicID = TopicIndex.TopicID
+ ON Storage.TopicID = Topics.TopicID
ORDER BY SortOrder
--------------------------------------------------------------------------------------------------------------------------------
-- SELECT TOPIC ATTRIBUTES
--------------------------------------------------------------------------------------------------------------------------------
SELECT Attributes.TopicID,
- Attributes.AttributeKey,
- Attributes.AttributeValue,
- Attributes.Version
-FROM AttributeIndex Attributes
+ AttributeKey,
+ AttributeValue,
+ Version
+FROM AttributeIndex AS 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,18 +121,29 @@ JOIN #Topics AS Storage
--------------------------------------------------------------------------------------------------------------------------------
-- SELECT RELATIONSHIPS
--------------------------------------------------------------------------------------------------------------------------------
-SELECT Relationships.Source_TopicID,
- Relationships.RelationshipKey,
- Relationships.Target_TopicID
-FROM Relationships Relationships
+SELECT Source_TopicID,
+ RelationshipKey,
+ Target_TopicID,
+ IsDeleted
+FROM RelationshipIndex AS Relationships
JOIN #Topics AS Storage
ON Storage.TopicID = Relationships.Source_TopicID
+--------------------------------------------------------------------------------------------------------------------------------
+-- SELECT REFERENCES
+--------------------------------------------------------------------------------------------------------------------------------
+SELECT Source_TopicID,
+ ReferenceKey,
+ Target_TopicID
+FROM ReferenceIndex AS TopicReferences
+JOIN #Topics AS Storage
+ ON Storage.TopicID = TopicReferences.Source_TopicID
+
--------------------------------------------------------------------------------------------------------------------------------
-- SELECT HISTORY
--------------------------------------------------------------------------------------------------------------------------------
-SELECT VersionHistory.TopicID,
- VersionHistory.Version
-FROM VersionHistoryIndex VersionHistory
+SELECT History.TopicID,
+ Version
+FROM VersionHistoryIndex AS 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
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/MoveTopic.sql
index 27064b92..e60bd99b 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
@@ -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
@@ -92,37 +96,40 @@ 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.',
+ N'The topic ("%d") could not be found.',
15, -- Severity,
1, -- State,
@TopicID
);
+ COMMIT
RETURN
END
-IF @ParentID is null or @InsertionPoint is null
+IF @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
);
+ COMMIT
RETURN
END
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,
@ParentID
);
+ COMMIT
RETURN
END
@@ -299,10 +306,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
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..ddfd65df
--- /dev/null
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateAttributes.sql
@@ -0,0 +1,62 @@
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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 DATETIME2(7) = NULL ,
+ @DeleteUnmatched BIT = 0
+AS
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- INSERT NEW ATTRIBUTES
+--------------------------------------------------------------------------------------------------------------------------------
+INSERT
+INTO Attributes (
+ TopicID ,
+ AttributeKey ,
+ AttributeValue ,
+ Version
+ )
+SELECT @TopicID,
+ AttributeKey,
+ ISNULL(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(ExistingValue, '') != ISNULL(AttributeValue, '')
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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/UpdateExtendedAttributes.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateExtendedAttributes.sql
new file mode 100644
index 00000000..f438bd9b
--- /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 DATETIME2(7) = 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/UpdateReferences.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql
new file mode 100644
index 00000000..7db8b5d9
--- /dev/null
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateReferences.sql
@@ -0,0 +1,68 @@
+--------------------------------------------------------------------------------------------------------------------------------
+-- UPDATE REFERENCES
+--------------------------------------------------------------------------------------------------------------------------------
+-- Saves the 1:1 mappings for referenced topics.
+--------------------------------------------------------------------------------------------------------------------------------
+
+CREATE PROCEDURE [dbo].[UpdateReferences]
+ @TopicID INT,
+ @ReferencedTopics TopicReferences READONLY ,
+ @Version DATETIME2(7) = NULL ,
+ @DeleteUnmatched BIT = 0
+AS
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET DEFAULT VERSION DATETIME
+--------------------------------------------------------------------------------------------------------------------------------
+IF @Version IS NULL
+SET @Version = SYSUTCDATETIME()
+
+--------------------------------------------------------------------------------------------------------------------------------
+-- INSERT NOVEL VALUES
+--------------------------------------------------------------------------------------------------------------------------------
+INSERT
+INTO TopicReferences (
+ Source_TopicID,
+ ReferenceKey,
+ Target_TopicID,
+ Version
+ )
+SELECT @TopicID,
+ 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
+ INSERT
+ INTO TopicReferences
+ SELECT @TopicID,
+ Existing.ReferenceKey,
+ NULL,
+ @Version
+ FROM @ReferencedTopics New
+ RIGHT JOIN ReferenceIndex 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
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql
index cd53dfcb..86507b74 100644
--- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateRelationships.sql
@@ -5,12 +5,19 @@
--------------------------------------------------------------------------------------------------------------------------------
CREATE PROCEDURE [dbo].[UpdateRelationships]
- @TopicID INT = -1,
- @RelationshipKey VARCHAR(255) = 'related',
- @RelatedTopics TopicList READONLY,
- @DeleteUnmatched BIT = 1
+ @TopicID INT,
+ @RelationshipKey VARCHAR(255),
+ @RelatedTopics TopicList READONLY ,
+ @Version DATETIME2(7) = NULL ,
+ @DeleteUnmatched BIT = 0
AS
+--------------------------------------------------------------------------------------------------------------------------------
+-- SET DEFAULT VERSION DATETIME
+--------------------------------------------------------------------------------------------------------------------------------
+IF @Version IS NULL
+SET @Version = SYSUTCDATETIME()
+
--------------------------------------------------------------------------------------------------------------------------------
-- INSERT NOVEL VALUES
--------------------------------------------------------------------------------------------------------------------------------
@@ -18,28 +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
-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
+ RIGHT JOIN RelationshipIndex Existing
ON Target_TopicID = TopicID
WHERE Source_TopicID = @TopicID
AND ISNULL(TopicID, '') = ''
+ AND RelationshipKey = @RelationshipKey
+ AND IsDeleted = 0
END
--------------------------------------------------------------------------------------------------------------------------------
diff --git a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql
index 7172a7c0..8ac81acc 100644
--- a/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql
+++ b/OnTopic.Data.Sql.Database/Stored Procedures/UpdateTopic.sql
@@ -1,104 +1,68 @@
--------------------------------------------------------------------------------------------------------------------------------
-- 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]
- @TopicID INT = -1 ,
+ @TopicID INT ,
+ @Key VARCHAR(128) = NULL ,
+ @ContentType VARCHAR(128) = NULL ,
@Attributes AttributeValues READONLY ,
- @ExtendedAttributes XML = null ,
- @Version DATETIME = null ,
- @DeleteRelationships BIT = 0
+ @ExtendedAttributes XML = NULL ,
+ @Version DATETIME2(7) = NULL ,
+ @DeleteUnmatched BIT = 0
AS
--------------------------------------------------------------------------------------------------------------------------------
-- SET DEFAULT VERSION DATETIME
--------------------------------------------------------------------------------------------------------------------------------
IF @Version IS NULL
-SET @Version = GetDate()
+SET @Version = SYSUTCDATETIME()
--------------------------------------------------------------------------------------------------------------------------------
--- INSERT NEW ATTRIBUTES
+-- UPDATE KEY 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
+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,
+ LastModified = @Version
WHERE TopicID = @TopicID
- AND AttributeKey = New.AttributeKey
- ORDER BY Version DESC
- ) Existing
-WHERE AttributeKey != 'ParentId'
- AND ISNULL(AttributeValue, '') != ''
- AND ISNULL(ExistingValue, '') != ISNULL(AttributeValue, '')
+ END
--------------------------------------------------------------------------------------------------------------------------------
--- PULL PREVIOUS EXTENDED ATTRIBUTES
+-- UPDATE ATTRIBUTES
--------------------------------------------------------------------------------------------------------------------------------
-DECLARE @PreviousExtendedAttributes XML
-
-SELECT TOP 1
- @PreviousExtendedAttributes = AttributesXml
-FROM ExtendedAttributes
-WHERE TopicID = @TopicID
-ORDER BY Version DESC
+IF EXISTS (SELECT TOP 1 NULL FROM @Attributes)
+ BEGIN
+ EXEC UpdateAttributes @TopicID,
+ @Attributes,
+ @Version,
+ @DeleteUnmatched
+ END
--------------------------------------------------------------------------------------------------------------------------------
-- ADD EXTENDED ATTRIBUTES, IF CHANGED
--------------------------------------------------------------------------------------------------------------------------------
-IF 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
---------------------------------------------------------------------------------------------------------------------------------
--- 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 != ''
-
--------------------------------------------------------------------------------------------------------------------------------
-- RETURN TOPIC ID
--------------------------------------------------------------------------------------------------------------------------------
diff --git a/OnTopic.Data.Sql.Database/Tables/Attributes.sql b/OnTopic.Data.Sql.Database/Tables/Attributes.sql
index 4afab276..6949851f 100644
--- a/OnTopic.Data.Sql.Database/Tables/Attributes.sql
+++ b/OnTopic.Data.Sql.Database/Tables/Attributes.sql
@@ -9,12 +9,9 @@
CREATE
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,
+ [AttributeKey] VARCHAR(128) NOT NULL,
+ [AttributeValue] NVARCHAR(255) NOT NULL,
+ [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 15ffe748..b64c64b6 100644
--- a/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql
+++ b/OnTopic.Data.Sql.Database/Tables/ExtendedAttributes.sql
@@ -8,15 +8,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,
+ [AttributesXml] XML NOT NULL,
+ [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 69b4ae3d..d62a56c0 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] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME()
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]
diff --git a/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql
new file mode 100644
index 00000000..94cdc791
--- /dev/null
+++ b/OnTopic.Data.Sql.Database/Tables/TopicReferences.sql
@@ -0,0 +1,29 @@
+--------------------------------------------------------------------------------------------------------------------------------
+-- 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 NULL,
+ [Version] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME()
+ CONSTRAINT [PK_TopicReferences] PRIMARY KEY
+ CLUSTERED ( [Source_TopicID] ASC,
+ [ReferenceKey] ASC,
+ [Version] DESC
+ ),
+ 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
diff --git a/OnTopic.Data.Sql.Database/Tables/Topics.sql b/OnTopic.Data.Sql.Database/Tables/Topics.sql
index 26ec012e..c3de682e 100644
--- a/OnTopic.Data.Sql.Database/Tables/Topics.sql
+++ b/OnTopic.Data.Sql.Database/Tables/Topics.sql
@@ -6,13 +6,19 @@
--------------------------------------------------------------------------------------------------------------------------------
CREATE
TABLE [dbo].[Topics] (
- [Stack_Top] INT NULL,
[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,
+ [LastModified] DATETIME2(7) NOT NULL DEFAULT SYSUTCDATETIME()
+ CONSTRAINT [PK_Topics] PRIMARY KEY
CLUSTERED ( [TopicID] ASC
- )
+ ),
+ CONSTRAINT [FK_Topics_Topics]
+ FOREIGN KEY ( [ParentID] )
+ REFERENCES [Topics]([TopicID])
);
GO
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.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
--------------------------------------------------------------------------------------------------------------------------------
diff --git a/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql b/OnTopic.Data.Sql.Database/Utilities/Stored Procedures/DeleteConsecutiveAttributes.sql
index 9b9ff760..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.');
@@ -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)
@@ -65,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..d553f8a6 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.');
@@ -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)
@@ -65,7 +63,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,
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,
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
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
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
diff --git a/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs b/OnTopic.Data.Sql/Models/AttributeValuesDataTable.cs
index bf116c0d..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 {
@@ -38,7 +39,7 @@ internal AttributeValuesDataTable() {
| COLUMN: Attribute Value
\-----------------------------------------------------------------------------------------------------------------------*/
Columns.Add(
- new DataColumn("AttributeValue") {
+ new DataColumn("AttributeRecord") {
MaxLength = 255
}
);
@@ -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) {
/*------------------------------------------------------------------------------------------------------------------------
@@ -60,7 +61,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/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
diff --git a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj
index 4159a9ab..c50a4672 100644
--- a/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj
+++ b/OnTopic.Data.Sql/OnTopic.Data.Sql.csproj
@@ -2,57 +2,35 @@
{1DE1F923-C7C2-435B-B49A-975ACBCB5FF0}
+ netstandard2.1
OnTopic.Data.Sql
- netstandard2.0;netstandard2.1
- 9.0
- enable
OnTopic SQL Server Repository
- Ignia
- OnTopic
Provides Microsoft SQL Server support for persisting the OnTopic graph to a database.
- ©2020 Ignia, LLC
bin\$(Configuration)\
- Ignia
-
-
-
- https://github.com/Ignia/Topics-Library
C# .NET CMS SQL Data Repository
- true
-
-
-
- full
- false
- latest
- 1701;1702;CA1303
-
-
- pdbonly
- 1701;1702;CA1303
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
- runtime; build; native; contentfiles; analyzers
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
\ No newline at end of file
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.Data.Sql/README.md b/OnTopic.Data.Sql/README.md
index cef68ae2..b6a8b452 100644
--- a/OnTopic.Data.Sql/README.md
+++ b/OnTopic.Data.Sql/README.md
@@ -1,8 +1,9 @@
# 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.
[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=15c8a666-efa5-4b23-b08b-1de907478d2d&preferRelease=true)
[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
> *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.
@@ -16,7 +17,7 @@ Installation can be performed by providing a ` to the `OnTo
…
-
+
```
@@ -24,7 +25,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.Data.Sql/SqlCommandExtensions.cs b/OnTopic.Data.Sql/SqlCommandExtensions.cs
index 89b0395b..3fed756a 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."
);
@@ -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.
@@ -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
///
@@ -152,6 +150,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,
diff --git a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs
index 42650cd6..d456ff04 100644
--- a/OnTopic.Data.Sql/SqlDataReaderExtensions.cs
+++ b/OnTopic.Data.Sql/SqlDataReaderExtensions.cs
@@ -5,13 +5,14 @@
\=============================================================================================================================*/
using System;
using System.Collections.Generic;
+using System.Data;
using System.Diagnostics;
-using System.Globalization;
using System.Linq;
using System.Net;
using Microsoft.Data.SqlClient;
-using OnTopic.Attributes;
+using OnTopic.Collections.Specialized;
using OnTopic.Internal.Diagnostics;
+using OnTopic.Querying;
namespace OnTopic.Data.Sql {
@@ -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
@@ -27,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 {
@@ -36,28 +37,48 @@ internal static class SqlDataReaderExtensions {
| METHOD: LOAD TOPIC GRAPH
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Given a from a call to the GetTopics stored procedure, will extract a list of
- /// topics and populate their attributes, relationships, and children.
+ /// Given a from a call to the GetTopics stored procedure, will extract a list of
+ /// topics and populate their attributes, associations, 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
+ /// 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
+ /// 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
+ /// 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(this SqlDataReader reader, bool includeExternalReferences = true) {
+ internal static Topic? LoadTopicGraph(
+ this IDataReader reader,
+ Topic? referenceTopic = null,
+ bool? markDirty = null,
+ bool includeExternalReferences = true
+ ) {
/*----------------------------------------------------------------------------------------------------------------------
| Establish topic index
\---------------------------------------------------------------------------------------------------------------------*/
- var topics = new Dictionary();
+ var sqlDataReader = reader as SqlDataReader;
+ 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()) {
- reader.AddTopic(topics);
+ if (rootTopicId < 0) {
+ rootTopicId = reader.GetTopicId();
+ }
+ reader.AddTopic(topics, markDirty);
}
/*----------------------------------------------------------------------------------------------------------------------
@@ -69,7 +90,7 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte
reader.NextResult();
while (reader.Read()) {
- reader.SetIndexedAttributes(topics);
+ reader.SetIndexedAttributes(topics, markDirty);
}
/*----------------------------------------------------------------------------------------------------------------------
@@ -82,7 +103,9 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte
// Loop through each extended attribute record associated with a specific topic
while (reader.Read()) {
- reader.SetExtendedAttributes(topics);
+ if (sqlDataReader is not null) {
+ sqlDataReader.SetExtendedAttributes(topics, markDirty);
+ }
}
/*----------------------------------------------------------------------------------------------------------------------
@@ -96,35 +119,42 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte
// Loop through each relationship; multiple records may exist per topic
if (includeExternalReferences) {
while (reader.Read()) {
- reader.SetRelationships(topics);
+ reader.SetRelationships(topics, markDirty);
}
}
/*----------------------------------------------------------------------------------------------------------------------
- | Read version history
+ | Read referenced items
\---------------------------------------------------------------------------------------------------------------------*/
- Debug.WriteLine("SqlTopicRepository.Load(): SetVersionHistory() [" + DateTime.Now + "]");
+ 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.SetVersionHistory(topics);
+ reader.SetReferences(topics, markDirty);
}
/*----------------------------------------------------------------------------------------------------------------------
- | Populate strongly typed references
+ | Read version history
\---------------------------------------------------------------------------------------------------------------------*/
- Debug.WriteLine("SqlTopicRepository.Load(): SetDerivedTopics() [" + DateTime.Now + "]");
+ Debug.WriteLine("SqlTopicRepository.Load(): SetVersionHistory() [" + DateTime.Now + "]");
- if (includeExternalReferences) {
- SetDerivedTopics(topics);
+ // Move to the version history dataset
+ reader.NextResult();
+
+ // Loop through each version; multiple records may exist per topic
+ while (reader.Read()) {
+ reader.SetVersionHistory(topics);
}
/*------------------------------------------------------------------------------------------------------------------------
| Return objects
\-----------------------------------------------------------------------------------------------------------------------*/
+ if (topics.ContainsKey(rootTopicId)) {
+ return topics[rootTopicId];
+ }
return topics.Values.FirstOrDefault();
}
@@ -136,9 +166,15 @@ internal static Topic LoadTopicGraph(this SqlDataReader reader, bool includeExte
/// 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.
- private static void AddTopic(this SqlDataReader reader, Dictionary 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 IDataReader reader, TopicIndex topics, bool? markDirty) {
/*------------------------------------------------------------------------------------------------------------------------
| Identify attributes
@@ -147,22 +183,35 @@ private static void AddTopic(this SqlDataReader reader, Dictionary t
var key = reader.GetString("TopicKey");
var contentType = reader.GetString("ContentType");
var parentId = reader.GetInteger("ParentID");
+ var wasDirty = false;
/*------------------------------------------------------------------------------------------------------------------------
| 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);
+ }
+ else {
+ wasDirty = current.IsDirty();
+ 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];
}
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Mark clean
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (wasDirty is false && markDirty is not null and false) {
+ current.MarkClean();
+ }
+
}
/*==========================================================================================================================
@@ -172,9 +221,15 @@ private static void AddTopic(this SqlDataReader reader, Dictionary t
/// 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.
- private static void SetIndexedAttributes(this SqlDataReader reader, Dictionary 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 IDataReader reader, TopicIndex topics, bool? markDirty) {
/*------------------------------------------------------------------------------------------------------------------------
| Identify attributes
@@ -182,13 +237,7 @@ private static void SetIndexedAttributes(this SqlDataReader reader, Dictionary 3) {
- version = reader.GetVersion();
- }
+ var version = reader.GetVersion();
/*------------------------------------------------------------------------------------------------------------------------
| Handle empty attributes (treat empty as null)
@@ -203,7 +252,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) {
+ ///
+ /// 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
\-----------------------------------------------------------------------------------------------------------------------*/
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
@@ -258,7 +307,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, Dictionary<
/*----------------------------------------------------------------------------------------------------------------------
| Identify attributes
\---------------------------------------------------------------------------------------------------------------------*/
- var attributeKey = (string)xmlReader.GetAttribute("key");
+ var attributeKey = (string?)xmlReader.GetAttribute("key");
var attributeValue = WebUtility.HtmlDecode(xmlReader.ReadInnerXml());
/*----------------------------------------------------------------------------------------------------------------------
@@ -274,7 +323,7 @@ private static void SetExtendedAttributes(this SqlDataReader reader, Dictionary<
| 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");
@@ -290,9 +339,15 @@ private static void SetExtendedAttributes(this SqlDataReader reader, Dictionary<
/// 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.
- private static void SetRelationships(this SqlDataReader reader, Dictionary 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 IDataReader reader, TopicIndex topics, bool? markDirty = false) {
/*------------------------------------------------------------------------------------------------------------------------
| Identify attributes
@@ -300,6 +355,7 @@ 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 associations. 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.
+ ///
+ /// 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 IDataReader reader, TopicIndex topics, bool? markDirty) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Identify attributes
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var sourceTopicId = reader.GetTopicId("Source_TopicID");
+ var referenceKey = reader.GetString("ReferenceKey");
+ var targetTopicId = reader.GetNullableTopicId("Target_TopicID");
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Identify affected topics
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var current = topics[sourceTopicId];
+ var referenced = (Topic?)null;
+
+ // Fetch the related topic
+ if (targetTopicId is not null && topics.Keys.Contains(targetTopicId.Value)) {
+ referenced = topics[targetTopicId.Value];
+ }
+
+ // Bypass if the target object is missing
+ if (referenced is null) {
+ current.References.IsFullyLoaded = false;
+ return;
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Set reference on object
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ current.References.SetValue(referenceKey, referenced, markDirty);
}
@@ -333,9 +448,9 @@ private static void SetRelationships(this SqlDataReader reader, Dictionary
- /// 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, Dictionary topics) {
+ private static void SetVersionHistory(this IDataReader reader, TopicIndex topics) {
/*------------------------------------------------------------------------------------------------------------------------
| Identify attributes
@@ -351,37 +466,33 @@ private static void SetVersionHistory(this SqlDataReader reader, Dictionary
- /// Sets references to .
+ /// Retrieves a string value by column name.
///
- ///
- /// 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];
- }
-
- }
+ /// The object.
+ /// The name of the column to retrieve the value from.
+ private static string GetString(this IDataReader reader, string columnName) =>
+ reader.GetString(reader.GetOrdinal(columnName));
- }
+ /*==========================================================================================================================
+ | 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 IDataReader reader, string columnName) =>
+ reader.GetBoolean(reader.GetOrdinal(columnName));
/*==========================================================================================================================
| METHOD: GET INTEGER
@@ -389,9 +500,9 @@ private static void SetDerivedTopics(Dictionary topics) {
///
/// Retrieves an integer value by column name.
///
- /// The object.
+ /// 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 IDataReader reader, string columnName) =>
Int32.TryParse(reader.GetValue(reader.GetOrdinal(columnName)).ToString(), out var output)? output : -1;
/*==========================================================================================================================
@@ -400,21 +511,21 @@ internal 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.
- internal static int GetTopicId(this SqlDataReader reader, string columnName = "TopicID") =>
+ private static int GetTopicId(this IDataReader reader, string columnName = "TopicID") =>
reader.GetInt32(reader.GetOrdinal(columnName));
/*==========================================================================================================================
- | METHOD: GET STRING
+ | METHOD: GET NULLABLE TOPIC ID
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Retrieves a string value by column name.
+ /// Retrieves a value by column name, while accepting null values.
///
- /// The object.
+ /// The object.
/// The name of the column to retrieve the value from.
- internal static string GetString(this SqlDataReader reader, string columnName) =>
- reader.GetString(reader.GetOrdinal(columnName));
+ private static int? GetNullableTopicId(this IDataReader reader, string columnName = "TopicID") =>
+ reader.IsDBNull(reader.GetOrdinal(columnName))? null : reader.GetInt32(reader.GetOrdinal(columnName));
/*==========================================================================================================================
| METHOD: GET VERSION
@@ -422,8 +533,8 @@ 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) =>
+ /// The object.
+ private static DateTime GetVersion(this IDataReader reader) =>
reader.GetDateTime(reader.GetOrdinal("Version"));
} //Class
diff --git a/OnTopic.Data.Sql/SqlTopicRepository.cs b/OnTopic.Data.Sql/SqlTopicRepository.cs
index 0f5448d4..79880e57 100644
--- a/OnTopic.Data.Sql/SqlTopicRepository.cs
+++ b/OnTopic.Data.Sql/SqlTopicRepository.cs
@@ -4,9 +4,7 @@
| 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;
@@ -14,6 +12,7 @@
using Microsoft.Data.SqlClient;
using OnTopic.Data.Sql.Models;
using OnTopic.Internal.Diagnostics;
+using OnTopic.Querying;
using OnTopic.Repositories;
namespace OnTopic.Data.Sql {
@@ -25,9 +24,9 @@ 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 : TopicRepositoryBase, ITopicRepository {
+ public class SqlTopicRepository : TopicRepository, ITopicRepository {
/*==========================================================================================================================
| PRIVATE VARIABLES
@@ -48,10 +47,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
@@ -64,7 +60,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, Topic? referenceTopic = null, bool isRecursive = true) {
/*------------------------------------------------------------------------------------------------------------------------
| Handle empty topic
@@ -72,8 +68,8 @@ 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)) {
- return Load(-1, isRecursive);
+ if (String.IsNullOrEmpty(uniqueKey)) {
+ return Load(-1, referenceTopic, isRecursive);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -89,7 +85,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,18 +111,18 @@ public override Topic Load(string? topicKey = null, bool isRecursive = true) {
| Validate results
\-----------------------------------------------------------------------------------------------------------------------*/
if (topicId < 0) {
- throw new TopicNotFoundException(topicKey);
+ throw new TopicNotFoundException(uniqueKey);
}
/*------------------------------------------------------------------------------------------------------------------------
| 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
@@ -151,7 +147,7 @@ public override Topic Load(int topicId, bool isRecursive = true) {
try {
connection.Open();
using var reader = command.ExecuteReader();
- topic = reader.LoadTopicGraph();
+ topic = reader.LoadTopicGraph(referenceTopic, false);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -183,6 +179,11 @@ public override Topic Load(int topicId, bool isRecursive = true) {
\-----------------------------------------------------------------------------------------------------------------------*/
base.SetContentTypeDescriptors(topic);
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Raise event
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ OnTopicLoaded(new(topic, isRecursive));
+
/*------------------------------------------------------------------------------------------------------------------------
| Return objects
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -191,7 +192,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
@@ -203,10 +204,31 @@ public override Topic Load(int topicId, DateTime version) {
);
/*------------------------------------------------------------------------------------------------------------------------
- | Establish database connection
+ | 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 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;
+ 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.Clear(relationship.Key);
+ }
+ topic.References.Clear();
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Establish database connection
+ \-----------------------------------------------------------------------------------------------------------------------*/
using var connection = new SqlConnection(_connectionString);
using var command = new SqlCommand("GetTopicVersion", connection) {
CommandType = CommandType.StoredProcedure,
@@ -227,104 +249,118 @@ public override Topic Load(int topicId, DateTime version) {
try {
connection.Open();
using var reader = command.ExecuteReader();
- topic = reader.LoadTopicGraph(false);
+ topic = reader.LoadTopicGraph(referenceTopic, includeExternalReferences: referenceTopic is not null);
}
/*------------------------------------------------------------------------------------------------------------------------
| 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);
}
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate result
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (topic is null) {
+ throw new TopicNotFoundException(topicId);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | 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);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Raise event
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ OnTopicLoaded(new(topic, false, version));
+
/*------------------------------------------------------------------------------------------------------------------------
| Return objects
\-----------------------------------------------------------------------------------------------------------------------*/
- return topic?? throw new TopicNotFoundException(topicId);
+ return topic;
}
/*==========================================================================================================================
- | METHOD: SAVE
+ | METHOD: REFRESH
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public override int Save([NotNull]Topic topic, bool isRecursive = false, bool isDraft = false) {
+ ///
+ public override void Refresh(Topic referenceTopic, DateTime since) {
/*------------------------------------------------------------------------------------------------------------------------
- | Establish dependencies
+ | Validate parameters
\-----------------------------------------------------------------------------------------------------------------------*/
- var version = new SqlDateTime(DateTime.UtcNow);
- var unresolvedTopics = new List();
+ 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
+ };
- connection.Open();
+ command.CommandType = CommandType.StoredProcedure;
/*------------------------------------------------------------------------------------------------------------------------
- | Handle first pass
+ | Establish query parameters
\-----------------------------------------------------------------------------------------------------------------------*/
- var topicId = Save(topic, isRecursive, isDraft, connection, unresolvedTopics, version);
+ command.AddParameter("Since", since);
/*------------------------------------------------------------------------------------------------------------------------
- | Attempt to resolve outstanding relationships
+ | Process database query
\-----------------------------------------------------------------------------------------------------------------------*/
- foreach (var unresolvedTopic in unresolvedTopics) {
- Save(unresolvedTopic, false, isDraft, connection, new(), version);
+ try {
+ connection.Open();
+ using var reader = command.ExecuteReader();
+ reader.LoadTopicGraph(referenceTopic.GetRootTopic(), false);
}
/*------------------------------------------------------------------------------------------------------------------------
- | Return value
+ | Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- connection.Close();
- return topicId;
+ catch (SqlException exception) {
+ throw new TopicRepositoryException($"Topics failed to update: '{exception.Message}'", exception);
+ }
}
- ///
- /// 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 .
- /// 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(
+ /*==========================================================================================================================
+ | METHOD: SAVE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ protected override sealed void SaveTopic(
[NotNull]Topic topic,
- bool isRecursive,
- bool isDraft,
- SqlConnection connection,
- List unresolvedRelationships,
- SqlDateTime version
+ DateTime version,
+ bool persistRelationships
) {
- /*------------------------------------------------------------------------------------------------------------------------
- | Call base method - will trigger any events associated with the save
- \-----------------------------------------------------------------------------------------------------------------------*/
- base.Save(topic, isRecursive, isDraft);
-
/*------------------------------------------------------------------------------------------------------------------------
| Define variables
\-----------------------------------------------------------------------------------------------------------------------*/
- var areReferencesResolved = true;
+ var isTopicDirty = topic.IsDirty();
var areRelationshipsDirty = topic.Relationships.IsDirty();
- var areAttributesDirty = topic.Attributes.IsDirty(excludeLastModified: !areRelationshipsDirty);
+ var areReferencesDirty = topic.References.IsDirty();
+ var areAttributesDirty = topic.Attributes.IsDirty(true);
var extendedAttributeList = GetAttributes(topic, isExtendedAttribute: true);
var indexedAttributeList = GetAttributes(
topic : topic,
@@ -336,32 +372,43 @@ SqlDateTime version
/*------------------------------------------------------------------------------------------------------------------------
| 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.
\-----------------------------------------------------------------------------------------------------------------------*/
+ areAttributesDirty = areAttributesDirty || extendedAttributeList.Any(a => a.IsExtendedAttribute == false);
+
var isDirty =
+ isTopicDirty ||
areRelationshipsDirty ||
- areAttributesDirty ||
- indexedAttributeList.Any() ||
- extendedAttributeList.Any(a => a.IsExtendedAttribute == false);
+ areReferencesDirty ||
+ areAttributesDirty;
/*------------------------------------------------------------------------------------------------------------------------
| Bypass is not dirty
\-----------------------------------------------------------------------------------------------------------------------*/
if (!isDirty) {
- recurse();
- return topic.Id;
+ return;
}
/*------------------------------------------------------------------------------------------------------------------------
| 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);
+ }
+
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -369,68 +416,60 @@ 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);
}
/*------------------------------------------------------------------------------------------------------------------------
| 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.DerivedTopic?.Id < 0 || topic.Relationships.Any(r => r.Any(t => t.Id < 0)))) {
- unresolvedRelationships.Add(topic);
- areReferencesResolved = false;
- }
-
/*------------------------------------------------------------------------------------------------------------------------
| Establish query parameters
\-----------------------------------------------------------------------------------------------------------------------*/
if (!topic.IsNew) {
command.AddParameter("TopicID", topic.Id);
- command.AddParameter("DeleteRelationships", areReferencesResolved && areRelationshipsDirty);
+ command.AddParameter("DeleteUnmatched", false);
}
else if (topic.Parent is not null) {
command.AddParameter("ParentID", topic.Parent.Id);
}
- command.AddParameter("Version", version.Value);
- command.AddParameter("ExtendedAttributes", extendedAttributes);
- command.AddParameter("Attributes", attributeValues);
+ if (isTopicDirty || topic.IsNew) {
+ command.AddParameter("Key", topic.Key);
+ command.AddParameter("ContentType", topic.ContentType);
+ }
+ command.AddParameter("Version", version);
+ if (areAttributesDirty) {
+ command.AddParameter("Attributes", attributeValues);
+ command.AddParameter("ExtendedAttributes", extendedAttributes);
+ }
command.AddOutputParameter();
/*------------------------------------------------------------------------------------------------------------------------
@@ -438,25 +477,24 @@ SqlDateTime version
\-----------------------------------------------------------------------------------------------------------------------*/
try {
- command.ExecuteNonQuery();
-
- topic.Id = command.GetReturnCode();
+ if (topic.IsNew || isTopicDirty || areAttributesDirty) {
+ command.ExecuteNonQuery();
+ topic.Id = command.GetReturnCode();
+ }
- Contract.Assume(
+ Contract.Assume(
!topic.IsNew,
"The call to the CreateTopic stored procedure did not return the expected 'Id' parameter."
);
- if (areReferencesResolved && areRelationshipsDirty) {
- PersistRelations(topic, connection);
+ if (persistRelationships && areRelationshipsDirty) {
+ PersistRelationships(topic, version, connection);
}
- if (!topic.VersionHistory.Contains(version.Value)) {
- topic.VersionHistory.Insert(0, version.Value);
+ if (persistRelationships && areReferencesDirty) {
+ PersistReferences(topic, version, connection);
}
- topic.Attributes.MarkClean(version.Value);
-
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -470,35 +508,25 @@ SqlDateTime version
}
/*------------------------------------------------------------------------------------------------------------------------
- | Return value
+ | Close connection
\-----------------------------------------------------------------------------------------------------------------------*/
- recurse();
- return topic.Id;
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Recurse
- \-----------------------------------------------------------------------------------------------------------------------*/
- 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);
- }
- }
+ finally {
+ connection.Close();
}
}
/*==========================================================================================================================
- | METHOD: MOVE
+ | METHOD: MOVE TOPIC
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override void Move(Topic topic, Topic target, Topic? sibling) {
+ protected override sealed 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
@@ -537,23 +565,18 @@ public override void Move(Topic topic, Topic target, Topic? sibling) {
);
}
- /*------------------------------------------------------------------------------------------------------------------------
- | Reset dirty status
- \-----------------------------------------------------------------------------------------------------------------------*/
- topic.Attributes.MarkClean("ParentId");
-
}
/*==========================================================================================================================
- | METHOD: DELETE
+ | METHOD: DELETE TOPIC
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override void Delete(Topic topic, bool isRecursive = true) {
+ protected override sealed void DeleteTopic(Topic topic) {
/*------------------------------------------------------------------------------------------------------------------------
- | Delete from memory
+ | Validate parameters
\-----------------------------------------------------------------------------------------------------------------------*/
- base.Delete(topic, isRecursive);
+ Contract.Requires(topic, nameof(topic));
/*------------------------------------------------------------------------------------------------------------------------
| Delete from database
@@ -589,14 +612,15 @@ public override void Delete(Topic topic, bool isRecursive = true) {
}
/*==========================================================================================================================
- | 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 version that should be associated with the updated value.
/// The SQL connection.
- private static void PersistRelations(Topic topic, SqlConnection connection) {
+ private static void PersistRelationships(Topic topic, DateTime version, SqlConnection connection) {
/*------------------------------------------------------------------------------------------------------------------------
| Return blank if the topic has no relations.
@@ -613,29 +637,26 @@ private static void PersistRelations(Topic topic, SqlConnection connection) {
\---------------------------------------------------------------------------------------------------------------------*/
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.GetValues(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);
+ command.AddParameter("DeleteUnmatched", topic.Relationships.IsFullyLoaded);
command.ExecuteNonQuery();
- //Reset isDirty, assuming there aren't any unresolved references
- relatedTopics.IsDirty = savedTopics.Count() < relatedTopics.Count;
-
}
}
@@ -657,5 +678,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 version that should be associated with the updated value.
+ /// The SQL connection.
+ private static void PersistReferences(Topic topic, DateTime version, SqlConnection connection) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Persist relations to database
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ try {
+
+ using var references = new TopicReferencesDataTable();
+ using var command = new SqlCommand("UpdateReferences", connection) {
+ CommandType = CommandType.StoredProcedure
+ };
+
+ foreach (var relatedTopic in topic.References) {
+ if (!relatedTopic.Value?.IsNew?? false) {
+ references.AddRow(relatedTopic.Key, relatedTopic.Value!.Id);
+ }
+ }
+
+ // Add Parameters
+ command.AddParameter("TopicID", topic.Id.ToString(CultureInfo.InvariantCulture));
+ command.AddParameter("ReferencedTopics", references);
+ command.AddParameter("Version", version);
+ command.AddParameter("DeleteUnmatched", topic.References.IsFullyLoaded);
+
+ command.ExecuteNonQuery();
+
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | 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
diff --git a/OnTopic.TestDoubles/DummyTopicMappingService.cs b/OnTopic.TestDoubles/DummyTopicMappingService.cs
index ba282e33..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() {
}
@@ -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 associations = 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 associations = 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 associations = AssociationTypes.All)
=> throw new NotImplementedException();
} //Class
diff --git a/OnTopic.TestDoubles/DummyTopicRepository.cs b/OnTopic.TestDoubles/DummyTopicRepository.cs
index b4d1957e..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 : TopicRepositoryBase, ITopicRepository {
+ public class DummyTopicRepository : ObservableTopicRepository {
/*==========================================================================================================================
| CONSTRUCTOR
@@ -26,23 +28,44 @@ public class DummyTopicRepository : TopicRepositoryBase, ITopicRepository {
/// A new instance of the .
public DummyTopicRepository() : base() { }
+ /*==========================================================================================================================
+ | METHOD: GET CONTENT TYPE DESCRIPTORS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public override ContentTypeDescriptorCollection GetContentTypeDescriptors() => new();
+
/*==========================================================================================================================
| 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, Topic? referenceTopic = null, bool isRecursive = true) => null;
///
- public override Topic? Load(string? topicKey = null, bool isRecursive = true) => null;
+ public override Topic? Load(Topic? topic, DateTime version) => throw new NotImplementedException();
///
- 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: ROLLBACK
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public override void Rollback(Topic topic, DateTime version) => throw new NotImplementedException();
+
+ /*==========================================================================================================================
+ | METHOD: REFRESH
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public override void Refresh(Topic referenceTopic, DateTime since) => throw new NotImplementedException();
/*==========================================================================================================================
| METHOD: SAVE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) => throw new NotImplementedException();
+ public override void Save(Topic topic, bool isRecursive = false) => throw new NotImplementedException();
/*==========================================================================================================================
| METHOD: MOVE
@@ -54,7 +77,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/Metadata/AttributeTypes/BooleanAttribute.cs b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs
similarity index 87%
rename from OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs
rename to OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs
index 13c17362..4d20e6d0 100644
--- a/OnTopic/Metadata/AttributeTypes/BooleanAttribute.cs
+++ b/OnTopic.TestDoubles/Metadata/BooleanAttributeDescriptor.cs
@@ -3,11 +3,12 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using OnTopic.Metadata;
-namespace OnTopic.Metadata.AttributeTypes {
+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
@@ -17,13 +18,13 @@ 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 BooleanAttributeDescriptor : AttributeDescriptor {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public BooleanAttribute(
+ public BooleanAttributeDescriptor(
string key,
string contentType,
Topic parent,
diff --git a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs
similarity index 79%
rename from OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs
rename to OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs
index 4d05afa0..9d1ade75 100644
--- a/OnTopic/Metadata/AttributeTypes/NestedTopicListAttribute.cs
+++ b/OnTopic.TestDoubles/Metadata/NestedTopicListAttributeDescriptor.cs
@@ -3,11 +3,12 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using OnTopic.Metadata;
-namespace OnTopic.Metadata.AttributeTypes {
+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
@@ -17,13 +18,13 @@ 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 NestedTopicListAttributeDescriptor : AttributeDescriptor {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public NestedTopicListAttribute(
+ public NestedTopicListAttributeDescriptor(
string key,
string contentType,
Topic parent,
@@ -34,13 +35,13 @@ public NestedTopicListAttribute(
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/Metadata/AttributeTypes/RelationshipAttribute.cs b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs
similarity index 79%
rename from OnTopic/Metadata/AttributeTypes/RelationshipAttribute.cs
rename to OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs
index da9551ce..92db8132 100644
--- a/OnTopic/Metadata/AttributeTypes/RelationshipAttribute.cs
+++ b/OnTopic.TestDoubles/Metadata/RelationshipAttributeDescriptor.cs
@@ -3,11 +3,12 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using OnTopic.Metadata;
-namespace OnTopic.Metadata.AttributeTypes {
+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
@@ -17,13 +18,13 @@ 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 RelationshipAttributeDescriptor : AttributeDescriptor {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public RelationshipAttribute(
+ public RelationshipAttributeDescriptor(
string key,
string contentType,
Topic parent,
@@ -34,13 +35,13 @@ public RelationshipAttribute(
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/Metadata/AttributeTypes/TextAttribute.cs b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs
similarity index 88%
rename from OnTopic/Metadata/AttributeTypes/TextAttribute.cs
rename to OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs
index 7f07c508..8411f658 100644
--- a/OnTopic/Metadata/AttributeTypes/TextAttribute.cs
+++ b/OnTopic.TestDoubles/Metadata/TextAttributeDescriptor.cs
@@ -3,11 +3,12 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using OnTopic.Metadata;
-namespace OnTopic.Metadata.AttributeTypes {
+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
@@ -17,13 +18,13 @@ 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 TextAttributeDescriptor : AttributeDescriptor {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public TextAttribute(
+ public TextAttributeDescriptor(
string key,
string contentType,
Topic parent,
diff --git a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs
similarity index 79%
rename from OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs
rename to OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs
index a073a3eb..088d7dbb 100644
--- a/OnTopic/Metadata/AttributeTypes/TopicReferenceAttribute.cs
+++ b/OnTopic.TestDoubles/Metadata/TopicReferenceAttributeDescriptor.cs
@@ -3,11 +3,12 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using OnTopic.Metadata;
-namespace OnTopic.Metadata.AttributeTypes {
+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
@@ -17,13 +18,13 @@ 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 TopicReferenceAttributeDescriptor : AttributeDescriptor {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public TopicReferenceAttribute(
+ public TopicReferenceAttributeDescriptor(
string key,
string contentType,
Topic parent,
@@ -34,13 +35,13 @@ public TopicReferenceAttribute(
parent,
id
) {
- }
- /*==========================================================================================================================
- | PROPERTY: MODEL TYPE
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public override ModelType ModelType => ModelType.Reference;
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Initialize values
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ ModelType = ModelType.Reference;
+
+ }
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj
index 973f048c..17d68fa1 100644
--- a/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj
+++ b/OnTopic.TestDoubles/OnTopic.TestDoubles.csproj
@@ -1,29 +1,22 @@
- netstandard2.0
- 9.0
- enable
+ netstandard2.1
OnTopic Test Doubles
- Ignia
- OnTopic
Test doubles, such as dummies and stubs, useful in setting up unit and integration tests for OnTopic.
- ©2020 Ignia, LLC
bin\$(Configuration)\
- Ignia
-
-
-
- https://github.com/Ignia/Topics-Library
C# .NET AspDotNet Unit-Tests CMS Test-Doubles
- true
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/OnTopic.TestDoubles/Properties/AssemblyInfo.cs b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..4bb6a499
--- /dev/null
+++ b/OnTopic.TestDoubles/Properties/AssemblyInfo.cs
@@ -0,0 +1,24 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Diagnostics.CodeAnalysis;
+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")]
+
+/*==============================================================================================================================
+| 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.TestDoubles")]
\ No newline at end of file
diff --git a/OnTopic.TestDoubles/README.md b/OnTopic.TestDoubles/README.md
index 8be2351a..4446a0de 100644
--- a/OnTopic.TestDoubles/README.md
+++ b/OnTopic.TestDoubles/README.md
@@ -1,8 +1,11 @@
# `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.
[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=3a741b7a-7fa1-4bdb-bc55-efbac3f04e6c&preferRelease=true)
[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
### Contents
- [Installation](#installation)
@@ -16,7 +19,7 @@ Installation can be performed by providing a ` to the `OnTo
…
-
+
```
@@ -34,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
diff --git a/OnTopic.TestDoubles/StubTopicRepository.cs b/OnTopic.TestDoubles/StubTopicRepository.cs
index 072b4cd4..2fd6da1a 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;
@@ -25,7 +26,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
@@ -49,18 +50,18 @@ 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? topicKey = null, bool isRecursive = true) {
+ public override Topic? Load(string? uniqueKey = null, Topic? referenceTopic = 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.InvariantCultureIgnoreCase));
+ if (uniqueKey is not null && uniqueKey.Length > 0) {
+ uniqueKey = uniqueKey.Contains(":", StringComparison.Ordinal) ? uniqueKey : "Root:" + uniqueKey;
+ return _cache.FindFirst(t => t.GetUniqueKey().Equals(uniqueKey, StringComparison.OrdinalIgnoreCase));
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -71,7 +72,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 +86,7 @@ public StubTopicRepository() : base() {
/*------------------------------------------------------------------------------------------------------------------------
| Get topic
\-----------------------------------------------------------------------------------------------------------------------*/
- var topic = Load(topicId);
+ var topic = Load(topicId, referenceTopic, false);
/*------------------------------------------------------------------------------------------------------------------------
| Reset version
@@ -101,69 +102,44 @@ public StubTopicRepository() : base() {
}
+ /*==========================================================================================================================
+ | METHOD: REFRESH
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public override void Refresh(Topic referenceTopic, DateTime since) { }
+
/*==========================================================================================================================
| METHOD: SAVE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override int Save(Topic topic, bool isRecursive = false, bool isDraft = false) {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Call base method - will trigger any events associated with the save
- \-----------------------------------------------------------------------------------------------------------------------*/
- base.Save(topic, isRecursive, isDraft);
+ protected override void SaveTopic([NotNull]Topic topic, DateTime version, bool persistRelationships) {
/*------------------------------------------------------------------------------------------------------------------------
- | 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, isDraft);
- }
- }
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Return identity
- \-----------------------------------------------------------------------------------------------------------------------*/
- return topic.Id;
-
}
/*==========================================================================================================================
| METHOD: MOVE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override void Move(Topic topic, Topic target, Topic? sibling = null) {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Delete from memory
- \-----------------------------------------------------------------------------------------------------------------------*/
- base.Move(topic, target, sibling);
-
- /*------------------------------------------------------------------------------------------------------------------------
- | 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
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public override void Delete(Topic topic, bool isRecursive = true) => base.Delete(topic, isRecursive);
+ protected override void DeleteTopic(Topic topic) { }
/*==========================================================================================================================
| METHOD: GET ATTRIBUTES (PROXY)
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public IEnumerable GetAttributesProxy(
+ ///
+ public IEnumerable GetAttributesProxy(
Topic topic,
bool? isExtendedAttribute,
bool? isDirty = null,
@@ -173,28 +149,28 @@ 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.", false)]
+ ///
+ [Obsolete("Deprecated. Instead, use the new SetContentTypeDescriptorsProxy(), which provides the same function.", true)]
public ContentTypeDescriptorCollection GetContentTypeDescriptorsProxy(ContentTypeDescriptor topicGraph) =>
base.SetContentTypeDescriptors(topicGraph);
/*==========================================================================================================================
| 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);
@@ -218,15 +194,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, "TopicId", "TopicReferenceAttribute", false);
+ addAttribute(contentTypes, "Key", "TextAttributeDescriptor", false, true);
+ addAttribute(contentTypes, "ContentType", "TextAttributeDescriptor", false, true);
+ addAttribute(contentTypes, "Title", "TextAttributeDescriptor", true, true);
+ addAttribute(contentTypes, "BaseTopic", "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);
@@ -235,25 +211,25 @@ 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);
+ pageContentType.Relationships.SetValue("ContentTypes", pageContentType);
+ pageContentType.Relationships.SetValue("ContentTypes", contentTypeDescriptor);
var contactContentType = TopicFactory.Create("Contact", "ContentTypeDescriptor", contentTypes);
@@ -267,16 +243,16 @@ private Topic CreateFakeData() {
AttributeDescriptor addAttribute(
Topic contentType,
string attributeKey,
- string editorType = "TextAttribute",
+ string editorType = "TextAttributeDescriptor",
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);
}
- 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;
@@ -296,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);
@@ -320,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/AttributeValueCollectionTest.cs b/OnTopic.Tests/AttributeCollectionTest.cs
similarity index 60%
rename from OnTopic.Tests/AttributeValueCollectionTest.cs
rename to OnTopic.Tests/AttributeCollectionTest.cs
index 5ed3fcbe..e52a6195 100644
--- a/OnTopic.Tests/AttributeValueCollectionTest.cs
+++ b/OnTopic.Tests/AttributeCollectionTest.cs
@@ -4,33 +4,35 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Collections;
using System.Globalization;
-using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OnTopic.Attributes;
-using OnTopic.Collections;
+using OnTopic.Collections.Specialized;
+using OnTopic.Tests.Entities;
namespace OnTopic.Tests {
/*============================================================================================================================
- | CLASS: ATTRIBUTE VALUE COLLECTION TEST
+ | CLASS: ATTRIBUTE COLLECTION TEST
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides unit tests for the class.
+ /// Provides unit tests for the class.
///
[TestClass]
- public class AttributeValueCollectionTest {
+ public class AttributeCollectionTest {
/*==========================================================================================================================
| 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"));
}
/*==========================================================================================================================
@@ -76,6 +78,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"));
}
@@ -91,6 +94,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"));
}
@@ -125,6 +129,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"));
}
@@ -140,6 +145,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"));
}
@@ -177,6 +183,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"));
}
@@ -196,6 +203,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"));
}
@@ -233,6 +241,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"));
}
@@ -249,6 +258,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"));
}
@@ -283,17 +293,37 @@ 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", 1);
+
+ topic.Attributes.SetValue("Foo", "Bar", false);
+
+ topic.Attributes.Clear();
+
+ Assert.IsTrue(topic.Attributes.IsDirty());
+ Assert.IsTrue(topic.Attributes.DeletedItems.Contains("Foo"));
+
+ }
+
/*==========================================================================================================================
| 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]
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");
@@ -306,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() {
@@ -325,13 +354,13 @@ 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() {
- var topic = TopicFactory.Create("Test", "Container");
+ var topic = TopicFactory.Create("Test", "Container", 1);
topic.Attributes.SetValue("Foo", "Bar");
topic.Attributes.Remove("Foo");
@@ -344,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() {
@@ -363,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() {
@@ -380,18 +409,40 @@ 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", 1);
+ 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
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Populates the with a and then deletes it. Confirms
- /// that returns false after calling .
+ /// Populates the with a and then deletes it. Confirms
+ /// that returns false after calling .
///
[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");
@@ -408,14 +459,14 @@ 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() {
- var topic = TopicFactory.Create("Test", "Container");
+ var topic = TopicFactory.Create("Test", "Container", 1);
topic.Attributes.SetValue("Foo", "Bar");
topic.Attributes.MarkClean("Foo");
@@ -424,6 +475,52 @@ 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: 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
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -432,60 +529,181 @@ 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."
+ 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() {
var topic = TopicFactory.Create("Test", "Container");
- topic.Attributes.SetValue("Key", "# ?");
+ topic.Attributes.SetValue("View", "# ?");
}
/*==========================================================================================================================
- | 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
+ /// 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");
- 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);
}
/*==========================================================================================================================
- | TEST: SET VALUE: INSERT INVALID ATTRIBUTE VALUE: THROWS EXCEPTION
+ | TEST: ADD: NUMERIC VALUE WITH BUSINESS LOGIC: IS RETURNED
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Attempts to violate the business logic by bypassing SetValue() entirely; ensures that business logic is enforced.
+ /// 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 Key property."
+ typeof(ArgumentOutOfRangeException),
+ "The topic allowed a key to be set via a back door, without routing it through the NumericValue property."
)]
- public void Add_InvalidAttributeValue_ThrowsException() {
+ 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 attribute on a topic instance with an invalid value; ensures an exception is thrown.
+ ///
+ [TestMethod]
+ [ExpectedException(
+ 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() {
+
+ var topic = new CustomTopic("Test", "Page");
+
+ topic.Attributes.SetDateTime("DateTimeAttribute", DateTime.MinValue);
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: SET VALUE: INSERT INVALID ATTRIBUTE RECORD: THROWS EXCEPTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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_InvalidAttributeRecord_ThrowsException() {
var topic = TopicFactory.Create("Test", "Container");
- topic.Attributes.Remove("Key");
- topic.Attributes.Add(new("Key", "# ?"));
+ topic.Attributes.Add(new("View", "# ?"));
+ }
+
+ /*==========================================================================================================================
+ | 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.View = "Test";
+ topic.Attributes.TryGetValue("View", out var originalValue);
+
+ var index = topic.Attributes.IndexOf(originalValue);
+
+ topic.Attributes[index] = new AttributeRecord("View", "NewValue", false);
+ topic.Attributes.TryGetValue("View", out var newAttribute);
+
+ topic.Attributes.SetValue("View", "NewerValue", false);
+ topic.Attributes.TryGetValue("View", out var newerAttribute);
+
+ Assert.IsFalse(newAttribute.IsDirty);
+ Assert.IsFalse(newerAttribute.IsDirty);
+
}
/*==========================================================================================================================
- | 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 . 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() {
+ public void SetValue_EmptyAttributeRecord_Skips() {
var topic = TopicFactory.Create("Test", "Container");
@@ -496,15 +714,15 @@ public void SetValue_EmptyAttributeValue_Skips() {
}
/*==========================================================================================================================
- | TEST: SET VALUE: UPDATE EMPTY ATTRIBUTE VALUE: REPLACES
+ | TEST: SET VALUE: UPDATE EMPTY ATTRIBUTE RECORD: 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() {
+ public void SetValue_EmptyAttributeRecord_Replaces() {
var topic = TopicFactory.Create("Test", "Container");
@@ -541,19 +759,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;
}
@@ -576,7 +794,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/BasicTopicBindingModel.cs b/OnTopic.Tests/BindingModels/BasicTopicBindingModel.cs
index c50c8f98..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,15 +22,16 @@ public class BasicTopicBindingModel : ITopicBindingModel {
public BasicTopicBindingModel() { }
- public BasicTopicBindingModel(string? key, string? contentType) {
+ public BasicTopicBindingModel(string key, string contentType) {
Key = key;
ContentType = contentType;
}
- public string? Key { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? Key { get; init; }
- [Required]
- public string? ContentType { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? ContentType { get; init; }
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs
index 56857732..2b14faa3 100644
--- a/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs
+++ b/OnTopic.Tests/BindingModels/ContentTypeDescriptorTopicBindingModel.cs
@@ -3,8 +3,8 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using System.Collections.Generic;
-using OnTopic.Models;
+using System.Collections.ObjectModel;
+using OnTopic.ViewModels.BindingModels;
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/InvalidReferenceTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidReferenceTypeTopicBindingModel.cs
index b78b1354..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 {
@@ -14,8 +13,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.
@@ -24,8 +23,7 @@ public class InvalidReferenceTypeTopicBindingModel : BasicTopicBindingModel {
public InvalidReferenceTypeTopicBindingModel(string? key = null) : base(key, "Page") { }
- [AttributeKey("TopicId")]
- public TopicViewModel DerivedTopic { 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 8f8d7eac..357cc130 100644
--- a/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs
+++ b/OnTopic.Tests/BindingModels/InvalidRelationshipBaseTypeTopicBindingModel.cs
@@ -4,9 +4,9 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
using OnTopic.Models;
-using OnTopic.ViewModels;
+using OnTopic.Tests.ViewModels;
namespace OnTopic.Tests.BindingModels {
@@ -14,8 +14,8 @@ 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.
+ /// 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.
///
///
/// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
@@ -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/InvalidRelationshipListTypeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/InvalidRelationshipListTypeTopicBindingModel.cs
index 69892bfb..88af3b68 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 {
@@ -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 d25b69bc..13126ea8 100644
--- a/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs
+++ b/OnTopic.Tests/BindingModels/InvalidRelationshipTypeTopicBindingModel.cs
@@ -4,9 +4,9 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
using OnTopic.Mapping.Annotations;
-using OnTopic.Models;
+using OnTopic.ViewModels.BindingModels;
namespace OnTopic.Tests.BindingModels {
@@ -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,8 +25,8 @@ public class InvalidRelationshipTypeTopicBindingModel : BasicTopicBindingModel {
public InvalidRelationshipTypeTopicBindingModel(string? key = null) : base(key, "ContentTypeDescriptor") { }
- [Relationship(RelationshipType.NestedTopics)]
- public List ContentTypes { get; } = new();
+ [Collection(CollectionType.NestedTopics)]
+ public Collection ContentTypes { get; } = new();
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs
similarity index 62%
rename from OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs
rename to OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs
index 706316eb..6c194123 100644
--- a/OnTopic.Tests/BindingModels/InvalidReferenceNameTopicBindingModel.cs
+++ b/OnTopic.Tests/BindingModels/RecordTopicBindingModel.cs
@@ -3,26 +3,31 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using System;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
using OnTopic.Models;
namespace OnTopic.Tests.BindingModels {
/*============================================================================================================================
- | BINDING MODEL: REFERENCE NAME TOPIC (INVALID)
+ | BINDING MODEL: RECORD
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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.
+ /// 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 InvalidReferenceNameTopicBindingModel : BasicTopicBindingModel {
+ public class RecordTopicBindingModel : ITopicBindingModel {
- public InvalidReferenceNameTopicBindingModel(string? key = null) : base(key, "Page") { }
+ public RecordTopicBindingModel() { }
- public RelatedTopicBindingModel TopicReference { get; } = new();
+ [Required, NotNull, DisallowNull]
+ public string? Key { get; init; }
+
+ [Required, NotNull, DisallowNull]
+ public string? ContentType { get; init; }
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs
index 4dede0f8..26a770c5 100644
--- a/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs
+++ b/OnTopic.Tests/BindingModels/ReferenceTopicBindingModel.cs
@@ -3,8 +3,7 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using OnTopic.Mapping.Annotations;
-using OnTopic.Models;
+using OnTopic.ViewModels.BindingModels;
namespace OnTopic.Tests.BindingModels {
@@ -20,10 +19,9 @@ namespace OnTopic.Tests.BindingModels {
///
public class ReferenceTopicBindingModel : BasicTopicBindingModel {
- public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttribute") { }
+ public ReferenceTopicBindingModel(string key) : base(key, "TopicReferenceAttributeDescriptor") { }
- [AttributeKey("TopicId")]
- public RelatedTopicBindingModel? DerivedTopic { get; set; }
+ public AssociatedTopicBindingModel? BaseTopic { get; set; }
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs b/OnTopic.Tests/BindingModels/TextAttributeTopicBindingModel.cs
index 0bd3db84..d1b87d78 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 {
@@ -13,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/ContractTest.cs b/OnTopic.Tests/ContractTest.cs
index 726b322d..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");
@@ -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);
@@ -102,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");
diff --git a/OnTopic.Tests/Entities/CustomTopic.cs b/OnTopic.Tests/Entities/CustomTopic.cs
new file mode 100644
index 00000000..8141faea
--- /dev/null
+++ b/OnTopic.Tests/Entities/CustomTopic.cs
@@ -0,0 +1,109 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Globalization;
+using OnTopic.Attributes;
+using OnTopic.Internal.Diagnostics;
+using OnTopic.Associations;
+
+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");
+ 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");
+ 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");
+ set {
+ Contract.Requires(
+ value.Year > 2000,
+ $"{nameof(DateTimeAttribute)} expects a date after 2000."
+ );
+ SetAttributeValue("DateTimeAttribute", value.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+
+ /*==========================================================================================================================
+ | TOPIC REFERENCE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a topic reference property which is intended to be mapped to a topic reference.
+ ///
+ [ReferenceSetter]
+ public Topic? 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.SetValue("TopicReference", value);
+ }
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs
new file mode 100644
index 00000000..f90d45cc
--- /dev/null
+++ b/OnTopic.Tests/HierarchicalTopicMappingServiceTest.cs
@@ -0,0 +1,155 @@
+/*==============================================================================================================================
+| 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
+ )
+ );
+
+ }
+
+ /*==========================================================================================================================
+ | 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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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
diff --git a/OnTopic.Tests/ITopicRepositoryTest.cs b/OnTopic.Tests/ITopicRepositoryTest.cs
index 4da294b1..27224957 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;
@@ -17,11 +17,11 @@ 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
- /// underlying functions are also operating correctly.
+ /// underlying functions are also operating correctly.
///
[TestClass]
public class ITopicRepositoryTest {
@@ -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);
@@ -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.TopicDeleted += eventHandler;
+ _topicRepository.Delete(topic);
+
+ Assert.IsTrue(hasFired);
+
+ void eventHandler(object? sender, TopicEventArgs eventArgs) => hasFired = true;
+
+ }
+
+
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/ITypeLookupServiceTest.cs b/OnTopic.Tests/ITypeLookupServiceTest.cs
new file mode 100644
index 00000000..3b816b9d
--- /dev/null
+++ b/OnTopic.Tests/ITypeLookupServiceTest.cs
@@ -0,0 +1,81 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OnTopic.Lookup;
+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 Composite_LookupValidType_ReturnsType() {
+
+ 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(null, compositeLookup.Lookup(nameof(Topic)));
+
+ }
+
+ /*==========================================================================================================================
+ | 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", "FallbackViewModel");
+
+ Assert.AreEqual(typeof(FallbackViewModel), topicViewModel);
+
+ }
+
+ /*==========================================================================================================================
+ | 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", "FallbackViewModel");
+
+ Assert.AreEqual(typeof(FallbackViewModel), topicViewModel);
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
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/TopicCollectionTest.cs b/OnTopic.Tests/KeyedTopicCollectionTest.cs
similarity index 85%
rename from OnTopic.Tests/TopicCollectionTest.cs
rename to OnTopic.Tests/KeyedTopicCollectionTest.cs
index 9c7182d5..3dd8db39 100644
--- a/OnTopic.Tests/TopicCollectionTest.cs
+++ b/OnTopic.Tests/KeyedTopicCollectionTest.cs
@@ -11,13 +11,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
@@ -28,7 +28,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"));
@@ -42,7 +42,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() {
@@ -53,22 +53,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.Tests/NamedTopicCollection.cs b/OnTopic.Tests/NamedTopicCollection.cs
deleted file mode 100644
index 2f70eccb..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.Collections;
-
-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.IsDirty = false;
-
- 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.IsDirty = false;
- 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.IsDirty = false;
- 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/OnTopic.Tests.csproj b/OnTopic.Tests/OnTopic.Tests.csproj
index 1b966a42..0cad5ed3 100644
--- a/OnTopic.Tests/OnTopic.Tests.csproj
+++ b/OnTopic.Tests/OnTopic.Tests.csproj
@@ -1,50 +1,27 @@

- netcoreapp3.1
+ net5.0
false
CS1591,1701,1702,CA1707,CA1062,CS8602,CS8604;CA1303;IDE0059
- 9.0
- enable
-
-
-
- Ignia OnTopic Unit Tests
- Ignia
- Ignia OnTopic Library
- Provides unit tests for the OnTopic library.
- ©2020 Ignia, LLC
- bin\$(Configuration)\
-
-
-
- full
- bin\$(Configuration)\OnTopic.Tests.XML
- latest
-
-
- pdbonly
-
+
all
- runtime; build; native; contentfiles; analyzers
+ runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
+
-
-
-
-
\ No newline at end of file
diff --git a/OnTopic.Tests/Properties/AssemblyInfo.cs b/OnTopic.Tests/Properties/AssemblyInfo.cs
index cd19192f..475bbcfc 100644
--- a/OnTopic.Tests/Properties/AssemblyInfo.cs
+++ b/OnTopic.Tests/Properties/AssemblyInfo.cs
@@ -13,4 +13,4 @@
\-----------------------------------------------------------------------------------------------------------------------------*/
[assembly: ComVisible(false)]
[assembly: CLSCompliant(true)]
-[assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")]
+[assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")]
\ No newline at end of file
diff --git a/OnTopic.Tests/RelatedTopicCollectionTest.cs b/OnTopic.Tests/RelatedTopicCollectionTest.cs
deleted file mode 100644
index 08396cf3..00000000
--- a/OnTopic.Tests/RelatedTopicCollectionTest.cs
+++ /dev/null
@@ -1,183 +0,0 @@
-/*==============================================================================================================================
-| Author Ignia, LLC
-| Client Ignia, LLC
-| Project Topics Library
-\=============================================================================================================================*/
-using System;
-using System.Linq;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using OnTopic.Collections;
-
-namespace OnTopic.Tests {
-
- /*============================================================================================================================
- | CLASS: RELATED TOPIC COLLECTION TEST
- \---------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Provides unit tests for the class.
- ///
- [TestClass]
- public class RelatedTopicCollectionTest {
-
- /*==========================================================================================================================
- | TEST: SET TOPIC: CREATES RELATIONSHIP
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets a relationship and confirms that it is accessible.
- ///
- [TestMethod]
- public void SetTopic_CreatesRelationship() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var related = TopicFactory.Create("Related", "Page");
-
- parent.Relationships.SetTopic("Friends", related);
-
- Assert.ReferenceEquals(parent.Relationships.GetTopics("Friends").First(), related);
-
- }
-
- /*==========================================================================================================================
- | 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
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets a relationship and then removes it by key, and confirms that it is removed.
- ///
- [TestMethod]
- public void RemoveTopic_RemovesRelationship() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var related = TopicFactory.Create("Related", "Page");
-
- parent.Relationships.SetTopic("Friends", related);
- parent.Relationships.RemoveTopic("Friends", related.Key);
-
- Assert.IsNull(parent.Relationships.GetTopics("Friends").FirstOrDefault());
-
- }
-
- /*==========================================================================================================================
- | TEST: REMOVE TOPIC: REMOVES INCOMING RELATIONSHIP
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets a relationship and then removes it by key, and confirms that it is removed from the incoming relationships
- /// property.
- ///
- [TestMethod]
- public void RemoveTopic_RemovesIncomingRelationship() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var related = TopicFactory.Create("Related", "Page");
- var relationships = new RelatedTopicCollection(parent);
-
- relationships.SetTopic("Friends", related);
- relationships.RemoveTopic("Friends", related.Key);
-
- Assert.IsNull(related.IncomingRelationships.GetTopics("Friends").FirstOrDefault());
-
- }
-
- /*==========================================================================================================================
- | TEST: REMOVE TOPIC: REMOVES INCOMING RELATIONSHIP
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets a relationship and confirms that it is accessible on incoming relationships property.
- ///
- [TestMethod]
- public void SetTopic_CreatesIncomingRelationship() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var related = TopicFactory.Create("Related", "Page");
- var relationships = new RelatedTopicCollection(parent);
-
- relationships.SetTopic("Friends", related);
-
- Assert.ReferenceEquals(related.IncomingRelationships.GetTopics("Friends").First(), parent);
-
- }
-
- /*==========================================================================================================================
- | TEST: SET TOPIC: UPDATES KEY COUNT
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets relationships in multiple namespaces, and the correct number of keys are returned.
- ///
- [TestMethod]
- public void SetTopic_UpdatesKeyCount() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var relationships = new RelatedTopicCollection(parent);
-
- for (var i = 0; i < 5; i++) {
- relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page"));
- }
-
- Assert.AreEqual(5, relationships.Keys.Count);
- Assert.IsTrue(relationships.Keys.Contains("Relationship3"));
-
- }
-
- /*==========================================================================================================================
- | TEST: GET ALL TOPICS: RETURNS ALL TOPICS
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets relationships in multiple namespaces, and ensures they are all returned via GetAllTopics().
- ///
- [TestMethod]
- public void GetAllTopics_ReturnsAllTopics() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var relationships = new RelatedTopicCollection(parent);
-
- for (var i = 0; i < 5; i++) {
- relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "Page"));
- }
-
- Assert.AreEqual(5, relationships.Count);
- Assert.AreEqual("Related3", relationships.GetTopics("Relationship3").First().Key);
- Assert.AreEqual(5, relationships.GetAllTopics().Count);
-
- }
-
- /*==========================================================================================================================
- | TEST: GET ALL CONTENT TYPES: RETURNS ALL CONTENT TYPES
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Sets relationships in multiple namespaces, with different ContentTypes, then filters the results of
- /// by content type.
- ///
- [TestMethod]
- public void GetAllContentTypes_ReturnsAllContentTypes() {
-
- var parent = TopicFactory.Create("Parent", "Page");
- var relationships = new RelatedTopicCollection(parent);
-
- for (var i = 0; i < 5; i++) {
- relationships.SetTopic("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i));
- }
-
- Assert.AreEqual(5, relationships.Count);
- Assert.AreEqual(1, relationships.GetAllTopics("ContentType3").Count);
-
- }
-
- } //Class
-} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs
index 26a73f71..d1a7e1a7 100644
--- a/OnTopic.Tests/ReverseTopicMappingServiceTest.cs
+++ b/OnTopic.Tests/ReverseTopicMappingServiceTest.cs
@@ -9,15 +9,15 @@
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;
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;
@@ -30,7 +30,7 @@ namespace OnTopic.Tests {
/// Provides unit tests for the using local DTOs.
///
[TestClass]
- public class ReverseReverseTopicMappingServiceTest {
+ public class ReverseTopicMappingServiceTest {
/*==========================================================================================================================
| PRIVATE VARIABLES
@@ -41,7 +41,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
@@ -49,7 +49,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());
}
@@ -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);
@@ -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
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -223,7 +248,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(
@@ -262,19 +287,19 @@ 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";
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"));
}
@@ -290,16 +315,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 = (TopicReferenceAttribute?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false);
-
- target.DerivedTopic = _topicRepository.Load(target.Attributes.GetInteger("TopicId", -5));
+ var target = (TopicReferenceAttributeDescriptor?)await mappingService.MapAsync(bindingModel).ConfigureAwait(false);
- Assert.IsNotNull(target.DerivedTopic);
+ Assert.IsNotNull(target.BaseTopic);
+ Assert.AreEqual("Title", target.BaseTopic.Key);
Assert.AreEqual("TopicReference", target.EditorType);
}
@@ -370,7 +394,7 @@ public async Task Map_ExceedsMinimumValue_ThrowsValidationException() {
/// cref="InvalidOperationException"/>.
///
[TestMethod]
- [ExpectedException(typeof(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -388,7 +412,7 @@ public async Task Map_InvalidChildrenProperty_ThrowsInvalidOperationException()
/// cref="InvalidOperationException"/>.
///
[TestMethod]
- [ExpectedException(typeof(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -409,7 +433,7 @@ public async Task Map_InvalidParentProperty_ThrowsInvalidOperationException() {
/// .
///
[TestMethod]
- [ExpectedException(typeof(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidAttribute_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -423,11 +447,11 @@ 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(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidRelationshipBaseType_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -441,13 +465,12 @@ 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(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -466,7 +489,7 @@ public async Task Map_InvalidRelationshipType_ThrowsInvalidOperationException()
/// cref="IList"/>. This is invalid, and expected to throw an .
///
[TestMethod]
- [ExpectedException(typeof(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidRelationshipListType_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
@@ -476,34 +499,16 @@ 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(InvalidOperationException))]
- 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
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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(InvalidOperationException))]
+ [ExpectedException(typeof(MappingModelValidationException))]
public async Task Map_InvalidTopicReferenceType_ThrowsInvalidOperationException() {
var mappingService = new ReverseTopicMappingService(_topicRepository);
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
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
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
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
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
diff --git a/OnTopic.Tests/SqlTopicRepositoryTest.cs b/OnTopic.Tests/SqlTopicRepositoryTest.cs
new file mode 100644
index 00000000..555d91e6
--- /dev/null
+++ b/OnTopic.Tests/SqlTopicRepositoryTest.cs
@@ -0,0 +1,222 @@
+/*==============================================================================================================================
+| 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.Associations;
+using OnTopic.Tests.Schemas;
+
+namespace OnTopic.Tests {
+
+ /*============================================================================================================================
+ | CLASS: SQL TOPIC REPOSITORY TEST
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides unit tests for the class.
+ ///
+ [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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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"));
+
+ }
+
+ /*==========================================================================================================================
+ | 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.GetValues("Test").FirstOrDefault()?.Id);
+
+ }
+
+ /*==========================================================================================================================
+ | 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.GetValue("Test")?.Id);
+ Assert.IsTrue(topic.References.IsDirty());
+
+ }
+
+ /*==========================================================================================================================
+ | 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.GetValue("Test")?.Id);
+ Assert.IsTrue(topic.References.IsFullyLoaded);
+ Assert.IsFalse(topic.References.IsDirty());
+
+ }
+
+ /*==========================================================================================================================
+ | 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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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", topic, 2);
+ var related = TopicFactory.Create("Related", "Container", topic, 3);
+
+ child.Relationships.SetValue("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.GetValues("Test").Count);
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs b/OnTopic.Tests/TestDoubles/FakeViewModelLookupService.cs
index eb6e9414..190a26b5 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;
using OnTopic.ViewModels;
@@ -27,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
@@ -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));
@@ -49,12 +51,13 @@ 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));
Add(typeof(RequiredObjectTopicViewModel));
Add(typeof(RequiredTopicViewModel));
- Add(typeof(TopicReferenceAttributeTopicViewModel));
+ Add(typeof(TopicReferenceAttributeDescriptorTopicViewModel));
Add(typeof(TopicReferenceTopicViewModel));
/*------------------------------------------------------------------------------------------------------------------------
@@ -63,7 +66,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 ed449504..15c46178 100644
--- a/OnTopic.Tests/TopicMappingServiceTest.cs
+++ b/OnTopic.Tests/TopicMappingServiceTest.cs
@@ -11,12 +11,14 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OnTopic.Attributes;
using OnTopic.Data.Caching;
+using OnTopic.Lookup;
using OnTopic.Mapping;
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;
@@ -36,6 +38,7 @@ public class TopicMappingServiceTest {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
+ readonly ITypeLookupService _typeLookupService;
readonly ITopicRepository _topicRepository;
readonly ITopicMappingService _mappingService;
@@ -52,8 +55,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);
+
}
/*==========================================================================================================================
@@ -69,13 +85,29 @@ 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);
+
+ }
+
+ /*==========================================================================================================================
+ | 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);
}
@@ -93,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);
}
@@ -266,6 +296,30 @@ public async Task Map_AlternateAttributeKey_ReturnsMappedModel() {
}
+ /*==========================================================================================================================
+ | 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
+ /// associations.
+ ///
+ [TestMethod]
+ public void MappedTopicCacheEntry_GetMissingAssociations_ReturnsDifference() {
+
+ var cacheEntry = new MappedTopicCacheEntry() {
+ Associations = AssociationTypes.Children | AssociationTypes.Parents
+ };
+ var associations = AssociationTypes.Children | AssociationTypes.References;
+
+ var difference = cacheEntry.GetMissingAssociations(associations);
+
+ Assert.IsTrue(difference.HasFlag(AssociationTypes.References));
+ Assert.IsFalse(difference.HasFlag(AssociationTypes.Children));
+ Assert.IsFalse(difference.HasFlag(AssociationTypes.Parents));
+
+ }
+
/*==========================================================================================================================
| TEST: MAP: RELATIONSHIPS: RETURNS MAPPED MODEL
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -280,9 +334,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);
@@ -293,20 +347,46 @@ 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.SetValue("Cousins", relatedTopic1);
+ topic.Relationships.SetValue("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
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// 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
- /// 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() {
@@ -318,12 +398,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);
@@ -407,15 +487,42 @@ 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"
));
}
+ /*==========================================================================================================================
+ | 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
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -442,6 +549,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
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -451,12 +582,12 @@ 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");
- topic.Attributes.SetInteger("TopicReferenceId", topicReference.Id);
+ topic.References.SetValue("TopicReference", topicReference);
var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false);
@@ -465,6 +596,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.SetValue("TopicReference", topicReference);
+
+ var target = (TopicReferenceTopicViewModel?)await mappingService.MapAsync(topic).ConfigureAwait(false);
+
+ Assert.IsNull(target.TopicReference);
+
+ }
+
/*==========================================================================================================================
| TEST: MAP: RECURSIVE RELATIONSHIPS: RETURNS GRAPH
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -493,12 +648,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);
@@ -559,9 +714,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"));
@@ -574,8 +729,8 @@ 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? getRelatedTopic(RelatedEntityTopicViewModel topic, string key)
+ => topic.RelatedTopics.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.Ordinal));
}
@@ -589,7 +744,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);
@@ -609,7 +764,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);
@@ -618,14 +773,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);
@@ -674,7 +829,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));
@@ -793,7 +948,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);
@@ -801,6 +962,48 @@ 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
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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(2, target.Children.Count);
+
+ }
+
/*==========================================================================================================================
| TEST: MAP: FLATTEN ATTRIBUTE: RETURNS FLAT COLLECTION
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -853,11 +1056,11 @@ 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));
+ public static KeyOnlyTopicViewModel? GetChildTopic(IEnumerable topicCollection, string key)
+ => 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));
+ public static TopicViewModel? GetChildTopic(IEnumerable topicCollection, string key)
+ => topicCollection.FirstOrDefault((t) => t.Key.StartsWith(key, StringComparison.Ordinal));
} //Class
} //Namespace
\ No newline at end of file
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");
diff --git a/OnTopic.Tests/TopicReferenceCollectionTest.cs b/OnTopic.Tests/TopicReferenceCollectionTest.cs
new file mode 100644
index 00000000..b80250ee
--- /dev/null
+++ b/OnTopic.Tests/TopicReferenceCollectionTest.cs
@@ -0,0 +1,411 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OnTopic.Associations;
+using OnTopic.Tests.Entities;
+using OnTopic.Collections.Specialized;
+using System.Collections.ObjectModel;
+
+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.
+ ///
+ [TestClass]
+ public class TopicReferenceCollectionTest {
+
+ /*==========================================================================================================================
+ | 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.SetValue("Reference", reference);
+
+ Assert.AreEqual(1, topic.References.Count);
+ Assert.IsTrue(topic.References.IsDirty());
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: SET VALUE: NEW REFERENCE: NOT DIRTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Assembles a new , adds a new reference using , and confirms that
+ /// is not set.
+ ///
+ [TestMethod]
+ public void SetValue_NewReference_NotDirty() {
+
+ var topic = TopicFactory.Create("Topic", "Page", 1);
+ var reference = TopicFactory.Create("Reference", "Page", 2);
+
+ topic.References.SetValue("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", 1);
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.References.SetValue("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", 1);
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.References.SetValue("Reference", reference, false);
+ topic.References.Clear();
+
+ Assert.AreEqual(0, topic.References.Count);
+ Assert.IsTrue(topic.References.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
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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.SetValue("Reference", reference);
+
+ Assert.AreEqual(1, reference.IncomingRelationships.GetValues("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.SetValue("Reference", reference);
+ topic.References.Remove("Reference");
+
+ Assert.AreEqual(0, reference.IncomingRelationships.GetValues("Reference").Count);
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: SET VALUE: 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 SetValue_ExistingReference_TopicUpdated() {
+
+ var topic = TopicFactory.Create("Topic", "Page");
+ var reference = TopicFactory.Create("Reference", "Page");
+ var newReference = TopicFactory.Create("NewReference", "Page");
+
+ topic.References.SetValue("Reference", reference);
+ topic.References.SetValue("Reference", newReference);
+
+ Assert.AreEqual(newReference, topic.References.GetValue("Reference"));
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: SET VALUE: 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 SetValue_NullReference_TopicRemoved() {
+
+ var topic = TopicFactory.Create("Topic", "Page");
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.References.SetValue("Reference", reference);
+ topic.References.SetValue("Reference", null);
+
+ Assert.AreEqual(1, topic.References.Count);
+ Assert.IsNull(topic.References.GetValue("Reference"));
+
+ }
+
+ /*==========================================================================================================================
+ | 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.SetValue("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.SetValue("Reference", reference);
+
+ Assert.AreEqual(reference, topic.References.GetValue("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.SetValue("Reference", reference);
+
+ Assert.IsNull(topic.References.GetValue("MissingReference"));
+
+ }
+
+ /*==========================================================================================================================
+ | 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.
+ ///
+ [TestMethod]
+ public void GetTopic_InheritedReference_ReturnsTopic() {
+
+ var topic = TopicFactory.Create("Topic", "Page");
+ var baseTopic = TopicFactory.Create("Base", "Page");
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.BaseTopic = baseTopic;
+ baseTopic.References.SetValue("Reference", reference);
+
+ Assert.AreEqual(reference, topic.References.GetValue("Reference"));
+
+ }
+
+ /*==========================================================================================================================
+ | 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.
+ ///
+ [TestMethod]
+ public void GetTopic_InheritedReference_ReturnsNull() {
+
+ var topic = TopicFactory.Create("Topic", "Page");
+ var baseTopic = TopicFactory.Create("Base", "Page");
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.BaseTopic = baseTopic;
+ baseTopic.References.SetValue("Reference", reference);
+
+ Assert.IsNull(topic.References.GetValue("MissingReference"));
+
+ }
+
+ /*==========================================================================================================================
+ | 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 .
+ ///
+ [TestMethod]
+ public void GetTopic_InheritedReferenceWithoutInheritance_ReturnsNull() {
+
+ var topic = TopicFactory.Create("Topic", "Page");
+ var baseTopic = TopicFactory.Create("Base", "Page");
+ var reference = TopicFactory.Create("Reference", "Page");
+
+ topic.BaseTopic = baseTopic;
+ baseTopic.References.SetValue("Reference", reference);
+
+ Assert.IsNull(topic.References.GetValue("Reference", null, false, false));
+
+ }
+
+ /*==========================================================================================================================
+ | 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.SetValue("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(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() {
+
+ var topic = new CustomTopic("Test", "Page");
+ var reference = TopicFactory.Create("Reference", "Container");
+
+ topic.References.SetValue("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(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() {
+
+ var topic = new CustomTopic("Test", "Page");
+ var reference = TopicFactory.Create("Reference", "Container");
+
+ topic.References.Add(new("TopicReference", reference));
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/TopicRelationshipMultiMapTest.cs b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs
new file mode 100644
index 00000000..24e33448
--- /dev/null
+++ b/OnTopic.Tests/TopicRelationshipMultiMapTest.cs
@@ -0,0 +1,385 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OnTopic.Collections.Specialized;
+using OnTopic.Associations;
+
+namespace OnTopic.Tests {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC RELATIONSHIP MULTI-MAP TEST
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides unit tests for the class.
+ ///
+ [TestClass]
+ public class TopicRelationshipMultiMapTest {
+
+ /*==========================================================================================================================
+ | TEST: SET TOPIC: CREATES RELATIONSHIP
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets a relationship and confirms that it is accessible.
+ ///
+ [TestMethod]
+ public void SetTopic_CreatesRelationship() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var related = TopicFactory.Create("Related", "Page");
+
+ parent.Relationships.SetValue("Friends", related);
+
+ Assert.ReferenceEquals(parent.Relationships.GetValues("Friends").First(), related);
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: REMOVE TOPIC: REMOVES RELATIONSHIP
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets a relationship and then removes it by key, and confirms that it is removed.
+ ///
+ [TestMethod]
+ public void RemoveTopic_RemovesRelationship() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var related = TopicFactory.Create("Related", "Page");
+
+ parent.Relationships.SetValue("Friends", related);
+ parent.Relationships.Remove("Friends", related);
+
+ Assert.IsNull(parent.Relationships.GetValues("Friends").FirstOrDefault());
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: REMOVE TOPIC: REMOVES INCOMING RELATIONSHIP
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets a relationship and then removes it by key, and confirms that it is removed from the incoming relationships
+ /// property.
+ ///
+ [TestMethod]
+ public void RemoveTopic_RemovesIncomingRelationship() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var related = TopicFactory.Create("Related", "Page");
+ var relationships = new TopicRelationshipMultiMap(parent);
+
+ relationships.SetValue("Friends", related);
+ relationships.Remove("Friends", related);
+
+ Assert.IsNull(related.IncomingRelationships.GetValues("Friends").FirstOrDefault());
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: REMOVE TOPIC: REMOVES INCOMING RELATIONSHIP
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets a relationship and confirms that it is accessible on incoming relationships property.
+ ///
+ [TestMethod]
+ public void SetTopic_CreatesIncomingRelationship() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var related = TopicFactory.Create("Related", "Page");
+ var relationships = new TopicRelationshipMultiMap(parent);
+
+ relationships.SetValue("Friends", related);
+
+ Assert.ReferenceEquals(related.IncomingRelationships.GetValues("Friends").First(), parent);
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: SET TOPIC: UPDATES KEY COUNT
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets relationships in multiple namespaces, and the correct number of keys are returned.
+ ///
+ [TestMethod]
+ public void SetTopic_UpdatesKeyCount() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var relationships = new TopicRelationshipMultiMap(parent);
+
+ for (var i = 0; i < 5; i++) {
+ relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "Page"));
+ }
+
+ Assert.AreEqual(5, relationships.Keys.Count);
+ Assert.IsTrue(relationships.Keys.Contains("Relationship3"));
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: GET ALL VALUES: RETURNS ALL TOPICS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets relationships in multiple namespaces, and ensures they are all returned via .
+ ///
+ [TestMethod]
+ public void GetAllValues_ReturnsAllTopics() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var relationships = new TopicRelationshipMultiMap(parent);
+
+ for (var i = 0; i < 5; i++) {
+ relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "Page"));
+ }
+
+ Assert.AreEqual(5, relationships.Count);
+ Assert.AreEqual("Related3", relationships.GetValues("Relationship3").First().Key);
+ Assert.AreEqual(5, relationships.GetAllValues().Count);
+
+ }
+
+ /*==========================================================================================================================
+ | 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.
+ ///
+ [TestMethod]
+ public void GetAllValues_ContentTypes_ReturnsAllContentTypes() {
+
+ var parent = TopicFactory.Create("Parent", "Page");
+ var relationships = new TopicRelationshipMultiMap(parent);
+
+ for (var i = 0; i < 5; i++) {
+ relationships.SetValue("Relationship" + i, TopicFactory.Create("Related" + i, "ContentType" + i));
+ }
+
+ Assert.AreEqual(5, relationships.Keys.Count);
+ Assert.AreEqual(1, relationships.GetAllValues("ContentType3").Count);
+
+ }
+
+ /*==========================================================================================================================
+ | 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 TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic", "Page");
+
+ relationships.SetValue("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", 1);
+ var relationships = new TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic", "Page", 2);
+
+ relationships.SetValue("Related", related);
+ relationships.MarkClean();
+
+ relationships.SetValue("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 TopicRelationshipMultiMap(topic);
+ var related1 = TopicFactory.Create("Topic", "Page");
+ var related2 = TopicFactory.Create("Topic", "Page");
+
+ relationships.SetValue("Related", related1);
+ relationships.SetValue("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", 1);
+ var relationships = new TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic", "Page");
+
+ relationships.SetValue("Related", related);
+ relationships.MarkClean();
+ relationships.Remove("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 TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic", "Page");
+
+ var isSuccessful = relationships.Remove("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 TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic1", "Page");
+ var missing = TopicFactory.Create("Topic2", "Page");
+
+ relationships.SetValue("Related", related);
+
+ var isSuccessful = relationships.Remove("Related", missing);
+
+ Assert.IsFalse(isSuccessful);
+ 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 topic = TopicFactory.Create("Test", "Page", 1);
+ var relationships = new TopicRelationshipMultiMap(topic);
+ var related = TopicFactory.Create("Topic", "Page");
+
+ relationships.SetValue("Related", related);
+ relationships.MarkClean();
+ relationships.Clear("Related");
+
+ Assert.IsTrue(relationships.IsDirty());
+
+ }
+
+ /*==========================================================================================================================
+ | TEST: CLEAR: NO TOPICS: IS NOT DIRTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Call with no existing s and confirms that
+ /// the value of is set to false .
+ ///
+ [TestMethod]
+ public void Clear_NoTopics_IsNotDirty() {
+
+ var topic = TopicFactory.Create("Test", "Page");
+ var relationships = new TopicRelationshipMultiMap(topic);
+
+ relationships.Clear("Related");
+
+ Assert.IsFalse(relationships.IsDirty());
+
+ }
+
+ /*==========================================================================================================================
+ | 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.SetValue("Related", related, false);
+
+ Assert.IsTrue(relationships.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.SetValue("Related", related, false);
+
+ Assert.IsTrue(relationships.IsDirty());
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/TopicRepositoryBaseTest.cs b/OnTopic.Tests/TopicRepositoryBaseTest.cs
index dce1031f..1551abc5 100644
--- a/OnTopic.Tests/TopicRepositoryBaseTest.cs
+++ b/OnTopic.Tests/TopicRepositoryBaseTest.cs
@@ -7,11 +7,13 @@
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OnTopic.Attributes;
+using OnTopic.Collections.Specialized;
using OnTopic.Data.Caching;
using OnTopic.Metadata;
-using OnTopic.Metadata.AttributeTypes;
+using OnTopic.Associations;
using OnTopic.Repositories;
using OnTopic.TestDoubles;
+using OnTopic.TestDoubles.Metadata;
namespace OnTopic.Tests {
@@ -19,10 +21,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 {
@@ -49,21 +51,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);
@@ -83,7 +85,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);
@@ -161,11 +163,11 @@ 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);
- Assert.AreEqual(0, related.IncomingRelationships.GetTopics("Related").Count);
+ Assert.AreEqual(0, related.IncomingRelationships.GetValues("Related").Count);
}
@@ -184,11 +186,11 @@ 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);
- Assert.AreEqual(0, related.Relationships.GetTopics("Related").Count);
+ Assert.AreEqual(0, related.Relationships.GetValues("Related").Count);
}
@@ -208,7 +210,7 @@ public void GetAttributes_AnyAttributes_ReturnsAllAttributes() {
var attributes = _topicRepository.GetAttributesProxy(topic, null);
- Assert.AreEqual(3, attributes.Count());
+ Assert.AreEqual(1, attributes.Count());
}
@@ -217,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]
@@ -250,7 +252,7 @@ public void GetAttributes_IndexedAttributes_ReturnsIndexedAttributes() {
var attributes = _topicRepository.GetAttributesProxy(topic, false);
- Assert.AreEqual(2, attributes.Count());
+ Assert.AreEqual(0, attributes.Count());
}
@@ -277,16 +279,16 @@ 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
- /// disagrees with .
+ /// 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]
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);
@@ -299,22 +301,21 @@ 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 to not be returned even though its
+ /// 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() {
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);
- //Expect Key and ContentType, but not Title
- Assert.AreEqual(2, attributes.Count());
+ Assert.AreEqual(0, attributes.Count());
}
@@ -323,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() {
@@ -335,8 +336,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());
}
@@ -345,8 +345,8 @@ 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 .
+ /// ensures that it is returned as an an indexed when calling .
///
[TestMethod]
public void GetAttributes_ArbitraryAttributeWithShortValue_ReturnsAsIndexedAttributes() {
@@ -367,7 +367,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() {
@@ -386,13 +386,13 @@ 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]
public void GetUnmatchedAttributes_ReturnsAttributes() {
- var topic = TopicFactory.Create("Test", "ContentTypeDescriptor", 1);
+ var topic = TopicFactory.Create("Test", "Page", 1);
topic.Attributes.SetValue("Title", "Title");
@@ -407,7 +407,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.
@@ -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"));
}
@@ -508,9 +508,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() {
@@ -528,33 +528,118 @@ 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]
public void Save_ContentTypeDescriptor_UpdatesPermittedContentTypes() {
var contentTypes = _topicRepository.GetContentTypeDescriptors();
- 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.SetTopic("ContentTypes", lookupContentType);
+ pageContentType.Relationships.SetValue("ContentTypes", lookupContentType);
- _topicRepository.Save(pageContentType);
+ _topicRepository.Save(contentTypesRoot, true);
Assert.AreNotEqual(initialCount, pageContentType.PermittedContentTypes.Count);
}
+ /*==========================================================================================================================
+ | 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: 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: 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.SetValue("Test", reference);
+
+ _topicRepository.Save(topic, true);
+
+ }
+
+ /*==========================================================================================================================
+ | 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.SetValue("Test", reference);
+
+ _topicRepository.Save(topic, true);
+
+ }
+
/*==========================================================================================================================
| 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() {
@@ -572,9 +657,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]
@@ -607,7 +692,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);
@@ -628,7 +713,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;
@@ -638,5 +723,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.TopicDeleted += eventHandler;
+ _topicRepository.Delete(topic);
+
+ Assert.IsTrue(hasFired);
+
+ void eventHandler(object? sender, TopicEventArgs eventArgs) => hasFired = true;
+
+ }
+
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/TopicTest.cs b/OnTopic.Tests/TopicTest.cs
index 06c114c8..3e613324 100644
--- a/OnTopic.Tests/TopicTest.cs
+++ b/OnTopic.Tests/TopicTest.cs
@@ -32,15 +32,15 @@ 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");
}
/*==========================================================================================================================
| 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() {
@@ -56,15 +56,12 @@ 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);
topic.Id = 124;
- Assert.AreEqual(123, topic.Id);
- Assert.AreNotEqual(124, topic.Id);
-
}
/*==========================================================================================================================
@@ -123,10 +120,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 +144,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);
}
@@ -264,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() {
@@ -280,69 +271,142 @@ 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.Attributes.GetInteger("TopicID", 0));
+ Assert.ReferenceEquals(topic.BaseTopic, finalBaseTopic);
+ Assert.AreEqual(2, topic.References.GetValue("BaseTopic").Id);
}
/*==========================================================================================================================
- | TEST: DERIVED TOPIC: UNSAVED VALUE: RETURNS EXPECTED VALUE
+ | TEST: BASE TOPIC: RESAVED 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 .
+ /// 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 DerivedTopic_UnsavedValue_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;
+ topic.BaseTopic = baseTopic;
+ baseTopic.Id = 5;
+ topic.BaseTopic = baseTopic;
- Assert.ReferenceEquals(topic.DerivedTopic, derivedTopic);
- Assert.AreEqual(-2, topic.Attributes.GetInteger("TopicID", -2));
+ Assert.ReferenceEquals(topic.BaseTopic, baseTopic);
+ Assert.AreEqual(5, topic.References.GetValue("BaseTopic").Id);
}
/*==========================================================================================================================
- | TEST: DERIVED TOPIC: RESAVED VALUE: RETURNS EXPECTED VALUE
+ | IS DIRTY: NEW TOPIC: RETURNS TRUE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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 .
+ /// Creates a new topic, and confirms that returns true .
///
[TestMethod]
- public void DerivedTopic_ResavedValue_ReturnsExpectedValue() {
+ public void IsDirty_NewTopic_ReturnsTrue() {
var topic = TopicFactory.Create("Topic", "Page");
- var derivedTopic = TopicFactory.Create("DerivedTopic", "Page");
- topic.DerivedTopic = derivedTopic;
- derivedTopic.Id = 5;
- topic.DerivedTopic = derivedTopic;
+ 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());
+
+ }
+
+ /*==========================================================================================================================
+ | 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.SetValue("Related", related);
+ topic.Relationships.SetValue("Related", related);
+
+ Assert.IsTrue(topic.IsDirty(true));
+
+ }
+
+ /*==========================================================================================================================
+ | 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", 1);
+ var related = TopicFactory.Create("Related", "Page", 2);
+
+ topic.Attributes.SetValue("Related", related.Key);
+ topic.References.SetValue("Related", related);
+ topic.Relationships.SetValue("Related", related);
+
+ topic.MarkClean(true);
- Assert.ReferenceEquals(topic.DerivedTopic, derivedTopic);
- Assert.AreEqual(5, topic.Attributes.GetInteger("TopicID", -2));
+ Assert.IsFalse(topic.IsDirty(true));
}
diff --git a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs b/OnTopic.Tests/TypeMemberInfoCollectionTest.cs
index d955204c..78f2b12f 100644
--- a/OnTopic.Tests/TypeMemberInfoCollectionTest.cs
+++ b/OnTopic.Tests/TypeMemberInfoCollectionTest.cs
@@ -17,13 +17,13 @@ namespace OnTopic.Tests {
| CLASS: TYPE COLLECTION TESTS
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides unit tests for the 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 types mismatch.
///
[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.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/AmbiguousRelationTopicViewModel.cs
index 0caa89db..d3fdbb6b 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 {
@@ -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,8 +26,8 @@ namespace OnTopic.Tests.ViewModels {
///
public class AmbiguousRelationTopicViewModel: KeyOnlyTopicViewModel {
- [Relationship("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)]
- public List RelationshipAlias { get; } = new();
+ [Collection("AmbiguousRelationship", Type= CollectionType.IncomingRelationship)]
+ public Collection RelationshipAlias { get; } = new();
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs b/OnTopic.Tests/ViewModels/AscendentTopicViewModel.cs
index a50dac93..f215a9a6 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)]
+ [Include(AssociationTypes.Parents)]
public AscendentTopicViewModel? Parent { get; set; }
} //Class
diff --git a/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs b/OnTopic.Tests/ViewModels/CircularTopicViewModel.cs
index 305f0fe1..ad767a93 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 {
@@ -19,11 +19,11 @@ namespace OnTopic.Tests.ViewModels {
///
public class CircularTopicViewModel {
- [Follow(Relationships.Parents)]
+ [Include(AssociationTypes.Parents)]
public CircularTopicViewModel? Parent { get; set; }
- [Follow(Relationships.Children | Relationships.Parents)]
- public List Children { get; } = new();
+ [Include(AssociationTypes.Children | AssociationTypes.Parents)]
+ 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..d301008f 100644
--- a/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs
+++ b/OnTopic.Tests/ViewModels/CompatiblePropertyTopicViewModel.cs
@@ -4,10 +4,9 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
-using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using OnTopic.Metadata;
-using OnTopic.ViewModels;
namespace OnTopic.Tests.ViewModels {
@@ -21,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 List? VersionHistory { get; set; }
+ public Collection? VersionHistory { 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..0b29920d 100644
--- a/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs
+++ b/OnTopic.Tests/ViewModels/DescendentTopicViewModel.cs
@@ -12,7 +12,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.
///
///
///
@@ -23,9 +23,9 @@ 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)]
+ [Include(AssociationTypes.Children)]
public TopicViewModelCollection Children { get; } = new();
} //Class
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
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
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.Tests/ViewModels/FilteredInvalidTopicViewModel.cs b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.cs
new file mode 100644
index 00000000..9a2d2049
--- /dev/null
+++ b/OnTopic.Tests/ViewModels/FilteredInvalidTopicViewModel.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 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
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
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..9e6b9686 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();
+ [Collection(CollectionType.MappedCollection)]
+ [Include(AssociationTypes.None)]
+ public Collection PermittedContentTypes { get; } = new();
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs b/OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs
similarity index 76%
rename from OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs
rename to OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs
index 002b7319..87c6542d 100644
--- a/OnTopic.Tests/ViewModels/Metadata/TextAttributeTopicViewModel.cs
+++ b/OnTopic.Tests/ViewModels/Metadata/TextAttributeDescriptorTopicViewModel.cs
@@ -3,21 +3,22 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using OnTopic.Metadata.AttributeTypes;
+using OnTopic.Metadata;
+using OnTopic.TestDoubles.Metadata;
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.
+ /// 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.
///
///
/// 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 75%
rename from OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs
rename to OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeDescriptorTopicViewModel.cs
index 28c722eb..c058b7b9 100644
--- a/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeTopicViewModel.cs
+++ b/OnTopic.Tests/ViewModels/Metadata/TopicReferenceAttributeDescriptorTopicViewModel.cs
@@ -3,21 +3,22 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-using OnTopic.Metadata.AttributeTypes;
+using OnTopic.Metadata;
+using OnTopic.TestDoubles.Metadata;
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.
+ /// 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.
///
///
/// 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
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.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/RecordTopicViewModel.cs b/OnTopic.Tests/ViewModels/RecordTopicViewModel.cs
new file mode 100644
index 00000000..50285f5e
--- /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; init; }
+
+ } //Class
+} //Namespace
\ No newline at end of file
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
diff --git a/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs b/OnTopic.Tests/ViewModels/RelationTopicViewModel.cs
index 3fe4224d..88c00a9f 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 {
@@ -25,8 +25,8 @@ namespace OnTopic.Tests.ViewModels {
///
public class RelationTopicViewModel: KeyOnlyTopicViewModel {
- [Follow(Relationships.Children)]
- public List Cousins { get; } = new();
+ [Include(AssociationTypes.Children)]
+ 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..c71d59ae 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 {
@@ -25,8 +25,8 @@ namespace OnTopic.Tests.ViewModels {
///
public class RelationWithChildrenTopicViewModel: RelationTopicViewModel {
- [Follow(Relationships.Relationships)]
- public List Children { get; } = new();
+ [Include(AssociationTypes.Relationships)]
+ public Collection Children { get; } = new();
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs
new file mode 100644
index 00000000..cdde217d
--- /dev/null
+++ b/OnTopic.ViewModels/BindingModels/AssociatedTopicBindingModel.cs
@@ -0,0 +1,40 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
+using OnTopic.Mapping.Reverse;
+using OnTopic.Models;
+
+namespace OnTopic.ViewModels.BindingModels {
+
+ /*============================================================================================================================
+ | BINDING MODEL: ASSOCIATED TOPIC
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a model for binding an association of a to another .
+ ///
+ ///
+ /// 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 AssociatedTopicBindingModel : IAssociatedTopicBindingModel {
+
+ /*==========================================================================================================================
+ | PROPERTY: UNIQUE KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Gets the topic's attribute, the unique text identifier for the topic.
+ ///
+ ///
+ /// value is not null
+ ///
+ [Required, NotNull, DisallowNull]
+ public string? UniqueKey { get; init; }
+
+ } //Class
+} //Namespaces
\ No newline at end of file
diff --git a/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs b/OnTopic.ViewModels/BindingModels/RelatedTopicBindingModel.cs
new file mode 100644
index 00000000..7cd56b3c
--- /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 {
+
+ /*============================================================================================================================
+ | BINDING MODEL: RELATED TOPIC
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a model for binding a relationship of a 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.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/NavigationTopicViewModel.cs b/OnTopic.ViewModels/NavigationTopicViewModel.cs
index 918fc0e1..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 {
@@ -13,22 +15,29 @@ 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.
///
///
///
/// 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 .
///
///
- public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicViewModel {
+ public sealed record NavigationTopicViewModel : INavigationTopicViewModel {
+
+ /*==========================================================================================================================
+ | TITLE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ [Required, NotNull, DisallowNull]
+ public string? Title { get; init; }
/*==========================================================================================================================
| SHORT TITLE
@@ -36,7 +45,14 @@ 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; }
+
+ /*==========================================================================================================================
+ | WEB PATH
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ [Required, NotNull, DisallowNull]
+ public string? WebPath { get; init; }
/*==========================================================================================================================
| CHILDREN
@@ -53,8 +69,8 @@ public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicV
/// 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.InvariantCultureIgnoreCase) ?? false;
+ public bool IsSelected(string webPath) =>
+ $"{webPath}/".StartsWith($"{WebPath}", StringComparison.OrdinalIgnoreCase);
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.ViewModels/OnTopic.ViewModels.csproj b/OnTopic.ViewModels/OnTopic.ViewModels.csproj
index 01b79cbb..ce06e2c7 100644
--- a/OnTopic.ViewModels/OnTopic.ViewModels.csproj
+++ b/OnTopic.ViewModels/OnTopic.ViewModels.csproj
@@ -2,57 +2,34 @@
{E52FC633-B4C5-4A2B-8CAF-30E756D7A6A7}
+ netstandard2.1
OnTopic.ViewModels
- netstandard2.0;netstandard2.1
- True
- False
- 9.0
- enable
OnTopic View Models
- Ignia
- OnTopic
Provides view models that map to the factory default content type schemas.
- ©2020 Ignia, LLC
bin\$(Configuration)\
- Ignia
-
-
-
- https://github.com/Ignia/Topics-Library
C# .NET CMS Presentation View Models POCO
- true
-
-
-
- full
- CS1591,CA1056,CA1303
- false
- latest
-
-
- pdbonly
- CA1303
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
- runtime; build; native; contentfiles; analyzers
+ runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
\ No newline at end of file
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
diff --git a/OnTopic.ViewModels/README.md b/OnTopic.ViewModels/README.md
index 12196e82..545a142a 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
[](https://igniasoftware.visualstudio.com/OnTopic/_packaging?_a=package&feed=46d5f49c-5e1e-47bb-8b14-43be6c719ba8&package=b22ec8a0-3966-4dc8-8bf5-69e6264dabd1&preferRelease=true)
[](https://igniasoftware.visualstudio.com/OnTopic/_build/latest?definitionId=7&branchName=master)
+
> *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.
@@ -12,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
@@ -21,7 +22,7 @@ Installation can be performed by providing a ` to the `OnTo
…
-
+
```
@@ -30,37 +31,37 @@ Installation can be performed by providing a ` to the `OnTo
## Inventory
- [`TopicViewModel`](TopicViewModel.cs)
- - [`PageTopicViewModel`](PageTopicViewModel.cs)
- - [`ContentListTopicViewModel`](ContentListTopicViewModel.cs) ([`ContentItemTopicViewModel`](ContentItemTopicViewModel.cs))
- - [`IndexTopicViewModel`](IndexTopicViewModel.cs)
- - [`SlideshowTopicViewModel`](SlideshowTopicViewModel.cs) ([`SlideTopicViewModel`](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`](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)
+- [`AssociatedTopicBindingModel`](BindingModels/AssociatedTopicBindingModel.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.
### `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 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.
+### 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.).
+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/TopicViewModel.cs b/OnTopic.ViewModels/TopicViewModel.cs
index 796f2d9a..68b2213f 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;
@@ -13,62 +15,69 @@ 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
/// 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, ICoreTopicViewModel, IAssociatedTopicBindingModel, ITopicBindingModel {
/*==========================================================================================================================
| ID
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public int Id { get; set; }
+ public int Id { get; init; }
/*==========================================================================================================================
| KEY
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? Key { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? Key { get; init; }
/*==========================================================================================================================
| CONTENT TYPE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? ContentType { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? ContentType { get; init; }
/*==========================================================================================================================
| UNIQUE KEY
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? UniqueKey { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? UniqueKey { get; init; }
/*==========================================================================================================================
| WEB PATH
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? WebPath { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? WebPath { get; init; }
/*==========================================================================================================================
| VIEW
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? View { get; set; }
+ public string? View { get; init; }
/*==========================================================================================================================
| TITLE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- public string? Title { get; set; }
+ [Required, NotNull, DisallowNull]
+ public string? Title { get; init; }
/*==========================================================================================================================
| IS HIDDEN?
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public bool IsHidden { get; set; }
+ ///
+ [Obsolete("The IsHidden property is no longer supported by TopicViewModel.", true)]
+ [DisableMapping]
+ public bool IsHidden { get; init; }
/*==========================================================================================================================
| LAST MODIFIED
@@ -76,7 +85,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
@@ -86,13 +95,13 @@ public class 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)]
- public TopicViewModel? Parent { get; set; }
+ [Include(AssociationTypes.Parents)]
+ public TopicViewModel? Parent { get; init; }
} //Class
} //Namespace
\ No newline at end of file
diff --git a/OnTopic.ViewModels/TopicViewModelLookupService.cs b/OnTopic.ViewModels/TopicViewModelLookupService.cs
index c4875e1e..341550dd 100644
--- a/OnTopic.ViewModels/TopicViewModelLookupService.cs
+++ b/OnTopic.ViewModels/TopicViewModelLookupService.cs
@@ -5,6 +5,8 @@
\=============================================================================================================================*/
using System;
using System.Collections.Generic;
+using System.Reflection;
+using OnTopic.Lookup;
namespace OnTopic.ViewModels {
@@ -28,28 +30,34 @@ 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
\-----------------------------------------------------------------------------------------------------------------------*/
- 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
diff --git a/OnTopic.ViewModels/TopicViewModelCollection.cs b/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs
similarity index 85%
rename from OnTopic.ViewModels/TopicViewModelCollection.cs
rename to OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs
index a3ac3e24..07271449 100644
--- a/OnTopic.ViewModels/TopicViewModelCollection.cs
+++ b/OnTopic.ViewModels/_collections/TopicViewModelCollection{TItem}.cs
@@ -16,15 +16,15 @@ namespace OnTopic.ViewModels {
| 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
/// 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
@@ -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.InvariantCultureIgnoreCase)?? false)
- );
+ Contract.Requires(!String.IsNullOrWhiteSpace(contentType), nameof(contentType));
+ return new(Items.Where(t => t.ContentType?.Equals(contentType, StringComparison.OrdinalIgnoreCase)?? false));
}
/*==========================================================================================================================
@@ -73,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.ViewModels/ContentListTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs
similarity index 92%
rename from OnTopic.ViewModels/ContentListTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs
index 505624cd..89d8112e 100644
--- a/OnTopic.ViewModels/ContentListTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/ContentListTopicViewModel.cs
@@ -10,14 +10,14 @@ 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
/// 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/_contentTypes/IndexTopicViewModel.cs
similarity index 86%
rename from OnTopic.ViewModels/IndexTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs
index e2941843..215adeb0 100644
--- a/OnTopic.ViewModels/IndexTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/IndexTopicViewModel.cs
@@ -10,14 +10,14 @@ 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
/// 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/PageGroupTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs
similarity index 86%
rename from OnTopic.ViewModels/PageGroupTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs
index b1562426..175c32c3 100644
--- a/OnTopic.ViewModels/PageGroupTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/PageGroupTopicViewModel.cs
@@ -10,14 +10,14 @@ 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
/// 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/_contentTypes/PageTopicViewModel.cs
similarity index 88%
rename from OnTopic.ViewModels/PageTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs
index 5cc1a0c2..d3919208 100644
--- a/OnTopic.ViewModels/PageTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/PageTopicViewModel.cs
@@ -11,14 +11,22 @@ 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
/// 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, 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
@@ -26,7 +34,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 +42,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,15 +62,7 @@ public class PageTopicViewModel: TopicViewModel, IPageTopicViewModel {
///
/// Determines whether or not search engines are expected to index the page.
///
- public bool? NoIndex { get; set; }
-
- /*==========================================================================================================================
- | 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; set; }
+ public bool? NoIndex { 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/_contentTypes/SectionTopicViewModel.cs
similarity index 87%
rename from OnTopic.ViewModels/SectionTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs
index 146554df..8edc18a8 100644
--- a/OnTopic.ViewModels/SectionTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/SectionTopicViewModel.cs
@@ -3,7 +3,6 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-
using System;
namespace OnTopic.ViewModels {
@@ -12,14 +11,14 @@ 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
/// 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 +26,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/SlideshowTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs
similarity index 88%
rename from OnTopic.ViewModels/SlideshowTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs
index d88c3774..5d2574fd 100644
--- a/OnTopic.ViewModels/SlideshowTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/SlideshowTopicViewModel.cs
@@ -10,14 +10,14 @@ 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
/// 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/VideoTopicViewModel.cs b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs
similarity index 84%
rename from OnTopic.ViewModels/VideoTopicViewModel.cs
rename to OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs
index 720b1042..b052e0be 100644
--- a/OnTopic.ViewModels/VideoTopicViewModel.cs
+++ b/OnTopic.ViewModels/_contentTypes/VideoTopicViewModel.cs
@@ -3,8 +3,9 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-
using System;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
namespace OnTopic.ViewModels {
@@ -12,14 +13,14 @@ 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
/// 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 +28,8 @@ public class VideoTopicViewModel: PageTopicViewModel {
///
/// Provides a URL reference to a video to display on the page.
///
- public Uri? VideoUrl { get; set; }
+ [Required, NotNull, DisallowNull]
+ public Uri? VideoUrl { get; init; }
/*==========================================================================================================================
| POSTER URL
@@ -35,7 +37,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.ViewModels/ContentItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs
similarity index 85%
rename from OnTopic.ViewModels/ContentItemTopicViewModel.cs
rename to OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs
index 3a8987eb..587163e6 100644
--- a/OnTopic.ViewModels/ContentItemTopicViewModel.cs
+++ b/OnTopic.ViewModels/_items/ContentItemTopicViewModel.cs
@@ -11,14 +11,15 @@ namespace OnTopic.ViewModels {
| 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
/// 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 +27,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 +35,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 +43,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 +51,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/ItemTopicViewModel.cs b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs
similarity index 87%
rename from OnTopic.ViewModels/ItemTopicViewModel.cs
rename to OnTopic.ViewModels/_items/ItemTopicViewModel.cs
index a7e1f4da..5c0dd63c 100644
--- a/OnTopic.ViewModels/ItemTopicViewModel.cs
+++ b/OnTopic.ViewModels/_items/ItemTopicViewModel.cs
@@ -10,14 +10,14 @@ namespace OnTopic.ViewModels {
| 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
/// 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/_items/ListTopicViewModel.cs
similarity index 89%
rename from OnTopic.ViewModels/ListTopicViewModel.cs
rename to OnTopic.ViewModels/_items/ListTopicViewModel.cs
index 1948be01..cdf96d50 100644
--- a/OnTopic.ViewModels/ListTopicViewModel.cs
+++ b/OnTopic.ViewModels/_items/ListTopicViewModel.cs
@@ -3,6 +3,7 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using System.Collections;
namespace OnTopic.ViewModels {
@@ -10,7 +11,7 @@ namespace OnTopic.ViewModels {
| 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.
///
///
///
@@ -25,7 +26,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/_items/LookupListItemTopicViewModel.cs
similarity index 85%
rename from OnTopic.ViewModels/LookupListItemTopicViewModel.cs
rename to OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs
index 9e357bed..20845298 100644
--- a/OnTopic.ViewModels/LookupListItemTopicViewModel.cs
+++ b/OnTopic.ViewModels/_items/LookupListItemTopicViewModel.cs
@@ -10,14 +10,14 @@ namespace OnTopic.ViewModels {
| 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
/// 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/SlideTopicViewModel.cs b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs
similarity index 82%
rename from OnTopic.ViewModels/SlideTopicViewModel.cs
rename to OnTopic.ViewModels/_items/SlideTopicViewModel.cs
index 7f1e4af8..be402f25 100644
--- a/OnTopic.ViewModels/SlideTopicViewModel.cs
+++ b/OnTopic.ViewModels/_items/SlideTopicViewModel.cs
@@ -10,14 +10,15 @@ namespace OnTopic.ViewModels {
| 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
/// 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.sln b/OnTopic.sln
index b0c4fa43..aeb7d4e1 100644
--- a/OnTopic.sln
+++ b/OnTopic.sln
@@ -18,7 +18,9 @@ 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
+ Icon.png = Icon.png
README.md = README.md
EndProjectSection
EndProject
@@ -30,6 +32,10 @@ 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
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnTopic.All", "OnTopic.All\OnTopic.All.csproj", "{5AE0A248-0243-4E41-B6AB-CB8ACB5A6E04}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -76,6 +82,13 @@ 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}.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
diff --git a/OnTopic/Associations/ReferenceSetterAttribute.cs b/OnTopic/Associations/ReferenceSetterAttribute.cs
new file mode 100644
index 00000000..9f5446f4
--- /dev/null
+++ b/OnTopic/Associations/ReferenceSetterAttribute.cs
@@ -0,0 +1,44 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using OnTopic.Collections.Specialized;
+
+namespace OnTopic.Associations {
+
+ /*============================================================================================================================
+ | 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 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.SetValue("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.
+ ///
+ ///
+ [AttributeUsage(AttributeTargets.Property)]
+ public sealed class ReferenceSetterAttribute : Attribute {
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Associations/TopicReferenceCollection.cs b/OnTopic/Associations/TopicReferenceCollection.cs
new file mode 100644
index 00000000..d6221bd5
--- /dev/null
+++ b/OnTopic/Associations/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.Associations {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC REFERENCE COLLECTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Represents a collection of objects associated with particular reference keys.
+ ///
+ public class TopicReferenceCollection : TrackedRecordCollection {
+
+ /*==========================================================================================================================
+ | 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 TrackedRecordCollection? ParentCollection =>
+ AssociatedTopic.Parent?.References;
+
+ /*==========================================================================================================================
+ | PROPERTY: BASE COLLECTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ protected override TrackedRecordCollection? 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, TopicReferenceRecord item) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Provide base logic
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ base.InsertItem(index, item);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle recipricol references
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ item?.Value?.IncomingRelationships.SetValue(item.Key, AssociatedTopic, null, true);
+
+ }
+
+ /*==========================================================================================================================
+ | OVERRIDE: SET ITEM
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ protected override void SetItem(int index, TopicReferenceRecord item) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Get existing reference
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var existingItem = this[index];
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Provide base logic
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ base.SetItem(index, item);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle recipricol references
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ item?.Value?.IncomingRelationships.SetValue(item.Key, AssociatedTopic, null, true);
+ existingItem?.Value?.IncomingRelationships.SetValue(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.Remove(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.Remove(item.Key, AssociatedTopic, true);
+ }
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Associations/TopicReferenceRecord.cs b/OnTopic/Associations/TopicReferenceRecord.cs
new file mode 100644
index 00000000..19e0394c
--- /dev/null
+++ b/OnTopic/Associations/TopicReferenceRecord.cs
@@ -0,0 +1,81 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using OnTopic.Collections.Specialized;
+using OnTopic.Metadata;
+using OnTopic.Repositories;
+
+namespace OnTopic.Associations {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC REFERENCE RECORD
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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 TopicReferenceRecord: TrackedRecord {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public TopicReferenceRecord(): 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 TopicReferenceRecord(
+ string key,
+ Topic value,
+ bool isDirty = true,
+ DateTime? lastModified = null
+ ): base(key, value, isDirty, lastModified) {
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Associations/TopicRelationshipMultiMap.cs b/OnTopic/Associations/TopicRelationshipMultiMap.cs
new file mode 100644
index 00000000..391a9995
--- /dev/null
+++ b/OnTopic/Associations/TopicRelationshipMultiMap.cs
@@ -0,0 +1,295 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using OnTopic.Collections.Specialized;
+using OnTopic.Internal.Diagnostics;
+using OnTopic.Querying;
+using OnTopic.Repositories;
+
+namespace OnTopic.Associations {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC RELATIONSHIP MULTIMAP
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a simple interface for accessing collections of topic collections.
+ ///
+ ///
+ /// 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, ITrackDirtyKeys {
+
+ /*==========================================================================================================================
+ | PRIVATE VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ readonly Topic _parent;
+ readonly bool _isIncoming;
+ readonly DirtyKeyCollection _dirtyKeys = new();
+ readonly TopicMultiMap _storage = new();
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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 overload.
+ ///
+ public TopicRelationshipMultiMap(Topic parent, bool isIncoming = false): base() {
+ _parent = parent;
+ _isIncoming = isIncoming;
+ base.Source = _storage;
+ }
+
+ /*==========================================================================================================================
+ | METHOD: CLEAR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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 Clear(string relationshipKey) {
+ Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey));
+ if (_storage.Contains(relationshipKey)) {
+ var relationship = _storage.GetValues(relationshipKey);
+ if (relationship.Count > 0) {
+ _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew);
+ }
+ _storage.Clear(relationshipKey);
+ }
+ }
+
+ ///
+ [Obsolete("The ClearTopics(relationshipKey) method has been renamed to Clear(relationshipKey).", true)]
+ public void ClearTopics(string relationshipKey) => Clear(relationshipKey);
+
+ /*==========================================================================================================================
+ | METHOD: REMOVE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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 specified or the cannot be found.
+ ///
+ public bool Remove(string relationshipKey, Topic topic) => Remove(relationshipKey, topic, false);
+
+ ///
+ /// Removes a specific object associated with a specific relationship key.
+ ///
+ /// The key of the relationship.
+ /// 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 Remove(string relationshipKey, Topic topic, bool isIncoming) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate contracts
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey));
+ Contract.Requires(topic);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Remove reciprocal relationship, if appropriate
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!isIncoming) {
+ if (_isIncoming) {
+ throw new InvalidOperationException(
+ "You are attempting to remove an incoming relationship on a TopicRelationshipMultiMap that is not flagged as " +
+ nameof(isIncoming)
+ );
+ }
+ topic.IncomingRelationships.Remove(relationshipKey, _parent, true);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate relationshipKey
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!_storage.Contains(relationshipKey, topic)) {
+ return false;
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Remove relationship
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ _dirtyKeys.MarkAs(relationshipKey, markDirty: !_parent.IsNew);
+ _storage.Remove(relationshipKey, topic);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Remove true
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ return true;
+
+ }
+
+ ///
+ [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().", true)]
+ public bool RemoveTopic(string relationshipKey, Topic topic, bool isIncoming) =>
+ Remove(relationshipKey, topic, isIncoming);
+
+ /*==========================================================================================================================
+ | METHOD: SET VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Ensures that a is associated with the specified .
+ ///
+ ///
+ /// 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 SetValue(string relationshipKey, Topic topic, bool? markDirty = null)
+ => SetValue(relationshipKey, topic, markDirty, false);
+
+ ///
+ /// Ensures that an incoming is associated with the specified .
+ ///
+ ///
+ /// 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.
+ ///
+ /// 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 SetValue(string relationshipKey, Topic topic, bool? markDirty, bool isIncoming) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate contracts
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(!String.IsNullOrWhiteSpace(relationshipKey), nameof(relationshipKey));
+ Contract.Requires(topic);
+ TopicFactory.ValidateKey(relationshipKey);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Add relationship
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var topics = _storage.GetValues(relationshipKey);
+ var wasDirty = _dirtyKeys.IsDirty(relationshipKey);
+ if (!topics.Contains(topic)) {
+ _storage.Add(relationshipKey, topic);
+ if (!_parent.IsNew && !topic.IsNew && markDirty.HasValue && !markDirty.Value && !wasDirty) {
+ MarkClean(relationshipKey);
+ }
+ else {
+ _dirtyKeys.MarkDirty(relationshipKey);
+ }
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Create reciprocal relationship, if appropriate
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!isIncoming) {
+ if (_isIncoming) {
+ throw new InvalidOperationException(
+ "You are attempting to set an incoming relationship on a TopicRelationshipMultiMap that is not flagged as " +
+ nameof(isIncoming)
+ );
+ }
+ topic.IncomingRelationships.SetValue(relationshipKey, _parent, markDirty, true);
+ }
+
+ }
+
+ ///
+ [Obsolete("The SetTopic() method has been renamed to SetValue().", true)]
+ public void SetTopic(string relationshipKey, Topic topic, bool? isDirty = null) => SetValue(relationshipKey, topic, isDirty);
+
+ ///
+ [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);
+
+ /*==========================================================================================================================
+ | 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 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 IsFullyLoaded { get; set; } = true;
+
+ /*==========================================================================================================================
+ | METHOD: IS DIRTY?
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public bool IsDirty() => _dirtyKeys.IsDirty();
+
+ ///
+ public bool IsDirty(string key) => _dirtyKeys.IsDirty(key);
+
+ /*==========================================================================================================================
+ | METHOD: MARK CLEAN
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public void MarkClean() {
+ if (_parent.IsNew) {
+ return;
+ }
+ foreach (var relationship in _storage) {
+ if (!relationship.Values.AnyNew()) {
+ _dirtyKeys.MarkClean(relationship.Key);
+ }
+ }
+ }
+
+ ///
+ public void MarkClean(string key) {
+ if (_parent.IsNew) {
+ return;
+ }
+ if (Contains(key) && !_storage[key].Values.AnyNew()) {
+ _dirtyKeys.MarkClean(key);
+ }
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Attributes/AttributeCollection.cs b/OnTopic/Attributes/AttributeCollection.cs
new file mode 100644
index 00000000..2b94afb7
--- /dev/null
+++ b/OnTopic/Attributes/AttributeCollection.cs
@@ -0,0 +1,139 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Linq;
+using OnTopic.Collections.Specialized;
+using OnTopic.Repositories;
+
+namespace OnTopic.Attributes {
+
+ /*============================================================================================================================
+ | CLASS: ATTRIBUTE 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 AttributeCollection : TrackedRecordCollection {
+
+ /*==========================================================================================================================
+ | 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 AttributeCollection(Topic parentTopic) : base(parentTopic) {
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: PARENT COLLECTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ protected override TrackedRecordCollection? ParentCollection =>
+ AssociatedTopic.Parent?.Attributes;
+
+ /*==========================================================================================================================
+ | PROPERTY: BASE COLLECTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ protected override TrackedRecordCollection? BaseCollection =>
+ AssociatedTopic.BaseTopic?.Attributes;
+
+ /*==========================================================================================================================
+ | METHOD: IS DIRTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+
+ ///
+ /// 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)
+ => DeletedItems.Count > 0 || Items.Any(a =>
+ a.IsDirty &&
+ (!excludeLastModified || !a.Key.StartsWith("LastModified", StringComparison.OrdinalIgnoreCase))
+ );
+
+ /*==========================================================================================================================
+ | 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 .
+ /// 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
+ /// 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
+ ) {
+ base.SetValue(key, value, markDirty, version);
+ if (Contains(key)) {
+ var attributeValue = this[key];
+ var attributeIndex = IndexOf(attributeValue);
+ if (isExtendedAttribute is not null && isExtendedAttribute != attributeValue.IsExtendedAttribute) {
+ attributeValue = attributeValue with {
+ IsExtendedAttribute = isExtendedAttribute
+ };
+ base[attributeIndex] = attributeValue;
+ }
+ }
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs b/OnTopic/Attributes/AttributeCollectionExtensions.cs
similarity index 75%
rename from OnTopic/Attributes/AttributeValueCollectionExtensions.cs
rename to OnTopic/Attributes/AttributeCollectionExtensions.cs
index c39e6a73..b581bbf2 100644
--- a/OnTopic/Attributes/AttributeValueCollectionExtensions.cs
+++ b/OnTopic/Attributes/AttributeCollectionExtensions.cs
@@ -5,54 +5,54 @@
\=============================================================================================================================*/
using System;
using System.Globalization;
-using OnTopic.Collections;
+using OnTopic.Collections.Specialized;
using OnTopic.Internal.Diagnostics;
using OnTopic.Repositories;
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.
+ /// 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
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// 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 .
+ /// 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.
///
/// 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 boolean.
public static bool GetBoolean(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string name,
- bool defaultValue,
+ bool defaultValue = default,
bool inheritFromParent = false,
- bool inheritFromDerived = true
+ bool inheritFromBase = true
) {
Contract.Requires(attributes);
- Contract.Requires(!String.IsNullOrWhiteSpace(name));
+ Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name));
return Int32.TryParse(
attributes.GetValue(
name,
defaultValue ? "1" : "0",
inheritFromParent,
- inheritFromDerived ? 5 : 0
+ inheritFromBase ? 5 : 0
),
out var result
) ? result is 1 : defaultValue;
@@ -63,34 +63,34 @@ 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 .
+ /// 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.
///
/// 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 an integer.
public static int GetInteger(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string name,
- int defaultValue,
+ int defaultValue = default,
bool inheritFromParent = false,
- bool inheritFromDerived = true
+ bool inheritFromBase = true
) {
Contract.Requires(attributes);
- Contract.Requires(!String.IsNullOrWhiteSpace(name));
+ Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name));
return Int32.TryParse(
attributes.GetValue(
name,
defaultValue.ToString(CultureInfo.InvariantCulture),
inheritFromParent,
- inheritFromDerived? 5 : 0
+ inheritFromBase? 5 : 0
),
out var result
) ? result : defaultValue;
@@ -101,34 +101,34 @@ 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 .
+ /// 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.
///
/// 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(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string name,
- double defaultValue,
+ double defaultValue = default,
bool inheritFromParent = false,
- bool inheritFromDerived = true
+ bool inheritFromBase = true
) {
Contract.Requires(attributes);
- Contract.Requires(!String.IsNullOrWhiteSpace(name));
+ Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name));
return Double.TryParse(
attributes.GetValue(
name,
defaultValue.ToString(CultureInfo.InvariantCulture),
inheritFromParent,
- inheritFromDerived? 5 : 0
+ inheritFromBase? 5 : 0
),
out var result
) ? result : defaultValue;
@@ -139,34 +139,34 @@ 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 .
+ /// 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.
///
/// 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(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string name,
- DateTime defaultValue,
+ DateTime defaultValue = default,
bool inheritFromParent = false,
- bool inheritFromDerived = true
+ bool inheritFromBase = true
) {
Contract.Requires(attributes);
- Contract.Requires(!String.IsNullOrWhiteSpace(name));
+ Contract.Requires(!String.IsNullOrWhiteSpace(name), nameof(name));
return DateTime.TryParse(
attributes.GetValue(
name,
defaultValue.ToString(CultureInfo.InvariantCulture),
inheritFromParent,
- inheritFromDerived ? 5 : 0
+ inheritFromBase ? 5 : 0
),
out var result
) ? result : defaultValue;
@@ -176,25 +176,25 @@ 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 instance of the this extension is bound to.
+ /// 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 .
+ /// persisted to the data store on .
///
///
/// !String.IsNullOrWhiteSpace(key)
///
///
/// !String.IsNullOrWhiteSpace(value)
///
@@ -204,35 +204,35 @@ out var result
/// !value.Contains(" ")
///
public static void SetBoolean(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
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
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// 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 instance of the this extension is bound to.
+ /// 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 .
+ /// persisted to the data store on .
///
///
/// !String.IsNullOrWhiteSpace(key)
///
///
/// !String.IsNullOrWhiteSpace(value)
///
@@ -242,39 +242,39 @@ public static void SetBoolean(
/// !value.Contains(" ")
///
public static void SetInteger(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string key,
int value,
bool? isDirty = null
) => attributes?.SetValue(
key,
value.ToString(CultureInfo.InvariantCulture),
- isDirty, true
+ isDirty
);
/*==========================================================================================================================
| 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 instance of the this extension is bound to.
+ /// 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 .
+ /// persisted to the data store on .
///
///
/// !String.IsNullOrWhiteSpace(key)
///
///
/// !String.IsNullOrWhiteSpace(value)
///
@@ -284,39 +284,39 @@ public static void SetInteger(
/// !value.Contains(" ")
///
public static void SetDouble(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string key,
double value,
bool? isDirty = null
) => attributes?.SetValue(
key,
value.ToString(CultureInfo.InvariantCulture),
- isDirty, true
+ isDirty
);
/*==========================================================================================================================
| 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 instance of the this extension is bound to.
+ /// 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 .
+ /// persisted to the data store on .
///
///
/// !String.IsNullOrWhiteSpace(key)
///
///
/// !String.IsNullOrWhiteSpace(value)
///
@@ -326,15 +326,14 @@ public static void SetDouble(
/// !value.Contains(" ")
///
public static void SetDateTime(
- this AttributeValueCollection attributes,
+ this AttributeCollection attributes,
string key,
DateTime value,
bool? isDirty = null
) => attributes?.SetValue(
key,
value.ToString(CultureInfo.InvariantCulture),
- isDirty,
- true
+ isDirty
);
} //Class
diff --git a/OnTopic/Attributes/AttributeRecord.cs b/OnTopic/Attributes/AttributeRecord.cs
new file mode 100644
index 00000000..416398bf
--- /dev/null
+++ b/OnTopic/Attributes/AttributeRecord.cs
@@ -0,0 +1,118 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using OnTopic.Collections.Specialized;
+using OnTopic.Metadata;
+using OnTopic.Repositories;
+
+namespace OnTopic.Attributes {
+
+ /*============================================================================================================================
+ | CLASS: ATTRIBUTE RECORD
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Represents the immutable value of a particular attribute 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 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 's method.
+ ///
+ ///
+ public record AttributeRecord: TrackedRecord {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ public AttributeRecord(): 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.
+ ///
+ /// Determines if the attribute originated from an extended attributes data store.
+ ///
+ /// !String.IsNullOrWhiteSpace(key)
+ ///
+ public AttributeRecord(
+ string key,
+ string value,
+ bool isDirty = true,
+ DateTime? lastModified = null,
+ bool? isExtendedAttribute = null
+ ): base(key, value, isDirty, lastModified) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Set local values
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ IsExtendedAttribute = isExtendedAttribute;
+
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: IS EXTENDED ATTRIBUTE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines if this attribute originated from a data store as an extended attribute.
+ ///
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// The property maps to the
+ /// property. The former describes where the data was actually stored, whereas the latter describes where the
+ /// data should be stored.
+ ///
+ ///
+ public bool? IsExtendedAttribute { get; init; }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Attributes/AttributeSetterAttribute.cs b/OnTopic/Attributes/AttributeSetterAttribute.cs
index d2272a00..8e7bcb89 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 {
@@ -12,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
@@ -26,20 +27,19 @@ 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
- /// 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/Metadata/AttributeTypes/InstructionAttribute.cs b/OnTopic/Collections/KeyedTopicCollection.cs
similarity index 53%
rename from OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs
rename to OnTopic/Collections/KeyedTopicCollection.cs
index a9bbe1c2..089f54c5 100644
--- a/OnTopic/Metadata/AttributeTypes/InstructionAttribute.cs
+++ b/OnTopic/Collections/KeyedTopicCollection.cs
@@ -1,38 +1,28 @@
-/*==============================================================================================================================
+/*==============================================================================================================================
| Author Ignia, LLC
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using System.Collections.Generic;
-namespace OnTopic.Metadata.AttributeTypes {
+namespace OnTopic.Collections {
/*============================================================================================================================
- | CLASS: INSTRUCTION ATTRIBUTE (DESCRIPTOR)
+ | CLASS: KEYED TOPIC COLLECTION
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Represents metadata for describing an instruction attribute type.
+ /// Represents a collection of objects.
///
- ///
- /// 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 KeyedTopicCollection : KeyedTopicCollection {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public InstructionAttribute(
- string key,
- string contentType,
- Topic parent,
- int id = -1
- ) : base(
- key,
- contentType,
- parent,
- id
- ) {
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// Seeds the collection with an optional list of topic references.
+ public KeyedTopicCollection(IEnumerable? topics = null) : base(topics) {
}
} //Class
diff --git a/OnTopic/Collections/KeyedTopicCollection{T}.cs b/OnTopic/Collections/KeyedTopicCollection{T}.cs
new file mode 100644
index 00000000..516cb0a0
--- /dev/null
+++ b/OnTopic/Collections/KeyedTopicCollection{T}.cs
@@ -0,0 +1,128 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using OnTopic.Internal.Diagnostics;
+
+namespace OnTopic.Collections {
+
+ /*============================================================================================================================
+ | CLASS: KEYED TOPIC COLLECTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a strongly-typed collection of instances, or a derived type.
+ ///
+ public class KeyedTopicCollection: KeyedCollection, IEnumerable where T : Topic {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Seeds the collection with an optional list of topic references.
+ public KeyedTopicCollection(IEnumerable? topics = null) : base(StringComparer.OrdinalIgnoreCase) {
+ if (topics is not null) {
+ foreach (var topic in topics) {
+ Add(topic);
+ }
+ }
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a by .
+ ///
+ public T? GetValue(string key) {
+ TopicFactory.ValidateKey(key);
+ if (Contains(key)) {
+ return this[key];
+ }
+ return null;
+ }
+
+ ///
+ [Obsolete("The GetTopic() method has been renamed to GetValue().", true)]
+ public T? GetTopic(string key) => GetValue(key);
+
+ /*==========================================================================================================================
+ | METHOD: AS READ ONLY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a read-only version of this .
+ ///
+ public ReadOnlyKeyedTopicCollection AsReadOnly() => new(this);
+
+ /*==========================================================================================================================
+ | OVERRIDE: INSERT ITEM
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ /// 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 's , so
+ /// it's clear what key is being duplicated.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The instance to insert.
+ ///
+ /// 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 sealed void InsertItem(int index, T item) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate parameters
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(item, nameof(item));
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Insert item, if not already present
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!Contains(item.Key)) {
+ base.InsertItem(index, 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()}'.",
+ nameof(item)
+ );
+ }
+ }
+
+ /*==========================================================================================================================
+ | METHOD: CHANGE KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Changes the key associated with a topic to maintain referential integrity.
+ ///
+ ///
+ /// By default, doesn't permit mutable keys; this mitigates that issue by
+ /// allowing the collection's lookup dictionary to be updated whenever the key is updated in the corresponding topic
+ /// object.
+ ///
+ /// The topic object for which the should be changed.
+ /// The string value for the new key.
+ internal void ChangeKey(T topic, string newKey) => base.ChangeItemKey(topic, newKey);
+
+ /*==========================================================================================================================
+ | 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(T 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
diff --git a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs
similarity index 51%
rename from OnTopic/Metadata/AttributeTypes/NumberAttribute.cs
rename to OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs
index 21f58aca..c93b0173 100644
--- a/OnTopic/Metadata/AttributeTypes/NumberAttribute.cs
+++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection.cs
@@ -1,39 +1,28 @@
-/*==============================================================================================================================
+/*==============================================================================================================================
| Author Ignia, LLC
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using System.Collections.Generic;
-namespace OnTopic.Metadata.AttributeTypes {
+namespace OnTopic.Collections {
/*============================================================================================================================
- | CLASS: NUMBER ATTRIBUTE (DESCRIPTOR)
+ | CLASS: READ-ONLY KEYED TOPIC COLLECTION
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Represents metadata for describing a number attribute type, including information on how it will be presented and
- /// validated in the editor.
+ /// Represents a collection of objects.
///
- ///
- /// 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 ReadOnlyKeyedTopicCollection : ReadOnlyKeyedTopicCollection {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public NumberAttribute(
- string key,
- string contentType,
- Topic parent,
- int id = -1
- ) : base(
- key,
- contentType,
- parent,
- id
- ) {
+ ///
+ /// Establishes a new based on an existing .
+ ///
+ /// The underlying .
+ public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection) {
}
} //Class
diff --git a/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs
new file mode 100644
index 00000000..136b5f78
--- /dev/null
+++ b/OnTopic/Collections/ReadOnlyKeyedTopicCollection{T}.cs
@@ -0,0 +1,79 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using OnTopic.Internal.Diagnostics;
+
+namespace OnTopic.Collections {
+
+ /*============================================================================================================================
+ | CLASS: READ-ONLY KEYED TOPIC COLLECTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a read-only collection of topics.
+ ///
+ public class ReadOnlyKeyedTopicCollection : ReadOnlyCollection where T : Topic {
+
+ /*==========================================================================================================================
+ | PRIVATE VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ private readonly KeyedTopicCollection _innerCollection;
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new based on an existing .
+ ///
+ /// The underlying .
+ public ReadOnlyKeyedTopicCollection(IList? innerCollection = null) : base(innerCollection?? new List()) {
+ Contract.Requires(innerCollection, "innerCollection should not be null");
+ _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
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a by .
+ ///
+ public T? GetValue(string key) {
+ TopicFactory.ValidateKey(key);
+ if (_innerCollection.Contains(key)) {
+ return _innerCollection[key];
+ }
+ return null;
+ }
+
+ ///
+ [Obsolete("The GetTopic() method has been renamed to GetValue().", true)]
+ public T? GetTopic(string key) => GetValue(key);
+
+ /*==========================================================================================================================
+ | INDEXER
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves an by key.
+ ///
+ /// The topic key.
+ public Topic this[string key] => _innerCollection[key];
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Collections/ReadOnlyTopicCollection.cs b/OnTopic/Collections/ReadOnlyTopicCollection.cs
index 8ba0855c..f9f008b1 100644
--- a/OnTopic/Collections/ReadOnlyTopicCollection.cs
+++ b/OnTopic/Collections/ReadOnlyTopicCollection.cs
@@ -3,18 +3,19 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
+using System;
using System.Collections.Generic;
-using OnTopic.Internal.Diagnostics;
+using System.Collections.ObjectModel;
namespace OnTopic.Collections {
/*============================================================================================================================
- | CLASS: READ ONLY TOPIC COLLECTION
+ | CLASS: READ-ONLY TOPIC COLLECTION
\---------------------------------------------------------------------------------------------------------------------------*/
///
/// Represents a collection of objects.
///
- public class ReadOnlyTopicCollection : ReadOnlyTopicCollection {
+ public class ReadOnlyTopicCollection : ReadOnlyCollection {
/*==========================================================================================================================
| CONSTRUCTOR
@@ -22,8 +23,8 @@ public class ReadOnlyTopicCollection : ReadOnlyTopicCollection {
///
/// Establishes a new based on an existing .
///
- /// The underlying .
- public ReadOnlyTopicCollection(IList innerCollection) : base(innerCollection) {
+ /// The underlying .
+ public ReadOnlyTopicCollection(IList? innerCollection = null) : base(innerCollection?? new List()) {
}
/*==========================================================================================================================
@@ -33,13 +34,31 @@ public ReadOnlyTopicCollection(IList innerCollection) : base(innerCollect
/// 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) {
- Contract.Requires(innerCollection, "innerCollection should not be null");
- return new(innerCollection);
- }
+ /// 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
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ [Obsolete(
+ "The GetTopic() method is not implemented on ReadOnlyTopicCollection. Use ReadOnlyKeyedTopicCollection instead.",
+ true
+ )]
+ public Topic? GetValue(string key) => throw new NotImplementedException();
+
+ /*==========================================================================================================================
+ | INDEXER
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ [Obsolete(
+ "Indexing by key is not implemented on ReadOnlyTopicCollection. Use ReadOnlyKeyedTopicCollection instead.",
+ true
+ )]
+ public Topic this[string key] => throw new ArgumentOutOfRangeException(nameof(key));
} //Class
} //Namespace
\ No newline at end of file
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
diff --git a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs b/OnTopic/Collections/Specialized/ITrackDirtyKeys.cs
similarity index 52%
rename from OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs
rename to OnTopic/Collections/Specialized/ITrackDirtyKeys.cs
index 9428966e..f9ca2a33 100644
--- a/OnTopic/Metadata/AttributeTypes/HtmlAttribute.cs
+++ b/OnTopic/Collections/Specialized/ITrackDirtyKeys.cs
@@ -1,46 +1,44 @@
-/*==============================================================================================================================
+/*==============================================================================================================================
| Author Ignia, LLC
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-namespace OnTopic.Metadata.AttributeTypes {
+namespace OnTopic.Collections.Specialized {
/*============================================================================================================================
- | CLASS: HTML ATTRIBUTE (DESCRIPTOR)
+ | INTERFACE: TRACK DIRTY KEYS
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Represents metadata for describing an HTML attribute type, including information on how it will be presented and
- /// validated in the editor.
+ /// Defines an interface for tracking dirty keys.
///
- ///
- /// 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 interface ITrackDirtyKeys {
/*==========================================================================================================================
- | CONSTRUCTOR
+ | METHOD: IS DIRTY?
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public HtmlAttribute(
- string key,
- string contentType,
- Topic parent,
- int id = -1
- ) : base(
- key,
- contentType,
- parent,
- id
- ) {
- }
+ ///
+ /// Determines whether the collection is dirty.
+ ///
+ bool IsDirty();
+
+ ///
+ /// Determines whether the provided in the collection is dirty.
+ ///
+ bool IsDirty(string key);
/*==========================================================================================================================
- | PROPERTY: IS EXTENDED ATTRIBUTE?
+ | METHOD: MARK CLEAN
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- public override bool IsExtendedAttribute => true;
+ ///
+ /// Marks the collection as clean.
+ ///
+ void MarkClean();
+
+ ///
+ /// Marks the specified in the collection as clean.
+ ///
+ void MarkClean(string key);
- } //Class
+ } //Interface
} //Namespace
\ No newline at end of file
diff --git a/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs b/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.cs
new file mode 100644
index 00000000..f3dda410
--- /dev/null
+++ b/OnTopic/Collections/Specialized/KeyValuesPair{TKey,TValue}.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.Specialized {
+
+ /*============================================================================================================================
+ | 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/Specialized/ReadOnlyTopicMultiMap.cs b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs
new file mode 100644
index 00000000..c5f3b9c7
--- /dev/null
+++ b/OnTopic/Collections/Specialized/ReadOnlyTopicMultiMap.cs
@@ -0,0 +1,173 @@
+/*==============================================================================================================================
+| 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;
+
+namespace OnTopic.Collections.Specialized {
+
+ /*============================================================================================================================
+ | 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 ReadOnlyCollection Keys => new(Source.Select(m => m.Key).ToList());
+
+ /*==========================================================================================================================
+ | 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 ReadOnlyTopicCollection 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 VALUES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// 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 ReadOnlyTopicCollection GetValues(string key) {
+ Contract.Requires(!String.IsNullOrWhiteSpace(key), nameof(key));
+ if (Contains(key)) {
+ return new(Source[key].Values);
+ }
+ return new(new List());
+ }
+
+ ///
+ [Obsolete("The GetTopics() method has been renamed to GetValues().", true)]
+ public ReadOnlyTopicCollection GetTopics(string key) => GetValues(key);
+
+ /*==========================================================================================================================
+ | METHOD: GET ALL VALUES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a list of all related objects, independent of collection key.
+ ///
+ ///
+ /// Returns an enumerable list of objects.
+ ///
+ public ReadOnlyTopicCollection GetAllValues() =>
+ new(Source.SelectMany(list => list.Values).Distinct().ToList());
+
+ ///
+ /// Retrieves a list of all related objects, independent of key, filtered by content
+ /// type.
+ ///
+ ///
+ /// Returns an enumerable list of objects.
+ ///
+ public ReadOnlyTopicCollection GetAllValues(string contentType) =>
+ new(GetAllValues().Where(t => t.ContentType == contentType).ToList());
+
+ ///
+ [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().", true)]
+ public ReadOnlyTopicCollection GetAllTopics() => GetAllValues();
+
+ /*==========================================================================================================================
+ | 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
\ No newline at end of file
diff --git a/OnTopic/Collections/Specialized/TopicIndex.cs b/OnTopic/Collections/Specialized/TopicIndex.cs
new file mode 100644
index 00000000..5c2ea8b5
--- /dev/null
+++ b/OnTopic/Collections/Specialized/TopicIndex.cs
@@ -0,0 +1,34 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.Collections.Generic;
+
+namespace OnTopic.Collections.Specialized {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC INDEX
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Represents a collection of objects indexed by .
+ ///
+ public class TopicIndex : Dictionary {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance of the