diff --git a/Ignia.Topics.Data.Caching/CachedTopicRepository.cs b/Ignia.Topics.Data.Caching/CachedTopicRepository.cs
index f6d8d15b..d922b11f 100644
--- a/Ignia.Topics.Data.Caching/CachedTopicRepository.cs
+++ b/Ignia.Topics.Data.Caching/CachedTopicRepository.cs
@@ -7,6 +7,7 @@
using System.Diagnostics.Contracts;
using Ignia.Topics.Collections;
using Ignia.Topics.Repositories;
+using Ignia.Topics.Querying;
namespace Ignia.Topics.Data.Caching {
@@ -50,6 +51,11 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
\-----------------------------------------------------------------------------------------------------------------------*/
_dataProvider = dataProvider;
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Ensure topics are loaded
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ _cache = _dataProvider.Load();
+
}
/*==========================================================================================================================
@@ -64,7 +70,7 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
| METHOD: LOAD
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic identifier.
/// Determines whether or not to recurse through and load a topic's children.
@@ -72,46 +78,27 @@ public CachedTopicRepository(ITopicRepository dataProvider) : base() {
public override Topic Load(int topicId, bool isRecursive = true) {
/*------------------------------------------------------------------------------------------------------------------------
- | Validate contracts
+ | Handle request for entire tree
\-----------------------------------------------------------------------------------------------------------------------*/
- Contract.Ensures(Contract.Result() != null);
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Ensure topics are loaded
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (_cache == null) {
- _cache = _dataProvider.Load();
+ if (topicId < 0) {
+ return _cache;
}
/*------------------------------------------------------------------------------------------------------------------------
- | Lookup by TopicId
+ | Recursive search
\-----------------------------------------------------------------------------------------------------------------------*/
- if (topicId >= 0) {
- return GetTopic(_cache, topicId);
- }
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Return entire cache
- \-----------------------------------------------------------------------------------------------------------------------*/
- return _cache;
+ return _cache.FindFirst(t => t.Id.Equals(topicId));
}
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified key name.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified key name.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
/// A topic object.
public override Topic Load(string topicKey = null, bool isRecursive = true) {
- /*------------------------------------------------------------------------------------------------------------------------
- | Ensure topics are loaded
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (_cache == null) {
- _cache = _dataProvider.Load();
- }
-
/*------------------------------------------------------------------------------------------------------------------------
| Lookup by TopicKey
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -142,45 +129,6 @@ public override Topic Load(string topicKey = null, bool isRecursive = true) {
/*==========================================================================================================================
| METHOD: GET TOPIC
\-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Retrieves a topic object based on the current topic scope and the specified integer identifier.
- ///
- ///
- /// If the specified ID does not match the identifier for the current topic, its children will be searched.
- ///
- /// The root topic to search from.
- /// The integer identifier for the topic.
- /// The topic or null, if the topic is not found.
- ///
- /// topicId <= 0
- ///
- private Topic GetTopic(Topic sourceTopic, int topicId) {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Validate input
- \-----------------------------------------------------------------------------------------------------------------------*/
- Contract.Requires(topicId >= 0, "The topicId is expected to be a non-negative integer.");
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Return if current
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (sourceTopic.Id == topicId) return sourceTopic;
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Iterate through children
- \-----------------------------------------------------------------------------------------------------------------------*/
- foreach (var childTopic in sourceTopic.Children) {
- var foundTopic = GetTopic(childTopic, topicId);
- if (foundTopic != null) return foundTopic;
- }
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Return null if not found
- \-----------------------------------------------------------------------------------------------------------------------*/
- return null;
-
- }
-
///
/// Retrieves a topic object based on the specified partial or full (prefixed) topic key.
///
@@ -211,7 +159,7 @@ private Topic GetTopic(Topic sourceTopic, string uniqueKey) {
| Provide implicit root
>-------------------------------------------------------------------------------------------------------------------------
| ###NOTE JJC080313: While a root topic is required by the data structure, it should be implicit from the perspective of
- | the calling application. A developer should be able to call GetTopic("Namepace:TopicPath") to get to a topic, without
+ | the calling application. A developer should be able to call GetTopic("Namespace:TopicPath") to get to a topic, without
| needing to be aware of the root.
\-----------------------------------------------------------------------------------------------------------------------*/
if (
@@ -261,8 +209,8 @@ private Topic GetTopic(Topic sourceTopic, string uniqueKey) {
/// Boolean indicator as to the topic's publishing status.
/// The integer return value from the execution of the topics_UpdateTopic stored procedure.
///
- /// The Content Type topic.Attributes.GetValue(ContentType, Page) referenced by topic.Key could not be found under
- /// Configuration:ContentTypes. There are TopicRepository.ContentTypes.Count ContentTypes in the Repository.
+ /// The Content Type topic.Attributes.GetValue(ContentType, Page) referenced by topic.Key could not be found
+ /// under Configuration:ContentTypes. There are TopicRepository.ContentTypes.Count ContentTypes in the Repository.
///
///
/// Failed to save Topic topic.Key (topic.Id) via
diff --git a/Ignia.Topics.Data.Caching/Ignia.Topics.Data.Caching.csproj b/Ignia.Topics.Data.Caching/Ignia.Topics.Data.Caching.csproj
index 78e5e758..3816ab05 100644
--- a/Ignia.Topics.Data.Caching/Ignia.Topics.Data.Caching.csproj
+++ b/Ignia.Topics.Data.Caching/Ignia.Topics.Data.Caching.csproj
@@ -10,7 +10,7 @@
Properties
Ignia.Topics.Data.Caching
Ignia.Topics.Data.Caching
- v4.5
+ v4.7
512
@@ -45,6 +45,17 @@
True
Full
0
+
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -66,6 +77,8 @@
False
False
%28none%29
+ true
+ latest
pdbonly
diff --git a/Ignia.Topics.Data.Caching/Properties/AssemblyInfo.cs b/Ignia.Topics.Data.Caching/Properties/AssemblyInfo.cs
index f51fefcf..7817e369 100644
--- a/Ignia.Topics.Data.Caching/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.Data.Caching/Properties/AssemblyInfo.cs
@@ -1,36 +1,29 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.Data.Caching")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2018 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("OnTopic Cached Repository")]
+[assembly: AssemblyDescription("Provides a caching decorator for ITopicRepository implementations.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.Data.Caching")]
-[assembly: AssemblyCopyright("Copyright © 2017")]
[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: AssemblyVersion("3.6.1762.0")]
+[assembly: AssemblyFileVersion("3.5.1794.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("206b7f91-ca25-4e9d-9576-60d2e54a2c0a")]
-// 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("2.0.0.0")]
-[assembly: AssemblyFileVersion("2.0.0.0")]
+
diff --git a/Ignia.Topics.Data.Sql.Database/Ignia.Topics.Data.Sql.Database.sqlproj b/Ignia.Topics.Data.Sql.Database/Ignia.Topics.Data.Sql.Database.sqlproj
index 6430b703..bafd59c9 100644
--- a/Ignia.Topics.Data.Sql.Database/Ignia.Topics.Data.Sql.Database.sqlproj
+++ b/Ignia.Topics.Data.Sql.Database/Ignia.Topics.Data.Sql.Database.sqlproj
@@ -17,7 +17,7 @@
1033,CI
BySchemaAndSchemaType
True
- v4.5
+ v4.7
CS
Properties
False
@@ -27,6 +27,7 @@
PRIMARY
False
OnTopic
+
bin\Release\
diff --git a/Ignia.Topics.Data.Sql.Database/README.md b/Ignia.Topics.Data.Sql.Database/README.md
index 7a2ed894..f7d21856 100644
--- a/Ignia.Topics.Data.Sql.Database/README.md
+++ b/Ignia.Topics.Data.Sql.Database/README.md
@@ -5,7 +5,7 @@ The `Ignia.Topics.Data.Sql.Database` provides a default schema for supporting th
## Tables
The following is a summary of the most relevant tables.
-- **[`topics_Topics`](dbo/Tables/topics_Topics.sql)**: Represents the core hiearchy of topics, encoded in a nested set format.
+- **[`topics_Topics`](dbo/Tables/topics_Topics.sql)**: Represents the core hierarchy of topics, encoded in a nested set format.
- **[`topics_TopicAttributes`](dbo/Tables/topics_Topics.sql)**: Represents key/value pairs of topic attributes, including historical versions.
- **[`topics_Blob`](dbo/Tables/topics_Blob.sql)**: Represents an XML-based blob of non-indexed attributes, which are too long for `topics_TopicAttributes`.
- **[`topics_Relationships`](dbo/Tables/topics_Relationships.sql)**: Represents relationships between topics, segmented by namespace.
@@ -24,7 +24,7 @@ The following is a summary of the most relevant stored procedures.
- **[`topics_CreateTopic`](dbo/Stored%20Procedures/topics_CreateTopic.sql)**: Creates a new topic based on a `@ParentId`, an array of `@Attributes`, and an XML `@Blob`. Returns a new `@@Identity`.
- **[`topics_DeleteTopic`](dbo/Stored%20Procedures/topics_DeleteTopic.sql)**: Deletes an existing topic based on a `@Id`.
- **[`topics_MoveTopic`](dbo/Stored%20Procedures/topics_MoveTopic.sql)**: Moves an existing topic based on an `@Id`, `@ParentId`, and `@SiblingId`.
-- **[`topics_UpdateTopic`](dbo/Stored%20Procedures/topics_UpdateTopic.sql)**: Updates an existing topic based on an `@Id`, an array of `@Attributes`, and a `@Blob`. Optionally deletes all relationships; these will need to be readded using `topics_PersistRelations`. Old attributes are persisted as previous versions.
+- **[`topics_UpdateTopic`](dbo/Stored%20Procedures/topics_UpdateTopic.sql)**: Updates an existing topic based on an `@Id`, an array of `@Attributes`, and a `@Blob`. Optionally deletes all relationships; these will need to be re-added using `topics_PersistRelations`. Old attributes are persisted as previous versions.
- **[`topics_PersistRelations`](dbo/Stored%20Procedures/topics_PersistRelations.sql)**: Associates a relationship with a topic based on a `@Source_TopicId`, array of `@Target_TopicIds`, and `@RelationshipTypeID` (which can be any string label).
## Views
diff --git a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_CreateTopic.sql b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_CreateTopic.sql
index 70381535..6029ee19 100644
--- a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_CreateTopic.sql
+++ b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_CreateTopic.sql
@@ -3,7 +3,7 @@
--
-- Purpose Creates a new topic.
--
--- History Casey Margell 04062009 Initial Creation baseaed on code from Celko's Sql For Smarties.
+-- History Casey Margell 04062009 Initial Creation based on code from Celko's SQL For Smarties.
-- Jeremy Caney 05282010 Reformatted code and refactored identifiers for improved readability.
-- Katherine Trunkey 08142014 Updated topic_TopicAttributes insertion script to use uncommon, multi-character delimiters
-- rather than a colon and semicolon in order to provide better escaping safety for @Attributes.
diff --git a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_GetTopics.sql b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_GetTopics.sql
index cc078912..6121f03e 100644
--- a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_GetTopics.sql
+++ b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_GetTopics.sql
@@ -4,7 +4,7 @@
-- Purpose Gets the tree of current topics rooted FROM the provided TopicID. If no TopicID is provided then the sproc returns everything
-- under the topic with the lowest id.
--
--- History Casey Margell 04062009 Initial Creation baseaed on code FROM Celko's Sql For Smarties.
+-- History Casey Margell 04062009 Initial Creation based on code FROM Celko's SQL For Smarties.
-- Jeremy Caney 07192009 Added support for AttributeID lookup.
-- Jeremy Caney 05282010 Reformatted code AND refactored identifiers for improved readability.
-- Jeremy Caney 06072010 Added support for blob fields.
diff --git a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_MoveTopic.sql b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_MoveTopic.sql
index 7454ce9c..9529864f 100644
--- a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_MoveTopic.sql
+++ b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_MoveTopic.sql
@@ -8,7 +8,7 @@
-- Hedley Robertson 07062010 Added support for SiblingID (Ordering)
-- Hedley Robertson 08122010 Inline test cases, debugging statements and check for re-parenting;
-- now avoids moving item to start of SET when sibling move requested
--- and parentid has not changed
+-- and ParentId has not changed
-- Hedley Robertson 08172010 Rebuilt with externalized MoveSubTree function
-- Jeremy Caney 09222014 Updated logic for ParentID attribute to be based on Key, not ID
-- Jeremy Caney 12092017 Refactored based on Celko's alternative formulation.
@@ -124,7 +124,7 @@ SET @Offset =
-- MOVE SOURCE RANGE TO INSERTION POINT
-----------------------------------------------------------------------------------------------------------------------------------------------
-- The basic idea behind moving nodes is that we're going to a) shift the target subtree (@Parent) by the delta (@Offset) between its original
--- position (@OriginalLeft, @OriginalRight) and the the target location (@InsertionPoint), while b) closing the gap left behind by shifting all
+-- position (@OriginalLeft, @OriginalRight) and the target location (@InsertionPoint), while b) closing the gap left behind by shifting all
-- intermediate nodes by the width of the target subtree (@OriginalRange).
-----------------------------------------------------------------------------------------------------------------------------------------------
-- EXAMPLE: If we're moving a target subtree of width 12 down 26 nodes, then we'd a) subtract 26 (the @Offset) from all nodes between RangeLeft
diff --git a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_UpdateTopic.sql b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_UpdateTopic.sql
index c56a7cc0..34413691 100644
--- a/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_UpdateTopic.sql
+++ b/Ignia.Topics.Data.Sql.Database/dbo/Stored Procedures/topics_UpdateTopic.sql
@@ -3,7 +3,7 @@
--
-- Purpose Used to update the attributes of a provided node
--
--- History Casey Margell 04062009 Initial Creation baseaed on code from Celko's Sql For Smarties
+-- History Casey Margell 04062009 Initial Creation based on code from Celko's SQL For Smarties
-- Jeremy Caney 05282010 Reformatted code and refactored identifiers for improved readability.
-- Katherine Trunkey 08052014 Added parameter for Version (datetime). Updated procedure to always create new attribute
-- records rather than deleting the existing attributes for the topic and recreating them.
diff --git a/Ignia.Topics.Data.Sql/Ignia.Topics.Data.Sql.csproj b/Ignia.Topics.Data.Sql/Ignia.Topics.Data.Sql.csproj
index ec48f436..a1f5f6df 100644
--- a/Ignia.Topics.Data.Sql/Ignia.Topics.Data.Sql.csproj
+++ b/Ignia.Topics.Data.Sql/Ignia.Topics.Data.Sql.csproj
@@ -10,7 +10,7 @@
Properties
Ignia.Topics.Data.Sql
Ignia.Topics.Data.Sql
- v4.5
+ v4.7
512
1
@@ -68,6 +68,8 @@
Full
%28none%29
0
+ true
+ latest
true
diff --git a/Ignia.Topics.Data.Sql/Properties/AssemblyInfo.cs b/Ignia.Topics.Data.Sql/Properties/AssemblyInfo.cs
index 47ec470f..7c43f8c1 100644
--- a/Ignia.Topics.Data.Sql/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.Data.Sql/Properties/AssemblyInfo.cs
@@ -1,36 +1,27 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.Data.Sql")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2018 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia SQL Server Repository")]
+[assembly: AssemblyDescription("Provides Microsoft SQL Server support for persisting the OnTopic graph to a database.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.Data.Sql")]
-[assembly: AssemblyCopyright("Copyright © 2017")]
[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: AssemblyVersion("3.6.1739.0")]
+[assembly: AssemblyFileVersion("3.5.1763.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("1de1f923-c7c2-435b-b49a-975acbcb5ff0")]
-
-// 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("2.0.0.0")]
-[assembly: AssemblyFileVersion("2.0.0.0")]
diff --git a/Ignia.Topics.Data.Sql/SqlTopicRepository.cs b/Ignia.Topics.Data.Sql/SqlTopicRepository.cs
index 25376a4d..ee2e1366 100644
--- a/Ignia.Topics.Data.Sql/SqlTopicRepository.cs
+++ b/Ignia.Topics.Data.Sql/SqlTopicRepository.cs
@@ -14,8 +14,6 @@
using System.Text;
using System.Web;
using System.Xml;
-using Ignia.Topics.Collections;
-using Ignia.Topics.Querying;
using Ignia.Topics.Repositories;
namespace Ignia.Topics.Data.Sql {
@@ -34,7 +32,6 @@ public class SqlTopicRepository : TopicRepositoryBase, ITopicRepository {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- private ContentTypeDescriptorCollection _contentTypeDescriptors = null;
private readonly string _connectionString = null;
/*==========================================================================================================================
@@ -191,7 +188,7 @@ private static void SetBlobAttributes(SqlDataReader reader, Dictionary
///
- /// Topics can be cross-referenced with each other via a many-to-many relationships.Once the topics are populated in
+ /// 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 that representing the current record.
@@ -214,7 +211,7 @@ private static void SetRelationships(SqlDataReader reader, Dictionary topics) {
| Loop through topics
\-----------------------------------------------------------------------------------------------------------------------*/
foreach (var topic in topics.Values) {
- var success = Int32.TryParse(topic.Attributes.GetValue("TopicId", "-1", false, false), out var derivedTopicId);
- if (!success || derivedTopicId < 0) continue;
+ var derivedTopicId = topic.Attributes.GetInteger("TopicId", -1, false, false);
+ if (derivedTopicId < 0) continue;
if (topics.Keys.Contains(derivedTopicId)) {
topic.DerivedTopic = topics[derivedTopicId];
}
@@ -264,56 +261,6 @@ private static void SetDerivedTopics(Dictionary topics) {
}
- /*==========================================================================================================================
- | GET CONTENT TYPE DESCRIPTORS
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Retrieves a collection of Content Type Descriptor objects from the configuration section of the data provider.
- ///
- public override ContentTypeDescriptorCollection GetContentTypeDescriptors() {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Initialize content types
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (_contentTypeDescriptors == null) {
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Load configuration data
- \---------------------------------------------------------------------------------------------------------------------*/
- var configuration = Load("Configuration");
-
- /*--------------------------------------------------------------------------------------------------------------------
- | Add available Content Types to the collection
- \-------------------------------------------------------------------------------------------------------------------*/
- _contentTypeDescriptors = new ContentTypeDescriptorCollection();
-
- /*--------------------------------------------------------------------------------------------------------------------
- | Ensure the parent ContentTypes topic is available to iterate over
- \-------------------------------------------------------------------------------------------------------------------*/
- if (configuration.Children.GetTopic("ContentTypes") == null) {
- throw new Exception("Unable to load section Configuration:ContentTypes.");
- }
-
- /*--------------------------------------------------------------------------------------------------------------------
- | Add available Content Types to the collection
- \-------------------------------------------------------------------------------------------------------------------*/
- foreach (var topic in configuration.Children.GetTopic("ContentTypes").FindAllByAttribute("ContentType", "ContentType")) {
- // Ensure the Topic is used as the strongly-typed ContentType
- // Add ContentType Topic to collection if not already added
- if (
- topic is ContentTypeDescriptor contentTypeDescriptor &&
- !_contentTypeDescriptors.Contains(contentTypeDescriptor.Key)
- ) {
- _contentTypeDescriptors.Add(contentTypeDescriptor);
- }
- }
-
- }
-
- return _contentTypeDescriptors;
-
- }
-
/*==========================================================================================================================
| METHOD: SET VERSION HISTORY
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -357,7 +304,7 @@ private static void SetVersionHistory(SqlDataReader reader, Dictionary
- /// Loads a topic (and, optionally, all of its descendents) based on the specified topic key.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified topic key.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
@@ -377,8 +324,8 @@ public override Topic Load(string topicKey = null, bool isRecursive = true) {
/*------------------------------------------------------------------------------------------------------------------------
| Establish database connection
\-----------------------------------------------------------------------------------------------------------------------*/
- var connection = new SqlConnection(_connectionString);
- var command = new SqlCommand("topics_GetTopicID", connection);
+ var connection = new SqlConnection(_connectionString);
+ var command = new SqlCommand("topics_GetTopicID", connection);
int topicId;
command.CommandType = CommandType.StoredProcedure;
@@ -413,8 +360,8 @@ public override Topic Load(string topicKey = null, bool isRecursive = true) {
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Topic(s) failed to load: " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException($"Topic(s) failed to load: '{exception.Message}'", exception);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -433,7 +380,7 @@ public override Topic Load(string topicKey = null, bool isRecursive = true) {
}
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic's unique identifier.
/// Determines whether or not to recurse through and load a topic's children.
@@ -456,11 +403,11 @@ public override Topic Load(int topicId, bool isRecursive = true) {
\-----------------------------------------------------------------------------------------------------------------------*/
var topics = new Dictionary();
var connection = new SqlConnection(_connectionString);
- var command = new SqlCommand("topics_GetTopics", connection);
-
- command.CommandType = CommandType.StoredProcedure;
- command.CommandTimeout = 120;
- SqlDataReader reader = null;
+ var command = new SqlCommand("topics_GetTopics", connection) {
+ CommandType = CommandType.StoredProcedure,
+ CommandTimeout = 120
+ };
+ var reader = (SqlDataReader)null;
try {
@@ -552,8 +499,8 @@ public override Topic Load(int topicId, bool isRecursive = true) {
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Topics failed to load: " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException($"Topics failed to load: '{exception.Message}'", exception);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -595,13 +542,13 @@ public override Topic Load(int topicId, DateTime version) {
\-----------------------------------------------------------------------------------------------------------------------*/
var topics = new Dictionary();
var connection = new SqlConnection(_connectionString);
- var command = new SqlCommand("topics_GetVersion", connection);
+ var command = new SqlCommand("topics_GetVersion", connection) {
+ CommandType = CommandType.StoredProcedure,
+ CommandTimeout = 120
+ };
+ var reader = (SqlDataReader)null;
command.CommandType = CommandType.StoredProcedure;
- command.CommandTimeout = 120;
- SqlDataReader reader = null;
-
- command.CommandType = CommandType.StoredProcedure;
try {
@@ -687,8 +634,8 @@ public override Topic Load(int topicId, DateTime version) {
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Topics failed to load: " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException($"Topics failed to load: '{exception.Message}'", exception);
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -752,27 +699,12 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f
/*------------------------------------------------------------------------------------------------------------------------
| Validate content type
\-----------------------------------------------------------------------------------------------------------------------*/
- var contentTypes = GetContentTypeDescriptors();
- if (!contentTypes.Contains(topic.Attributes.GetValue("ContentType"))) {
- throw new Exception(
- "The Content Type \"" + topic.Attributes.GetValue("ContentType", "Page") + "\" referenced by \"" + topic.Key +
- "\" could not be found. under \"Configuration:ContentTypes\". There are " + contentTypes.Count +
- " ContentTypes in the Repository."
- );
- }
- var contentType = contentTypes[topic.Attributes.GetValue("ContentType", "Page")];
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Update content types collection, if appropriate
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (topic is ContentTypeDescriptor && !contentTypes.Contains(topic.Key)) {
- _contentTypeDescriptors.Add(topic as ContentTypeDescriptor);
- }
+ var contentType = GetContentTypeDescriptors()[topic.Attributes.GetValue("ContentType", "Page")];
/*------------------------------------------------------------------------------------------------------------------------
| Establish attribute strings
\-----------------------------------------------------------------------------------------------------------------------*/
- // Strings are immutable, use a stringbuilder to save memory
+ // Strings are immutable, use a StringBuilder to save memory
var attributes = new StringBuilder();
var nullAttributes = new StringBuilder();
var blob = new StringBuilder();
@@ -908,10 +840,13 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f
}
/*------------------------------------------------------------------------------------------------------------------------
- | Catch excewption
+ | Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Failed to save Topic " + topic.Key + " (" + topic.Id + ") via " + _connectionString + ": " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException(
+ $"Failed to save Topic '{topic.Key}' ({topic.Id}) via '{_connectionString}': '{exception.Message}'",
+ exception
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -927,7 +862,7 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f
\-----------------------------------------------------------------------------------------------------------------------*/
if (isRecursive) {
foreach (var childTopic in topic.Children) {
- Contract.Assume(childTopic.Attributes.GetValue("ParentID") != null, "Assumes the Parent ID AttributeValue is available.");
+ Contract.Assume(childTopic.Attributes.GetInteger("ParentID", -1) > 0, "Assumes the Parent ID AttributeValue is available.");
childTopic.Attributes.SetValue("ParentID", returnVal.ToString());
Save(childTopic, isRecursive, isDraft);
}
@@ -996,8 +931,11 @@ public override void Move(Topic topic, Topic target, Topic sibling) {
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Failed to move Topic " + topic.Key + " (" + topic.Id + ") to " + target.Key + " (" + target.Id + "): " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException(
+ $"Failed to move Topic '{topic.Key}' ({topic.Id}) to '{target.Key}' ({target.Id}): '{exception.Message}'",
+ exception
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -1063,8 +1001,11 @@ public override void Delete(Topic topic, bool isRecursive = false) {
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Failed to delete Topic " + topic.Key + " (" + topic.Id + "): " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException(
+ $"Failed to delete Topic '{topic.Key}' ({topic.Id}): '{exception.Message}'",
+ exception
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -1158,8 +1099,11 @@ private static string PersistRelations(Topic topic, SqlConnection connection, bo
/*------------------------------------------------------------------------------------------------------------------------
| Catch exception
\-----------------------------------------------------------------------------------------------------------------------*/
- catch (Exception ex) {
- throw new Exception("Failed to persist relationships for Topic " + topic.Key + " (" + topic.Id + "): " + ex.Message);
+ catch (SqlException exception) {
+ throw new TopicRepositoryException(
+ $"Failed to persist relationships for Topic '{topic.Key}' ({topic.Id}): '{exception.Message}'",
+ exception
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -1183,9 +1127,9 @@ private static string PersistRelations(Topic topic, SqlConnection connection, bo
| METHOD: CREATE RELATIONSHIPS BLOB
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Internal helper function to build string of related xml nodes for each scope of related items in model.
+ /// Internal helper function to build string of related XML nodes for each scope of related items in model.
///
- /// The topic object for which to create the relationsihps.
+ /// The topic object for which to create the relationships.
/// The blob string.
/// topic != null
private static string CreateRelationshipsBlob(Topic topic) {
@@ -1196,7 +1140,7 @@ private static string CreateRelationshipsBlob(Topic topic) {
Contract.Requires(topic != null, "The topic must not be null.");
/*------------------------------------------------------------------------------------------------------------------------
- | Create blog string container
+ | Create blob string container
\-----------------------------------------------------------------------------------------------------------------------*/
var blob = new StringBuilder("");
@@ -1259,7 +1203,7 @@ private static SqlDbType ConvDbType(string sqlDbType) {
| METHOD: ADD SQL PARAMETER
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Wrapper function that adds a SQL paramter to a command object.
+ /// Wrapper function that adds a SQL parameter to a command object.
///
/// The SQL command object.
/// The SQL parameter.
@@ -1273,7 +1217,7 @@ SqlDbType sqlDbType
) => AddSqlParameter(commandObject, sqlParameter, fieldValue, sqlDbType, ParameterDirection.Input, -1);
///
- /// Adds a SQL paramter to a command object, additionally setting the specified parameter direction.
+ /// Adds a SQL parameter to a command object, additionally setting the specified parameter direction.
///
/// The SQL command object.
/// The SQL parameter.
@@ -1287,7 +1231,7 @@ SqlDbType sqlDbType
) => AddSqlParameter(commandObject, sqlParameter, null, sqlDbType, paramDirection, -1);
///
- /// Adds a SQL paramter to a command object, additionally setting the specified SQL data length for the field.
+ /// Adds a SQL parameter to a command object, additionally setting the specified SQL data length for the field.
///
/// The SQL command object.
/// The SQL parameter.
diff --git a/Ignia.Topics.Tests/AttributeValueCollectionTest.cs b/Ignia.Topics.Tests/AttributeValueCollectionTest.cs
index 1737ed9a..a2c2e887 100644
--- a/Ignia.Topics.Tests/AttributeValueCollectionTest.cs
+++ b/Ignia.Topics.Tests/AttributeValueCollectionTest.cs
@@ -26,7 +26,7 @@ public class AttributeValueCollectionTest {
/// Creates a new topic and ensures that the key can be returned as an attribute.
///
[TestMethod]
- public void AttributeValueCollection_GetValueTest() {
+ public void GetValue() {
var topic = TopicFactory.Create("Test", "Container");
Assert.AreEqual("Test", topic.Attributes.GetValue("Key"));
}
@@ -38,7 +38,7 @@ public void AttributeValueCollection_GetValueTest() {
/// Ensures that integer values can be set and retrieved as expected.
///
[TestMethod]
- public void AttributeValueCollection_GetIntegerTest() {
+ public void GetInteger() {
var topic = TopicFactory.Create("Test", "Container");
@@ -60,7 +60,7 @@ public void AttributeValueCollection_GetIntegerTest() {
/// Ensures that integer values can be set and retrieved as expected.
///
[TestMethod]
- public void AttributeValueCollection_GetDateTimeTest() {
+ public void GetDateTime() {
var topic = TopicFactory.Create("Test", "Container");
var dateTime1 = new DateTime(1976, 10, 15);
@@ -84,7 +84,7 @@ public void AttributeValueCollection_GetDateTimeTest() {
/// Ensures that boolean values can be set and retrieved as expected.
///
[TestMethod]
- public void AttributeValueCollection_GetBooleanTest() {
+ public void GetBoolean() {
var topic = TopicFactory.Create("Test", "Container");
@@ -110,7 +110,7 @@ public void AttributeValueCollection_GetBooleanTest() {
/// Creates a new topic and requests an invalid attribute; ensures falls back to the default.
///
[TestMethod]
- public void AttributeValueCollection_DefaultValueTest() {
+ public void DefaultValue() {
var topic = TopicFactory.Create("Test", "Container");
Assert.AreEqual("Foo", topic.Attributes.GetValue("InvalidAttribute", "Foo"));
}
@@ -122,7 +122,7 @@ public void AttributeValueCollection_DefaultValueTest() {
/// Sets a custom attribute on a topic and ensures it can be retrieved.
///
[TestMethod]
- public void AttributeValueCollection_SetValueTest() {
+ public void SetValue() {
var topic = TopicFactory.Create("Test", "Container");
topic.Attributes.SetValue("Foo", "Bar");
Assert.AreEqual("Bar", topic.Attributes.GetValue("Foo"));
@@ -135,7 +135,7 @@ public void AttributeValueCollection_SetValueTest() {
/// Modifies the value of a custom attribute on a topic and ensures it is marked as IsDirty.
///
[TestMethod]
- public void AttributeValueCollection_SetValue_IsDirtyTest() {
+ public void SetValue_IsDirtyTest() {
var topic = TopicFactory.Create("Test", "Container");
@@ -159,7 +159,7 @@ public void AttributeValueCollection_SetValue_IsDirtyTest() {
/// retrieved.
///
[TestMethod]
- public void AttributeValueCollection_SetValue_BackdoorTest() {
+ public void SetValue_BackdoorTest() {
var topic = TopicFactory.Create("Test", "Container");
@@ -178,8 +178,8 @@ public void AttributeValueCollection_SetValue_BackdoorTest() {
/// Attempts to violate the business logic by bypassing the property setter; ensures that business logic is enforced.
///
[TestMethod]
- [ExpectedException(typeof(TargetInvocationException), "The topic allowed a key to be set via a backdoor, without routing it through the Key property.")]
- public void AttributeValueCollection_EnforceBusinessLogicTest() {
+ [ExpectedException(typeof(TargetInvocationException), "The topic allowed a key to be set via a back door, without routing it through the Key property.")]
+ public void EnforceBusinessLogic() {
var topic = TopicFactory.Create("Test", "Container");
topic.Attributes.SetValue("Key", "# ?");
}
@@ -191,8 +191,8 @@ public void AttributeValueCollection_EnforceBusinessLogicTest() {
/// Attempts to violate the business logic by bypassing SetValue() entirely; ensures that business logic is enforced.
///
[TestMethod]
- [ExpectedException(typeof(TargetInvocationException), "The topic allowed a key to be set via a backdoor, without routing it through the Key property.")]
- public void AttributeValueCollection_EnforceBusinessLogic_BackdoorTest() {
+ [ExpectedException(typeof(TargetInvocationException), "The topic allowed a key to be set via a back door, without routing it through the Key property.")]
+ public void EnforceBusinessLogic_Backdoor() {
var topic = TopicFactory.Create("Test", "Container");
topic.Attributes.Remove("Key");
topic.Attributes.Add(new AttributeValue("Key", "# ?"));
@@ -205,7 +205,7 @@ public void AttributeValueCollection_EnforceBusinessLogic_BackdoorTest() {
/// Sets an attribute on the parent of a topic and ensures it can be retrieved using inheritance.
///
[TestMethod]
- public void AttributeValueCollection_InheritFromParentTest() {
+ public void InheritFromParent() {
var topics = new Topic[8];
@@ -230,7 +230,7 @@ public void AttributeValueCollection_InheritFromParentTest() {
/// Establishes a long tree of derives topics, and ensures that inheritance will pursue no more than five hops.
///
[TestMethod]
- public void AttributeValueCollection_MaxHopsTest() {
+ public void MaxHops() {
var topics = new Topic[8];
diff --git a/Ignia.Topics.Tests/ITopicRepositoryTest.cs b/Ignia.Topics.Tests/ITopicRepositoryTest.cs
index 85be5757..715ef268 100644
--- a/Ignia.Topics.Tests/ITopicRepositoryTest.cs
+++ b/Ignia.Topics.Tests/ITopicRepositoryTest.cs
@@ -54,7 +54,7 @@ public ITopicRepositoryTest() {
/// Loads topics and ensures there are the expected number of children.
///
[TestMethod]
- public void ITopicRepository_LoadTest() {
+ public void Load() {
var rootTopic = _topicRepository.Load();
var topic = _topicRepository.Load("Root:Configuration:ContentTypes:Page");
@@ -66,6 +66,22 @@ public void ITopicRepository_LoadTest() {
}
+ /*==========================================================================================================================
+ | TEST: LOAD BY ID
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Loads topic by ID and ensures it is found.
+ ///
+ [TestMethod]
+ public void LoadById() {
+
+ var topic = _topicRepository.Load(11111);
+
+ Assert.IsNotNull(topic);
+ Assert.AreEqual("Web_1_1_1_1", topic.Key);
+
+ }
+
/*==========================================================================================================================
| TEST: SAVE
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -73,7 +89,7 @@ public void ITopicRepository_LoadTest() {
/// Saves topics and ensures their identifiers are properly set.
///
[TestMethod]
- public void ITopicRepository_SaveTest() {
+ public void Save() {
var rootTopic = _topicRepository.Load();
var web = _topicRepository.Load("Root:Web");
@@ -100,22 +116,22 @@ public void ITopicRepository_SaveTest() {
/// Moves topics and ensures their parents are correctly set.
///
[TestMethod]
- public void ITopicRepository_MoveTest() {
+ public void Move() {
var rootTopic = _topicRepository.Load();
var source = _topicRepository.Load("Root:Web:Web_0");
var destination = _topicRepository.Load("Root:Web:Web_1");
- var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_2");
+ var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1");
Assert.ReferenceEquals(topic.Parent, source);
- Assert.AreEqual(3, destination.Children.Count());
- Assert.AreEqual(3, source.Children.Count());
+ Assert.AreEqual(2, destination.Children.Count());
+ Assert.AreEqual(2, source.Children.Count());
_topicRepository.Move(topic, destination);
Assert.ReferenceEquals(topic.Parent, destination);
- Assert.AreEqual(2, source.Children.Count());
- Assert.AreEqual(4, destination.Children.Count());
+ Assert.AreEqual(1, source.Children.Count());
+ Assert.AreEqual(3, destination.Children.Count());
}
@@ -126,7 +142,7 @@ public void ITopicRepository_MoveTest() {
/// Moves topic next to a different sibling and ensures it ends up in the correct location.
///
[TestMethod]
- public void ITopicRepository_MoveToSiblingTest() {
+ public void MoveToSibling() {
var rootTopic = _topicRepository.Load();
var parent = _topicRepository.Load("Root:Web:Web_0");
@@ -135,12 +151,12 @@ public void ITopicRepository_MoveToSiblingTest() {
Assert.ReferenceEquals(topic.Parent, parent);
Assert.AreEqual("Web_0_0", parent.Children.First().Key);
- Assert.AreEqual(3, parent.Children.Count());
+ Assert.AreEqual(2, parent.Children.Count());
_topicRepository.Move(topic, parent, sibling);
Assert.ReferenceEquals(topic.Parent, parent);
- Assert.AreEqual(3, parent.Children.Count());
+ Assert.AreEqual(2, parent.Children.Count());
Assert.AreEqual("Web_0_1", parent.Children.First().Key);
Assert.AreEqual("Web_0_0", parent.Children[1].Key);
@@ -153,26 +169,45 @@ public void ITopicRepository_MoveToSiblingTest() {
/// Deletes a topic to ensure it is properly removed.
///
[TestMethod]
- public void ITopicRepository_DeleteTest() {
+ public void Delete() {
- var parent = _topicRepository.Load("Root:Web:Web_2");
- var topic = _topicRepository.Load("Root:Web:Web_2:Web_2_2");
- var child = _topicRepository.Load("Root:Web:Web_2:Web_2_2:Web_2_2_0");
+ var parent = _topicRepository.Load("Root:Web:Web_1");
+ var topic = _topicRepository.Load("Root:Web:Web_1:Web_1_1");
+ var child = _topicRepository.Load("Root:Web:Web_1:Web_1_1:Web_1_1_0");
- Assert.AreEqual(3, parent.Children.Count());
+ Assert.AreEqual(2, parent.Children.Count());
_topicRepository.Delete(topic);
- Assert.AreEqual(2, parent.Children.Count());
+ Assert.AreEqual(1, parent.Children.Count());
- topic = _topicRepository.Load("Root:Web:Web_2:Web_2_2");
- child = _topicRepository.Load("Root:Web:Web_2:Web_2_2:Web_2_2_0");
+ topic = _topicRepository.Load("Root:Web:Web_1:Web_1_1");
+ child = _topicRepository.Load("Root:Web:Web_1:Web_1_1:Web_1_1_0");
Assert.IsNull(topic);
Assert.IsNull(child);
}
+ /*==========================================================================================================================
+ | TEST: GET CONTENT TYPE DESCRIPTORS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a list of s from the and ensures that
+ /// the expected number (2) are present.
+ ///
+ [TestMethod]
+ public void GetContentTypeDescriptors() {
+
+ var contentTypes = _topicRepository.GetContentTypeDescriptors();
+
+ Assert.AreEqual(7, contentTypes.Count);
+ Assert.IsNotNull(contentTypes.GetTopic("ContentTypeDescriptor"));
+ Assert.IsNotNull(contentTypes.GetTopic("Page"));
+ Assert.IsNotNull(contentTypes.GetTopic("LookupListItem"));
+
+ }
+
} //Class
diff --git a/Ignia.Topics.Tests/Ignia.Topics.Tests.csproj b/Ignia.Topics.Tests/Ignia.Topics.Tests.csproj
index 58dd991a..16447f63 100644
--- a/Ignia.Topics.Tests/Ignia.Topics.Tests.csproj
+++ b/Ignia.Topics.Tests/Ignia.Topics.Tests.csproj
@@ -9,7 +9,7 @@
Properties
Ignia.Topics.Tests
Ignia.Topics.Tests
- v4.5
+ v4.7
512
{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
10.0
@@ -19,6 +19,17 @@
UnitTest
+
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -30,6 +41,7 @@
4
bin\Debug\Ignia.Topics.Tests.XML
CS1591
+ latest
pdbonly
@@ -96,6 +108,8 @@
+
+
@@ -108,12 +122,15 @@
+
+
+
diff --git a/Ignia.Topics.Tests/Properties/AssemblyInfo.cs b/Ignia.Topics.Tests/Properties/AssemblyInfo.cs
index cac244f3..47a6b760 100644
--- a/Ignia.Topics.Tests/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.Tests/Properties/AssemblyInfo.cs
@@ -1,36 +1,27 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.Tests")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2018 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia OnTopic Unit Tests")]
+[assembly: AssemblyDescription("Provides unit tests for the OnTopic library.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.Tests")]
-[assembly: AssemblyCopyright("Copyright © 2015")]
[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: AssemblyVersion("3.6.1791.0")]
+[assembly: AssemblyFileVersion("3.5.1839.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("27632801-bfe3-41d9-8678-3c4bbe45e6c9")]
-
-// 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/Ignia.Topics.Tests/RelatedTopicCollectionTest.cs b/Ignia.Topics.Tests/RelatedTopicCollectionTest.cs
index 577ad25d..584eae75 100644
--- a/Ignia.Topics.Tests/RelatedTopicCollectionTest.cs
+++ b/Ignia.Topics.Tests/RelatedTopicCollectionTest.cs
@@ -26,7 +26,7 @@ public class RelatedTopicCollectionTest {
/// Sets a relationship and confirms that it is accessible.
///
[TestMethod]
- public void RelatedTopicCollection_SetTopic() {
+ public void SetTopic() {
var parent = TopicFactory.Create("Parent", "Page");
var related = TopicFactory.Create("Related", "Page");
@@ -44,7 +44,7 @@ public void RelatedTopicCollection_SetTopic() {
/// Sets a relationship and confirms that it is accessible on incoming relationships property.
///
[TestMethod]
- public void RelatedTopicCollection_IncomingRelationshipTest() {
+ public void IncomingRelationship() {
var parent = TopicFactory.Create("Parent", "Page");
var related = TopicFactory.Create("Related", "Page");
@@ -63,7 +63,7 @@ public void RelatedTopicCollection_IncomingRelationshipTest() {
/// Sets relationships in multiple namespaces, and the correct number of keys are returned.
///
[TestMethod]
- public void RelatedTopicCollection_KeysTest() {
+ public void Keys() {
var parent = TopicFactory.Create("Parent", "Page");
var relationships = new RelatedTopicCollection(parent);
@@ -84,7 +84,7 @@ public void RelatedTopicCollection_KeysTest() {
/// Sets relationships in multiple namespaces, and ensures they are all returned via GetAllTopics().
///
[TestMethod]
- public void RelatedTopicCollection_GetAllTopicsTest() {
+ public void GetAllTopics() {
var parent = TopicFactory.Create("Parent", "Page");
var relationships = new RelatedTopicCollection(parent);
@@ -107,7 +107,7 @@ public void RelatedTopicCollection_GetAllTopicsTest() {
/// by content type.
///
[TestMethod]
- public void RelatedTopicCollection_GetAllContentTypesTest() {
+ public void GetAllContentTypes() {
var parent = TopicFactory.Create("Parent", "Page");
var relationships = new RelatedTopicCollection(parent);
diff --git a/Ignia.Topics.Tests/TestDoubles/FakeTopicLookupService.cs b/Ignia.Topics.Tests/TestDoubles/FakeTopicLookupService.cs
new file mode 100644
index 00000000..8406464f
--- /dev/null
+++ b/Ignia.Topics.Tests/TestDoubles/FakeTopicLookupService.cs
@@ -0,0 +1,34 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+
+namespace Ignia.Topics.Tests.TestDoubles {
+
+ /*============================================================================================================================
+ | CLASS: FAKE TOPIC LOOKUP SERVICE
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides access to derived types of classes.
+ ///
+ ///
+ /// Allows testing of services that depend on without using expensive reflection.
+ ///
+ internal class FakeTopicLookupService: StaticTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Instantiates a new instance of the .
+ ///
+ /// A new instance of the .
+ internal FakeTopicLookupService(): base(null, typeof(Topic)) {
+ Add(typeof(Topic));
+ Add(typeof(ContentTypeDescriptor));
+ Add(typeof(AttributeDescriptor));
+ }
+
+ }
+}
diff --git a/Ignia.Topics.Tests/TestDoubles/FakeTopicRepository.cs b/Ignia.Topics.Tests/TestDoubles/FakeTopicRepository.cs
index 578a46e5..51488d42 100644
--- a/Ignia.Topics.Tests/TestDoubles/FakeTopicRepository.cs
+++ b/Ignia.Topics.Tests/TestDoubles/FakeTopicRepository.cs
@@ -5,7 +5,7 @@
\=============================================================================================================================*/
using System;
using System.Diagnostics.Contracts;
-using Ignia.Topics.Collections;
+using Ignia.Topics.Querying;
using Ignia.Topics.Repositories;
namespace Ignia.Topics.Tests.TestDoubles {
@@ -20,7 +20,6 @@ namespace Ignia.Topics.Tests.TestDoubles {
/// Allows testing of services that depend on without maintaining a dependency on a live
/// database, or working against actual data. This is faster and safer for test methods.
///
-
public class FakeTopicRepository : TopicRepositoryBase, ITopicRepository {
/*==========================================================================================================================
@@ -40,31 +39,20 @@ public FakeTopicRepository() : base() {
CreateFakeData();
}
- /*==========================================================================================================================
- | GET CONTENT TYPE DESCRIPTORS
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Retrieves a collection of Content Type Descriptor objects from the configuration section of the data provider.
- ///
- public override ContentTypeDescriptorCollection GetContentTypeDescriptors() {
- throw new NotImplementedException();
- }
-
/*==========================================================================================================================
| METHOD: LOAD
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic identifier.
/// Determines whether or not to recurse through and load a topic's children.
/// A topic object.
- public override Topic Load(int topicId, bool isRecursive = true) {
- throw new NotImplementedException();
- }
+ public override Topic Load(int topicId, bool isRecursive = true) =>
+ (topicId < 0)? _cache :_cache.FindFirst(t => t.Id.Equals(topicId));
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified key name.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified key name.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
@@ -75,7 +63,8 @@ public override Topic Load(string topicKey = null, bool isRecursive = true) {
| Lookup by TopicKey
\-----------------------------------------------------------------------------------------------------------------------*/
if (!String.IsNullOrWhiteSpace(topicKey)) {
- throw new NotImplementedException();
+ topicKey = topicKey.Contains(":") ? topicKey : "Root:" + topicKey;
+ return _cache.FindFirst(t => t.GetUniqueKey().Equals(topicKey));
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -215,20 +204,13 @@ public override void Move(Topic topic, Topic target, Topic sibling) {
/// topic != null
/// topic
/// Failed to delete Topic topic.Key (topic.Id): ex.Message
- public override void Delete(Topic topic, bool isRecursive = false) {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Delete from memory
- \-----------------------------------------------------------------------------------------------------------------------*/
- base.Delete(topic, isRecursive);
-
- }
+ public override void Delete(Topic topic, bool isRecursive = false) => base.Delete(topic, isRecursive);
/*==========================================================================================================================
| METHOD: CREATE FAKE DATA
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Creates a collection of fake data that loosely mimics a barebones database.
+ /// Creates a collection of fake data that loosely mimics a bare bones database.
///
private void CreateFakeData() {
@@ -243,9 +225,12 @@ private void CreateFakeData() {
var configuration = TopicFactory.Create("Configuration", "Container", rootTopic);
var contentTypes = TopicFactory.Create("ContentTypes", "ContentTypeDescriptor", configuration);
- TopicFactory.Create("ContentType", "ContentTypeDescriptor", contentTypes);
+ TopicFactory.Create("ContentTypeDescriptor", "ContentTypeDescriptor", contentTypes);
TopicFactory.Create("Page", "ContentTypeDescriptor", contentTypes);
TopicFactory.Create("Container", "ContentTypeDescriptor", contentTypes);
+ TopicFactory.Create("Lookup", "ContentTypeDescriptor", contentTypes);
+ TopicFactory.Create("LookupListItem", "ContentTypeDescriptor", contentTypes);
+ TopicFactory.Create("List", "ContentTypeDescriptor", contentTypes);
/*------------------------------------------------------------------------------------------------------------------------
| Establish metadata
@@ -263,7 +248,7 @@ private void CreateFakeData() {
\-----------------------------------------------------------------------------------------------------------------------*/
var web = TopicFactory.Create("Web", "Page", 10000, rootTopic);
- CreateFakeData(web, 3, 3);
+ CreateFakeData(web, 2, 3);
/*------------------------------------------------------------------------------------------------------------------------
| Set to cache
diff --git a/Ignia.Topics.Tests/TestDoubles/FakeViewModelLookupService.cs b/Ignia.Topics.Tests/TestDoubles/FakeViewModelLookupService.cs
new file mode 100644
index 00000000..c5a662b4
--- /dev/null
+++ b/Ignia.Topics.Tests/TestDoubles/FakeViewModelLookupService.cs
@@ -0,0 +1,39 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using Ignia.Topics.Tests.ViewModels;
+using Ignia.Topics.ViewModels;
+
+namespace Ignia.Topics.Tests.TestDoubles {
+
+ /*============================================================================================================================
+ | CLASS: FAKE TOPIC LOOKUP SERVICE
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides access to derived types of classes.
+ ///
+ ///
+ /// Allows testing of services that depend on without using expensive reflection.
+ ///
+ internal class FakeViewModelLookupService: TopicViewModelLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Instantiates a new instance of the .
+ ///
+ /// A new instance of the .
+ internal FakeViewModelLookupService(): base(null, typeof(object)) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Add test specific view models
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Add(typeof(CircularTopicViewModel));
Add(typeof(DefaultValueTopicViewModel));
Add(typeof(FilteredTopicViewModel));
Add(typeof(FlattenChildrenTopicViewModel));
Add(typeof(MetadataLookupTopicViewModel));
Add(typeof(MethodBasedViewModel));
Add(typeof(MinimumLengthPropertyTopicViewModel));
Add(typeof(RequiredObjectTopicViewModel));
Add(typeof(RequiredTopicViewModel));
Add(typeof(SampleTopicViewModel));
+
+ }
+
+ }
+}
diff --git a/Ignia.Topics.Tests/TopicCollectionTest.cs b/Ignia.Topics.Tests/TopicCollectionTest.cs
index a10715a4..0d81c2c1 100644
--- a/Ignia.Topics.Tests/TopicCollectionTest.cs
+++ b/Ignia.Topics.Tests/TopicCollectionTest.cs
@@ -26,7 +26,7 @@ public class TopicCollectionTest {
/// Establishes a number of topics, then accesses them by key.
///
[TestMethod]
- public void TopicCollection_SetTopicTest() {
+ public void SetTopic() {
var topics = new TopicCollection();
@@ -45,7 +45,7 @@ public void TopicCollection_SetTopicTest() {
/// Establishes a number of topics, then seeds a new with them.
///
[TestMethod]
- public void TopicCollection_PrepopulateTest() {
+ public void Prepopulate() {
var topics = new List();
@@ -66,7 +66,7 @@ public void TopicCollection_PrepopulateTest() {
/// Establishes a number of topics, converts the collection to read only, and ensures they are still present.
///
[TestMethod]
- public void TopicCollection_AsReadOnlyTest() {
+ public void AsReadOnly() {
var topics = new TopicCollection();
diff --git a/Ignia.Topics.Tests/TopicControllerTest.cs b/Ignia.Topics.Tests/TopicControllerTest.cs
index 5715e9e7..f86da300 100644
--- a/Ignia.Topics.Tests/TopicControllerTest.cs
+++ b/Ignia.Topics.Tests/TopicControllerTest.cs
@@ -5,6 +5,7 @@
\=============================================================================================================================*/
using System;
using System.Linq;
+using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Ignia.Topics.Data.Caching;
@@ -33,6 +34,9 @@ public class TopicControllerTest {
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
ITopicRepository _topicRepository = null;
+ RouteData _routeData = new RouteData();
+ Uri _uri = new Uri("http://localhost/Web/Web_0/Web_0_1/Web_0_1_1");
+ Topic _topic = null;
/*==========================================================================================================================
| CONSTRUCTOR
@@ -44,20 +48,43 @@ public class TopicControllerTest {
/// 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.
+ /// crawling the object graph. In addition, it initializes a shared reference to use for the various
+ /// tests.
///
public TopicControllerTest() {
- _topicRepository = new CachedTopicRepository(new FakeTopicRepository());
+ _topicRepository = new CachedTopicRepository(new FakeTopicRepository());
+ _topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1:Web_0_1_1");
+ }
+
+ /*==========================================================================================================================
+ | TEST: TOPIC
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Triggers the action.
+ ///
+ [TestMethod]
+ public async Task TopicController_IndexAsync() {
+
+ var topicRoutingService = new MvcTopicRoutingService(_topicRepository, _uri, _routeData);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+
+ var controller = new TopicController(_topicRepository, topicRoutingService, mappingService);
+ var result = await controller.IndexAsync(_topic.GetWebPath()) as TopicViewResult;
+ var model = result.Model as PageTopicViewModel;
+
+ Assert.IsNotNull(model);
+ Assert.AreEqual("Web_0_1_1", model.Title);
+
}
/*==========================================================================================================================
| TEST: ERROR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Triggers the action.
+ /// Triggers the action.
///
[TestMethod]
- public void ErrorController_ErrorTest() {
+ public void ErrorController_Error() {
var controller = new ErrorController();
var result = controller.Error("ErrorPage") as ViewResult;
@@ -72,10 +99,10 @@ public void ErrorController_ErrorTest() {
| TEST: NOT FOUND ERROR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Triggers the action.
+ /// Triggers the action.
///
[TestMethod]
- public void ErrorController_NotFoundTest() {
+ public void ErrorController_NotFound() {
var controller = new ErrorController();
var result = controller.Error("NotFoundPage") as ViewResult;
@@ -90,10 +117,10 @@ public void ErrorController_NotFoundTest() {
| TEST: INTERNAL SERVER ERROR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Triggers the action.
+ /// Triggers the action.
///
[TestMethod]
- public void ErrorController_InternalServerTest() {
+ public void ErrorController_InternalServer() {
var controller = new ErrorController();
var result = controller.Error("InternalServer") as ViewResult;
@@ -111,7 +138,7 @@ public void ErrorController_InternalServerTest() {
/// Triggers the action.
///
[TestMethod]
- public void FallbackController_IndexTest() {
+ public void FallbackController_Index() {
var controller = new FallbackController();
var result = controller.Index() as HttpNotFoundResult;
@@ -129,7 +156,7 @@ public void FallbackController_IndexTest() {
/// Triggers the action.
///
[TestMethod]
- public void RedirectController_TopicRedirectTest() {
+ public void RedirectController_TopicRedirect() {
var controller = new RedirectController(_topicRepository);
var result = controller.Redirect(11110) as RedirectResult;
@@ -153,7 +180,7 @@ public void RedirectController_TopicRedirectTest() {
///
[TestMethod]
[ExpectedException(typeof(NullReferenceException), AllowDerivedTypes=false)]
- public void SitemapController_IndexTest() {
+ public void SitemapController_Index() {
var controller = new SitemapController(_topicRepository);
var result = controller.Index() as ViewResult;
@@ -173,24 +200,20 @@ public void SitemapController_IndexTest() {
/// Triggers the action.
///
[TestMethod]
- public void LayoutController_MenuTest() {
-
- var routes = new RouteData();
- var uri = new Uri("http://localhost/Web/Web_0/Web_0_1/Web_0_1_1");
- var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1:Web_0_1_1");
+ public async Task Menu() {
- var topicRoutingService = new MvcTopicRoutingService(_topicRepository, uri, routes);
- var mappingService = new TopicMappingService(_topicRepository);
+ var topicRoutingService = new MvcTopicRoutingService(_topicRepository, _uri, _routeData);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var controller = new LayoutController(_topicRepository, topicRoutingService, mappingService);
- var result = controller.Menu() as PartialViewResult;
+ var result = await controller.Menu() as PartialViewResult;
var model = result.Model as NavigationViewModel;
Assert.IsNotNull(model);
- Assert.AreEqual(topic.GetUniqueKey(), model.CurrentKey);
+ 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(2, model.NavigationRoot.Children.Count());
+ Assert.IsTrue(model.NavigationRoot.IsSelected(_topic.GetUniqueKey()));
}
diff --git a/Ignia.Topics.Tests/TopicMappingServiceTest.cs b/Ignia.Topics.Tests/TopicMappingServiceTest.cs
index 526a24f8..a6ef8f35 100644
--- a/Ignia.Topics.Tests/TopicMappingServiceTest.cs
+++ b/Ignia.Topics.Tests/TopicMappingServiceTest.cs
@@ -5,6 +5,7 @@
\=============================================================================================================================*/
using System.ComponentModel.DataAnnotations;
using System.Linq;
+using System.Threading.Tasks;
using Ignia.Topics.Data.Caching;
using Ignia.Topics.Mapping;
using Ignia.Topics.Repositories;
@@ -46,23 +47,22 @@ public TopicMappingServiceTest() {
}
/*==========================================================================================================================
- | TEST: MAP (INSTANCE)
+ | TEST: MAP (GENERIC)
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Establishes a and tests setting basic scalar values by providing it with an already
- /// constructed instance of a DTO.
+ /// Establishes a and tests setting basic scalar values by specifying an explicit type.
///
[TestMethod]
- public void TopicMappingService_MapGeneric() {
+ public async Task MapGeneric() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var topic = TopicFactory.Create("Test", "Page");
topic.Attributes.SetValue("MetaTitle", "ValueA");
topic.Attributes.SetValue("Title", "Value1");
topic.Attributes.SetValue("IsHidden", "1");
- var target = (PageTopicViewModel)mappingService.Map(topic, new PageTopicViewModel());
+ var target = await mappingService.MapAsync(topic);
Assert.AreEqual("ValueA", target.MetaTitle);
Assert.AreEqual("Value1", target.Title);
@@ -78,16 +78,16 @@ public void TopicMappingService_MapGeneric() {
/// determine the instance type.
///
[TestMethod]
- public void TopicMappingService_MapDynamic() {
+ public async Task MapDynamic() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var topic = TopicFactory.Create("Test", "Page");
topic.Attributes.SetValue("MetaTitle", "ValueA");
topic.Attributes.SetValue("Title", "Value1");
topic.Attributes.SetValue("IsHidden", "1");
- var target = (PageTopicViewModel)mappingService.Map(topic);
+ var target = (PageTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual("ValueA", target.MetaTitle);
Assert.AreEqual("Value1", target.Title);
@@ -102,9 +102,9 @@ public void TopicMappingService_MapDynamic() {
/// Establishes a and tests whether it successfully crawls the parent tree.
///
[TestMethod]
- public void TopicMappingService_MapParents() {
+ public async Task MapParents() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var grandParent = TopicFactory.Create("Grandparent", "Sample");
var parent = TopicFactory.Create("Parent", "Page", grandParent);
var topic = TopicFactory.Create("Test", "Page", parent);
@@ -114,13 +114,11 @@ public void TopicMappingService_MapParents() {
topic.Attributes.SetValue("IsHidden", "1");
parent.Attributes.SetValue("Title", "Value2");
- parent.Attributes.SetValue("IsHidden", "1");
grandParent.Attributes.SetValue("Title", "Value3");
- grandParent.Attributes.SetValue("IsHidden", "1");
grandParent.Attributes.SetValue("Property", "ValueB");
- var viewModel = (PageTopicViewModel)mappingService.Map(topic);
+ var viewModel = (PageTopicViewModel) await mappingService.MapAsync(topic);
var parentViewModel = viewModel?.Parent;
var grandParentViewModel = parentViewModel?.Parent as SampleTopicViewModel;
@@ -144,18 +142,17 @@ public void TopicMappingService_MapParents() {
/// .
///
[TestMethod]
- public void TopicMappingService_InheritValues() {
+ public async Task InheritValues() {
- var mappingService = new TopicMappingService(_topicRepository);
-
- var grandParent = TopicFactory.Create("Grandparent", "Page");
- var parent = TopicFactory.Create("Parent", "Page", grandParent);
- var topic = TopicFactory.Create("Test", "Sample", parent);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var grandParent = TopicFactory.Create("Grandparent", "Page");
+ var parent = TopicFactory.Create("Parent", "Page", grandParent);
+ var topic = TopicFactory.Create("Test", "Sample", parent);
grandParent.Attributes.SetValue("Property", "ValueA");
grandParent.Attributes.SetValue("InheritedProperty", "ValueB");
- var viewModel = (SampleTopicViewModel)mappingService.Map(topic);
+ var viewModel = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual(null, viewModel.Property);
Assert.AreEqual("ValueB", viewModel.InheritedProperty);
@@ -170,16 +167,15 @@ public void TopicMappingService_InheritValues() {
/// specified by .
///
[TestMethod]
- public void TopicMappingService_AlternateAttributeKey() {
+ public async Task AlternateAttributeKey() {
- var mappingService = new TopicMappingService(_topicRepository);
-
- var topic = TopicFactory.Create("Test", "Sample");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Test", "Sample");
topic.Attributes.SetValue("Property", "ValueA");
topic.Attributes.SetValue("PropertyAlias", "ValueB");
- var viewModel = (SampleTopicViewModel)mappingService.Map(topic);
+ var viewModel = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual("ValueA", viewModel.PropertyAlias);
@@ -192,19 +188,19 @@ public void TopicMappingService_AlternateAttributeKey() {
/// Establishes a and tests whether it successfully crawls the relationships.
///
[TestMethod]
- public void TopicMappingService_MapRelationships() {
+ public async Task MapRelationships() {
- var mappingService = new TopicMappingService(_topicRepository);
- var relatedTopic1 = TopicFactory.Create("RelatedTopic1", "Page");
- var relatedTopic2 = TopicFactory.Create("RelatedTopic2", "Index");
- var relatedTopic3 = TopicFactory.Create("RelatedTopic3", "Page");
- var topic = TopicFactory.Create("Test", "Sample");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var relatedTopic1 = TopicFactory.Create("RelatedTopic1", "Page");
+ var relatedTopic2 = TopicFactory.Create("RelatedTopic2", "Index");
+ var relatedTopic3 = TopicFactory.Create("RelatedTopic3", "Page");
+ var topic = TopicFactory.Create("Test", "Sample");
topic.Relationships.SetTopic("Cousins", relatedTopic1);
topic.Relationships.SetTopic("Cousins", relatedTopic2);
topic.Relationships.SetTopic("Siblings", relatedTopic3);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual(2, target.Cousins.Count);
Assert.IsNotNull(target.Cousins.FirstOrDefault((t) => t.Key.StartsWith("RelatedTopic1")));
@@ -228,9 +224,9 @@ public void TopicMappingService_MapRelationships() {
/// RelationshipAlias, and b) source from the collection.
///
[TestMethod]
- public void TopicMappingService_AlternateRelationship() {
+ public async Task AlternateRelationship() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var relatedTopic1 = TopicFactory.Create("RelatedTopic1", "Page");
var relatedTopic2 = TopicFactory.Create("RelatedTopic2", "Index");
var relatedTopic3 = TopicFactory.Create("RelatedTopic3", "Page");
@@ -249,7 +245,7 @@ public void TopicMappingService_AlternateRelationship() {
relatedTopic5.Relationships.SetTopic("AmbiguousRelationship", topic);
relatedTopic6.Relationships.SetTopic("AmbiguousRelationship", topic);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual(2, target.RelationshipAlias.Count);
Assert.IsNotNull(target.RelationshipAlias.FirstOrDefault((t) => t.Key.StartsWith("RelatedTopic5")));
@@ -264,16 +260,16 @@ public void TopicMappingService_AlternateRelationship() {
/// Establishes a and tests whether it successfully crawls the nested topics.
///
[TestMethod]
- public void TopicMappingService_MapNestedTopics() {
+ public async Task MapNestedTopics() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var topic = TopicFactory.Create("Test", "Sample");
var childTopic = TopicFactory.Create("ChildTopic", "Page", topic);
var topicList = TopicFactory.Create("Categories", "List", topic);
var nestedTopic1 = TopicFactory.Create("NestedTopic1", "Page", topicList);
var nestedTopic2 = TopicFactory.Create("NestedTopic2", "Index", topicList);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual(2, target.Categories.Count);
Assert.IsNotNull(target.Categories.FirstOrDefault((t) => t.Key.StartsWith("NestedTopic1")));
@@ -290,16 +286,16 @@ public void TopicMappingService_MapNestedTopics() {
/// Establishes a and tests whether it successfully crawls the nested topics.
///
[TestMethod]
- public void TopicMappingService_MapChildren() {
+ public async Task MapChildren() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var topic = TopicFactory.Create("Test", "Sample");
var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", topic);
var childTopic2 = TopicFactory.Create("ChildTopic2", "Page", topic);
var childTopic3 = TopicFactory.Create("ChildTopic3", "Sample", topic);
var childTopic4 = TopicFactory.Create("ChildTopic4", "Index", childTopic3);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel)await mappingService.MapAsync(topic);
Assert.AreEqual(3, target.Children.Count);
Assert.IsNotNull(target.Children.FirstOrDefault((t) => t.Key.StartsWith("ChildTopic1")));
@@ -310,6 +306,28 @@ public void TopicMappingService_MapChildren() {
}
+ /*==========================================================================================================================
+ | TEST: MAP TOPIC REFERENCES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a and tests whether it successfully maps referenced topics.
+ ///
+ [TestMethod]
+ public async Task MapTopicReferences() {
+
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+
+ var topic = TopicFactory.Create("Test", "Sample");
+
+ topic.Attributes.SetInteger("TopicReferenceId", 11111);
+
+ var target = (SampleTopicViewModel)await mappingService.MapAsync(topic);
+
+ Assert.IsNotNull(target.TopicReference);
+ Assert.AreEqual(11111, target.TopicReference.Id);
+
+ }
+
/*==========================================================================================================================
| TEST: RECURSIVE RELATIONSHIPS
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -318,26 +336,26 @@ public void TopicMappingService_MapChildren() {
/// instructions of each model class.
///
[TestMethod]
- public void TopicMappingService_RecursiveRelationships() {
+ public async Task RecursiveRelationships() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
//Self
- var topic = TopicFactory.Create("Test", "Sample");
+ var topic = TopicFactory.Create("Test", "Sample");
//First cousins
- var cousinTopic1 = TopicFactory.Create("CousinTopic1", "Page");
- var cousinTopic2 = TopicFactory.Create("CousinTopic2", "Index");
- var cousinTopic3 = TopicFactory.Create("CousinTopic3", "Sample");
+ var cousinTopic1 = TopicFactory.Create("CousinTopic1", "Page");
+ var cousinTopic2 = TopicFactory.Create("CousinTopic2", "Index");
+ var cousinTopic3 = TopicFactory.Create("CousinTopic3", "Sample");
//Children of cousins
- var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", cousinTopic3);
- var childTopic2 = TopicFactory.Create("ChildTopic2", "Page", cousinTopic3);
- var childTopic3 = TopicFactory.Create("ChildTopic3", "Sample", cousinTopic3);
+ var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", cousinTopic3);
+ var childTopic2 = TopicFactory.Create("ChildTopic2", "Page", cousinTopic3);
+ var childTopic3 = TopicFactory.Create("ChildTopic3", "Sample", cousinTopic3);
//Other cousins
- var secondCousin = TopicFactory.Create("SecondCousin", "Page");
- var cousinTwiceRemoved = TopicFactory.Create("CousinOnceRemoved", "Page", childTopic3);
+ var secondCousin = TopicFactory.Create("SecondCousin", "Page");
+ var cousinTwiceRemoved = TopicFactory.Create("CousinOnceRemoved", "Page", childTopic3);
//Set first cousins
topic.Relationships.SetTopic("Cousins", cousinTopic1);
@@ -347,9 +365,9 @@ public void TopicMappingService_RecursiveRelationships() {
//Set ancillary relationships
cousinTopic3.Relationships.SetTopic("Cousins", secondCousin);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
- var cousinTarget = (SampleTopicViewModel)target.Cousins.FirstOrDefault((t) => t.Key.StartsWith("CousinTopic3"));
- var distantCousinTarget = (SampleTopicViewModel)cousinTarget.Children.FirstOrDefault((t) => t.Key.StartsWith("ChildTopic3"));
+ var target = (SampleTopicViewModel) await mappingService.MapAsync(topic);
+ var cousinTarget = (SampleTopicViewModel)target.Cousins.FirstOrDefault((t) => t.Key.StartsWith("CousinTopic3"));
+ var distantCousinTarget = (SampleTopicViewModel)cousinTarget.Children.FirstOrDefault((t) => t.Key.StartsWith("ChildTopic3"));
//Because Cousins is set to recurse over Children, its children should be set
Assert.AreEqual(3, cousinTarget.Children.Count);
@@ -371,17 +389,17 @@ public void TopicMappingService_RecursiveRelationships() {
/// collection of objects (from which .
///
[TestMethod]
- public void TopicMappingService_MapSlideshow() {
+ public async Task MapSlideshow() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Test", "Slideshow");
- var slides = TopicFactory.Create("ContentItems", "List", topic);
- var childTopic1 = TopicFactory.Create("ChildTopic1", "Slide", slides);
- var childTopic2 = TopicFactory.Create("ChildTopic2", "Slide", slides);
- var childTopic3 = TopicFactory.Create("ChildTopic3", "Slide", slides);
- var childTopic4 = TopicFactory.Create("ChildTopic4", "ContentItem", slides);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Test", "Slideshow");
+ var slides = TopicFactory.Create("ContentItems", "List", topic);
+ var childTopic1 = TopicFactory.Create("ChildTopic1", "Slide", slides);
+ var childTopic2 = TopicFactory.Create("ChildTopic2", "Slide", slides);
+ var childTopic3 = TopicFactory.Create("ChildTopic3", "Slide", slides);
+ var childTopic4 = TopicFactory.Create("ChildTopic4", "ContentItem", slides);
- var target = (SlideshowTopicViewModel)mappingService.Map(topic);
+ var target = (SlideshowTopicViewModel) await mappingService.MapAsync(topic);
Assert.AreEqual(4, target.ContentItems.Count);
Assert.IsNotNull(target.ContentItems.FirstOrDefault((t) => t.Key.StartsWith("ChildTopic1")));
@@ -399,9 +417,9 @@ public void TopicMappingService_MapSlideshow() {
/// instances if called for by the model. This isn't a best practice, but is maintained for edge cases.
///
[TestMethod]
- public void TopicMappingService_MapTopics() {
+ public async Task MapTopics() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var relatedTopic1 = TopicFactory.Create("RelatedTopic1", "Page");
var relatedTopic2 = TopicFactory.Create("RelatedTopic2", "Index");
var relatedTopic3 = TopicFactory.Create("RelatedTopic3", "Page");
@@ -411,7 +429,7 @@ public void TopicMappingService_MapTopics() {
topic.Relationships.SetTopic("Related", relatedTopic2);
topic.Relationships.SetTopic("Related", relatedTopic3);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel) await mappingService.MapAsync(topic);
var relatedTopic3copy = ((Topic)target.Related.FirstOrDefault((t) => t.Key.StartsWith("RelatedTopic3")));
Assert.AreEqual(3, target.Related.Count);
@@ -430,17 +448,38 @@ public void TopicMappingService_MapTopics() {
/// .
///
[TestMethod]
- public void TopicMappingService_MapMetadata() {
+ public async Task MapMetadata() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Test", "MetadataLookup");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Test", "MetadataLookup");
- var target = (MetadataLookupTopicViewModel)mappingService.Map(topic);
+ var target = (MetadataLookupTopicViewModel) await mappingService.MapAsync(topic);
Assert.AreEqual(5, target.Categories.Count);
}
+ /*==========================================================================================================================
+ | TEST: MAP CIRCULAR REFERENCE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a and tests whether it successfully handles a circular reference by
+ /// taking advantage of its internal caching mechanism.
+ ///
+ [TestMethod]
+ public async Task MapCircularReference() {
+
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+
+ var topic = TopicFactory.Create("Test", "Circular", 1);
+ var childTopic = TopicFactory.Create("ChildTopic", "Circular", 2, topic);
+
+ var mappedTopic = (CircularTopicViewModel) await mappingService.MapAsync(topic);
+
+ Assert.AreEqual(mappedTopic, mappedTopic.Children.First().Parent);
+
+ }
+
/*==========================================================================================================================
| TEST: FILTER BY CONTENT TYPE
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -449,18 +488,18 @@ public void TopicMappingService_MapMetadata() {
/// cref="SampleTopicViewModel.Children"/> property can be filtered by .
///
[TestMethod]
- public void TopicMappingService_FilterByContentType() {
+ public async Task FilterByContentType() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Test", "Sample");
- var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", topic);
- var childTopic2 = TopicFactory.Create("ChildTopic2", "Index", topic);
- var childTopic3 = TopicFactory.Create("ChildTopic3", "Index", topic);
- var childTopic4 = TopicFactory.Create("ChildTopic4", "Index", childTopic3);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Test", "Sample");
+ var childTopic1 = TopicFactory.Create("ChildTopic1", "Page", topic);
+ var childTopic2 = TopicFactory.Create("ChildTopic2", "Index", topic);
+ var childTopic3 = TopicFactory.Create("ChildTopic3", "Index", topic);
+ var childTopic4 = TopicFactory.Create("ChildTopic4", "Index", childTopic3);
- var target = (SampleTopicViewModel)mappingService.Map(topic);
+ var target = (SampleTopicViewModel) await mappingService.MapAsync(topic);
- var indexes = target.Children.GetByContentType("Index");
+ var indexes = target.Children.GetByContentType("Index");
Assert.AreEqual(2, indexes.Count);
Assert.IsNotNull(indexes.FirstOrDefault((t) => t.Key.StartsWith("ChildTopic2")));
@@ -477,14 +516,14 @@ public void TopicMappingService_FilterByContentType() {
/// correctly populated.
///
[TestMethod]
- public void TopicMappingService_MapGetterMethods() {
+ public async Task MapGetterMethods() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "Sample");
- var childTopic = TopicFactory.Create("Child", "Page", topic);
- var grandChildTopic = TopicFactory.Create("GrandChild", "Index", childTopic);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "Sample");
+ var childTopic = TopicFactory.Create("Child", "Page", topic);
+ var grandChildTopic = TopicFactory.Create("GrandChild", "Index", childTopic);
- var target = (IndexTopicViewModel)mappingService.Map(grandChildTopic);
+ var target = (IndexTopicViewModel) await mappingService.MapAsync(grandChildTopic);
Assert.AreEqual("Topic:Child:GrandChild", target.UniqueKey);
@@ -497,14 +536,14 @@ public void TopicMappingService_MapGetterMethods() {
/// Maps a content type that has a required property. Ensures that an error is not thrown if it is set.
///
[TestMethod]
- public void TopicMappingService_MapRequiredProperty() {
+ public async Task MapRequiredProperty() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "Required");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "Required");
topic.Attributes.SetValue("RequiredAttribute", "Required");
- var target = (RequiredTopicViewModel)mappingService.Map(topic);
+ var target = (RequiredTopicViewModel) await mappingService.MapAsync(topic);
Assert.AreEqual("Required", target.RequiredAttribute);
@@ -518,12 +557,12 @@ public void TopicMappingService_MapRequiredProperty() {
///
[TestMethod]
[ExpectedException(typeof(ValidationException))]
- public void TopicMappingService_MapRequiredPropertyException() {
+ public async Task MapRequiredPropertyException() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "Required");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "Required");
- var target = (RequiredTopicViewModel)mappingService.Map(topic);
+ var target = (RequiredTopicViewModel) await mappingService.MapAsync(topic);
}
@@ -535,12 +574,12 @@ public void TopicMappingService_MapRequiredPropertyException() {
///
[TestMethod]
[ExpectedException(typeof(ValidationException))]
- public void TopicMappingService_MapRequiredObjectPropertyException() {
+ public async Task MapRequiredObjectPropertyException() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "RequiredObject");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "RequiredObject");
- var target = (RequiredTopicViewModel)mappingService.Map(topic);
+ var target = (RequiredTopicViewModel) await mappingService.MapAsync(topic);
}
@@ -551,12 +590,12 @@ public void TopicMappingService_MapRequiredObjectPropertyException() {
/// Maps a content type that has default properties. Ensures that each is set appropriately.
///
[TestMethod]
- public void TopicMappingService_MapDefaultValueProperties() {
+ public async Task MapDefaultValueProperties() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "DefaultValue");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "DefaultValue");
- var target = (DefaultValueTopicViewModel)mappingService.Map(topic);
+ var target = (DefaultValueTopicViewModel) await mappingService.MapAsync(topic);
Assert.AreEqual("Default", target.DefaultString);
Assert.AreEqual(10, target.DefaultInt);
@@ -572,14 +611,14 @@ public void TopicMappingService_MapDefaultValueProperties() {
///
[TestMethod]
[ExpectedException(typeof(ValidationException))]
- public void TopicMappingService_MapMinimumValueProperties() {
+ public async Task MapMinimumValueProperties() {
- var mappingService = new TopicMappingService(_topicRepository);
- var topic = TopicFactory.Create("Topic", "MinimumLengthProperty");
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ var topic = TopicFactory.Create("Topic", "MinimumLengthProperty");
topic.Attributes.SetValue("MinimumLength", "Hello World");
- var target = (MinimumLengthPropertyTopicViewModel)mappingService.Map(topic);
+ var target = (MinimumLengthPropertyTopicViewModel) await mappingService.MapAsync(topic);
}
@@ -592,26 +631,53 @@ public void TopicMappingService_MapMinimumValueProperties() {
/// instances.
///
[TestMethod]
- public void TopicMappingService_FilterByAttribute() {
+ public async Task FilterByAttribute() {
- var mappingService = new TopicMappingService(_topicRepository);
- 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 mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+ 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);
childTopic1.Attributes.SetValue("SomeAttribute", "ValueA");
childTopic2.Attributes.SetValue("SomeAttribute", "ValueA");
childTopic3.Attributes.SetValue("SomeAttribute", "ValueA");
childTopic4.Attributes.SetValue("SomeAttribute", "ValueB");
- var target = (FilteredTopicViewModel)mappingService.Map(topic);
+ var target = (FilteredTopicViewModel) await mappingService.MapAsync(topic);
Assert.AreEqual(2, target.Children.Count);
}
+ /*==========================================================================================================================
+ | TEST: FLATTEN
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a and tests whether the resulting object's property is properly flattened.
+ ///
+ [TestMethod]
+ public async Task Flatten() {
+
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
+
+ var topic = TopicFactory.Create("Test", "FlattenChildren");
+
+ for (var i=0; i<5; i++) {
+ var childTopic = TopicFactory.Create("Child" + i, "Page", topic);
+ for (var j=0; j<5; j++) {
+ TopicFactory.Create("GrandChild" + i + j, "FlattenChildren", childTopic);
+ }
+ }
+
+ var target = (FlattenChildrenTopicViewModel) await mappingService.MapAsync(topic);
+
+ Assert.AreEqual(25, target.Children.Count);
+
+ }
+
/*==========================================================================================================================
| TEST: CACHING
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -620,15 +686,15 @@ public void TopicMappingService_FilterByAttribute() {
/// the same instance of a mapped object is turned after two calls.
///
[TestMethod]
- public void TopicMappingService_Caching() {
+ public async Task Caching() {
- var mappingService = new TopicMappingService(_topicRepository);
+ var mappingService = new TopicMappingService(_topicRepository, new FakeViewModelLookupService());
var cachedMappingService = new CachedTopicMappingService(mappingService);
var topic = TopicFactory.Create("Test", "Filtered", 5);
- var target1 = (FilteredTopicViewModel)cachedMappingService.Map(topic);
- var target2 = (FilteredTopicViewModel)cachedMappingService.Map(topic);
+ var target1 = (FilteredTopicViewModel)await cachedMappingService.MapAsync(topic);
+ var target2 = (FilteredTopicViewModel)await cachedMappingService.MapAsync(topic);
Assert.AreEqual(target1, target2);
diff --git a/Ignia.Topics.Tests/TopicRoutingServiceTest.cs b/Ignia.Topics.Tests/TopicRoutingServiceTest.cs
index a778e56f..26f6da4b 100644
--- a/Ignia.Topics.Tests/TopicRoutingServiceTest.cs
+++ b/Ignia.Topics.Tests/TopicRoutingServiceTest.cs
@@ -50,21 +50,21 @@ public TopicRoutingServiceTest() {
/// Establishes route data and ensures that a topic is correctly identified based on that route.
///
[TestMethod]
- public void TopicRoutingService_TopicRouteTest() {
+ public void TopicRoute() {
var routes = new RouteData();
var uri = new Uri("http://localhost/Topics/Web/Web_0/Web_0_1/Web_0_1_1");
var topic = _topicRepository.Load("Root:Web:Web_0:Web_0_1:Web_0_1_1");
routes.Values.Add("rootTopic", "Web");
- routes.Values.Add("path", "Web_0/Web_0_1/Web_0_1_2");
+ routes.Values.Add("path", "Web_0/Web_0_1/Web_0_1_1");
var topicRoutingService = new MvcTopicRoutingService(_topicRepository, uri, routes);
var currentTopic = topicRoutingService.GetCurrentTopic();
Assert.IsNotNull(currentTopic);
Assert.ReferenceEquals(topic, currentTopic);
- Assert.AreEqual("Web_0_1_2", currentTopic.Key);
+ Assert.AreEqual("Web_0_1_1", currentTopic.Key);
}
@@ -75,7 +75,7 @@ public void TopicRoutingService_TopicRouteTest() {
/// Establishes a URI based on a path and ensures that a topic is correctly identified based on that URI.
///
[TestMethod]
- public void TopicRoutingService_TopicUriTest() {
+ public void TopicUri() {
var routes = new RouteData();
var uri = new Uri("http://localhost/Web/Web_0/Web_0_1/Web_0_1_1");
@@ -98,21 +98,21 @@ public void TopicRoutingService_TopicUriTest() {
/// .
///
[TestMethod]
- public void TopicRoutingService_RoutesTest() {
+ public void Routes() {
var routes = new RouteData();
var rootTopic = _topicRepository.Load();
var uri = new Uri("http://localhost/Web/Web_0/Web_0_1/Web_0_1_1");
routes.Values.Add("rootTopic", "Web");
- routes.Values.Add("path", "Web_0/Web_0_1/Web_0_1_2");
+ routes.Values.Add("path", "Web_0/Web_0_1/Web_0_1_1");
var topicRoutingService = new MvcTopicRoutingService(_topicRepository, uri, routes);
var currentTopic = topicRoutingService.GetCurrentTopic();
Assert.IsNotNull(currentTopic);
Assert.AreEqual("Web", routes.GetRequiredString("rootTopic"));
- Assert.AreEqual("Web_0/Web_0_1/Web_0_1_2", routes.GetRequiredString("path"));
+ Assert.AreEqual("Web_0/Web_0_1/Web_0_1_1", routes.GetRequiredString("path"));
Assert.AreEqual("Page", routes.GetRequiredString("contenttype"));
}
diff --git a/Ignia.Topics.Tests/TopicTest.cs b/Ignia.Topics.Tests/TopicTest.cs
index bd6eba84..2850ccbe 100644
--- a/Ignia.Topics.Tests/TopicTest.cs
+++ b/Ignia.Topics.Tests/TopicTest.cs
@@ -6,6 +6,7 @@
using System;
using System.Linq;
using Ignia.Topics.Querying;
+using Ignia.Topics.Tests.TestDoubles;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Ignia.Topics.Tests {
@@ -26,7 +27,7 @@ public class TopicTest {
/// Creates a topic using the factory method, and ensures it's correctly returned.
///
[TestMethod]
- public void Topic_CreateTest() {
+ public void Create() {
var topic = TopicFactory.Create("Test", "ContentTypeDescriptor");
Assert.IsNotNull(topic);
Assert.IsInstanceOfType(topic, typeof(ContentTypeDescriptor));
@@ -42,7 +43,7 @@ public void Topic_CreateTest() {
///
[TestMethod]
[ExpectedException(typeof(ArgumentException), "Topic permitted the ID to be reset; this should never happen.")]
- public void Topic_Change_IdTest() {
+ public void Change_IdTest() {
var topic = TopicFactory.Create("Test", "ContentTypeDescriptor", 123);
topic.Id = 124;
@@ -56,11 +57,11 @@ public void Topic_Change_IdTest() {
| TEST: IS (CONTENT) TYPE OF
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Associates a new topic with several contnet types, and confirms that the topic is reported as a type of those content
+ /// Associates a new topic with several content types, and confirms that the topic is reported as a type of those content
/// types.
///
[TestMethod]
- public void Topic_IsContentTypeOf() {
+ public void IsContentTypeOf() {
var contentType = (ContentTypeDescriptor)TopicFactory.Create("Root", "ContentTypeDescriptor");
for (var i=0; i<5; i++) {
@@ -79,7 +80,7 @@ public void Topic_IsContentTypeOf() {
/// Sets the parent of a topic and ensures it is correctly reflected in the object model.
///
[TestMethod]
- public void Topic_Set_ParentTest() {
+ public void Set_ParentTest() {
var parentTopic = TopicFactory.Create("Parent", "ContentTypeDescriptor");
var childTopic = TopicFactory.Create("Child", "ContentTypeDescriptor");
@@ -98,9 +99,8 @@ public void Topic_Set_ParentTest() {
///
/// Changes the parent of a topic and ensures it is correctly reflected in the object model.
///
- // ### TODO JJC20150816: This invokes dependencies on the TopicDataProvider and, in turn, the Configuration namespace. This
- // is going to call for the creation of mocks and dependency injection before it will pass. In the meanwhile, it is disabled.
- public void Topic_Change_ParentTest() {
+ [TestMethod]
+ public void Change_ParentTest() {
var sourceParent = TopicFactory.Create("SourceParent", "ContentTypeDescriptor");
var targetParent = TopicFactory.Create("TargetParent", "ContentTypeDescriptor");
@@ -124,7 +124,7 @@ public void Topic_Change_ParentTest() {
/// Ensures the Unique Key is correct for a deeply nested child.
///
[TestMethod]
- public void Topic_UniqueKeyTest() {
+ public void UniqueKey() {
var parentTopic = TopicFactory.Create("ParentTopic", "Page");
var childTopic = TopicFactory.Create("ChildTopic", "Page");
@@ -145,7 +145,7 @@ public void Topic_UniqueKeyTest() {
/// Looks for a deeply nested child topic using only the attribute value.
///
[TestMethod]
- public void Topic_FindAllByAttributeValueTest() {
+ public void FindAllByAttributeValue() {
var parentTopic = TopicFactory.Create("ParentTopic", "Page", 1);
var childTopic = TopicFactory.Create("ChildTopic", "Page", 5);
@@ -175,7 +175,7 @@ public void Topic_FindAllByAttributeValueTest() {
/// Ensures that IsVisible returns expected values based on IsHidden and IsDisabled.
///
[TestMethod]
- public void Topic_IsVisibleTest() {
+ public void IsVisible() {
var hiddenTopic = TopicFactory.Create("HiddenTopic", "Page");
var disabledTopic = TopicFactory.Create("DisabledTopic", "Page");
@@ -200,7 +200,7 @@ public void Topic_IsVisibleTest() {
/// Ensures that the title falls back appropriately.
///
[TestMethod]
- public void Topic_TitleTest() {
+ public void Title() {
var untitledTopic = TopicFactory.Create("UntitledTopic", "Page");
var titledTopic = TopicFactory.Create("TitledTopic", "Page");
@@ -219,7 +219,7 @@ public void Topic_TitleTest() {
/// Returns the last modified date using a couple of techniques, and ensures it's returned correctly.
///
[TestMethod]
- public void Topic_LastModifiedTest() {
+ public void LastModified() {
var topic1 = TopicFactory.Create("Topic1", "Page");
var topic2 = TopicFactory.Create("Topic2", "Page");
@@ -245,7 +245,7 @@ public void Topic_LastModifiedTest() {
/// Sets a derived topic, and ensures it is referenced correctly.
///
[TestMethod]
- public void Topic_DerivedTopicTest() {
+ public void DerivedTopic() {
var topic = TopicFactory.Create("Topic", "Page");
var derivedTopic = TopicFactory.Create("DerivedTopic", "Page");
@@ -264,7 +264,7 @@ public void Topic_DerivedTopicTest() {
/// correctly parsed via the property.
///
[TestMethod]
- public void Topic_AttributeConfigurationTest() {
+ public void AttributeConfiguration() {
var attribute = (AttributeDescriptor)TopicFactory.Create("Topic", "AttributeDescriptor");
attribute.DefaultConfiguration = "IsRequired=\"True\" DisplayName=\"Display Name\"";
diff --git a/Ignia.Topics.Tests/TypeCollectionTest.cs b/Ignia.Topics.Tests/TypeCollectionTest.cs
index e76e5243..18f923db 100644
--- a/Ignia.Topics.Tests/TypeCollectionTest.cs
+++ b/Ignia.Topics.Tests/TypeCollectionTest.cs
@@ -4,7 +4,10 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Reflection;
using Ignia.Topics.Collections;
+using Ignia.Topics.Reflection;
+using Ignia.Topics.Tests.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Ignia.Topics.Tests {
@@ -13,7 +16,7 @@ namespace Ignia.Topics.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.
@@ -25,13 +28,13 @@ public class TypeCollectionTest {
| TEST: PROPERTY INFO COLLECTION CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Establishes a based on a type, and confirms that the property collection is
+ /// Establishes a based on a type, and confirms that the property collection is
/// returning expected types.
///
[TestMethod]
- public void PropertyInfoCollection_ConstructorTest() {
+ public void Constructor() {
- var properties = new PropertyInfoCollection(typeof(ContentTypeDescriptor));
+ var properties = new MemberInfoCollection(typeof(ContentTypeDescriptor));
Assert.IsTrue(properties.Contains("Key")); //Inherited string property
Assert.IsTrue(properties.Contains("AttributeDescriptors")); //First class collection property
@@ -44,15 +47,15 @@ public void PropertyInfoCollection_ConstructorTest() {
| TEST: GET TYPE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Establishes a and confirms that
+ /// Establishes a and confirms that
/// functions.
///
[TestMethod]
- public void TypeCollection_GetPropertiesTest() {
+ public void GetProperties() {
var types = new TypeCollection();
- var properties = types.GetProperties(typeof(ContentTypeDescriptor));
+ var properties = types.GetMembers(typeof(ContentTypeDescriptor));
Assert.IsTrue(properties.Contains("Key"));
Assert.IsTrue(properties.Contains("AttributeDescriptors"));
@@ -62,21 +65,23 @@ public void TypeCollection_GetPropertiesTest() {
}
/*==========================================================================================================================
- | TEST: GET PROPERTY
+ | TEST: GET MEMBER
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Establishes a and confirms that
+ /// Establishes a and confirms that
/// correctly returns the expected properties.
///
[TestMethod]
- public void TypeCollection_GetPropertyTest() {
+ public void GetMember() {
var types = new TypeCollection();
- Assert.IsTrue(types.GetProperty(typeof(ContentTypeDescriptor), "Key") != null);
- Assert.IsTrue(types.GetProperty(typeof(ContentTypeDescriptor), "AttributeDescriptors") != null);
- Assert.IsFalse(types.GetProperty(typeof(ContentTypeDescriptor), "IsTypeOf") != null);
- Assert.IsFalse(types.GetProperty(typeof(ContentTypeDescriptor), "InvalidPropertyName") != null);
+ Assert.IsTrue(types.GetMember(typeof(ContentTypeDescriptor), "Key") != null);
+ Assert.IsTrue(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors") != null);
+ Assert.IsFalse(types.GetMember(typeof(ContentTypeDescriptor), "IsTypeOf") != null);
+ Assert.IsFalse(types.GetMember(typeof(ContentTypeDescriptor), "InvalidPropertyName") != null);
+ Assert.IsTrue(types.GetMember(typeof(ContentTypeDescriptor), "GetWebPath") != null);
+ Assert.IsFalse(types.GetMember(typeof(ContentTypeDescriptor), "AttributeDescriptors") != null);
}
@@ -85,31 +90,61 @@ public void TypeCollection_GetPropertyTest() {
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// Establishes a and confirms that a value can be properly set using the
- /// method.
+ /// method.
///
[TestMethod]
- public void TypeCollection_SetPropertyTest() {
+ public void SetProperty() {
var types = new TypeCollection();
var topic = TopicFactory.Create("Test", "ContentType");
- types.SetProperty(topic, "IsHidden", "1");
+ types.SetPropertyValue(topic, "IsHidden", "1");
- var isDateSet = types.SetProperty(topic, "LastModified", "June 3, 2008");
- isDateSet = types.SetProperty(topic, "LastModified", "2008-06-03") && isDateSet;
- isDateSet = types.SetProperty(topic, "LastModified", "06/03/2008") && isDateSet;
- var isKeySet = types.SetProperty(topic, "Key", "NewKey");
- var isInvalidPropertySet = types.SetProperty(topic, "InvalidProperty", "Invalid");
+ var isDateSet = types.SetPropertyValue(topic, "LastModified", "June 3, 2008");
+ isDateSet = types.SetPropertyValue(topic, "LastModified", "2008-06-03") && isDateSet;
+ isDateSet = types.SetPropertyValue(topic, "LastModified", "06/03/2008") && isDateSet;
+ var isKeySet = types.SetPropertyValue(topic, "Key", "NewKey");
+ var isInvalidPropertySet = types.SetPropertyValue(topic, "InvalidProperty", "Invalid");
+
+ var lastModified = DateTime.Parse(types.GetPropertyValue(topic, "LastModified", typeof(DateTime)).ToString());
+ var key = types.GetPropertyValue(topic, "Key", typeof(string)).ToString();
Assert.IsTrue(isDateSet);
Assert.IsTrue(isKeySet);
Assert.IsFalse(isInvalidPropertySet);
Assert.AreEqual("NewKey", topic.Key);
+ Assert.AreEqual("NewKey", key);
Assert.AreEqual(new DateTime(2008, 6, 3), topic.LastModified);
+ Assert.AreEqual(new DateTime(2008, 6, 3), lastModified);
Assert.IsTrue(topic.IsHidden);
}
+ /*==========================================================================================================================
+ | TEST: SET METHOD
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a and confirms that a value can be properly set using the
+ /// method.
+ ///
+ [TestMethod]
+ public void SetMethod() {
+
+ var types = new TypeCollection();
+ var source = new MethodBasedViewModel();
+
+ var isValueSet = types.SetMethodValue(source, "SetMethod", "123");
+ var isInvalidSet = types.SetMethodValue(source, "BogusMethod", "123");
+
+ var value = types.GetMethodValue(source, "GetMethod");
+
+ Assert.IsTrue(isValueSet);
+ Assert.IsFalse(isInvalidSet);
+ Assert.IsTrue(value is int);
+ Assert.AreEqual(123, (int)value);
+
+ }
+
/*==========================================================================================================================
| TEST: REFLECTION PERFORMANCE
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -123,7 +158,7 @@ public void TypeCollection_SetPropertyTest() {
/// the number of iterations, simply increment the "totalIterations" variable.
///
[TestMethod]
- public void TypeCollection_ReflectionPerformanceTest() {
+ public void ReflectionPerformance() {
var totalIterations = 1;
var types = new TypeCollection();
@@ -131,7 +166,7 @@ public void TypeCollection_ReflectionPerformanceTest() {
var i = 0;
for (i = 0; i < totalIterations; i++) {
- types.SetProperty(topic, "Key", "Key" + i);
+ types.SetPropertyValue(topic, "Key", "Key" + i);
}
Assert.AreEqual("Key" + (i-1), topic.Key);
diff --git a/Ignia.Topics.Tests/ViewModels/CircularTopicViewModel.cs b/Ignia.Topics.Tests/ViewModels/CircularTopicViewModel.cs
new file mode 100644
index 00000000..111d72ff
--- /dev/null
+++ b/Ignia.Topics.Tests/ViewModels/CircularTopicViewModel.cs
@@ -0,0 +1,30 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Ignia.Topics.Mapping;
+
+namespace Ignia.Topics.Tests.ViewModels {
+
+ /*============================================================================================================================
+ | VIEW MODEL: CIRCULAR TOPIC
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a strongly-typed data transfer object for testing views with a circular reference.
+ ///
+ ///
+ /// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
+ ///
+ public class CircularTopicViewModel {
+
+ [Follow(Relationships.Parents)]
+ public CircularTopicViewModel Parent { get; set; }
+
+ [Follow(Relationships.Children | Relationships.Parents)]
+ public List Children { get; set; }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics.Tests/ViewModels/FlattenChildrenTopicViewModel.cs b/Ignia.Topics.Tests/ViewModels/FlattenChildrenTopicViewModel.cs
new file mode 100644
index 00000000..52791327
--- /dev/null
+++ b/Ignia.Topics.Tests/ViewModels/FlattenChildrenTopicViewModel.cs
@@ -0,0 +1,28 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.Collections.Generic;
+using Ignia.Topics.Mapping;
+using Ignia.Topics.ViewModels;
+
+namespace Ignia.Topics.Tests.ViewModels {
+
+ /*============================================================================================================================
+ | VIEW MODEL: FLATTEN CHILDREN TOPIC
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a strongly-typed data transfer object for testing view 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 FlattenChildrenTopicViewModel {
+
+ [Flatten]
+ public List Children { get; set; }
+
+ } //Class
+} //Namespace
diff --git a/Ignia.Topics.Tests/ViewModels/MethodBasedViewModel.cs b/Ignia.Topics.Tests/ViewModels/MethodBasedViewModel.cs
new file mode 100644
index 00000000..5919328a
--- /dev/null
+++ b/Ignia.Topics.Tests/ViewModels/MethodBasedViewModel.cs
@@ -0,0 +1,28 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.ComponentModel;
+using Ignia.Topics.ViewModels;
+
+namespace Ignia.Topics.Tests.ViewModels {
+
+ /*============================================================================================================================
+ | VIEW MODEL: METHOD BASED
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a strongly-typed data transfer object for testing settable methods and gettable methods.
+ ///
+ ///
+ /// This is a sample class intended for test purposes only; it is not designed for use in a production environment.
+ ///
+ public class MethodBasedViewModel {
+
+ private int _methodValue = 0;
+
+ public void SetMethod(int methodValue) => _methodValue = methodValue;
+ public int GetMethod() => _methodValue;
+
+ } //Class
+} //Namespace
diff --git a/Ignia.Topics.Tests/ViewModels/SampleTopicViewModel.cs b/Ignia.Topics.Tests/ViewModels/SampleTopicViewModel.cs
index b16b7ccf..2eab92d9 100644
--- a/Ignia.Topics.Tests/ViewModels/SampleTopicViewModel.cs
+++ b/Ignia.Topics.Tests/ViewModels/SampleTopicViewModel.cs
@@ -29,17 +29,19 @@ public class SampleTopicViewModel : PageTopicViewModel {
[AttributeKey("Property")]
public string PropertyAlias { get; set; }
- [Recurse(Relationships.Relationships)]
+ public TopicViewModel TopicReference { get; set; }
+
+ [Follow(Relationships.Relationships)]
public TopicViewModelCollection Children { get; set; }
- [Recurse(Relationships.Children)]
+ [Follow(Relationships.Children)]
public TopicViewModelCollection Cousins { get; set; }
public TopicViewModelCollection Categories { get; set; }
public Collection Related { get; set; }
- [Relationship("AmbiguousRelationship", RelationshipType.IncomingRelationship)]
+ [Relationship("AmbiguousRelationship", Type=RelationshipType.IncomingRelationship)]
public TopicViewModelCollection RelationshipAlias { get; set; }
} //Class
diff --git a/Ignia.Topics.ViewModels/ContentItemTopicViewModel.cs b/Ignia.Topics.ViewModels/ContentItemTopicViewModel.cs
index 3cae4a69..7db78ae1 100644
--- a/Ignia.Topics.ViewModels/ContentItemTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/ContentItemTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/ContentListTopicViewModel.cs b/Ignia.Topics.ViewModels/ContentListTopicViewModel.cs
index 1cdf702d..44a6a181 100644
--- a/Ignia.Topics.ViewModels/ContentListTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/ContentListTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/Ignia.Topics.ViewModels.csproj b/Ignia.Topics.ViewModels/Ignia.Topics.ViewModels.csproj
index a6575485..473bddfe 100644
--- a/Ignia.Topics.ViewModels/Ignia.Topics.ViewModels.csproj
+++ b/Ignia.Topics.ViewModels/Ignia.Topics.ViewModels.csproj
@@ -9,9 +9,19 @@
Properties
Ignia.Topics.Models
Ignia.Topics.ViewModels
- v4.5
+ v4.7
512
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -22,6 +32,8 @@
prompt
4
CS1591
+ true
+ latest
pdbonly
@@ -44,6 +56,7 @@
+
diff --git a/Ignia.Topics.ViewModels/IndexTopicViewModel.cs b/Ignia.Topics.ViewModels/IndexTopicViewModel.cs
index 57d1ea77..5e42d4b7 100644
--- a/Ignia.Topics.ViewModels/IndexTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/IndexTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/ItemTopicViewModel.cs b/Ignia.Topics.ViewModels/ItemTopicViewModel.cs
index 4e0814f4..8baebda6 100644
--- a/Ignia.Topics.ViewModels/ItemTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/ItemTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/LookupListItemTopicViewModel.cs b/Ignia.Topics.ViewModels/LookupListItemTopicViewModel.cs
index f4185f60..b3b69384 100644
--- a/Ignia.Topics.ViewModels/LookupListItemTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/LookupListItemTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/NavigationTopicViewModel.cs b/Ignia.Topics.ViewModels/NavigationTopicViewModel.cs
index eeb150ef..1645e51c 100644
--- a/Ignia.Topics.ViewModels/NavigationTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/NavigationTopicViewModel.cs
@@ -26,10 +26,12 @@ namespace Ignia.Topics.ViewModels {
/// cref="NavigationTopicViewModel"/> class is marked as sealed.
///
///
- public sealed class NavigationTopicViewModel : PageTopicViewModel, INavigationTopicViewModel {
+ public sealed class NavigationTopicViewModel : TopicViewModel, INavigationTopicViewModel {
+ public string WebPath { get; set; }
+ public string ShortTitle { get; set; }
public Collection Children { get; set; }
- public bool IsSelected(string uniqueKey) => uniqueKey?.StartsWith(UniqueKey) ?? false;
+ public bool IsSelected(string uniqueKey) => $"{uniqueKey}:"?.StartsWith($"{UniqueKey}:") ?? false;
} // Class
diff --git a/Ignia.Topics.ViewModels/PageGroupTopicViewModel.cs b/Ignia.Topics.ViewModels/PageGroupTopicViewModel.cs
index abd4391e..40989063 100644
--- a/Ignia.Topics.ViewModels/PageGroupTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/PageGroupTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/PageTopicViewModel.cs b/Ignia.Topics.ViewModels/PageTopicViewModel.cs
index 15080511..b6ce06d1 100644
--- a/Ignia.Topics.ViewModels/PageTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/PageTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/Properties/AssemblyInfo.cs b/Ignia.Topics.ViewModels/Properties/AssemblyInfo.cs
index a4d7debf..d3c63ed1 100644
--- a/Ignia.Topics.ViewModels/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.ViewModels/Properties/AssemblyInfo.cs
@@ -1,36 +1,28 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.ViewModels")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2018 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia OnTopic View Models")]
+[assembly: AssemblyDescription("Provides view models that map to the factory default content type schemas.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.ViewModels")]
-[assembly: AssemblyCopyright("Copyright © 2018")]
[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: AssemblyVersion("3.6.1762.0")]
+[assembly: AssemblyFileVersion("3.5.1793.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("e52fc633-b4c5-4a2b-8caf-30e756d7a6a7")]
-// 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/Ignia.Topics.ViewModels/README.md b/Ignia.Topics.ViewModels/README.md
index 0ab94047..46498518 100644
--- a/Ignia.Topics.ViewModels/README.md
+++ b/Ignia.Topics.ViewModels/README.md
@@ -16,12 +16,14 @@ The `Ignia.Topics.ViewModels` assembly includes default implementations of basic
- [`ItemTopicViewModel`](ItemTopicViewModel.cs)
- [`ContentItemTopicViewModel`](ContentItemTopicViewModel.cs)
- [`LookupListItemTopicViewModel`](LookupListItemTopicViewModel.cs)
+- [`TopicViewModelLookupService`](TopicViewModelLookupService.cs)
- [`TopicViewModelCollection<>`](TopicViewModelCollection.cs)
## Usage
-By default, the [`Ignia.Topics.Web.Mvc`](../Ignia.Topics.Web.Mvc)'s [`TopicController`](../Ignia.Topics.Web.Mvc/TopicController.cs) uses the out-of-the-box [`TopicMappingService`](../Ignia.Topics/Mapping), which will attempt to map topics to view models based on the naming convention `{ContentType}TopicViewModel`, from any assembly or namespace. If the `Ignia.Topics.ViewModels.dll` is in an application's `/bin` directory then these view models will be available to the mapping service.
+By default, the [`Ignia.Topics.Web.Mvc`](../Ignia.Topics.Web.Mvc)'s [`TopicController`](../Ignia.Topics.Web.Mvc/Controllers/TopicController.cs) uses the out-of-the-box [`TopicMappingService`](../Ignia.Topics/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.
-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.
+### `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 `Ignia.Topics.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.
> *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`.
@@ -33,7 +35,7 @@ As view models, not all attributes and relationships are exposed by the view mod
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.
### 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, [`PageTopicContentType`](PageTopicContentType.cs) includes a `Body` property, which is introduced by the `Page` content type, but doesn't include e.g. `Key`, `ContentType`, or `Title`; these are all inherited from the base [`TopicViewModel`](TopicViewModel.cs).
+The view models map to the hierarchy of the content types in OnTopic, with each view model only including properties that are _specific_ to that content type. So, for example, [`PageTopicViewModel`](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.).
diff --git a/Ignia.Topics.ViewModels/SectionTopicViewModel.cs b/Ignia.Topics.ViewModels/SectionTopicViewModel.cs
index 417fe36f..7301c3a6 100644
--- a/Ignia.Topics.ViewModels/SectionTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/SectionTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/SlideTopicViewModel.cs b/Ignia.Topics.ViewModels/SlideTopicViewModel.cs
index 4dbbb467..36a3b1f3 100644
--- a/Ignia.Topics.ViewModels/SlideTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/SlideTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/SlideshowTopicViewModel.cs b/Ignia.Topics.ViewModels/SlideshowTopicViewModel.cs
index 7b91e79d..9b12f040 100644
--- a/Ignia.Topics.ViewModels/SlideshowTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/SlideshowTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/TopicViewModel.cs b/Ignia.Topics.ViewModels/TopicViewModel.cs
index cba33fc0..934853a5 100644
--- a/Ignia.Topics.ViewModels/TopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/TopicViewModel.cs
@@ -16,7 +16,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
@@ -24,13 +24,14 @@ public class TopicViewModel: ITopicViewModel {
public int Id { get; set; }
public string Key { get; set; }
public string ContentType { get; set; }
- [Recurse(Relationships.Parents)]
- public TopicViewModel Parent { get; set; }
public string UniqueKey { get; set; }
public string View { get; set; }
public string Title { get; set; }
public bool IsHidden { get; set; }
public DateTime LastModified { get; set; }
+ [Follow(Relationships.Parents)]
+ public TopicViewModel Parent { get; set; }
+
} //Class
} //Namespace
diff --git a/Ignia.Topics.ViewModels/TopicViewModelCollection.cs b/Ignia.Topics.ViewModels/TopicViewModelCollection.cs
index b7e399d0..e62f9979 100644
--- a/Ignia.Topics.ViewModels/TopicViewModelCollection.cs
+++ b/Ignia.Topics.ViewModels/TopicViewModelCollection.cs
@@ -20,7 +20,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.ViewModels/TopicViewModelLookupService.cs b/Ignia.Topics.ViewModels/TopicViewModelLookupService.cs
new file mode 100644
index 00000000..54107c65
--- /dev/null
+++ b/Ignia.Topics.ViewModels/TopicViewModelLookupService.cs
@@ -0,0 +1,64 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+
+namespace Ignia.Topics.ViewModels {
+
+ /*============================================================================================================================
+ | CLASS: TYPE INDEX
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The can be configured to provide a lookup of .
+ ///
+ public class TopicViewModelLookupService : StaticTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a . Optionally accepts a list of instances and a default value.
+ ///
+ ///
+ /// Any instances submitted via should be unique by ; if they are not, they will be removed.
+ ///
+ /// The list of instances to expose as part of this service.
+ /// The default type to return if no match can be found. Defaults to object.
+ public TopicViewModelLookupService(IEnumerable types = null, Type defaultType = null) :
+ base(types, defaultType?? typeof(TopicViewModel)) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Ensure local view models are accounted for
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ AddIfMissing(typeof(ContentItemTopicViewModel));
+ AddIfMissing(typeof(ContentListTopicViewModel));
+ AddIfMissing(typeof(IndexTopicViewModel));
+ AddIfMissing(typeof(ItemTopicViewModel));
+ AddIfMissing(typeof(LookupListItemTopicViewModel));
+ AddIfMissing(typeof(NavigationTopicViewModel));
+ AddIfMissing(typeof(PageGroupTopicViewModel));
+ AddIfMissing(typeof(PageTopicViewModel));
+ AddIfMissing(typeof(SectionTopicViewModel));
+ AddIfMissing(typeof(SlideTopicViewModel));
+ AddIfMissing(typeof(SlideshowTopicViewModel));
+ AddIfMissing(typeof(TopicViewModel));
+ AddIfMissing(typeof(VideoTopicViewModel));
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Function: Add If Missing
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ void AddIfMissing(Type type) {
+ if (!Contains(type.Name)) {
+ Add(type);
+ }
+ }
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics.ViewModels/VideoTopicViewModel.cs b/Ignia.Topics.ViewModels/VideoTopicViewModel.cs
index 5fbb15bd..2fb14dea 100644
--- a/Ignia.Topics.ViewModels/VideoTopicViewModel.cs
+++ b/Ignia.Topics.ViewModels/VideoTopicViewModel.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.ViewModels {
///
///
/// 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 presentative level. They
+ /// 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 {
diff --git a/Ignia.Topics.Web.Mvc/Controllers/CachedLayoutControllerBase{T}.cs b/Ignia.Topics.Web.Mvc/Controllers/CachedLayoutControllerBase{T}.cs
new file mode 100644
index 00000000..d6721bdf
--- /dev/null
+++ b/Ignia.Topics.Web.Mvc/Controllers/CachedLayoutControllerBase{T}.cs
@@ -0,0 +1,113 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System.Collections.Concurrent;
+using System.Threading.Tasks;
+using Ignia.Topics.Mapping;
+using Ignia.Topics.Repositories;
+using Ignia.Topics.ViewModels;
+
+namespace Ignia.Topics.Web.Mvc.Controllers {
+
+ /*============================================================================================================================
+ | CLASS: CACHED LAYOUT CONTROLLER
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides access to views for populating specific layout dependencies, such as the , while caching
+ /// the graphs for performance.
+ ///
+ ///
+ ///
+ /// As a best practice, global data required by the layout view are requested independently 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 . The facilitates this by not only providing a default
+ /// implementation for , but additionally providing protected helper methods that aid in locating and
+ /// assembling and references that are relevant to
+ /// specific layout elements.
+ ///
+ ///
+ /// 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 controllers 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.
+ ///
+ ///
+ /// By comparison to the , the will
+ /// automatically cache the graph for each action that uses the protected method to construct the graph. This is preferable over using e.g.
+ /// the since the requires tight control
+ /// over the shape of the graph. For instance, using a generic caching
+ /// decorator for the mapping might result in the edges of the action being expanded due to other
+ /// actions reusing cached instances (e.g., for page-level navigation). To mitigate this, the handles top-level caching at the level of the navigation root.
+ ///
+ ///
+ public abstract class CachedLayoutControllerBase : LayoutControllerBase
+ where T : class, INavigationTopicViewModel, new() {
+
+ /*==========================================================================================================================
+ | STATIC VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ private static ConcurrentDictionary _cache = new ConcurrentDictionary();
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance of a Topic Controller with necessary dependencies.
+ ///
+ /// A topic controller for loading OnTopic views.
+ protected CachedLayoutControllerBase(
+ ITopicRepository topicRepository,
+ ITopicRoutingService topicRoutingService,
+ ITopicMappingService topicMappingService
+ ) : base(topicRepository, topicRoutingService, topicMappingService) {}
+
+ /*==========================================================================================================================
+ | GET ROOT VIEW MODEL (ASYNC)
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a , maps a , as well as of
+ /// . If the graph has been
+ /// mapped before, then a cached instance is returned. Optionally excludes instance with the
+ /// ContentType of PageGroup.
+ ///
+ /// The to pull the values from.
+ /// Determines whether s should be crawled.
+ /// Determines how many tiers of children should be included in the graph.
+ protected override async Task GetRootViewModelAsync(
+ Topic sourceTopic,
+ bool allowPageGroups = true,
+ int tiers = 1
+ ) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle empty results
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (sourceTopic == null) {
+ return await Task.FromResult(null);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle cache hits
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (_cache.TryGetValue(sourceTopic.Id, out var dto)) {
+ return await Task.FromResult(dto);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Cache and return new version
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var viewModel = await GetViewModelAsync(sourceTopic, allowPageGroups, tiers);
+ return _cache.GetOrAdd(sourceTopic.Id, viewModel);
+
+ }
+
+ } // Class
+
+} // Namespace
\ No newline at end of file
diff --git a/Ignia.Topics.Web.Mvc/Controllers/ErrorControllerBase{T}.cs b/Ignia.Topics.Web.Mvc/Controllers/ErrorControllerBase{T}.cs
index 5e70fe9b..2140e822 100644
--- a/Ignia.Topics.Web.Mvc/Controllers/ErrorControllerBase{T}.cs
+++ b/Ignia.Topics.Web.Mvc/Controllers/ErrorControllerBase{T}.cs
@@ -120,13 +120,15 @@ public virtual T CreateErrorViewModel(string key, string title) {
/*------------------------------------------------------------------------------------------------------------------------
| Instantiate model
\-----------------------------------------------------------------------------------------------------------------------*/
- var viewModel = new T();
- viewModel.Key = key;
- viewModel.WebPath = "/Error/" + key;
- viewModel.ContentType = "Page";
- viewModel.Title = title;
- viewModel.MetaKeywords = "";
- viewModel.MetaDescription = "";
+ var viewModel = new T {
+ Key = key,
+ UniqueKey = "Error:" + key,
+ WebPath = "/Error/" + key,
+ ContentType = "Page",
+ Title = title,
+ MetaKeywords = "",
+ MetaDescription = ""
+ };
/*------------------------------------------------------------------------------------------------------------------------
| Return the view
diff --git a/Ignia.Topics.Web.Mvc/Controllers/LayoutControllerBase{T}.cs b/Ignia.Topics.Web.Mvc/Controllers/LayoutControllerBase{T}.cs
index 6a69d24a..bf77f130 100644
--- a/Ignia.Topics.Web.Mvc/Controllers/LayoutControllerBase{T}.cs
+++ b/Ignia.Topics.Web.Mvc/Controllers/LayoutControllerBase{T}.cs
@@ -4,7 +4,9 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using System.Web.Mvc;
using Ignia.Topics.Mapping;
using Ignia.Topics.Repositories;
@@ -38,7 +40,7 @@ namespace Ignia.Topics.Web.Mvc.Controllers {
/// abstract and suffixed with Base.
///
///
- public abstract class LayoutControllerBase : Controller where T : class, INavigationTopicViewModel, new() {
+ public abstract class LayoutControllerBase : AsyncController where T : class, INavigationTopicViewModel, new() {
/*==========================================================================================================================
| PRIVATE VARIABLES
@@ -72,11 +74,7 @@ ITopicMappingService topicMappingService
/// Provides a reference to the Topic Repository in order to gain arbitrary access to the entire topic graph.
///
/// The TopicRepository associated with the controller.
- protected ITopicRepository TopicRepository {
- get {
- return _topicRepository;
- }
- }
+ protected ITopicRepository TopicRepository => _topicRepository;
/*==========================================================================================================================
| CURRENT TOPIC
@@ -100,7 +98,7 @@ protected Topic CurrentTopic {
///
/// Provides the global menu for the site layout, which exposes the top two tiers of navigation.
///
- public virtual PartialViewResult Menu() {
+ public async virtual Task Menu() {
/*------------------------------------------------------------------------------------------------------------------------
| Establish variables
@@ -119,7 +117,7 @@ public virtual PartialViewResult Menu() {
| Construct view model
\-----------------------------------------------------------------------------------------------------------------------*/
var navigationViewModel = new NavigationViewModel() {
- NavigationRoot = AddNestedTopics(navigationRootTopic, false, 3),
+ NavigationRoot = await GetRootViewModelAsync(navigationRootTopic, false, 3),
CurrentKey = CurrentTopic?.GetUniqueKey()
};
@@ -180,7 +178,7 @@ protected Topic GetNavigationRoot(Topic currentTopic, int fromRoot = 2, string d
/// The to pull the values from.
private static int DistanceFromRoot(Topic sourceTopic) {
var distance = 1;
- while (sourceTopic.Parent != null) {
+ while (sourceTopic?.Parent != null) {
sourceTopic = sourceTopic.Parent;
distance++;
}
@@ -188,35 +186,97 @@ private static int DistanceFromRoot(Topic sourceTopic) {
}
/*==========================================================================================================================
- | ADD NESTED TOPICS
+ | GET ROOT VIEW MODEL (ASYNC)
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// A helper function that allows a set number of tiers to be added to a tree.
+ /// Given a , maps a , as well as of
+ /// . Optionally excludes instance with the
+ /// ContentType of PageGroup.
///
+ ///
+ /// In the out-of-the-box implementation, and provide the same functionality. It is recommended that actions call
+ /// , however, as it allows implementers the flexibility to
+ /// differentiate between the root view model (which the client application will be binding to) and any child view models
+ /// (which the client application may optionally iterate over).
+ ///
/// The to pull the values from.
/// Determines whether s should be crawled.
/// Determines how many tiers of children should be included in the graph.
- protected T AddNestedTopics(
+ protected virtual async Task GetRootViewModelAsync(
+ Topic sourceTopic,
+ bool allowPageGroups = true,
+ int tiers = 1
+ ) => await GetViewModelAsync(sourceTopic, allowPageGroups, tiers);
+
+ /*==========================================================================================================================
+ | GET VIEW MODEL (ASYNC)
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ /// Given a , maps a , as well as of
+ /// . Optionally excludes instance with the
+ /// ContentType of PageGroup.
+ ///
+ /// The to pull the values from.
+ /// Determines whether s should be crawled.
+ /// Determines how many tiers of children should be included in the graph.
+ protected async Task GetViewModelAsync(
Topic sourceTopic,
bool allowPageGroups = true,
int tiers = 1
) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate preconditions
+ \-----------------------------------------------------------------------------------------------------------------------*/
tiers--;
if (sourceTopic == null) {
return null;
}
- var viewModel = _topicMappingService.Map(sourceTopic, Relationships.None);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Establish variables
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var taskQueue = new List>();
+ var children = new List();
+ var viewModel = (T)null;
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Map object
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ viewModel = await _topicMappingService.MapAsync(sourceTopic, Relationships.None);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Request mapping of children
+ \-----------------------------------------------------------------------------------------------------------------------*/
if (tiers >= 0 && (allowPageGroups || !sourceTopic.ContentType.Equals("PageGroup")) && viewModel.Children.Count == 0) {
foreach (var topic in sourceTopic.Children.Where(t => t.IsVisible())) {
- viewModel.Children.Add(
- AddNestedTopics(
- topic,
- allowPageGroups,
- tiers
- )
- );
+ taskQueue.Add(GetViewModelAsync(topic, allowPageGroups, tiers));
}
}
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Process children
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ while (taskQueue.Count > 0 && viewModel.Children.Count == 0) {
+ var dtoTask = await Task.WhenAny(taskQueue);
+ taskQueue.Remove(dtoTask);
+ children.Add(await dtoTask);
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Add children to view model
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (viewModel.Children.Count == 0) {
+ lock (viewModel) {
+ if (viewModel.Children.Count == 0) {
+ children.ForEach(c => viewModel.Children.Add(c));
+ }
+ }
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Return view model
+ \-----------------------------------------------------------------------------------------------------------------------*/
return viewModel;
}
diff --git a/Ignia.Topics.Web.Mvc/Controllers/RedirectController.cs b/Ignia.Topics.Web.Mvc/Controllers/RedirectController.cs
index c7f33d84..387bd197 100644
--- a/Ignia.Topics.Web.Mvc/Controllers/RedirectController.cs
+++ b/Ignia.Topics.Web.Mvc/Controllers/RedirectController.cs
@@ -18,7 +18,7 @@ namespace Ignia.Topics.Web.Mvc.Controllers {
/// Typically, a page is requested based on the value, which is a hash of
/// its . When a is moved to a different location in the topic graph,
/// however, its will return a different value, corresponding to its new location. To allow
- /// permanent references to page, therefore, the the accepts paths based on the accepts paths based on the , which is expected to be stable for the lifetime of a entity.
///
public class RedirectController : Controller {
@@ -26,7 +26,7 @@ public class RedirectController : Controller {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- private ITopicRepository _topicRepository = null;
+ private readonly ITopicRepository _topicRepository = null;
/*==========================================================================================================================
| CONSTRUCTOR
diff --git a/Ignia.Topics.Web.Mvc/Controllers/SitemapController.cs b/Ignia.Topics.Web.Mvc/Controllers/SitemapController.cs
index 14e1773f..13d799c2 100644
--- a/Ignia.Topics.Web.Mvc/Controllers/SitemapController.cs
+++ b/Ignia.Topics.Web.Mvc/Controllers/SitemapController.cs
@@ -21,7 +21,7 @@ public class SitemapController : Controller {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- private ITopicRepository _topicRepository = null;
+ private readonly ITopicRepository _topicRepository = null;
/*==========================================================================================================================
| CONSTRUCTOR
diff --git a/Ignia.Topics.Web.Mvc/Controllers/TopicController.cs b/Ignia.Topics.Web.Mvc/Controllers/TopicController.cs
index 98618a8e..eddad172 100644
--- a/Ignia.Topics.Web.Mvc/Controllers/TopicController.cs
+++ b/Ignia.Topics.Web.Mvc/Controllers/TopicController.cs
@@ -6,6 +6,7 @@
using System;
using System.Diagnostics.Contracts;
using System.Linq;
+using System.Threading.Tasks;
using System.Web.Mvc;
using Ignia.Topics.Mapping;
using Ignia.Topics.Repositories;
@@ -20,7 +21,7 @@ namespace Ignia.Topics.Web.Mvc.Controllers {
/// identifying the topic associated with the given path, determining its content type, and returning a view associated with
/// that content type (with potential overrides for multiple views).
///
- public class TopicController : Controller {
+ public class TopicController : AsyncController {
/*==========================================================================================================================
| PRIVATE VARIABLES
@@ -66,11 +67,7 @@ ITopicMappingService topicMappingService
/// Provides a reference to the Topic Repository in order to gain arbitrary access to the entire topic graph.
///
/// The TopicRepository associated with the controller.
- protected ITopicRepository TopicRepository {
- get {
- return _topicRepository;
- }
- }
+ protected ITopicRepository TopicRepository => _topicRepository;
/*==========================================================================================================================
| CURRENT TOPIC
@@ -96,12 +93,12 @@ protected Topic CurrentTopic {
/// query string or topic's view.
///
/// A view associated with the requested topic's Content Type and view.
- public virtual ActionResult Index(string path) {
+ public async virtual Task IndexAsync(string path) {
/*------------------------------------------------------------------------------------------------------------------------
| Establish default view model
\-----------------------------------------------------------------------------------------------------------------------*/
- var topicViewModel = _topicMappingService.Map(CurrentTopic);
+ var topicViewModel = await _topicMappingService.MapAsync(CurrentTopic);
/*------------------------------------------------------------------------------------------------------------------------
| Return topic view
diff --git a/Ignia.Topics.Web.Mvc/Ignia.Topics.Web.Mvc.csproj b/Ignia.Topics.Web.Mvc/Ignia.Topics.Web.Mvc.csproj
index dc925a0e..0a5c026c 100644
--- a/Ignia.Topics.Web.Mvc/Ignia.Topics.Web.Mvc.csproj
+++ b/Ignia.Topics.Web.Mvc/Ignia.Topics.Web.Mvc.csproj
@@ -10,11 +10,21 @@
Properties
Ignia.Topics.Web.Mvc
Ignia.Topics.Web.Mvc
- v4.5
+ v4.7
512
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -24,6 +34,8 @@
DEBUG;TRACE
prompt
4
+ true
+ latest
pdbonly
@@ -65,6 +77,7 @@
+
diff --git a/Ignia.Topics.Web.Mvc/Models/NavigationViewModel{T}.cs b/Ignia.Topics.Web.Mvc/Models/NavigationViewModel{T}.cs
index 59520b3f..6bc39a59 100644
--- a/Ignia.Topics.Web.Mvc/Models/NavigationViewModel{T}.cs
+++ b/Ignia.Topics.Web.Mvc/Models/NavigationViewModel{T}.cs
@@ -3,7 +3,6 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-
using Ignia.Topics.ViewModels;
namespace Ignia.Topics.Web.Mvc.Models {
@@ -20,13 +19,13 @@ namespace Ignia.Topics.Web.Mvc.Models {
/// constructed by the .
///
///
- /// The can be any view model that implements ,
+ /// The can be any view model that implements ,
/// which provides a base level of support for properties associated with the typical Page content type as well as
- /// a method for determining if a given instance is the currently-selected
- /// topic. Implementations may support additional properties, as appropriate.
+ /// a method for determining if a given instance is the currently-selected topic.
+ /// Implementations may support additional properties, as appropriate.
///
///
- public class NavigationViewModel where T: IPageTopicViewModel {
+ public class NavigationViewModel where T: INavigationTopicViewModel {
public T NavigationRoot { get; set; }
public string CurrentKey { get; set; }
diff --git a/Ignia.Topics.Web.Mvc/Models/TopicEntityViewModel.cs b/Ignia.Topics.Web.Mvc/Models/TopicEntityViewModel.cs
index b93a6961..9c2ebb24 100644
--- a/Ignia.Topics.Web.Mvc/Models/TopicEntityViewModel.cs
+++ b/Ignia.Topics.Web.Mvc/Models/TopicEntityViewModel.cs
@@ -24,11 +24,6 @@ namespace Ignia.Topics.Web.Mvc.Models {
///
public class TopicEntityViewModel {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private Topic _rootTopic = null;
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -37,8 +32,15 @@ public class TopicEntityViewModel {
///
/// A Topic view model.
public TopicEntityViewModel(ITopicRepository topicRepository, Topic topic) {
+
TopicRepository = topicRepository;
Topic = topic;
+ RootTopic = topic;
+
+ while (RootTopic.Parent != null) {
+ RootTopic = RootTopic.Parent;
+ }
+
}
/*==========================================================================================================================
@@ -57,17 +59,7 @@ public TopicEntityViewModel(ITopicRepository topicRepository, Topic topic) {
/// Returns the root topic associated with the object graph. This can be used to easily find other topics in the tree.
///
/// The at the root of the object graph.
- public Topic RootTopic {
- get {
- if (_rootTopic == null) {
- _rootTopic = Topic;
- while (_rootTopic.Parent != null) {
- _rootTopic = _rootTopic.Parent;
- }
- }
- return _rootTopic;
- }
- }
+ public Topic RootTopic { get; }
/*==========================================================================================================================
| PROPERTY: TOPIC
diff --git a/Ignia.Topics.Web.Mvc/MvcTopicRoutingService.cs b/Ignia.Topics.Web.Mvc/MvcTopicRoutingService.cs
index 39684162..bed63bf3 100644
--- a/Ignia.Topics.Web.Mvc/MvcTopicRoutingService.cs
+++ b/Ignia.Topics.Web.Mvc/MvcTopicRoutingService.cs
@@ -27,9 +27,9 @@ public class MvcTopicRoutingService : ITopicRoutingService {
/*============================================================================================================================
| PRIVATE VARIABLES
\---------------------------------------------------------------------------------------------------------------------------*/
- private ITopicRepository _topicRepository = null;
- private RouteData _routes = null;
- private Uri _uri = null;
+ private readonly ITopicRepository _topicRepository = null;
+ private readonly RouteData _routes = null;
+ private readonly Uri _uri = null;
private Topic _topic = null;
/*==========================================================================================================================
@@ -37,7 +37,7 @@ public class MvcTopicRoutingService : ITopicRoutingService {
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// Initializes a new instance of the class based on a URL instance, a fully qualified
- /// path to the views Directory, and, optionally, the expected filename suffix fo each view file.
+ /// path to the views Directory, and, optionally, the expected filename suffix of each view file.
///
public MvcTopicRoutingService(
ITopicRepository topicRepository,
diff --git a/Ignia.Topics.Web.Mvc/Properties/AssemblyInfo.cs b/Ignia.Topics.Web.Mvc/Properties/AssemblyInfo.cs
index 8fbe89bb..2a603a7c 100644
--- a/Ignia.Topics.Web.Mvc/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.Web.Mvc/Properties/AssemblyInfo.cs
@@ -1,36 +1,27 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.Web.Mvc")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2015 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia OnTopic MVC Library")]
+[assembly: AssemblyDescription("Provides presentation-layer support for the ASP.NET MVC Framework.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.Web.Mvc")]
-[assembly: AssemblyCopyright("Copyright © 2017")]
[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: AssemblyVersion("3.6.1767.0")]
+[assembly: AssemblyFileVersion("3.5.1799.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("3b3ce34d-b5e5-47ca-bfef-e6740650f378")]
-
-// 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/Ignia.Topics.Web.Mvc/README.md b/Ignia.Topics.Web.Mvc/README.md
index c9a60860..ec3b7b81 100644
--- a/Ignia.Topics.Web.Mvc/README.md
+++ b/Ignia.Topics.Web.Mvc/README.md
@@ -3,6 +3,7 @@ The `Ignia.Topics.Web.Mvc` assembly provides a default implementation for utiliz
### Contents
- [Components](#components)
+- [Controllers](#controllers)
- [View Conventions](#view-conventions)
- [View Matching](#view-matching)
- [View Locations](#view-locations)
@@ -18,6 +19,17 @@ There are three key components at the heart of the MVC 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 `TopicViewResult` based on a view model, view name, and content type.
- **`TopicViewEngine`**: The `TopicViewEngine` is called every time a view is requested. It works in conjunction with `TopicViewResult` to identify matching MVC views based on predetermined locations and conventions. These are discussed below.
+## Controllers
+There are six main controllers that ship with the MVC implementation. In addition to the core **`TopicController`**, these include the following ancillary controllers:
+- **`ErrorControllerBase`**: Provides support for `Error`, `NotFound`, and `InternalServer` actions. Can accept any `IPageTopicViewModel` as a generic argument; that will be used as the view model.
+- **`FallbackController`**: Used in a [Controller Factory](#controller-factory) as a fallback, in case no other controllers can accept the request. Simply returns a `NotFoundResult` with a predefined message.
+- **`LayoutControllerBase`**: 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.
+ - **`CachedLayoutControllerBase`**: Introduces specialized caching of `INavigationTopicViewModel` graphs whenever a call to the `GetNavigationRoot()` is called. This avoids mapping the entirety of the navigation on each request.
+- **`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 returns a reference to the `ITopicRepository`, thus allowing a sitemap view to recurse over the entire Topic graph, including all attributes.
+
+> **Note:** There is not a practical way for MVC to provide routing for generic controllers. As such, these _must_ be subclassed by each implementation. The derived controller needn't do anything outside of provide a specific type reference to the generic base.
+
## View Conventions
By default, OnTopic matches views based on the current topic's `ContentType` and, if available, `View`.
@@ -78,6 +90,8 @@ As OnTopic relies on constructor injection, the application must be configured i
var connectionString = ConfigurationManager.ConnectionStrings["OnTopic"].ConnectionString;
var sqlTopicRepository = new SqlTopicRepository(connectionString);
var cachedTopicRepository = new CachedTopicRepository(sqlTopicRepository);
+var topicViewModelLookupService = new TopicViewModelLookupService();
+var topicMappingService = new TopicMappingService(cachedTopicRepository, topicViewModelLookupService);
var mvcTopicRoutingService = new MvcTopicRoutingService(
cachedTopicRepository,
@@ -85,12 +99,10 @@ var mvcTopicRoutingService = new MvcTopicRoutingService(
requestContext.RouteData
);
-var topicMappingService = new TopicMappingService(cachedTopicRepository);
-
switch (controllerType.Name) {
case nameof(TopicController):
- return new TopicController(_topicRepository, mvcTopicRoutingService, _topicMappingService);
+ return new TopicController(sqlTopicRepository, mvcTopicRoutingService, topicMappingService);
case default:
return base.GetControllerInstance(requestContext, controllerType);
@@ -98,7 +110,7 @@ switch (controllerType.Name) {
}
```
-For a complete reference template, see the [`OrganizationNameControllerFactory.cs`](https://gist.github.com/JeremyCaney/6ba4bb0465b7dd1992a7ffdaa1ebf813) Gist.
+For a complete reference template, including the ancillary controllers, see the [`OrganizationNameControllerFactory.cs`](https://gist.github.com/JeremyCaney/6ba4bb0465b7dd1992a7ffdaa1ebf813) Gist.
> *Note:* The default `TopicController` will automatically identify the current topic (based on e.g. the URL), map the current topic to a corresponding view model (based on [the `TopicMappingService` conventions](../Ignia.Topics/Mapping/)), 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`.
diff --git a/Ignia.Topics.Web.Mvc/TopicViewEngine.cs b/Ignia.Topics.Web.Mvc/TopicViewEngine.cs
index bf7223fb..7d9450ba 100644
--- a/Ignia.Topics.Web.Mvc/TopicViewEngine.cs
+++ b/Ignia.Topics.Web.Mvc/TopicViewEngine.cs
@@ -156,7 +156,7 @@ public override ViewEngineResult FindView(ControllerContext controllerContext, s
/// The requested name of the view.
/// The list of path format patterns.
/// Determines whether the request is appropriate for caching.
- private List GetSearchPaths(ControllerContext controllerContext, string viewName, string[] locationFormats, bool useCache) {
+ private static List GetSearchPaths(ControllerContext controllerContext, string viewName, string[] locationFormats, bool useCache) {
/*------------------------------------------------------------------------------------------------------------------------
| Establish variables
diff --git a/Ignia.Topics.Web.Mvc/TopicViewResult.cs b/Ignia.Topics.Web.Mvc/TopicViewResult.cs
index ac1dc3e3..245a61fc 100644
--- a/Ignia.Topics.Web.Mvc/TopicViewResult.cs
+++ b/Ignia.Topics.Web.Mvc/TopicViewResult.cs
@@ -22,8 +22,8 @@ public class TopicViewResult : ViewResult {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- string _contentType = "";
- string _topicView = "";
+ readonly string _contentType = "";
+ readonly string _topicView = "";
/*==========================================================================================================================
| CONSTRUCTOR
@@ -77,7 +77,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
var contentType = _contentType;
var viewEngine = ViewEngines.Engines;
var requestContext = context.HttpContext.Request;
- var view = new ViewEngineResult(new string[] { });
+ var view = new ViewEngineResult(Array.Empty());
var searchedPaths = new List();
/*------------------------------------------------------------------------------------------------------------------------
@@ -89,7 +89,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
var queryStringValue = requestContext.QueryString["View"];
if (queryStringValue != null) {
view = viewEngine.FindView(context, queryStringValue, MasterName);
- searchedPaths = searchedPaths.Union(view.SearchedLocations?? new string[] { }).ToList();
+ searchedPaths = searchedPaths.Union(view.SearchedLocations?? Array.Empty()).ToList();
}
}
@@ -108,7 +108,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
// Validate against available views; if content-type represents a valid view, stop validation
if (acceptHeader != null) {
view = viewEngine.FindView(context, acceptHeader, MasterName);
- searchedPaths = searchedPaths.Union(view.SearchedLocations ?? new string[] { }).ToList();
+ searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty()).ToList();
}
if (view != null) {
break;
@@ -125,7 +125,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
\-----------------------------------------------------------------------------------------------------------------------*/
if (view.View == null && !String.IsNullOrEmpty(_topicView)) {
view = viewEngine.FindView(context, _topicView, MasterName);
- searchedPaths = searchedPaths.Union(view.SearchedLocations ?? new string[] { }).ToList();
+ searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty()).ToList();
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -133,7 +133,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
\-----------------------------------------------------------------------------------------------------------------------*/
if (view.View == null) {
view = viewEngine.FindView(context, contentType, MasterName);
- searchedPaths = searchedPaths.Union(view.SearchedLocations ?? new string[] { }).ToList();
+ searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty()).ToList();
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -141,7 +141,7 @@ protected override ViewEngineResult FindView(ControllerContext context) {
\-----------------------------------------------------------------------------------------------------------------------*/
if (view.View == null) {
view = base.FindView(context);
- searchedPaths = searchedPaths.Union(view.SearchedLocations ?? new string[] { }).ToList();
+ searchedPaths = searchedPaths.Union(view.SearchedLocations ?? Array.Empty()).ToList();
}
/*------------------------------------------------------------------------------------------------------------------------
diff --git a/Ignia.Topics.Web/Configuration/EditorElement.cs b/Ignia.Topics.Web/Configuration/EditorElement.cs
index 507e3ab3..64173eee 100644
--- a/Ignia.Topics.Web/Configuration/EditorElement.cs
+++ b/Ignia.Topics.Web/Configuration/EditorElement.cs
@@ -13,7 +13,7 @@ namespace Ignia.Topics.Web.Configuration {
| CLASS: EDITOR ELEMENT
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides a custom which represents the (default: OnTopic) editor configuration.
+ /// Provides a custom which represents the (default: OnTopic) editor configuration.
///
///
///
@@ -31,14 +31,10 @@ public class EditorElement : ConfigurationElement {
| PROPRTY: ENABLED
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets whether the (CMS) editor is enabled as defined by the configuration attribute.
+ /// Gets whether the (CMS) editor is enabled as defined by the configuration attribute.
///
[ConfigurationProperty("enabled", DefaultValue = "True", IsRequired = false)]
- public bool Enabled {
- get {
- return Convert.ToBoolean(this["enabled"], CultureInfo.InvariantCulture);
- }
- }
+ public bool Enabled => Convert.ToBoolean(this["enabled"], CultureInfo.InvariantCulture);
/*==========================================================================================================================
| PROPRTY: LOCATION
@@ -47,11 +43,7 @@ public bool Enabled {
/// Gets the website location of the (CMS) editor as defined by the configuration attribute.
///
[ConfigurationProperty("location", IsRequired=false)]
- public string Location {
- get {
- return this["source"] as string;
- }
- }
+ public string Location => this["source"] as string;
/*==========================================================================================================================
| ELEMENT: ADMIN
@@ -60,11 +52,7 @@ public string Location {
/// Gets the admin element, which describes administrative rights on the system.
///
[ConfigurationProperty("admin")]
- public SourceElement Admin {
- get {
- return this["admin"] as SourceElement;
- }
- }
+ public SourceElement Admin => this["admin"] as SourceElement;
} // Class
diff --git a/Ignia.Topics.Web/Configuration/PageTypeElement.cs b/Ignia.Topics.Web/Configuration/PageTypeElement.cs
index 0a22b858..74bfbead 100644
--- a/Ignia.Topics.Web/Configuration/PageTypeElement.cs
+++ b/Ignia.Topics.Web/Configuration/PageTypeElement.cs
@@ -14,7 +14,7 @@ namespace Ignia.Topics.Web.Configuration {
| CLASS: PAGE TYPE ELEMENT
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides a custom which represents a page type (default:
+ /// Provides a custom which represents a page type (default:
/// ) as developed for the application.
///
///
@@ -46,11 +46,7 @@ public string Name {
///
[TypeConverter(typeof(TypeNameConverter))]
[ConfigurationProperty("type", IsRequired = false)]
- public Type Type {
- get {
- return this["type"] as Type;
- }
- }
+ public Type Type => this["type"] as Type;
} // Class
diff --git a/Ignia.Topics.Web/Configuration/PageTypeElementCollection.cs b/Ignia.Topics.Web/Configuration/PageTypeElementCollection.cs
index 99236a7b..5771ff9a 100644
--- a/Ignia.Topics.Web/Configuration/PageTypeElementCollection.cs
+++ b/Ignia.Topics.Web/Configuration/PageTypeElementCollection.cs
@@ -32,15 +32,13 @@ public class PageTypeElementCollection : ConfigurationElementCollection {
/// value != null
///
public PageTypeElement this[int index] {
- get {
- return base.BaseGet(index) as PageTypeElement;
- }
+ get => base.BaseGet(index) as PageTypeElement;
set {
Contract.Requires(value != null, "The value from the getter must not be null.");
if (base.BaseGet(index) != null) {
base.BaseRemoveAt(index);
}
- this.BaseAdd(index, value);
+ BaseAdd(index, value);
}
}
@@ -51,11 +49,7 @@ public PageTypeElement this[int index] {
/// Gets the default page type (e.g., ).
///
[ConfigurationProperty("default", DefaultValue="TopicPage", IsRequired = false)]
- public string Default {
- get {
- return (string)base["default"];
- }
- }
+ public string Default => (string)base["default"];
/*==========================================================================================================================
| METHOD: CREATE NEW ELEMENT
@@ -64,9 +58,7 @@ public string Default {
/// Creates a new .
///
/// A new instance of a .
- protected override ConfigurationElement CreateNewElement() {
- return new PageTypeElement();
- }
+ protected override ConfigurationElement CreateNewElement() => new PageTypeElement();
/*==========================================================================================================================
| METHOD: GET ELEMENT KEY
diff --git a/Ignia.Topics.Web/Configuration/SourceElement.cs b/Ignia.Topics.Web/Configuration/SourceElement.cs
index 7082a753..06b588c3 100644
--- a/Ignia.Topics.Web/Configuration/SourceElement.cs
+++ b/Ignia.Topics.Web/Configuration/SourceElement.cs
@@ -34,12 +34,8 @@ public class SourceElement : ConfigurationElement {
///
/// Gets the source for the configuration setting.
///
- [ConfigurationProperty("source", DefaultValue="QueryString", IsRequired=true, IsKey=true)]
- public string Source {
- get {
- return this["source"] as string;
- }
- }
+ [ConfigurationProperty("source", DefaultValue = "QueryString", IsRequired = true, IsKey = true)]
+ public string Source => this["source"] as string;
/*==========================================================================================================================
| ATTRIBUTE: ENABLED
@@ -48,11 +44,7 @@ public string Source {
/// Gets a value indicating whether this is enabled.
///
[ConfigurationProperty("enabled", DefaultValue="True", IsRequired=false)]
- public bool Enabled {
- get {
- return Convert.ToBoolean(this["enabled"], CultureInfo.InvariantCulture);
- }
- }
+ public bool Enabled => Convert.ToBoolean(this["enabled"], CultureInfo.InvariantCulture);
/*==========================================================================================================================
| ATTRIBUTE: LOCATION
@@ -61,11 +53,7 @@ public bool Enabled {
/// Gets the location attribute value.
///
[ConfigurationProperty("location", IsRequired=false)]
- public string Location {
- get {
- return this["location"] as string;
- }
- }
+ public string Location => this["location"] as string;
/*==========================================================================================================================
| ATTRIBUTE: TRUSTED
@@ -74,11 +62,7 @@ public string Location {
/// Gets a value indicating whether this is trusted.
///
[ConfigurationProperty("trusted", DefaultValue="False", IsRequired=false)]
- public bool Trusted {
- get {
- return Convert.ToBoolean(this["trusted"], CultureInfo.InvariantCulture);
- }
- }
+ public bool Trusted => Convert.ToBoolean(this["trusted"], CultureInfo.InvariantCulture);
/*==========================================================================================================================
| METHOD: GET ELEMENT
@@ -193,7 +177,7 @@ public static string GetValue(SourceElement element) {
| METHOD: IS ENABLED
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Looks up a source element at a given location and based on a specified parent configuration elemnt and the target
+ /// Looks up a source element at a given location and based on a specified parent configuration element and the target
/// element's key, identifies the source value, and verifies whether the element is available, enabled, or set to true.
///
/// The parent .
@@ -204,7 +188,7 @@ public static string GetValue(SourceElement element) {
public static bool IsEnabled(ConfigurationElement parent, string key) => IsEnabled(parent, key, true);
///
- /// Looks up a source element at a given location and based on a specified parent configuration elemnt and the target
+ /// Looks up a source element at a given location and based on a specified parent configuration element and the target
/// element's key, identifies the source value, and verifies whether the element is available, enabled, or set to true.
///
/// The parent .
@@ -213,12 +197,11 @@ public static string GetValue(SourceElement element) {
///
/// A boolean value representing whether or not the source is available, enabled or set to true.
///
- public static bool IsEnabled(ConfigurationElement parent, string key, bool evaluateValue) {
- return IsEnabled(GetElement(parent, key), evaluateValue);
- }
+ public static bool IsEnabled(ConfigurationElement parent, string key, bool evaluateValue) =>
+ IsEnabled(GetElement(parent, key), evaluateValue);
///
- /// Looks up a source element at a given location and based on a specified parent configuration elemnt collection and
+ /// Looks up a source element at a given location and based on a specified parent configuration element collection and
/// the target element's key, identifies the source value, and verifies whether the element is available, enabled, or
/// set to true.
///
@@ -230,7 +213,7 @@ public static bool IsEnabled(ConfigurationElement parent, string key, bool evalu
public static bool IsEnabled(ConfigurationElementCollection parent, string key) => IsEnabled(parent, key, false);
///
- /// Looks up a source element at a given location and based on a specified parent configuration elemnt collection and
+ /// Looks up a source element at a given location and based on a specified parent configuration element collection and
/// the target element's key, identifies the source value, and verifies whether the element is available, enabled, or
/// set to true.
///
@@ -240,9 +223,8 @@ public static bool IsEnabled(ConfigurationElement parent, string key, bool evalu
///
/// Boolean value representing whether or not the source is available, enabled or set to true.
///
- public static bool IsEnabled(ConfigurationElementCollection parent, string key, bool evaluateValue) {
- return IsEnabled(GetElement(parent, key), evaluateValue);
- }
+ public static bool IsEnabled(ConfigurationElementCollection parent, string key, bool evaluateValue) =>
+ IsEnabled(GetElement(parent, key), evaluateValue);
///
/// Looks up a source element at a given location, identifies the source value, and verifies whether the element is
diff --git a/Ignia.Topics.Web/Configuration/SourceElementCollection.cs b/Ignia.Topics.Web/Configuration/SourceElementCollection.cs
index 361a0dce..92fddb8f 100644
--- a/Ignia.Topics.Web/Configuration/SourceElementCollection.cs
+++ b/Ignia.Topics.Web/Configuration/SourceElementCollection.cs
@@ -33,15 +33,13 @@ public class SourceElementCollection : ConfigurationElementCollection {
/// value != null
///
public SourceElement this[int index] {
- get {
- return base.BaseGet(index) as SourceElement;
- }
+ get => base.BaseGet(index) as SourceElement;
set {
Contract.Requires(value != null, "The value from the getter must not be null.");
if (base.BaseGet(index) != null) {
base.BaseRemoveAt(index);
}
- this.BaseAdd(index, value);
+ BaseAdd(index, value);
}
}
@@ -52,9 +50,7 @@ public SourceElement this[int index] {
/// Creates a new .
///
/// A new instance of a .
- protected override ConfigurationElement CreateNewElement() {
- return new SourceElement();
- }
+ protected override ConfigurationElement CreateNewElement() => new SourceElement();
/*==========================================================================================================================
| METHOD: GET ELEMENT KEY
diff --git a/Ignia.Topics.Web/Configuration/TopicsSection.cs b/Ignia.Topics.Web/Configuration/TopicsSection.cs
index 6ad6d408..89d4be86 100644
--- a/Ignia.Topics.Web/Configuration/TopicsSection.cs
+++ b/Ignia.Topics.Web/Configuration/TopicsSection.cs
@@ -20,12 +20,10 @@ public class TopicsSection : ConfigurationSection {
| FACTORY METHOD: GET CONFIG
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets the topics element from the web.config as the configuation section.
+ /// Gets the topics element from the web.config as the configuration section.
///
/// The topics section of the implementing client's configuration.
- public static TopicsSection GetConfig() {
- return ConfigurationManager.GetSection("topics") as TopicsSection;
- }
+ public static TopicsSection GetConfig() => ConfigurationManager.GetSection("topics") as TopicsSection;
/*==========================================================================================================================
| ATTRIBUTE: ROOT TOPIC NAMESPACE
@@ -35,12 +33,8 @@ public static TopicsSection GetConfig() {
///
[ConfigurationProperty("rootTopicNamespace", DefaultValue = "Root")]
public string RootTopicNamespace {
- get {
- return (string)this["rootTopicNamespace"];
- }
- set {
- this["rootTopicNamespace"] = value;
- }
+ get => (string)this["rootTopicNamespace"];
+ set => this["rootTopicNamespace"] = value;
}
/*==========================================================================================================================
@@ -51,12 +45,8 @@ public string RootTopicNamespace {
///
[ConfigurationProperty("topicDelimiter", DefaultValue = "/")]
public string TopicDelimiter {
- get {
- return (string)this["topicDelimiter"];
- }
- set {
- this["topicDelimiter"] = value;
- }
+ get => (string)this["topicDelimiter"];
+ set => this["topicDelimiter"] = value;
}
/*==========================================================================================================================
@@ -66,11 +56,7 @@ public string TopicDelimiter {
/// Gets the value of the element from the configuration.
///
[ConfigurationProperty("versioning")]
- public VersioningElement Versioning {
- get {
- return this["versioning"] as VersioningElement;
- }
- }
+ public VersioningElement Versioning => this["versioning"] as VersioningElement;
/*==========================================================================================================================
| ELEMENT: EDITOR
@@ -79,11 +65,7 @@ public VersioningElement Versioning {
/// Gets the value of the element from the configuration.
///
[ConfigurationProperty("editor")]
- public EditorElement Editor {
- get {
- return this["editor"] as EditorElement;
- }
- }
+ public EditorElement Editor => this["editor"] as EditorElement;
/*==========================================================================================================================
| ELEMENT: VIEWS
@@ -92,11 +74,7 @@ public EditorElement Editor {
/// Gets the value of the element from the configuration.
///
[ConfigurationProperty("views")]
- public ViewsElement Views {
- get {
- return this["views"] as ViewsElement;
- }
- }
+ public ViewsElement Views => this["views"] as ViewsElement;
/*==========================================================================================================================
| COLLECTION: PAGE TYPES
@@ -104,12 +82,7 @@ public ViewsElement Views {
///
/// Gets the value of the element from the configuration.
///
- [ConfigurationProperty("pageTypes")]
- public PageTypeElementCollection PageTypes {
- get {
- return this["pageTypes"] as PageTypeElementCollection;
- }
- }
+ public PageTypeElementCollection GetPageTypes() => this["pageTypes"] as PageTypeElementCollection;
} // Class
diff --git a/Ignia.Topics.Web/Configuration/VersioningElement.cs b/Ignia.Topics.Web/Configuration/VersioningElement.cs
index c10653ef..7c2a988a 100644
--- a/Ignia.Topics.Web/Configuration/VersioningElement.cs
+++ b/Ignia.Topics.Web/Configuration/VersioningElement.cs
@@ -32,11 +32,7 @@ public class VersioningElement : ConfigurationElement {
/// Gets the draft mode attribute value from the configuration element.
///
[ConfigurationProperty("draftMode")]
- public SourceElement DraftMode {
- get {
- return this["draftMode"] as SourceElement;
- }
- }
+ public SourceElement DraftMode => this["draftMode"] as SourceElement;
} // Class
diff --git a/Ignia.Topics.Web/Configuration/ViewsElement.cs b/Ignia.Topics.Web/Configuration/ViewsElement.cs
index 84ca3a4e..a5acde5f 100644
--- a/Ignia.Topics.Web/Configuration/ViewsElement.cs
+++ b/Ignia.Topics.Web/Configuration/ViewsElement.cs
@@ -25,11 +25,7 @@ public class ViewsElement : ConfigurationElement {
/// Gets the path attribute value from the configuration element.
///
[ConfigurationProperty("path", IsRequired=false)]
- public string Path {
- get {
- return this["path"] as string;
- }
- }
+ public string Path => this["path"] as string;
} // Class
diff --git a/Ignia.Topics.Web/Editor/AttributeTypeControl.cs b/Ignia.Topics.Web/Editor/AttributeTypeControl.cs
index 4f9401aa..0fa450d7 100644
--- a/Ignia.Topics.Web/Editor/AttributeTypeControl.cs
+++ b/Ignia.Topics.Web/Editor/AttributeTypeControl.cs
@@ -31,7 +31,7 @@ public AttributeTypeControl() : base() { }
| PROPERTY: INHERITED VALUE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets or sets the the value of a control, as inherited from any Topic pointers.
+ /// Gets or sets the value of a control, as inherited from any Topic pointers.
///
///
/// If this value is set, then the control should not be marked as required, as it is inheriting its value from a
@@ -46,7 +46,7 @@ public AttributeTypeControl() : base() { }
/// Gets or sets the value of a control, ignoring inheritance.
///
///
- /// The value should not be set to an inherited value, as otherwise, that value will end up being duplicatd in the local
+ /// The value should not be set to an inherited value, as otherwise, that value will end up being duplicated in the local
/// Topic.
///
public virtual string Value { get; set; }
@@ -60,7 +60,7 @@ public AttributeTypeControl() : base() { }
///
/// This allows the type to retrieve configuration or extended attributes specifically from an attribute, as opposed to
/// having them set via the DefaultValues attribute. This allows the Attribute Content Type to be subclassed, thus
- /// permitting more user-friendly interfaces to be developed for particular attributes when using the Oroborus
+ /// permitting more user-friendly interfaces to be developed for particular attributes when using the Oroboros
/// Configuration. For instance, if an attribute-related control includes a property named Color, then an attribute could
/// be added to that Attribute's Content Type allowing the color to be defined, as opposed to the publisher needing to
/// know how to set the Color attribute using the DefaultValue attribute.
diff --git a/Ignia.Topics.Web/Editor/FilePath.cs b/Ignia.Topics.Web/Editor/FilePath.cs
index 86848164..c2c06ba9 100644
--- a/Ignia.Topics.Web/Editor/FilePath.cs
+++ b/Ignia.Topics.Web/Editor/FilePath.cs
@@ -15,15 +15,7 @@ namespace Ignia.Topics.Web.Editor {
/// Provides a strongly-typed class associated with the FilePath.ascx Attribute Type control and logic associated with
/// building a configured file path from values passed to the constructor.
///
- public class FilePath {
-
- /*==========================================================================================================================
- | CONSTRUCTOR
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Initializes a new instance of the class.
- ///
- public FilePath() { }
+ public static class FilePath {
/*==========================================================================================================================
| METHOD: GET PATH
@@ -47,7 +39,7 @@ public static string GetPath(
/*----------------------------------------------------------------------------------------------------------------------
| Validate return value
\---------------------------------------------------------------------------------------------------------------------*/
- Contract.Ensures(Contract.Result() != null);
+ Contract.Ensures(Contract.Result() != null);
/*------------------------------------------------------------------------------------------------------------------------
| Only process the path if both topic and attribtueKey are provided
@@ -55,12 +47,12 @@ public static string GetPath(
if (topic == null || String.IsNullOrEmpty(attributeKey)) return "";
/*------------------------------------------------------------------------------------------------------------------------
- | Build configured file path string base on values and settings paramters passed to the method
+ | Build configured file path string base on values and settings parameters passed to the method
\-----------------------------------------------------------------------------------------------------------------------*/
- string filePath = "";
- string relativePath = null;
- Topic startTopic = topic;
- Topic endTopic = includeLeafTopic? topic : topic.Parent;
+ var filePath = "";
+ var relativePath = (string)null;
+ var startTopic = topic;
+ var endTopic = includeLeafTopic? topic : topic.Parent;
/*------------------------------------------------------------------------------------------------------------------------
| Crawl up the topics tree to find file path values set at a higher level
@@ -73,7 +65,7 @@ public static string GetPath(
}
/*------------------------------------------------------------------------------------------------------------------------
- | Add topic keys (directory names) between the start topic and the end topic based on the topic's webpath property
+ | Add topic keys (directory names) between the start topic and the end topic based on the topic's WebPath property
\-----------------------------------------------------------------------------------------------------------------------*/
if (startTopic != null) {
Contract.Assume(
@@ -87,8 +79,8 @@ public static string GetPath(
| Perform path truncation based on topics included in TruncatePathAtTopic
\-----------------------------------------------------------------------------------------------------------------------*/
if (truncatePathAtTopic != null) {
- foreach (string truncationTopic in truncatePathAtTopic) {
- int truncateTopicLocation = relativePath.IndexOf(truncationTopic, StringComparison.InvariantCultureIgnoreCase);
+ foreach (var truncationTopic in truncatePathAtTopic) {
+ var truncateTopicLocation = relativePath.IndexOf(truncationTopic, StringComparison.InvariantCultureIgnoreCase);
if (truncateTopicLocation >= 0) {
relativePath = relativePath.Substring(0, truncateTopicLocation + truncationTopic.Length + 1);
}
@@ -101,7 +93,7 @@ public static string GetPath(
filePath += relativePath;
/*------------------------------------------------------------------------------------------------------------------------
- | Replace path slashes with backslahes if the resulting file path value uses a UNC or basic file path format
+ | Replace path slashes with backslashes if the resulting file path value uses a UNC or basic file path format
\-----------------------------------------------------------------------------------------------------------------------*/
if (filePath.IndexOf("\\") >= 0) {
filePath = filePath.Replace("/", "\\");
diff --git a/Ignia.Topics.Web/Editor/IEditControl.cs b/Ignia.Topics.Web/Editor/IEditControl.cs
index 56d9933f..0bf03f7e 100644
--- a/Ignia.Topics.Web/Editor/IEditControl.cs
+++ b/Ignia.Topics.Web/Editor/IEditControl.cs
@@ -22,7 +22,7 @@ public interface IEditControl {
| PROPERTY: INHERITED VALUE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets or sets the the value of a control, as inherited from any Topic pointers.
+ /// Gets or sets the value of a control, as inherited from any Topic pointers.
///
///
/// If this value is set, then the control should not be marked as required, as it is inheriting its value from a
@@ -37,7 +37,7 @@ public interface IEditControl {
/// Gets or sets the value of a control, ignoring inheritance.
///
///
- /// The value should not be set to an inherited value, as otherwise, that value will end up being duplicatd in the local
+ /// The value should not be set to an inherited value, as otherwise, that value will end up being duplicated in the local
/// Topic.
///
string Value { get; set; }
@@ -51,7 +51,7 @@ public interface IEditControl {
///
/// This allows the type to retrieve configuration or extended attributes specifically from an attribute, as opposed to
/// having them set via the DefaultValues attribute. This allows the Attribute Content Type to be subclassed, thus
- /// permitting more user-friendly interfaces to be developed for particular attributes when using the Oroborus
+ /// permitting more user-friendly interfaces to be developed for particular attributes when using the Oroboros
/// Configuration. For instance, if an attribute-related control includes a property named Color, then an attribute could
/// be added to that Attribute's Content Type allowing the color to be defined, as opposed to the publisher needing to
/// know how to set the Color attribute using the DefaultValue attribute.
diff --git a/Ignia.Topics.Web/Ignia.Topics.Web.csproj b/Ignia.Topics.Web/Ignia.Topics.Web.csproj
index ab1c40b6..e5f3a545 100644
--- a/Ignia.Topics.Web/Ignia.Topics.Web.csproj
+++ b/Ignia.Topics.Web/Ignia.Topics.Web.csproj
@@ -10,7 +10,7 @@
Properties
Ignia.Topics.Web
Ignia.Topics.Web
- v4.5
+ v4.7
512
1
@@ -48,6 +48,16 @@
Full
0
CS0618
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -69,6 +79,7 @@
False
False
%28none%29
+ latest
pdbonly
diff --git a/Ignia.Topics.Web/Properties/AssemblyInfo.cs b/Ignia.Topics.Web/Properties/AssemblyInfo.cs
index 5b64f7c4..d933bf58 100644
--- a/Ignia.Topics.Web/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics.Web/Properties/AssemblyInfo.cs
@@ -1,36 +1,27 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Reflection;
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("Ignia.Topics.Web")]
-[assembly: AssemblyDescription("")]
+/*==============================================================================================================================
+| DEFINE ASSEMBLY ATTRIBUTES
+>===============================================================================================================================
+| Declare and define attributes used in the compiling of the finished assembly.
+\-----------------------------------------------------------------------------------------------------------------------------*/
+[assembly: AssemblyCompany("Ignia, LLC")]
+[assembly: AssemblyCopyright("Copyright © 2015 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia OnTopic WebForms Library")]
+[assembly: AssemblyDescription("Deprecated. Provides backward compatibility for the ASP.NET WebForms Framework.")]
[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Ignia.Topics.Web")]
-[assembly: AssemblyCopyright("Copyright © 2017")]
[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: AssemblyVersion("3.6.1759.0")]
+[assembly: AssemblyFileVersion("3.5.1783.0")]
+[assembly: CLSCompliant(true)]
[assembly: Guid("c98f7b48-a085-4394-b820-c244f23868ce")]
-
-// 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/Ignia.Topics.Web/TopicsRouteHandler.cs b/Ignia.Topics.Web/TopicsRouteHandler.cs
index 840125f0..ebf8651e 100644
--- a/Ignia.Topics.Web/TopicsRouteHandler.cs
+++ b/Ignia.Topics.Web/TopicsRouteHandler.cs
@@ -41,7 +41,7 @@ public TopicsRouteHandler() { }
/// Gets the expected location for View files; attempts to retrieve the value from the configuration
/// section, but defaults to ~/Common/Templates/.
///
- private string ViewsPath {
+ private static string ViewsPath {
get {
var viewsPath = "~/Common/Templates/";
var topicsSection = (TopicsSection)ConfigurationManager.GetSection("topics");
diff --git a/Ignia.Topics.Web/WebFormsTopicRoutingService.cs b/Ignia.Topics.Web/WebFormsTopicRoutingService.cs
index 7f07c4a0..65a001ad 100644
--- a/Ignia.Topics.Web/WebFormsTopicRoutingService.cs
+++ b/Ignia.Topics.Web/WebFormsTopicRoutingService.cs
@@ -59,7 +59,7 @@ public class WebFormsTopicRoutingService : ITopicRoutingService {
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// Initializes a new instance of the class based on a URL instance, a fully qualified
- /// path to the views Directory, and, optionally, the expected filename suffix fo each view file.
+ /// path to the views Directory, and, optionally, the expected filename suffix of each view file.
///
///
/// Because the is distributed with, and intended exclusively for use with, the ASP.NET
@@ -188,7 +188,7 @@ public string View {
>-------------------------------------------------------------------------------------------------------------------------
| ### TODO JJC071617: This introduces an unwanted dependency on HttpContext. While this is possible, it may introduce
| incompatibilities with the context (e.g., do ASP.NET MVC and ASP.NET Web Forms expose the same HttpContext objects?
- | Preferrably, this would be handled by each environment as appropriate, but that might muddle the logic.
+ | Preferably, this would be handled by each environment as appropriate, but that might muddle the logic.
\-----------------------------------------------------------------------------------------------------------------------*/
if (viewName == null && _headers["Accept"] != null) {
var acceptHeaders = _headers["Accept"].ToString();
@@ -257,7 +257,7 @@ private List Views {
var subDirectories = viewsDirectoryInfo.GetDirectories("*", SearchOption.AllDirectories);
/*--------------------------------------------------------------------------------------------------------------------
- | Disvoer all view templates available via the configured path
+ | Discover all view templates available via the configured path
\-------------------------------------------------------------------------------------------------------------------*/
// Get top-level (generic) view files
foreach (var file in viewsDirectoryInfo.GetFiles(searchPattern, searchOption)) {
@@ -362,11 +362,7 @@ public bool IsValidView(string contentType, string viewName, out string matchedV
/// object.
///
/// An instance of key/value pairs.
- private NameValueCollection QueryParameters {
- get {
- return HttpUtility.ParseQueryString(_uri.Query);
- }
- }
+ private NameValueCollection QueryParameters => HttpUtility.ParseQueryString(_uri.Query);
} // Class
diff --git a/Ignia.Topics/AttributeDescriptor.cs b/Ignia.Topics/AttributeDescriptor.cs
index 4c716906..c9579513 100644
--- a/Ignia.Topics/AttributeDescriptor.cs
+++ b/Ignia.Topics/AttributeDescriptor.cs
@@ -44,7 +44,7 @@ public class AttributeDescriptor : Topic {
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- private Dictionary _configuration = new Dictionary();
+ private Dictionary _configuration = null;
/*==========================================================================================================================
| CONSTRUCTOR
@@ -59,7 +59,7 @@ public class AttributeDescriptor : Topic {
/// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the
- /// persistance store.
+ /// persistence store.
///
/// A string representing the key for the new topic instance.
/// A string representing the key of the target content type.
@@ -79,13 +79,15 @@ public AttributeDescriptor(
contentType,
parent,
id
- ) { }
+ ) {
+ _configuration = new Dictionary();
+ }
/*==========================================================================================================================
| PROPERTY: TYPE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets or sets the filename refence to the Attribute Type control associate with the Topic object.
+ /// Gets or sets the filename reference to the Attribute Type control associate with the Topic object.
///
///
/// The type attribute maps to the name of a control, directive, or partial view in the editor representing the specific
@@ -118,7 +120,8 @@ public string Type {
///
/// When attributes are displayed in the editor, they are grouped by tabs. The tabs are not predetermined, but rather set
/// by individual attributes. If five attributes, for instance, have a display group of "Settings", then a tab will be
- /// rendered called "Settings" and will list those five attributes (assuming none are set to ).
+ /// rendered called "Settings" and will list those five attributes (assuming none are set to ).
///
///
/// !String.IsNullOrWhiteSpace(value)
@@ -195,33 +198,6 @@ public string GetConfigurationValue(string key, string defaultValue = null) {
return defaultValue;
}
- /*==========================================================================================================================
- | PROPERTY: IS HIDDEN?
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Gets or sets whether the attribute should be hidden in the editor.
- ///
- ///
- ///
- /// By default, all attributes associated with a are rendered in the editor.
- /// Optionally, however, attributes can be set to be hidden. This is particularly advantageous when subtyping a Content
- /// Type Descriptor, as some parent attributes may not be necessary for child content types (e.g., they may be
- /// implicitly assigned). It c be valuable for attributes that are intended to be managed by the system, and not via the
- /// editor (e.g., a timestamp or version).
- ///
- ///
- /// The property does not hide the attribute from the library itself or the views. If the view
- /// associated with the property renders the attribute (e.g., via ) then the attribute will be displayed on the page. The
- /// property is used exclusively by the editor.
- ///
- ///
- [AttributeSetter]
- public new bool IsHidden {
- get => Attributes.GetBoolean("IsHidden", false);
- set => SetAttributeValue("IsHidden", value? "1" : "0");
- }
-
/*==========================================================================================================================
| PROPERTY: IS REQUIRED?
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -268,7 +244,7 @@ public string DefaultValue {
///
/// Attributes that are needed to provide indexes, sitemaps, navigation, etc. should be indexed so that they're always
/// available in memory without requiring an additional database query. These increase the memory requirements of the
- /// application, but reduce the number of database roundtrips required for topics that are accessed outside of a single
+ /// application, but reduce the number of database round-trips required for topics that are accessed outside of a single
/// page. For instance, the title and description of a topic may be cross-referenced on other pages or as part of the
/// navigation, and should thus be indexed.
///
diff --git a/Ignia.Topics/AttributeSetterAttribute.cs b/Ignia.Topics/AttributeSetterAttribute.cs
index c95781a6..0fe61157 100644
--- a/Ignia.Topics/AttributeSetterAttribute.cs
+++ b/Ignia.Topics/AttributeSetterAttribute.cs
@@ -21,7 +21,7 @@ namespace Ignia.Topics {
/// 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 bypassedby writing directly to the collection.
+ /// business logic to be potentially bypassed by writing directly to the collection.
///
///
/// As an example, the property is adorned with the . As a
diff --git a/Ignia.Topics/AttributeValue.cs b/Ignia.Topics/AttributeValue.cs
index cf2afd04..04178f82 100644
--- a/Ignia.Topics/AttributeValue.cs
+++ b/Ignia.Topics/AttributeValue.cs
@@ -38,11 +38,6 @@ namespace Ignia.Topics {
///
public class AttributeValue {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private string _key = null;
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -68,16 +63,15 @@ public AttributeValue(string key, string value, bool isDirty = true) {
/*------------------------------------------------------------------------------------------------------------------------
| Validate input
\-----------------------------------------------------------------------------------------------------------------------*/
- Contract.Requires(!String.IsNullOrWhiteSpace(key));
- TopicFactory.ValidateKey(key);
+ TopicFactory.ValidateKey(key, false);
/*------------------------------------------------------------------------------------------------------------------------
| Set local values
\-----------------------------------------------------------------------------------------------------------------------*/
- Key = key;
- Value = value;
- IsDirty = isDirty;
- EnforceBusinessLogic = true;
+ Key = key;
+ Value = value;
+ IsDirty = isDirty;
+ EnforceBusinessLogic = true;
}
@@ -120,14 +114,7 @@ internal AttributeValue(string key, string value, bool isDirty, bool enforceBusi
/// exception="T:System.ArgumentException">
/// !value.Contains(" ")
///
- public string Key {
- get => _key;
- private set {
- Contract.Requires(!String.IsNullOrWhiteSpace(value));
- TopicFactory.ValidateKey(value);
- _key = value;
- }
- }
+ public string Key { get; }
/*==========================================================================================================================
| PROPERTY: VALUE
@@ -135,10 +122,7 @@ private set {
///
/// Gets the current value of the attribute.
///
- public string Value {
- get;
- private set;
- }
+ public string Value { get; }
/*==========================================================================================================================
| PROPERTY: IS DIRTY
@@ -152,10 +136,7 @@ public string Value {
/// when is called. Otherwise, it is ignored,
/// thus preventing the need to update attributes (or create new versions of attributes) whose values haven't changed.
///
- public bool IsDirty {
- get;
- set;
- }
+ public bool IsDirty { get; set; }
/*==========================================================================================================================
| PROPERTY: ENFORCE BUSINESS LOGIC
@@ -182,10 +163,7 @@ public bool IsDirty {
/// exception="T:System.ArgumentException">
/// !value.Contains(" ")
///
- internal bool EnforceBusinessLogic {
- get;
- set;
- }
+ internal bool EnforceBusinessLogic { get; set; }
/*=========================================================================================================================
| PROPERTY: LAST MODIFIED
@@ -193,7 +171,7 @@ internal bool EnforceBusinessLogic {
///
/// Read-only reference to the last DateTime the instance was updated.
///
- public readonly DateTime LastModified = DateTime.Now;
+ public DateTime LastModified { get; } = DateTime.Now;
} // Class
diff --git a/Ignia.Topics/Collections/AttributeValueCollection.cs b/Ignia.Topics/Collections/AttributeValueCollection.cs
index fb1f161e..6c612735 100644
--- a/Ignia.Topics/Collections/AttributeValueCollection.cs
+++ b/Ignia.Topics/Collections/AttributeValueCollection.cs
@@ -6,7 +6,7 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
-using Ignia.Topics.Repositories;
+using Ignia.Topics.Reflection;
namespace Ignia.Topics.Collections {
@@ -23,11 +23,15 @@ namespace Ignia.Topics.Collections {
///
public class AttributeValueCollection : KeyedCollection {
+ /*==========================================================================================================================
+ | STATIC VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ static readonly TypeCollection _typeCache = new TypeCollection(typeof(AttributeSetterAttribute));
+
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- private Topic _associatedTopic = null;
- static TypeCollection _typeCache = new TypeCollection(typeof(AttributeSetterAttribute));
+ private readonly Topic _associatedTopic = null;
private int _setCounter = 0;
/*==========================================================================================================================
@@ -58,7 +62,7 @@ internal AttributeValueCollection(Topic parentTopic) : base(StringComparer.Invar
/// does not support inheritFromParent or inheritFromDerived (which otherwise default to true).
///
/// The string identifier for the .
- /// True if if the attribute value is marked as dirty; otherwise false.
+ /// True if the attribute value is marked as dirty; otherwise false.
public bool IsDirty(string name) {
if (!Contains(name)) {
return false;
@@ -166,7 +170,7 @@ private string GetValue(string name, string defaultValue, bool inheritFromParent
}
/*------------------------------------------------------------------------------------------------------------------------
- | Finaly, return default
+ | Finally, return default
\-----------------------------------------------------------------------------------------------------------------------*/
return defaultValue;
@@ -227,7 +231,7 @@ out var result
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// Gets a named attribute from the Attributes dictionary with a specified default value, an optional setting for enabling
- /// of inheritance, and an optional setting for searching through derived topics for values. Return as a datetime.
+ /// of inheritance, and an optional setting for searching through derived topics for values. Return as a DateTime.
///
/// The string identifier for the .
/// A string value to which to fall back in the case the value is not found.
@@ -470,17 +474,37 @@ internal void SetValue(string key, string value, bool? isDirty, bool enforceBusi
/// business logic is enforced.
///
///
- /// If a settable property is available corresponding to the , the call should be routed
- /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is
- /// set by the 's enforceBusinessLogic parameter. To avoid an
- /// infinite loop, internal setters _must_ call this overload.
+ ///
+ /// If a settable property is available corresponding to the , the call should be routed
+ /// through that to ensure local business logic is enforced. This is determined by looking for the "__" prefix, which is
+ /// set by the 's enforceBusinessLogic parameter. To avoid an
+ /// infinite loop, internal setters _must_ call this overload.
+ ///
+ ///
+ /// Compared to the base implementation, will throw a specific error if a duplicate key
+ /// is inserted. This conveniently provides the name of the so it's clear what key is
+ /// being duplicated.
+ ///
///
/// The location that the should be set.
/// The object which is being inserted.
/// The key for the specified collection item.
+ ///
+ /// An AttributeValue with the Key '{item.Key}' already exists. The Value of the existing item is "{this[item.Key].Value};
+ /// the new item's Value is '{item.Value}'. These AttributeValues are associated with the Topic '{GetUniqueKey()}'."
+ ///
protected override void InsertItem(int index, AttributeValue item) {
if (EnforceBusinessLogic(item, out item)) {
- base.InsertItem(index, item);
+ if (!Contains(item.Key)) {
+ base.InsertItem(index, item);
+ }
+ else {
+ throw new ArgumentException(
+ $"An {nameof(AttributeValue)} with the Key '{item.Key}' already exists. The Value of the existing item is " +
+ $"{this[item.Key].Value}; the new item's Value is '{item.Value}'. These {nameof(AttributeValue)}s are associated " +
+ $"with the {nameof(Topic)} '{_associatedTopic.GetUniqueKey()}'."
+ );
+ }
}
}
@@ -534,11 +558,11 @@ private bool EnforceBusinessLogic(AttributeValue originalAttribute, out Attribut
_setCounter++;
if (_setCounter > 3) {
throw new Exception(
- "An infinite loop has occurred when setting `" + originalAttribute.Key +
- "`; be sure to call `Topic.SetAttributeValue()` when setting attributes from `Topic` properties."
+ $"An infinite loop has occurred when setting '{originalAttribute.Key}'; be sure that you are referencing " +
+ $"`Topic.SetAttributeValue()` when setting attributes from `Topic` properties."
);
}
- _typeCache.SetProperty(_associatedTopic, originalAttribute.Key, originalAttribute.Value);
+ _typeCache.SetPropertyValue(_associatedTopic, originalAttribute.Key, originalAttribute.Value);
this[originalAttribute.Key].IsDirty = originalAttribute.IsDirty;
_setCounter = 0;
return false;
diff --git a/Ignia.Topics/Collections/NamedTopicCollection.cs b/Ignia.Topics/Collections/NamedTopicCollection.cs
index 75947e0d..95e23ce9 100644
--- a/Ignia.Topics/Collections/NamedTopicCollection.cs
+++ b/Ignia.Topics/Collections/NamedTopicCollection.cs
@@ -21,11 +21,6 @@ namespace Ignia.Topics.Collections {
///
public class NamedTopicCollection: TopicCollection {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- string _name = "";
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -35,7 +30,7 @@ public class NamedTopicCollection: TopicCollection {
/// Provides a name for the collection, used to identify different collections.
/// Optionally seeds the collection with an optional list of topic references.
public NamedTopicCollection(string name = "", IEnumerable topics = null) : base() {
- _name = name;
+ Name = name;
if (topics != null) {
CopyTo(topics.ToArray(), 0);
}
@@ -51,9 +46,7 @@ public NamedTopicCollection(string name = "", IEnumerable topics = null)
/// The Name property is optional, and primary intended to differentiate multiple
/// instances being referenced in a single collection, such as the .
///
- public string Name {
- get => _name;
- }
+ public string Name { get; }
} //Class
diff --git a/Ignia.Topics/Collections/ReadOnlyTopicCollection.cs b/Ignia.Topics/Collections/ReadOnlyTopicCollection.cs
index 6ced2a8a..30cd785a 100644
--- a/Ignia.Topics/Collections/ReadOnlyTopicCollection.cs
+++ b/Ignia.Topics/Collections/ReadOnlyTopicCollection.cs
@@ -3,7 +3,6 @@
| Client Ignia, LLC
| Project Topics Library
\=============================================================================================================================*/
-
using System.Collections.Generic;
using System.Diagnostics.Contracts;
diff --git a/Ignia.Topics/Collections/ReadOnlyTopicCollection{T}.cs b/Ignia.Topics/Collections/ReadOnlyTopicCollection{T}.cs
index ba3dced2..29e3d51b 100644
--- a/Ignia.Topics/Collections/ReadOnlyTopicCollection{T}.cs
+++ b/Ignia.Topics/Collections/ReadOnlyTopicCollection{T}.cs
@@ -12,7 +12,7 @@ namespace Ignia.Topics.Collections {
| CLASS: READ ONLY TOPIC COLLECTION
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides a read-only keyed collection of topics.
+ /// Provides a read-only collection of topics.
///
public class ReadOnlyTopicCollection : ReadOnlyCollection where T : Topic {
@@ -70,11 +70,7 @@ public static ReadOnlyTopicCollection FromList(IList innerCollection) {
/// Retrieves an by key.
///
/// The topic key.
- public Topic this[string key] {
- get {
- return _innerCollection[key];
- }
- }
+ public Topic this[string key] => _innerCollection[key];
} //Class
diff --git a/Ignia.Topics/Collections/RelatedTopicCollection.cs b/Ignia.Topics/Collections/RelatedTopicCollection.cs
index 043112d1..093f275c 100644
--- a/Ignia.Topics/Collections/RelatedTopicCollection.cs
+++ b/Ignia.Topics/Collections/RelatedTopicCollection.cs
@@ -21,8 +21,8 @@ public class RelatedTopicCollection : KeyedCollectionFires any time a is added to the collection.
+ ///
+ /// Compared to the base implementation, will throw a specific error if a duplicate key is
+ /// inserted. This conveniently provides the name of the , so it's clear what key is
+ /// being duplicated.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The instance to insert.
+ ///
+ /// A NamedTopicCollection with the Name '{item.Name}' already exists in this RelatedTopicCollection. The existing key is
+ /// {this[item.Name].Name}'; the new item's is '{item.Name}'. This collection is associated with the '{GetUniqueKey()}'
+ /// Topic.
+ ///
+ protected override void InsertItem(int index, NamedTopicCollection item) {
+ if (!Contains(item.Name)) {
+ base.InsertItem(index, item);
+ }
+ else {
+ throw new ArgumentException(
+ $"A {nameof(NamedTopicCollection)} with the Name '{item.Name}' already exists in this " +
+ $"{nameof(RelatedTopicCollection)}. The existing key is '{this[item.Name].Name}'; the new item's is '{item.Name}'. " +
+ $"This collection is associated with the '{_parent.GetUniqueKey()}' Topic."
+ );
+ }
+ }
+
/*==========================================================================================================================
| OVERRIDE: GET KEY FOR ITEM
\-------------------------------------------------------------------------------------------------------------------------*/
diff --git a/Ignia.Topics/Collections/TopicCollection{T}.cs b/Ignia.Topics/Collections/TopicCollection{T}.cs
index bc7ee5c8..86cfb318 100644
--- a/Ignia.Topics/Collections/TopicCollection{T}.cs
+++ b/Ignia.Topics/Collections/TopicCollection{T}.cs
@@ -21,7 +21,7 @@ public class TopicCollection: KeyedCollection, IEnumerable wher
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- Topic _parent = null;
+ readonly Topic _parent = null;
/*==========================================================================================================================
| CONSTRUCTOR
@@ -72,6 +72,33 @@ public ReadOnlyTopicCollection AsReadOnly() {
return new ReadOnlyTopicCollection(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 void InsertItem(int index, T item) {
+ 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()}'."
+ );
+ }
+ }
+
/*==========================================================================================================================
| METHOD: CHANGE KEY
\-------------------------------------------------------------------------------------------------------------------------*/
diff --git a/Ignia.Topics/Collections/TypeCollection.cs b/Ignia.Topics/Collections/TypeCollection.cs
deleted file mode 100644
index ac0adb59..00000000
--- a/Ignia.Topics/Collections/TypeCollection.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-/*==============================================================================================================================
-| Author Ignia, LLC
-| Client Ignia, LLC
-| Project Topics Library
-\=============================================================================================================================*/
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Diagnostics.Contracts;
-using System.Reflection;
-
-namespace Ignia.Topics.Collections {
-
- /*============================================================================================================================
- | CLASS: TYPE COLLECTION
- \---------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// A collection of instances, each associated with a specific .
- ///
- internal class TypeCollection : KeyedCollection {
-
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- static List _settableTypes = null;
- private Type _attributeFlag = null;
- /*==========================================================================================================================
- | CONSTRUCTOR
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// An optional which properties must have defined to be considered writable.
- ///
- internal TypeCollection(Type attributeFlag = null) : base() {
- _attributeFlag = attributeFlag;
- }
-
- /*==========================================================================================================================
- | METHOD: GET PROPERTIES
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Returns a collection of objects associated with a specific type.
- ///
- ///
- /// If the collection cannot be found locally, it will be created.
- ///
- /// The type for which the properties should be retrieved.
- internal PropertyInfoCollection GetProperties(Type type) {
- if (!Contains(type)) {
- Add(new PropertyInfoCollection(type));
- }
- return this[type];
- }
-
- /*==========================================================================================================================
- | METHOD: GET PROPERTY
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Used reflection to identify a local property by a given name, and returns the associated
- /// instance.
- ///
- internal PropertyInfo GetProperty(Type type, string name) {
- var properties = GetProperties(type);
- if (properties.Contains(name)) {
- return properties[name];
- }
- return null;
- }
-
- /*==========================================================================================================================
- | METHOD: HAS PROPERTY
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Used reflection to identify if a local property is available.
- ///
- internal bool HasProperty(Type type, string name) => GetProperty(type, name) != null;
-
- /*==========================================================================================================================
- | METHOD: HAS SETTABLE PROPERTY
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Used reflection to identify if a local property is available and settable.
- ///
- ///
- /// Will return false if the property is not available.
- ///
- internal bool HasSettableProperty(Type type, string name) {
- var property = GetProperty(type, name);
- return (
- property != null &&
- property.CanWrite &&
- SettableTypes.Contains(property.PropertyType) &&
- (_attributeFlag == null || System.Attribute.IsDefined(property, _attributeFlag))
- );
- }
-
- /*==========================================================================================================================
- | METHOD: SET PROPERTY
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Uses reflection to call a property, assuming that it is a) writable, and b) of type ,
- /// , or .
- ///
- internal bool SetProperty(object target, string name, string value) {
-
- if (!HasSettableProperty(target.GetType(), name)) {
- return false;
- }
-
- var property = GetProperty(target.GetType(), name);
-
- object valueObject = null;
-
- Contract.Assume(property != null);
-
- if (property.PropertyType.Equals(typeof(bool))) {
- valueObject = value.Equals("1") || value.Equals("true", StringComparison.InvariantCultureIgnoreCase);
- }
- else if (property.PropertyType.Equals(typeof(int))) {
- Int32.TryParse(value, out var intValue);
- valueObject = intValue;
- }
- else if (property.PropertyType.Equals(typeof(string))) {
- valueObject = value;
- }
- else if (property.PropertyType.Equals(typeof(DateTime))) {
- if (DateTime.TryParse(value, out var date)) {
- valueObject = date;
- }
- }
-
- if (valueObject == null) {
- return false;
- }
-
- property.SetValue(target, valueObject);
- return true;
-
- }
-
- /*==========================================================================================================================
- | PROPERTY: SETTABLE TYPES
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// A list of types that are allowed to be set using .
- ///
- internal List SettableTypes {
- get {
- if (_settableTypes == null) {
- _settableTypes = new List {
- typeof(bool),
- typeof(int),
- typeof(string),
- typeof(DateTime)
- };
- }
- return _settableTypes;
- }
- }
-
- /*==========================================================================================================================
- | OVERRIDE: GET KEY FOR ITEM
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Method must be overridden for the EntityCollection to extract the keys from the items.
- ///
- /// The object from which to extract the key.
- /// The key for the specified collection item.
- protected override Type GetKeyForItem(PropertyInfoCollection item) {
- Contract.Assume(item != null, "Assumes the item is available when deriving its key.");
- return item.Type;
- }
-
- } //Class
-
-} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/ContentTypeDescriptor.cs b/Ignia.Topics/ContentTypeDescriptor.cs
index 53a97362..86402540 100644
--- a/Ignia.Topics/ContentTypeDescriptor.cs
+++ b/Ignia.Topics/ContentTypeDescriptor.cs
@@ -53,7 +53,7 @@ public class ContentTypeDescriptor : Topic {
/// correctly save new topics to the database. When the parameter is set, however, the property is set to false on as well as on , since it is assumed these are being set to the same values currently used in the
- /// persistance store.
+ /// persistence store.
///
/// A string representing the key for the new topic instance.
/// A string representing the key of the target content type.
@@ -73,7 +73,8 @@ public ContentTypeDescriptor(
contentType,
parent,
id
- ) { }
+ ) {
+ }
/*==========================================================================================================================
| PROPERTY: DISABLE CHILD TOPICS
@@ -105,7 +106,7 @@ public bool DisableChildTopics {
| PROPERTY: PERMITTED CONTENT TYPES
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides a readonly collection of what types of content types are permitted to be created as children of instances of
+ /// Provides a read-only collection of what types of content types are permitted to be created as children of instances of
/// this content type.
///
///
@@ -183,15 +184,6 @@ public AttributeDescriptorCollection AttributeDescriptors {
\-------------------------------------------------------------------------------------------------------------------*/
_attributeDescriptors = new AttributeDescriptorCollection();
- /*--------------------------------------------------------------------------------------------------------------------
- | Validate Attributes collection
- \-------------------------------------------------------------------------------------------------------------------*/
- if (!Children.Contains("Attributes") || Children["Attributes"] == null) {
- throw new Exception(
- "The ContentType '" + Title + "' does not contain a nested topic named 'Attributes' as expected."
- );
- }
-
/*--------------------------------------------------------------------------------------------------------------------
| Get values from self
>---------------------------------------------------------------------------------------------------------------------
@@ -202,8 +194,10 @@ public AttributeDescriptorCollection AttributeDescriptors {
| SqlTopicDataProvider.cs (lines 408 - 422), where it is used to add Attributes to the null Attributes collection; the
| Type property is used for determining whether the Attribute Topic is a Relationships definition or Nested Topic.
\-------------------------------------------------------------------------------------------------------------------*/
- foreach (AttributeDescriptor attribute in Children["Attributes"].Children) {
- _attributeDescriptors.Add(attribute);
+ if (Children.Contains("Attributes")) {
+ foreach (AttributeDescriptor attribute in Children["Attributes"].Children) {
+ _attributeDescriptors.Add(attribute);
+ }
}
/*--------------------------------------------------------------------------------------------------------------------
diff --git a/Ignia.Topics/DefaultTopicLookupService.cs b/Ignia.Topics/DefaultTopicLookupService.cs
new file mode 100644
index 00000000..183363ee
--- /dev/null
+++ b/Ignia.Topics/DefaultTopicLookupService.cs
@@ -0,0 +1,47 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Reflection;
+
+namespace Ignia.Topics {
+
+ /*============================================================================================================================
+ | CLASS: TYPE INDEX
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The can be configured to provide a lookup of .
+ ///
+ public class DefaultTopicLookupService: StaticTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a . Optionally accepts a list of
+ /// instances and a default value.
+ ///
+ ///
+ /// Any instances submitted via should be unique by ; if they are not, they will be removed.
+ ///
+ /// The list of instances to expose as part of this service.
+ /// The default type to return if no match can be found. Defaults to object.
+ public DefaultTopicLookupService(IEnumerable types = null, Type defaultType = null) :
+ base(types, defaultType?? typeof(Topic)) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Ensure editor types are accounted for
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!Contains("ContentTypeDescriptor")) Add(typeof(ContentTypeDescriptor));
+ if (!Contains("AttributeDescriptor")) Add(typeof(AttributeDescriptor));
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/ITypeLookupService.cs b/Ignia.Topics/ITypeLookupService.cs
new file mode 100644
index 00000000..beb11036
--- /dev/null
+++ b/Ignia.Topics/ITypeLookupService.cs
@@ -0,0 +1,36 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using Ignia.Topics.Mapping;
+
+namespace Ignia.Topics {
+
+ /*============================================================================================================================
+ | INTERFACE: TYPE LOOKUP SERVICE
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides an interface for looking up s by string.
+ ///
+ ///
+ /// Various areas of the OnTopic library require looking up s dynamically. For instance, the and the both determine whether or not there is a corresponding to the ContentType (though the former is looking for a , and the
+ /// latter is looking for a data transfer object). The provides a standard interface for
+ /// libraries capable of providing this functionality, allowing them to be injected into other services.
+ ///
+ public interface ITypeLookupService {
+
+ /*==========================================================================================================================
+ | METHOD: GET TYPE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Gets the requested .
+ ///
+ /// The name of the to retrieve.
+ Type GetType(string typeName);
+
+ }
+}
diff --git a/Ignia.Topics/Ignia.Topics.csproj b/Ignia.Topics/Ignia.Topics.csproj
index 8e9195ce..4c1bb8ac 100644
--- a/Ignia.Topics/Ignia.Topics.csproj
+++ b/Ignia.Topics/Ignia.Topics.csproj
@@ -10,7 +10,7 @@
Properties
Ignia.Topics
Ignia.Topics
- v4.5
+ v4.7
512
@@ -47,6 +47,16 @@
Full
0
+ True
+ False
+ True
+ True
+ False
+ None.None.Increment.None
+ False
+ SettingsVersion
+ None
+ None.None.Increment.None
true
@@ -70,6 +80,8 @@
False
False
%28none%29
+ true
+ latest
pdbonly
@@ -137,23 +149,34 @@
-
-
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Ignia.Topics/InvalidKeyException.cs b/Ignia.Topics/InvalidKeyException.cs
index bc6950e7..8bab105c 100644
--- a/Ignia.Topics/InvalidKeyException.cs
+++ b/Ignia.Topics/InvalidKeyException.cs
@@ -4,6 +4,8 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Diagnostics.Contracts;
+using System.Runtime.Serialization;
namespace Ignia.Topics {
@@ -17,6 +19,7 @@ namespace Ignia.Topics {
/// s are alphanumeric, and may contain hyphens, periods, or underscores. Any other values, such as
/// spaces, slashes, or colons, are not permitted and will throw an exception.
///
+ [Serializable]
public class InvalidKeyException: ArgumentException {
/*==========================================================================================================================
@@ -68,6 +71,15 @@ public InvalidKeyException(string message, string paramName) : base(message, par
public InvalidKeyException(string message, string paramName, Exception inner) : base(message, paramName, inner) {
}
+ ///
+ /// Instantiates a new instance of a class for serialization.
+ ///
+ /// A instance with details about the serialization requirements.
+ /// A instance with details about the request context.
+ /// A new instance.
+ protected InvalidKeyException(SerializationInfo info, StreamingContext context) : base(info, context) {
+ Contract.Requires(info != null);
+ }
}
}
diff --git a/Ignia.Topics/Mapping/AttributeKeyAttribute.cs b/Ignia.Topics/Mapping/AttributeKeyAttribute.cs
index 88a3057e..2ab12c31 100644
--- a/Ignia.Topics/Mapping/AttributeKeyAttribute.cs
+++ b/Ignia.Topics/Mapping/AttributeKeyAttribute.cs
@@ -24,11 +24,6 @@ namespace Ignia.Topics.Mapping {
[System.AttributeUsage(System.AttributeTargets.Property)]
public sealed class AttributeKeyAttribute : System.Attribute {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private string _attributeKey = null;
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -38,7 +33,7 @@ public sealed class AttributeKeyAttribute : System.Attribute {
/// The key value of the attribute associated with the current property.
public AttributeKeyAttribute(string attributeKey) {
TopicFactory.ValidateKey(attributeKey, false);
- _attributeKey = attributeKey;
+ Value = attributeKey;
}
/*==========================================================================================================================
@@ -47,11 +42,7 @@ public AttributeKeyAttribute(string attributeKey) {
///
/// Gets the value of the attribute key.
///
- public string Value {
- get {
- return _attributeKey;
- }
- }
+ public string Value { get; }
} //Class
diff --git a/Ignia.Topics/Mapping/CachedTopicMappingService.cs b/Ignia.Topics/Mapping/CachedTopicMappingService.cs
index 6176b6a2..ac711f55 100644
--- a/Ignia.Topics/Mapping/CachedTopicMappingService.cs
+++ b/Ignia.Topics/Mapping/CachedTopicMappingService.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics.Contracts;
+using System.Threading.Tasks;
namespace Ignia.Topics.Mapping {
@@ -26,8 +27,8 @@ public class CachedTopicMappingService : ITopicMappingService {
/*==========================================================================================================================
| ESTABLISH CACHE
\-------------------------------------------------------------------------------------------------------------------------*/
- private readonly ConcurrentDictionary, object> _cache =
- new ConcurrentDictionary, object>();
+ private readonly ConcurrentDictionary<(int, Type, Relationships), object> _cache =
+ new ConcurrentDictionary<(int, Type, Relationships), object>();
/*==========================================================================================================================
| CONSTRUCTOR
@@ -53,7 +54,7 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) {
/// . These results may need to be cast to a specific type, depending on the context. That said,
/// strongly-typed views should be able to cast the object to the appropriate View Model type. If the type of the View
/// Model is known upfront, and it is imperative that it be strongly-typed, then prefer .
+ /// cref="MapAsync{T}(Topic, Relationships)"/>.
///
///
/// Because the target object is being dynamically constructed by the underlying implementation, it must implement a
@@ -63,12 +64,12 @@ public CachedTopicMappingService(ITopicMappingService topicMappingService) {
/// The entity to derive the data from.
/// Determines what relationships the mapping should follow, if any.
/// An instance of the dynamically determined View Model with properties appropriately mapped.
- public object Map(Topic topic, Relationships relationships = Relationships.All) {
+ public async Task
///
- /// The overrides this behavior. If set, the will
+ /// The overrides this behavior. If set, the will
/// populate the specified on the related topics. By default, it will crawl all
/// relationships, but the flag can optionally be used to specify one or multiple
/// relationship types, thus providing fine-tune control.
///
///
[System.AttributeUsage(System.AttributeTargets.Property)]
- public sealed class RecurseAttribute : System.Attribute {
-
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private Relationships _relationships = Relationships.All;
+ public sealed class FollowAttribute : System.Attribute {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Annotates a property with the by providing an .
+ /// Annotates a property with the by providing an .
///
/// The specific relationships that should be crawled.
- public RecurseAttribute(Relationships relationships = Relationships.All) {
- _relationships = relationships;
+ public FollowAttribute(Relationships relationships) {
+ Relationships = relationships;
}
/*==========================================================================================================================
| PROPERTY: RELATIONSHIPS
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Gets the type(s) of relationships that should be recused over.
+ /// Gets the type(s) of relationships that should be recursed over.
///
- public Relationships Relationships {
- get {
- return _relationships;
- }
- }
+ public Relationships Relationships { get; }
} //Class
diff --git a/Ignia.Topics/Mapping/ITopicMappingService.cs b/Ignia.Topics/Mapping/ITopicMappingService.cs
index 80857e40..986e4ada 100644
--- a/Ignia.Topics/Mapping/ITopicMappingService.cs
+++ b/Ignia.Topics/Mapping/ITopicMappingService.cs
@@ -4,6 +4,7 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Threading.Tasks;
namespace Ignia.Topics.Mapping {
@@ -28,7 +29,7 @@ public interface ITopicMappingService {
/// Because the class is using reflection to determine the target View Models, the return type is .
/// These results may need to be cast to a specific type, depending on the context. That said, strongly-typed views
/// should be able to cast the object to the appropriate View Model type. If the type of the View Model is known
- /// upfront, and it is imperative that it be strongly-typed, then prefer .
+ /// upfront, and it is imperative that it be strongly-typed, then prefer .
///
///
/// Because the target object is being dynamically constructed, it must implement a default constructor.
@@ -37,7 +38,7 @@ public interface ITopicMappingService {
/// The entity to derive the data from.
/// Determines what relationships the mapping should follow, if any.
/// An instance of the dynamically determined View Model with properties appropriately mapped.
- object Map(Topic topic, Relationships relationships = Relationships.All);
+ Task MapAsync(Topic topic, Relationships relationships = Relationships.All);
/*==========================================================================================================================
| METHOD: MAP (GENERIC)
@@ -56,7 +57,7 @@ public interface ITopicMappingService {
///
/// An instance of the requested View Model with properties appropriately mapped.
///
- T Map(Topic topic, Relationships relationships = Relationships.All) where T : class, new();
+ Task MapAsync(Topic topic, Relationships relationships = Relationships.All) where T : class, new();
/*==========================================================================================================================
| METHOD: MAP (INSTANCES)
@@ -71,7 +72,7 @@ public interface ITopicMappingService {
///
/// An instance of the requested View Model instance with properties appropriately mapped.
///
- object Map(Topic topic, object target, Relationships relationships = Relationships.All);
+ Task MapAsync(Topic topic, object target, Relationships relationships = Relationships.All);
} //Interface
} //Namespace
diff --git a/Ignia.Topics/Mapping/MetadataAttribute.cs b/Ignia.Topics/Mapping/MetadataAttribute.cs
index f36f1074..8166f3c0 100644
--- a/Ignia.Topics/Mapping/MetadataAttribute.cs
+++ b/Ignia.Topics/Mapping/MetadataAttribute.cs
@@ -13,7 +13,7 @@ namespace Ignia.Topics.Mapping {
/// Flags that a property should be mapped to a list of metadata available in the Configuration namespace.
///
///
- /// In the Topic Editor, the TopicLookup allows editors to select values from dropdown lists representing topics.
+ /// In the Topic Editor, the TopicLookup allows editors to select values from drop-down lists representing topics.
/// Those topics, by default, are stored in the Configuration:Metadata namespace. The metadata attribute allows a
/// strongly-typed reference to be created, thus pulling either a reference to a specific topic (in the case of a single
/// value property) or a collection of the metadata (in the case of a collection).
@@ -21,11 +21,6 @@ namespace Ignia.Topics.Mapping {
[System.AttributeUsage(System.AttributeTargets.Property)]
public sealed class MetadataAttribute : System.Attribute {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private string _key = null;
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -35,7 +30,7 @@ public sealed class MetadataAttribute : System.Attribute {
/// The key represents the name of the Metadata topic that should be mapped to.
public MetadataAttribute(string key) {
TopicFactory.ValidateKey(key, false);
- _key = key;
+ Key = key;
}
/*==========================================================================================================================
@@ -44,12 +39,7 @@ public MetadataAttribute(string key) {
///
/// Gets the value of the key.
///
- public string Key {
- get {
- return _key;
- }
- }
+ public string Key { get; }
} //Class
-
} //Namespace
diff --git a/Ignia.Topics/Mapping/PropertyConfiguration.cs b/Ignia.Topics/Mapping/PropertyConfiguration.cs
new file mode 100644
index 00000000..0cf37702
--- /dev/null
+++ b/Ignia.Topics/Mapping/PropertyConfiguration.cs
@@ -0,0 +1,350 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+
+namespace Ignia.Topics.Mapping {
+
+ /*============================================================================================================================
+ | CLASS: PROPERTY ATTRIBUTES
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Evaluates a instance for known , and exposes them through a set of
+ /// property values.
+ ///
+ ///
+ ///
+ /// The class is utilized by implementations of to
+ /// facilitate the mapping of source instances to Data Transfer Objects (DTOs), such as View Models.
+ /// The attribute values provide hints to the mapping service that help manage how the mapping occurs.
+ ///
+ ///
+ /// For example, by default a property on a DTO class will be mapped to a property or attribute of the same name on the
+ /// source . If the is attached to a property on the DTO, however,
+ /// then the will instead use the value defined by that attribute, thus allowing a
+ /// property on the DTO to be aliased to a different property or attribute name on the source .
+ ///
+ ///
+ public class PropertyConfiguration {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a instance, exposes a set of properties associated with known
+ /// instances.
+ ///
+ /// The instance to check for values.
+ public PropertyConfiguration(PropertyInfo property) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Set backing property
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ Property = property;
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Set default values
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ AttributeKey = property.Name;
+ DefaultValue = null;
+ InheritValue = false;
+ RelationshipKey = AttributeKey;
+ RelationshipType = RelationshipType.Any;
+ CrawlRelationships = Relationships.None;
+ MetadataKey = (string)null;
+ AttributeFilters = new Dictionary();
+ FlattenChildren = false;
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Attributes: Retrieve basic attributes
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ GetAttributeValue(property, a => DefaultValue = a.Value);
+ GetAttributeValue(property, a => InheritValue = true);
+ GetAttributeValue(property, a => AttributeKey = a.Value);
+ GetAttributeValue(property, a => CrawlRelationships = a.Relationships);
+ GetAttributeValue(property, a => FlattenChildren = true);
+ GetAttributeValue(property, a => MetadataKey = a.Key);
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Attributes: Determine relationship key and type
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ GetAttributeValue(
+ property,
+ a => {
+ RelationshipKey = a.Key ?? RelationshipKey;
+ RelationshipType = a.Type;
+ }
+ );
+
+ if (RelationshipType.Equals(RelationshipType.Any) && RelationshipKey.Equals("Children")) {
+ RelationshipType = RelationshipType.Children;
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Attributes: Set attribute filters
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var filterByAttribute = property.GetCustomAttributes(true);
+ if (filterByAttribute != null && filterByAttribute.Count() > 0) {
+ foreach (var filter in filterByAttribute) {
+ AttributeFilters.Add(filter.Key, filter.Value);
+ }
+ }
+
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: PROPERTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The that the current is associated with.
+ ///
+ public PropertyInfo Property { get; }
+
+ /*==========================================================================================================================
+ | PROPERTY: ATTRIBUTE KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The name of the corresponding attribute in the instance. Defaults to the property name
+ /// on DTO.
+ ///
+ ///
+ ///
+ /// By default, a property on a DTO class will be mapped to a property or attribute of the same name on the source . If the is attached to a property on the DTO, however, then the
+ /// will instead use the value defined by that attribute, thus allowing a property on
+ /// the DTO to be aliased to a different property or attribute name on the source .
+ ///
+ ///
+ /// The property corresponds to the property. It
+ /// can be assigned by decorating a DTO property with e.g. [AttributeKey("AlternateAttributeKey")].
+ ///
+ ///
+ public string AttributeKey { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: DEFAULT VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The default value to use if no corresponding value can be determined from the source .
+ ///
+ ///
+ /// The property corresponds to the property. It can
+ /// be assigned by decorating a DTO property with e.g. [DefaultValue("DefaultValue")].
+ ///
+ public object DefaultValue { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: INHERIT VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines whether the value should be inherited from if the value cannot be identified
+ /// on the source .
+ ///
+ ///
+ ///
+ /// The configuration is only applicable if the value is pulled from the collection. This is the equivalent to calling the method with an InheritFromParent parameter set to
+ /// True.
+ ///
+ ///
+ /// The property corresponds to the being set on a given
+ /// property. It can be assigned by decorating a DTO property with e.g. [Inherit].
+ ///
+ ///
+ public bool InheritValue { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: RELATIONSHIP KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The name of the relationship that a collection should map to. Defaults to the property name on DTO.
+ ///
+ ///
+ ///
+ /// By default, a collection property on a DTO class will be mapped to a corresponding relationship of the same name.
+ /// So, for instance, if the property on the DTO class is called Cousins then the will search , , and, finally, for an object named Cousins.
+ /// If the is set, however, then that value is used instead, thus allowing the property on
+ /// the DTO to be aliased to a different collection name on the source .
+ ///
+ ///
+ /// The property corresponds to the property. It
+ /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey")].
+ ///
+ ///
+ public string RelationshipKey { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: RELATIONSHIP TYPE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines the type of relationship that a collection property corresponds to.
+ ///
+ ///
+ ///
+ /// By default, a collection property on a DTO class will attempt to find a match from, in order, , , and, finally, .
+ /// If the is set, however, then the will only
+ /// map the collection to a relationship of that type. This can be valuable when the might
+ /// be ambiguous between multiple collections.
+ ///
+ ///
+ /// The property corresponds to the property. It
+ /// can be assigned by decorating a DTO property with e.g. [Relationship("AlternateRelationshipKey",
+ /// RelationshipType.Children)].
+ ///
+ ///
+ public RelationshipType RelationshipType { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: CRAWL RELATIONSHIPS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines which relationships, if any, the service should crawl for the current
+ /// property.
+ ///
+ ///
+ ///
+ /// By default, the all relationships will be mapped on the target DTO, unless the caller specifies otherwise. On any
+ /// related DTOs, however, only will be mapped. So, if a mapped DTO has a
+ /// collection for children, relationships, or even a parent property then any relationships on those DTOs will
+ /// not be mapped. This behavior can be changed by specifying the flag, which allows
+ /// one or multiple relationships to be specified for a given property.
+ ///
+ ///
+ /// The property corresponds to the
+ /// property. It can be assigned by decorating a DTO property with e.g. [Follow(Relationships.Children)].
+ ///
+ ///
+ public Relationships CrawlRelationships { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: METADATA KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Instructs the to pull in a collection of mapped topics from a corresponding
+ /// collection in the Root:Configuration:Metadata namespace of the Topic graph.
+ ///
+ ///
+ ///
+ /// Sometimes, a value will be associated with lookup s found in the
+ /// Root:Configuration:Metadata namespace of the Topic graph. In these cases, it can be useful to include the
+ /// full set of metadata on the mapped DTO so the view has access to it. For instance, a DTO may include a States
+ /// property which includes a full list of all states potentially associated with a , which might be
+ /// used in the user interface to provide filtering of e.g. child s.
+ ///
+ ///
+ /// The property corresponds to the property. It can be
+ /// assigned by decorating a DTO property with e.g. [Metadata("States")].
+ ///
+ ///
+ public string MetadataKey { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: ATTRIBUTE FILTERS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a list of Key/Value pairs corresponding to which can optionally
+ /// be used to filter a collection.
+ ///
+ ///
+ ///
+ /// By default, all s in a source collection (e.g., ) will be included in
+ /// a corresponding collection on the DTO (assuming the mapped DTO is compatible with the collection type). If any
+ /// are set, however, then each will be evaluated to confirm that it
+ /// satisfies the conditions of those filters.
+ ///
+ ///
+ /// The property corresponds to the and
+ /// properties. It can be assigned by decorating a DTO property with e.g.
+ /// [FilterByAttribute("Company", "Ignia")]. Multiple s can be assigned
+ /// to a single property, thus allowing each item in a collection to be filtered by multiple values.
+ ///
+ ///
+ public Dictionary AttributeFilters { get; }
+
+ /*==========================================================================================================================
+ | PROPERTY: FLATTEN CHILDREN
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines whether all descendants in a collection should be included in the collection.
+ ///
+ ///
+ ///
+ /// By default, only stored directly in a source collection will be included in the target
+ /// collection on a DTO class. If the property is set, however, then all descendants of
+ /// those s are also included. So, for instance, if a Children property is decorated
+ /// with the , then not only will the be included, but any
+ /// grandchildren, great-grandchildren, etc. will be included.
+ ///
+ ///
+ /// When is specified, implementations are expected to
+ /// apply all other constraints. For instance, if the target collection is strongly typed, then only DTOs that exhibit
+ /// polymorphic compatibility with that type will be included (i.e., DTOs of the same or a derived type as the target
+ /// collection). Similarly, any will be applied to each of the descendants. Finally, if
+ /// the target collection has a unique constraint (e.g., due to implementing )
+ /// then any items that would constitute a duplicate will be filtered out without raising an exception. This
+ /// can thus allow the method to be used to represent unique search results within a
+ /// graph.
+ ///
+ ///
+ /// The property corresponds to the being set on a given
+ /// property. It can be assigned by decorating a DTO property with e.g. [Flatten].
+ ///
+ ///
+ public bool FlattenChildren { get; set; }
+
+ /*==========================================================================================================================
+ | METHOD: SATISFIES ATTRIBUTE FILTERS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a , determines whether or not the satisfy all defined on the property.
+ ///
+ ///
+ ///
+ public bool SatisfiesAttributeFilters(Topic source) =>
+ AttributeFilters.All(f => source.Attributes.GetValue(f.Key, "").Equals(f.Value));
+
+ /*==========================================================================================================================
+ | METHOD: VALIDATE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a target DTO, will automatically identify any attributes that derive from and
+ /// ensure that their conditions are satisfied.
+ ///
+ /// The target DTO to validate the current property on.
+ public void Validate(object target) {
+ foreach (ValidationAttribute validator in Property.GetCustomAttributes(typeof(ValidationAttribute))) {
+ validator.Validate(Property.GetValue(target), Property.Name);
+ }
+ }
+
+ /*==========================================================================================================================
+ | PRIVATE: GET ATTRIBUTE VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Helper function evaluates an attribute and then, if it exists, executes an to process the
+ /// results.
+ ///
+ /// An type to evaluate.
+ /// The instance to pull the attribute from.
+ /// The to execute on the attribute.
+ private static void GetAttributeValue(PropertyInfo property, Action action) where T : Attribute {
+ var attribute = (T)property.GetCustomAttribute(typeof(T), true);
+ if (attribute != null) {
+ action(attribute);
+ }
+ }
+
+ } //Class
+} //Namespace
diff --git a/Ignia.Topics/Mapping/README.md b/Ignia.Topics/Mapping/README.md
index 4fa67bca..7a5a89cc 100644
--- a/Ignia.Topics/Mapping/README.md
+++ b/Ignia.Topics/Mapping/README.md
@@ -7,12 +7,17 @@ The [`ITopicMappingService`](ITopicMappingService.cs) provides the abstract inte
- [Properties](#properties)
- [Scalar Values](#scalar-values)
- [Collections](#collections)
+ - [References](#references)
- [Parent](#parent)
- [Example](#example)
- [Attributes](#attributes)
- [Example](#example-1)
- [Polymorphism](#polymorphism)
+ - [Filtering](#filtering)
+ - [Topics](#topics)
- [Caching](#caching)
+ - [Internal Caching](#internal-caching)
+ - [`CachedTopicMappingService`](#cachedtopicmappingservice)
## `TopicMappingService`
The [`TopicMappingService`](TopicMappingService.cs) provides a concrete implementation that is expected to satisfy the requirements of most consumers. This supports the following conventions.
@@ -31,9 +36,13 @@ The mapping service will automatically attempt to map any properties on a data t
#### Scalar Values
If a property is of the type `bool`, `int`, `string`, or `DateTime`, then:
- Will pull the value from a parameterless getter method with the same name.
- - E.g., if a property is named "Author" it will pull from a `topic.GetAuthor()` method.
+- Will pull the value from a property of the same name.
- Otherwise, will pull the value from the `topic.Attributes.GetValue()` method.
- - E.g., If a property is named `Author`, it will call `topic.Attributes.GetValue("Author")`.
+
+For example, if a property on a view model is named `Author`, it will automatically look, in order, for:
+- `topic.GetAuthor()`
+- `topic.Author`
+- `topic.Attributes.GetValue("Author")`
#### Collections
If a property implements `IList` (e.g., `List<>`, `Collection<>`, `TopicViewModelCollection<>`), then:
@@ -42,8 +51,15 @@ If a property implements `IList` (e.g., `List<>`, `Collection<>`, `TopicViewMode
- Will search, in order, `topic.Relationships`, `topic.IncomingRelationships`, `topic.Children`.
- E.g., If a `List<>` property is named `Cousins` then it might match `topic.Relationships.GetTopics("Cousins")`.
+#### References
+Topic references relate a single topic to another topic. If a property corresponds to an attribute named `{Property}Id`, that identifier refers to a `Topic`, and that `Topic` maps to an object that is assignable to the original property, then the `Topic` will be loaded, mapped, and assigned to that property. For instance, the following property:
+```
+public AuthorTopicViewModel Author { get; set; }
+```
+Would be mapped to an `AuthorTopicViewModel` if `topic.Attributes.GetValue("AuthorId")` returns the identifier of a `Topic` with a `ContentType` set to `Author`.
+
#### Parent
-If a property is named `Parent`, then the `TopicMappingService` will pull the value from `topic.Parent`.
+If a property is named `Parent`, then the `TopicMappingService` will pull the value from `topic.Parent`. This acts as a special version of a [Topic Reference](#references).
> *Note:* By default, collections and reference properties of related topics will not be pulled. For instance, if a `TopicViewModel` has a `Children` collection, then the relationships, nested topics, and children of those instances will not be populated. This is meant to constrain the size of the object graph delivered.
@@ -83,7 +99,8 @@ To support the mapping, a variety of `Attribute` classes are provided for decora
- **`[AttributeKey(key)]`**: Instructs the `TopicMappingService` to use the specified `key` instead of the property name when calling `topic.Attributes.GetValue()`.
- **`[FilterByAttribute(key, value)]`**: Ensures that all items in a collection have an attribute named "Key" with a value of "Value"; all else will be excluded. Multiple instances can be stacked.
- **`[Relationship(key, type)]`**: For a collection, optionally specifies the name of the key to look for, instead of the property name, and the relationship type, in case the key name is ambiguous.
-- **`[Recurse(relationships)]`**: Instructs the code to populate the specified relationships on any view models within a collection.
+- **`[Follow(relationships)]`**: Instructs the code to populate the specified relationships on any view models within a collection.
+- **`[Flatten]`**: Includes all descendants for every item in the collection. If the collection enforces uniqueness, duplicates will be removed.
### Example
The following is an example of a data transfer object that implements the above attributes:
@@ -101,16 +118,17 @@ public class CompanyTopicViewModel {
[Metadata("Countries")]
public TopicViewModelCollection Countries { get; set; }
- [Relationship("Companies", RelationshipType.IncomingRelationship)]
- [Recurse(Relationships.Children)]
+ [Relationship("Companies", Type=RelationshipType.IncomingRelationship)]
+ [Follow(Relationships.Children)]
public TopicViewModelCollection CaseStudies { get; set; }
- [Recurse(Relationships.Relationships)]
+ [Follow(Relationships.Relationships)]
public TopicViewModelCollection Children { get; set; }
- [Relationship("Employees", RelationshipType.NestedTopics)]
+ [Relationship("Employees", Type=RelationshipType.NestedTopics)]
[FilterByAttribute("IsActive", "1")]
[FilterByAttribute("Role", "Account Manager")]
+ [Flatten]
public TopicViewModelCollection Contacts { get; set; }
}
@@ -121,31 +139,47 @@ In this example, the properties would map to:
- `HideFromDirectory`: An attribute named `IsHidden` or a method named `GetIsHidden()`. If null, will look in `Parent` topics.
- `Countries`: Loads all `LookupListItem` instances in the `Root:Configuration:Metadata:Countries` metadata collection.
- `CaseStudies`: A collection of `CaseStudy` topics pointing to the current `Company` via a "Companies" relationship. Will load the children of each case study.
-- `Children`: A collection of child topics, with all relationships loaded.
-- `Contacts`: A list of `Employee` nested topics, filtered by those with `IsActive` set to `1` (`true`) and `Role` set to "Account Manager".
+- `Children`: A collection of child topics, with all relationships (but not e.g. grandchildren) loaded.
+- `Contacts`: A list of `Employee` nested topics, filtered by those with `IsActive` set to `1` (`true`) and `Role` set to "Account Manager". Includes any descendants of the nested topics that meet the previous criteria.
-> *Note*: Often times, data transfer objects won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional help. For instance, the `[Relationship(…)]` attribute is useful if the relationship key is ambigious between outgoing relationships and incoming relationships.
+> *Note*: Often times, data transfer objects won't require any attributes. These are only needed if the properties don't follow the built-in conventions and require additional help. For instance, the `[Relationship(…)]` attribute is useful if the relationship key is ambiguous between outgoing relationships and incoming relationships.
## Polymorphism
If a reference type (e.g., `TopicViewModel Parent`) or a strongly-typed collection property (e.g., `List`) are defined, then any target instances must be assignable by the base type (in these cases, `TopicViewModel`). If they cannot be, then they will not be included; no error will occur.
+### Filtering
This can be useful for filtering a collection. For instance, if a `CompanyTopicViewModel` includes an `Employees` collection of type `List` then it will only be populated by topics that can be mapped to either `ManagerTopicViewModel` or a derivative (perhaps, `ExecutiveTopicViewModel`). Other types (e.g., `EmployeeTopicViewModel`) will be excluded, even though they might otherwise be referenced by the `Employees` relationship.
> *Note:* For this reason, it is recommended that view models use inheritance based on the content type hierarchy. This provides an intuitive mapping to content type definitions, avoids needing to redefine base properties, and allows for polymorphism in assigning derived types.
+### Topics
+While it's not a best practice, this also works for strongly-typed collections of `Topic` objects. Typically, collections should return view models, but if the collection is strongly-typed to `Topic` (or a derivative) then the source `Topic` will not be mapped, and will be used as-is assuming it implements (or derives from) the target `Topic` type. This can be useful for scenarios where a view needs full access to the object graph (such as the `SitemapController`). In such cases, it is impractical to map the entirety of an object graph, along with all attributes, to a corresponding view model graph, and makes more sense to simply return the `Topic` graph.
+
## Caching
-By default, the `TopicMappingService` will cache all types discovered that end with `TopicViewModel`, as well as all `PropertyInfo` objects associated with each of those types. That mitigates much of the performance hit associated with the use of reflection. Despite that, simply setting properties—and, especially, on large object graphs—can require a lot of processing time. To mitigate this, OnTopic also offers the [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which accepts a concrete implementation of an `ITopicMappingService` and provides caching based on `topic.Id`, `Type`, and `Relationships`. Because the cache is based on all three of these, it will differentiate between the results of e.g.,
+By default, the `TopicMappingService` will cache a reference to all `MemberInfo` objects associated with each of view model it maps. That mitigates much of the performance hit associated with the use of reflection. Despite that, simply setting properties—and, especially, on large object graphs—can require a lot of processing time. To address this, OnTopic also offers two approaches.
+
+### Internal Caching
+When a request is made to `TopicMappingService`, and internal cache is constructed. If any mapping requests refer to a `Topic` that's already been mapped as part of the _current_ object graph, then that object will be returned. This prevents unnecessary duplication of mapping, and also avoids the potential for infinite loops. For instance, if a view model includes `Children`, and those children are set to `[Follow(Relationships.Parents)]`, the `TopicMappingService` will point back to the originally-mapped `Parent` object, instead of mapping a new instance of that `Topic`.
+
+### `CachedTopicMappingService`
+The [`CachedTopicMappingService`](CachedTopicMappingService.cs) Decorator, which accepts a concrete implementation of an `ITopicMappingService`, provides caching across requests based on `topic.Id`, `Type`, and `Relationships`. Because the cache is based on all three of these, it will differentiate between the results of e.g.,
- `topicMappingService.Map(topic, Relationships.All)`
- `topicMappingService.Map(topic, Relationships.Children)`
- `topicMappingService.Map(topic, Relationships.Children)`
-To implement the caching decorator, use the following construction as a Singleton lifecycle in your composer:
+To implement the caching decorator, use the following construction as a Singleton lifestyle in your composer:
```
var topicRepository = new SqlTopicRepository(…);
var topicMappingService = new TopicMappingService(topicRepository);
var cachedTopicMappingService = new CachedTopicMappingService(topicMappingService);
```
-> *Note:* Be aware that the `CachedTopicMappingService` may take up considerable memory, depending on how many permutations of mapped objects the application has. This is especially true since it caches each unique object graph; no effort is made to centralize references to e.g. relationships that reference the same object instance.
+> _**Important**_: Due to limitations discussed below, the application of the `CachedTopicMappingService` is quite restricted. It is likely inapprorpiate for page content, since that wouldn't reflect changes made via the editor. And it isn't appropriate for e.g. the `LayoutControllerBase{T}`, since it manually constructs its tree.
+
+
+#### Limitations
+While the `CachedTopicMappingService` can be useful for particular scenarios, it introduces several limitations that should be accounted for.
-> *Note:* The `CachedTopicMappingService` makes no effort to validate or evict cache entries. Topics whose values change during the lifetyime of the `CachedTopicMappingService` will not be reflected in the mapped responses.
\ No newline at end of file
+1. It may take up considerable memory, depending on how many permutations of mapped objects the application has. This is especially true since it caches each unique object graph; no effort is made to centralize object instances referenced by e.g. relationships in multiple graphs.
+2. It makes no effort to validate or evict cache entries. Topics whose values change during the lifetime of the `CachedTopicMappingService` will not be reflected in the mapped responses.
+3. If a graph is manually constructed (by e.g. programmatically mapping `Children`) then each instance will be separated cached, thus potentially allowing an instance to be shared between multiple graphs. This can introduce concerns if edge maintenance is important.
diff --git a/Ignia.Topics/Mapping/RelationshipAttribute.cs b/Ignia.Topics/Mapping/RelationshipAttribute.cs
index 2554f32f..715fdeaf 100644
--- a/Ignia.Topics/Mapping/RelationshipAttribute.cs
+++ b/Ignia.Topics/Mapping/RelationshipAttribute.cs
@@ -30,25 +30,16 @@ namespace Ignia.Topics.Mapping {
[System.AttributeUsage(System.AttributeTargets.Property)]
public sealed class RelationshipAttribute : System.Attribute {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private string _key = null;
- private RelationshipType _type = RelationshipType.Any;
-
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Annotates a property with the by providing an . Optionally
- /// specifies the as well.
+ /// Annotates a property with the by providing an .
///
/// The key value of the relationships associated with the current property.
- /// Optional. The type of collection the relationship is associated with.
- public RelationshipAttribute(string key, RelationshipType type = RelationshipType.Any) {
+ public RelationshipAttribute(string key) {
TopicFactory.ValidateKey(key, false);
- _key = key;
- _type = type;
+ Key = key;
}
///
@@ -56,7 +47,7 @@ public RelationshipAttribute(string key, RelationshipType type = RelationshipTyp
///
/// Optional. The type of collection the relationship is associated with.
public RelationshipAttribute(RelationshipType type = RelationshipType.Any) {
- _type = type;
+ Type = type;
}
/*==========================================================================================================================
@@ -65,11 +56,7 @@ public RelationshipAttribute(RelationshipType type = RelationshipType.Any) {
///
/// Gets the value of the relationship key.
///
- public string Key {
- get {
- return _key;
- }
- }
+ public string Key { get; }
/*==========================================================================================================================
| PROPERTY: TYPE
@@ -77,11 +64,7 @@ public string Key {
///
/// Gets the value of the relationship type.
///
- public RelationshipType Type {
- get {
- return _type;
- }
- }
+ public RelationshipType Type { get; set; }
} //Class
diff --git a/Ignia.Topics/Mapping/RelationshipMap.cs b/Ignia.Topics/Mapping/RelationshipMap.cs
new file mode 100644
index 00000000..55447847
--- /dev/null
+++ b/Ignia.Topics/Mapping/RelationshipMap.cs
@@ -0,0 +1,49 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Ignia.Topics.Mapping {
+
+ /*============================================================================================================================
+ | CLASS: RELATIONSHIP MAP
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides a mapping of the relationship between and .
+ ///
+ ///
+ /// While the and enumerations are distinct, there are times when
+ /// a single needs to be related to an item in the collection of .
+ /// This mapping makes that feasible.
+ ///
+ static internal class RelationshipMap {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR (STATIC)
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ static RelationshipMap() {
+
+ var mappings = new Dictionary {
+ { RelationshipType.Children, Relationships.Children },
+ { RelationshipType.Relationship, Relationships.Relationships },
+ { RelationshipType.NestedTopics, Relationships.None },
+ { RelationshipType.IncomingRelationship, Relationships.IncomingRelationships }
+ };
+
+ Mappings = mappings;
+
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: MAPPINGS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ static internal Dictionary Mappings { get; }
+
+ }
+}
diff --git a/Ignia.Topics/Mapping/Relationships.cs b/Ignia.Topics/Mapping/Relationships.cs
index f2d7f5af..e4285fdd 100644
--- a/Ignia.Topics/Mapping/Relationships.cs
+++ b/Ignia.Topics/Mapping/Relationships.cs
@@ -26,7 +26,8 @@ public enum Relationships {
Children = 1 << 1,
Relationships = 1 << 2,
IncomingRelationships = 1 << 3,
- All = Parents | Children | Relationships | IncomingRelationships
+ References = 1 << 4,
+ All = Parents | Children | Relationships | IncomingRelationships | References
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
diff --git a/Ignia.Topics/Mapping/TopicMappingService.cs b/Ignia.Topics/Mapping/TopicMappingService.cs
index 077753f1..3c757fc7 100644
--- a/Ignia.Topics/Mapping/TopicMappingService.cs
+++ b/Ignia.Topics/Mapping/TopicMappingService.cs
@@ -5,13 +5,13 @@
\=============================================================================================================================*/
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
-using Ignia.Topics.Collections;
+using System.Threading.Tasks;
+using Ignia.Topics.Reflection;
using Ignia.Topics.Repositories;
using Ignia.Topics.ViewModels;
@@ -29,13 +29,13 @@ public class TopicMappingService : ITopicMappingService {
/*==========================================================================================================================
| STATIC VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
- static Dictionary _typeLookup = null;
- static TypeCollection _typeCache = new TypeCollection();
+ static readonly TypeCollection _typeCache = new TypeCollection();
/*==========================================================================================================================
| PRIVATE VARIABLES
\-------------------------------------------------------------------------------------------------------------------------*/
readonly ITopicRepository _topicRepository = null;
+ readonly ITypeLookupService _typeLookupService = null;
/*==========================================================================================================================
| CONSTRUCTOR
@@ -43,97 +43,37 @@ public class TopicMappingService : ITopicMappingService {
///
/// Establishes a new instance of a with required dependencies.
///
- public TopicMappingService(ITopicRepository topicRepository) {
+ public TopicMappingService(ITopicRepository topicRepository, ITypeLookupService typeLookupService) {
Contract.Requires(topicRepository != null, "An instance of an ITopicRepository is required.");
+ Contract.Requires(typeLookupService != null, "An instance of an ITypeLookupService is required.");
_topicRepository = topicRepository;
+ _typeLookupService = typeLookupService;
}
/*==========================================================================================================================
- | METHOD: GET VIEW MODEL TYPE
+ | METHOD: MAP
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Static helper method for looking up a class type based on a string name.
+ /// Given a topic, will identify any View Models named, by convention, "{ContentType}TopicViewModel" and populate them
+ /// according to the rules of the mapping implementation.
///
///
///
- /// According to the default mapping rules, the following will each be mapped:
- ///
- /// - Scalar values of types , , or
- /// - Properties named Parent (will reference )
- /// - Collections named Children (will reference )
- /// -
- /// Collections starting with Related (will reference corresponding )
- ///
- /// -
- /// Collections corresponding to any of the same name, and the type ListItem,
- /// representing a nested topic list
- ///
- ///
+ /// Because the class is using reflection to determine the target View Models, the return type is .
+ /// These results may need to be cast to a specific type, depending on the context. That said, strongly-typed views
+ /// should be able to cast the object to the appropriate View Model type. If the type of the View Model is known
+ /// upfront, and it is imperative that it be strongly-typed, prefer .
///
///
- /// Currently, this method uses reflection to lookup all types ending with TopicViewModel across all assemblies
- /// and namespaces. This is incredibly non-performant, and can take over a second to execute. As such, this data is
- /// cached for the duration of the application (it is not expected that new classes will be generated during the scope
- /// of the application).
+ /// Because the target object is being dynamically constructed, it must implement a default constructor.
///
///
- /// A string representing the key of the target content type.
- /// A class type corresponding to the specified string, and ending with "TopicViewModel".
- ///
- /// !String.IsNullOrWhiteSpace(contentType)
- ///
- ///
- /// !contentType.Contains(" ")
- ///
- private static Type GetViewModelType(string contentType) {
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Validate contracts
- \---------------------------------------------------------------------------------------------------------------------*/
- Contract.Requires(!String.IsNullOrWhiteSpace(contentType));
- Contract.Ensures(Contract.Result() != null);
- TopicFactory.ValidateKey(contentType);
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Ensure cache is populated
- \---------------------------------------------------------------------------------------------------------------------*/
- if (_typeLookup == null) {
- var typeLookup = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var matchedTypes = AppDomain
- .CurrentDomain
- .GetAssemblies()
- .SelectMany(t => t.GetTypes())
- .Where(t => t.IsClass && t.Name.EndsWith("TopicViewModel", StringComparison.InvariantCultureIgnoreCase))
- .OrderBy(t => t.Namespace.Equals("Ignia.Topics.ViewModels"))
- .ToList();
- foreach (var type in matchedTypes) {
- var associatedContentType = type.Name.Replace("TopicViewModel", "");
- if (!typeLookup.ContainsKey(associatedContentType)) {
- typeLookup.Add(associatedContentType, type);
- }
- }
- _typeLookup = typeLookup;
- }
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Return cached entry
- \---------------------------------------------------------------------------------------------------------------------*/
- if (_typeLookup.Keys.Contains(contentType)) {
- return _typeLookup[contentType];
- }
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Return default
- \---------------------------------------------------------------------------------------------------------------------*/
- return typeof(object);
-
- }
+ /// The entity to derive the data from.
+ /// Determines what relationships the mapping should follow, if any.
+ /// An instance of the dynamically determined View Model with properties appropriately mapped.
+ public async Task MapAsync(Topic topic, Relationships relationships = Relationships.All) =>
+ await MapAsync(topic, relationships, new ConcurrentDictionary());
- /*==========================================================================================================================
- | METHOD: MAP
- \-------------------------------------------------------------------------------------------------------------------------*/
///
/// Given a topic, will identify any View Models named, by convention, "{ContentType}TopicViewModel" and populate them
/// according to the rules of the mapping implementation.
@@ -143,33 +83,45 @@ private static Type GetViewModelType(string contentType) {
/// Because the class is using reflection to determine the target View Models, the return type is .
/// These results may need to be cast to a specific type, depending on the context. That said, strongly-typed views
/// should be able to cast the object to the appropriate View Model type. If the type of the View Model is known
- /// upfront, and it is imperative that it be strongly-typed, then prefer .
+ /// upfront, and it is imperative that it be strongly-typed, prefer .
///
///
/// Because the target object is being dynamically constructed, it must implement a default constructor.
///
+ ///
+ /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with
+ /// recursion in case is referred to multiple times (e.g., a Children collection with
+ /// set to include ).
+ ///
///
/// The entity to derive the data from.
/// Determines what relationships the mapping should follow, if any.
+ /// A cache to keep track of already-mapped object instances.
/// An instance of the dynamically determined View Model with properties appropriately mapped.
- public object Map(Topic topic, Relationships relationships = Relationships.All) {
+ private async Task MapAsync(Topic topic, Relationships relationships, ConcurrentDictionary cache) {
/*----------------------------------------------------------------------------------------------------------------------
| Handle null source
\---------------------------------------------------------------------------------------------------------------------*/
if (topic == null) return null;
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle cached objects
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (cache.TryGetValue(topic.Id, out var dto)) {
+ return dto;
+ }
+
/*----------------------------------------------------------------------------------------------------------------------
| Instantiate object
\---------------------------------------------------------------------------------------------------------------------*/
- var contentType = topic.ContentType;
- var viewModelType = TopicMappingService.GetViewModelType(contentType);
+ var viewModelType = _typeLookupService.GetType($"{topic.ContentType}TopicViewModel");
var target = Activator.CreateInstance(viewModelType);
/*----------------------------------------------------------------------------------------------------------------------
| Provide mapping
\---------------------------------------------------------------------------------------------------------------------*/
- return Map(topic, target, relationships);
+ return await MapAsync(topic, target, relationships, cache);
}
@@ -190,14 +142,11 @@ public object Map(Topic topic, Relationships relationships = Relationships.All)
///
/// An instance of the requested View Model with properties appropriately mapped.
///
- public T Map(Topic topic, Relationships relationships = Relationships.All) where T : class, new() {
-
+ public async Task MapAsync(Topic topic, Relationships relationships = Relationships.All) where T : class, new() {
if (typeof(Topic).IsAssignableFrom(typeof(T))) {
return topic as T;
}
- var target = new T();
- return (T)Map(topic, target, relationships);
-
+ return (T)await MapAsync(topic, new T(), relationships);
}
/*==========================================================================================================================
@@ -212,7 +161,30 @@ public object Map(Topic topic, Relationships relationships = Relationships.All)
///
/// The target view model with the properties appropriately mapped.
///
- public object Map(Topic topic, object target, Relationships relationships = Relationships.All) {
+ public async Task MapAsync(Topic topic, object target, Relationships relationships = Relationships.All) =>
+ await MapAsync(topic, target, relationships, new ConcurrentDictionary());
+
+ ///
+ /// Given a topic and an instance of a DTO, will populate the DTO according to the default mapping rules.
+ ///
+ /// The entity to derive the data from.
+ /// The target object to map the data to.
+ /// Determines what relationships the mapping should follow, if any.
+ /// A cache to keep track of already-mapped object instances.
+ ///
+ /// This internal version passes a private cache of mapped objects from this run. This helps prevent problems with
+ /// recursion in case is referred to multiple times (e.g., a Children collection with
+ /// set to include ).
+ ///
+ ///
+ /// The target view model with the properties appropriately mapped.
+ ///
+ private async Task MapAsync(
+ Topic topic,
+ object target,
+ Relationships relationships,
+ ConcurrentDictionary cache
+ ) {
/*------------------------------------------------------------------------------------------------------------------------
| Validate input
@@ -228,12 +200,24 @@ public object Map(Topic topic, object target, Relationships relationships = Rela
return topic;
}
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle cached objects
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (cache.TryGetValue(topic.Id, out var dto)) {
+ return dto;
+ }
+ else if (topic.Id > 0) {
+ cache.GetOrAdd(topic.Id, target);
+ }
+
/*------------------------------------------------------------------------------------------------------------------------
| Loop through properties, mapping each one
\-----------------------------------------------------------------------------------------------------------------------*/
- foreach (var property in _typeCache.GetProperties(target.GetType())) {
- SetProperty(topic, target, relationships, property);
+ var taskQueue = new List();
+ foreach (var property in _typeCache.GetMembers(target.GetType())) {
+ taskQueue.Add(SetPropertyAsync(topic, target, relationships, property, cache));
}
+ await Task.WhenAll(taskQueue.ToArray());
/*------------------------------------------------------------------------------------------------------------------------
| Return result
@@ -243,231 +227,403 @@ public object Map(Topic topic, object target, Relationships relationships = Rela
}
/*==========================================================================================================================
- | PRIVATE: SET PROPERTY
+ | PROTECTED: SET PROPERTY (ASYNC)
\-------------------------------------------------------------------------------------------------------------------------*/
///
/// Helper function that evaluates each property on the target object and attempts to retrieve a value from the source
/// based on predetermined conventions.
///
- /// The entity to derive the data from.
+ /// The entity to derive the data from.
/// The target object to map the data to.
/// Determines what relationships the mapping should follow, if any.
/// Information related to the current property.
- private void SetProperty(Topic topic, object target, Relationships relationships, PropertyInfo property) {
+ /// A cache to keep track of already-mapped object instances.
+ protected async Task SetPropertyAsync(
+ Topic source,
+ object target,
+ Relationships relationships,
+ PropertyInfo property,
+ ConcurrentDictionary cache
+ ) {
/*------------------------------------------------------------------------------------------------------------------------
| Establish per-property variables
\-----------------------------------------------------------------------------------------------------------------------*/
- var sourceType = topic.GetType();
- var targetType = target.GetType();
- var defaultValue = (string)null;
- var inheritValue = false;
- var attributeKey = property.Name;
- var relationshipKey = property.Name;
- var relationshipType = RelationshipType.Any;
- var crawlRelationships = Relationships.None;
- var metadataKey = (string)null;
- var attributeFilters = new Dictionary();
+ var configuration = new PropertyConfiguration(property);
+ var topicReferenceId = source.Attributes.GetInteger($"{property.Name}Id", 0);
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Assign default value
+ | Assign default value
\-----------------------------------------------------------------------------------------------------------------------*/
- var defaultValueAttribute = (DefaultValueAttribute)property.GetCustomAttribute(typeof(DefaultValueAttribute), true);
- if (defaultValueAttribute != null) {
- property.SetValue(target, defaultValueAttribute.Value);
- defaultValue = defaultValueAttribute.Value.ToString();
+ if (configuration.DefaultValue != null) {
+ property.SetValue(target, configuration.DefaultValue);
}
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Determine inheritance
+ | Handle by type, attribute
\-----------------------------------------------------------------------------------------------------------------------*/
- if (property.GetCustomAttribute(typeof(InheritAttribute), true) != null) {
- inheritValue = true;
+ if (_typeCache.HasSettableProperty(target.GetType(), property.Name)) {
+ SetScalarValue(source, target, configuration);
+ }
+ else if (typeof(IList).IsAssignableFrom(property.PropertyType)) {
+ await SetCollectionValueAsync(source, target, relationships, configuration, cache);
+ }
+ else if (configuration.AttributeKey.Equals("Parent") && relationships.HasFlag(Relationships.Parents)) {
+ await SetTopicReferenceAsync(source.Parent, target, configuration, cache);
+ }
+ else if (topicReferenceId > 0 && relationships.HasFlag(Relationships.References)) {
+ var topicReference = _topicRepository.Load(topicReferenceId);
+ await SetTopicReferenceAsync(topicReference, target, configuration, cache);
}
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Determine attribute key
+ | Validate fields
\-----------------------------------------------------------------------------------------------------------------------*/
- var attributeKeyAttribute = (AttributeKeyAttribute)property.GetCustomAttribute(typeof(AttributeKeyAttribute), true);
- if (attributeKeyAttribute != null) {
- attributeKey = attributeKeyAttribute.Value;
- }
+ configuration.Validate(target);
+
+ }
+
+ /*==========================================================================================================================
+ | PROTECTED: SET SCALAR VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Sets a scalar property on a target DTO.
+ ///
+ ///
+ /// Assuming the 's is of the type , , , or , the method will attempt to set the property on the based on, in order, the 's Get{Property}() method, {Property}
+ /// property, and, finally, its collection (using ). If the property is not of a settable type, or the source
+ /// value cannot be identified on the , then the property is not set.
+ ///
+ /// The source from which to pull the value.
+ /// The target DTO on which to set the property value.
+ /// The with details about the property's attributes.
+ ///
+ protected static void SetScalarValue(Topic source, object target, PropertyConfiguration configuration) {
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Determine relationship key and type
+ | Escape clause if preconditions are not met
\-----------------------------------------------------------------------------------------------------------------------*/
- var relationshipAttribute = (RelationshipAttribute)property.GetCustomAttribute(typeof(RelationshipAttribute), true);
- if (relationshipAttribute != null) {
- relationshipKey = relationshipAttribute.Key?? relationshipKey;
- relationshipType = relationshipAttribute.Type;
+ if (!_typeCache.HasSettableProperty(target.GetType(), configuration.Property.Name)) {
+ return;
}
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Determine recusion settings
+ | Attempt to retrieve value from topic.Get{Property}()
\-----------------------------------------------------------------------------------------------------------------------*/
- var recurseAttribute = (RecurseAttribute)property.GetCustomAttribute(typeof(RecurseAttribute), true);
- if (recurseAttribute != null) {
- crawlRelationships = recurseAttribute.Relationships;
+ var attributeValue = _typeCache.GetMethodValue(source, $"Get{configuration.AttributeKey}")?.ToString();
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Attempt to retrieve value from topic.{Property}
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (String.IsNullOrEmpty(attributeValue)) {
+ attributeValue = _typeCache.GetPropertyValue(source, configuration.AttributeKey)?.ToString();
}
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Determine metadata key, if present
+ | Otherwise, attempt to retrieve value from topic.Attributes.GetValue({Property})
\-----------------------------------------------------------------------------------------------------------------------*/
- var metadataAttribute = (MetadataAttribute)property.GetCustomAttribute(typeof(MetadataAttribute), true);
- if (metadataAttribute != null) {
- metadataKey = metadataAttribute.Key;
+ if (String.IsNullOrEmpty(attributeValue)) {
+ attributeValue = source.Attributes.GetValue(
+ configuration.AttributeKey,
+ configuration.DefaultValue?.ToString(),
+ configuration.InheritValue
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
- | Attributes: Set attribute filters
+ | Assuming a value was retrieved, set it
\-----------------------------------------------------------------------------------------------------------------------*/
- var filterByAttribute = property.GetCustomAttributes(true);
- if (filterByAttribute != null && filterByAttribute.Count() > 0) {
- foreach (var filter in filterByAttribute) {
- attributeFilters.Add(filter.Key, filter.Value);
- }
+ if (attributeValue != null) {
+ _typeCache.SetPropertyValue(target, configuration.Property.Name, attributeValue);
}
+ }
+
+ /*==========================================================================================================================
+ | PROTECTED: SET COLLECTION VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a collection property, identifies a source collection, maps the values to DTOs, and attempts to add them to the
+ /// target collection.
+ ///
+ ///
+ /// Given a collection on a DTO, attempts to identify a source
+ /// collection on the . Collections can be mapped to , , or to a nested topic (which will be part of
+ /// ). By default, will attempt to map based on the
+ /// property name, though this behavior can be modified using the , based on annotations
+ /// on the DTO.
+ ///
+ /// The source from which to pull the value.
+ /// The target DTO on which to set the property value.
+ /// Determines what relationships the mapping should follow, if any.
+ ///
+ /// The with details about the property's attributes.
+ ///
+ /// A cache to keep track of already-mapped object instances.
+ protected async Task SetCollectionValueAsync(
+ Topic source,
+ object target,
+ Relationships relationships,
+ PropertyConfiguration configuration,
+ ConcurrentDictionary cache
+ ) {
+
/*------------------------------------------------------------------------------------------------------------------------
- | Property: Scalar Value
- \-----------------------------------------------------------------------------------------------------------------------*/
- if (_typeCache.HasSettableProperty(targetType, property.Name)) {
- var getterMethod = sourceType.GetRuntimeMethod("Get" + attributeKey, new Type[] { });
- var attributeValue = (string)null;
- //Attempt to get value from topic.Get{Property}()
- if (getterMethod != null) {
- attributeValue = getterMethod.Invoke(topic, new object[] { }).ToString();
- }
- //Otherwise, attempts to get value from topic.Attributes.GetValue({Property})
- if (String.IsNullOrEmpty(attributeValue)) {
- attributeValue = topic.Attributes.GetValue(attributeKey, defaultValue, inheritValue);
- }
- if (attributeValue != null) {
- _typeCache.SetProperty(target, property.Name, attributeValue);
- }
+ | Escape clause if preconditions are not met
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (!typeof(IList).IsAssignableFrom(configuration.Property.PropertyType)) return;
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Ensure target list is created
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var targetList = (IList)configuration.Property.GetValue(target, null);
+ if (targetList == null) {
+ targetList = (IList)Activator.CreateInstance(configuration.Property.PropertyType);
+ configuration.Property.SetValue(target, targetList);
}
/*------------------------------------------------------------------------------------------------------------------------
- | Property: Collections
+ | Establish source collection to store topics to be mapped
\-----------------------------------------------------------------------------------------------------------------------*/
- else if (typeof(IList).IsAssignableFrom(property.PropertyType)) {
+ var sourceList = GetSourceCollection(source, relationships, configuration);
- //Determine the type of item in the list
- var listType = typeof(ITopicViewModel);
- if (property.PropertyType.IsGenericType) {
- //Uses last argument in case it's a KeyedCollection; in that case, we want the TItem type
- listType = property.PropertyType.GetGenericArguments().Last();
- }
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate that source collection was identified
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (sourceList == null) return;
- //Get source for list
- IList listSource = new Topic[] { };
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Map the topics from the source collection, and add them to the target collection
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ await PopulateTargetCollectionAsync(sourceList, targetList, configuration, cache);
- //Handle children
- if (
- (relationshipKey.Equals("Children") || relationshipType.Equals(RelationshipType.Children)) &&
- relationships.HasFlag(Relationships.Children)
- ) {
- listSource = topic.Children.ToList();
- }
+ }
- //Handle (outgoing) relationships
- if (
- listSource.Count == 0 &&
- (relationshipType.Equals(RelationshipType.Any) || relationshipType.Equals(RelationshipType.Relationship)) &&
- relationships.HasFlag(Relationships.Relationships)
- ) {
- if (topic.Relationships.Contains(relationshipKey)) {
- listSource = topic.Relationships.GetTopics(relationshipKey);
- }
- }
+ /*==========================================================================================================================
+ | PROTECTED: GET SOURCE COLLECTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a source topic and a property configuration, attempts to identify a source collection that maps to the property.
+ ///
+ ///
+ /// Given a collection on a target DTO, attempts to identify a source collection on the
+ /// . Collections can be mapped to , , or to a nested topic (which will be part of
+ /// ). By default, will attempt to map based on the
+ /// property name, though this behavior can be modified using the , based on annotations
+ /// on the target DTO.
+ ///
+ /// The source from which to pull the value.
+ /// Determines what relationships the mapping should follow, if any.
+ ///
+ /// The with details about the property's attributes.
+ ///
+ protected IList GetSourceCollection(Topic source, Relationships relationships, PropertyConfiguration configuration) {
- //Handle nested topics, or children corresponding to the property name
- if (
- listSource.Count == 0 &&
- (relationshipType.Equals(RelationshipType.Any) || relationshipType.Equals(RelationshipType.NestedTopics))
- ) {
- if (topic.Children.Contains(relationshipKey)) {
- listSource = topic.Children[relationshipKey].Children.ToList();
- }
- }
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Establish source collection to store topics to be mapped
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var listSource = (IList)Array.Empty();
+ var relationshipKey = configuration.RelationshipKey;
+ var relationshipType = configuration.RelationshipType;
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle children
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ listSource = GetRelationship(
+ RelationshipType.Children,
+ s => true,
+ () => source.Children.ToList()
+ );
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle (outgoing) relationships
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ listSource = GetRelationship(
+ RelationshipType.Relationship,
+ source.Relationships.Contains,
+ () => source.Relationships.GetTopics(relationshipKey)
+ );
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle nested topics, or children corresponding to the property name
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ listSource = GetRelationship(
+ RelationshipType.NestedTopics,
+ source.Children.Contains,
+ () => source.Children[relationshipKey].Children
+ );
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle (incoming) relationships
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ listSource = GetRelationship(
+ RelationshipType.IncomingRelationship,
+ source.IncomingRelationships.Contains,
+ () => source.IncomingRelationships.GetTopics(relationshipKey)
+ );
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle Metadata relationship
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (listSource.Count == 0 && !String.IsNullOrWhiteSpace(configuration.MetadataKey)) {
+ var metadataKey = $"Root:Configuration:Metadata:{configuration.MetadataKey}:LookupList";
+ listSource = _topicRepository.Load(metadataKey)?.Children.ToList();
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Handle flattening of children
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (configuration.FlattenChildren) {
+ var flattenedList = new List();
+ listSource.ToList().ForEach(t => PopulateChildTopics(t, flattenedList));
+ listSource = flattenedList;
+ }
+
+ return listSource;
- //Handle (incoming) relationships
- if (
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Provide local function for evaluating current relationship
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ IList GetRelationship(RelationshipType relationship, Func contains, Func> getTopics) {
+ var targetRelationships = RelationshipMap.Mappings[relationship];
+ var preconditionsMet =
listSource.Count == 0 &&
- (relationshipType.Equals(RelationshipType.Any) || relationshipType.Equals(RelationshipType.IncomingRelationship)) &&
- relationships.HasFlag(Relationships.IncomingRelationships)
- ) {
- if (topic.IncomingRelationships.Contains(relationshipKey)) {
- listSource = topic.IncomingRelationships.GetTopics(relationshipKey);
- }
- }
+ (relationshipType.Equals(RelationshipType.Any) || relationshipType.Equals(relationship)) &&
+ (relationshipType.Equals(RelationshipType.Children) || !relationship.Equals(RelationshipType.Children)) &&
+ (targetRelationships.Equals(Relationships.None) || relationships.HasFlag(targetRelationships)) &&
+ contains(configuration.RelationshipKey);
+ return preconditionsMet? getTopics() : listSource;
+ }
+
+ }
+
+ /*==========================================================================================================================
+ | PROTECTED: POPULATE TARGET COLLECTION
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a source list, will populate a target list based on the configured behavior of the target property.
+ ///
+ /// The to pull the source objects from.
+ /// The target to add the mapped objects to.
+ ///
+ /// The with details about the property's attributes.
+ ///
+ /// A cache to keep track of already-mapped object instances.
+ protected async Task PopulateTargetCollectionAsync(
+ IList sourceList,
+ IList targetList,
+ PropertyConfiguration configuration,
+ ConcurrentDictionary cache
+ ) {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Determine the type of item in the list
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var listType = typeof(ITopicViewModel);
+ if (configuration.Property.PropertyType.IsGenericType) {
+ //Uses last argument in case it's a KeyedCollection; in that case, we want the TItem type
+ listType = configuration.Property.PropertyType.GetGenericArguments().Last();
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Queue up mapping tasks
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var taskQueue = new List>();
- //Handle Metadata relationship
- if (listSource.Count == 0 && !String.IsNullOrWhiteSpace(metadataKey)) {
- listSource = _topicRepository.Load("Root:Configuration:Metadata:" + metadataKey + ":LookupList")?.Children.ToList();
+ foreach (var childTopic in sourceList) {
+
+ //Ensure the source topic matches any [FilterByAttribute()] settings
+ if (!configuration.SatisfiesAttributeFilters(childTopic)) {
+ continue;
}
- //Ensure list is created
- var list = (IList)property.GetValue(target, null);
- if (list == null) {
- list = (IList)Activator.CreateInstance(property.PropertyType);
- property.SetValue(target, list);
+ //Ensure the source topic isn't disabled; disabled topics should never be returned to the presentation layer
+ if (childTopic.IsDisabled) {
+ continue;
}
- //Validate and populate target collection
- if (listSource != null) {
- foreach (Topic childTopic in listSource) {
- if (filterByAttribute.Any(f => !childTopic.Attributes.GetValue(f.Key, "").Equals(f.Value))) {
- continue;
- }
- if (!childTopic.IsDisabled) {
- //Handle scenario where the list type derives from Topic
- if (typeof(Topic).IsAssignableFrom(listType)) {
- //Ensure the list item derives from the list type (which may be more derived than Topic)
- if (listType.IsAssignableFrom(childTopic.GetType())) {
- list.Add(childTopic);
- }
- }
- //Otherwise, assume the list type is a DTO
- else {
- var childDto = Map(
- childTopic,
- crawlRelationships
- );
- //Ensure the mapped type derives from the list type
- if (listType.IsAssignableFrom(childDto.GetType())) {
- list.Add(childDto);
- }
- }
- }
- }
+ //Map child topic to target DTO
+ var childDto = (object)childTopic;
+ if (!typeof(Topic).IsAssignableFrom(listType)) {
+ taskQueue.Add(MapAsync(childTopic, configuration.CrawlRelationships, cache));
+ } else {
+ AddToList(childDto);
}
}
/*------------------------------------------------------------------------------------------------------------------------
- | Property: Parent
- \-----------------------------------------------------------------------------------------------------------------------*/
- else if (attributeKey.Equals("Parent") && relationships.HasFlag(Relationships.Parents)) {
- if (topic.Parent != null) {
- var parent = Map(
- topic.Parent,
- crawlRelationships
- );
- if (property.PropertyType.IsAssignableFrom(parent.GetType())) {
- property.SetValue(target, parent);
- }
- }
+ | Process mapping tasks
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ while (taskQueue.Count > 0) {
+ var dtoTask = await Task.WhenAny(taskQueue);
+ taskQueue.Remove(dtoTask);
+ AddToList(await dtoTask);
}
/*------------------------------------------------------------------------------------------------------------------------
- | Validate fields
+ | Function: Add to List
\-----------------------------------------------------------------------------------------------------------------------*/
- foreach (ValidationAttribute validator in property.GetCustomAttributes(typeof(ValidationAttribute))) {
- validator.Validate(property.GetValue(target), property.Name);
+ void AddToList(object dto) {
+ if (listType.IsAssignableFrom(dto.GetType())) {
+ try {
+ targetList.Add(dto);
+ }
+ catch (ArgumentException) {
+ //Ignore exceptions caused by duplicate keys, in case the IList represents a keyed collection
+ //We would defensively check for this, except IList doesn't provide a suitable method to do so
+ }
+ }
+ }
+
+ }
+
+ /*==========================================================================================================================
+ | PROTECTED: SET TOPIC REFERENCE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Given a reference to an external topic, attempts to match it to a matching property.
+ ///
+ /// The source from which to pull the value.
+ /// The target DTO on which to set the property value.
+ ///
+ /// The with details about the property's attributes.
+ ///
+ /// A cache to keep track of already-mapped object instances.
+ protected async Task SetTopicReferenceAsync(
+ Topic source,
+ object target,
+ PropertyConfiguration configuration,
+ ConcurrentDictionary cache
+ ) {
+ var topicDto = await MapAsync(source, configuration.CrawlRelationships, cache);
+ if (topicDto != null && configuration.Property.PropertyType.IsAssignableFrom(topicDto.GetType())) {
+ configuration.Property.SetValue(target, topicDto);
}
+ }
+ /*==========================================================================================================================
+ | PROTECTED: POPULATE CHILD TOPICS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Helper function recursively iterates through children and adds each to a collection.
+ ///
+ /// The entity pull the data from.
+ /// The list of instances to add each child to.
+ /// Optionally enable including nested topics in the list.
+ protected IList PopulateChildTopics(Topic source, IList targetList, bool includeNestedTopics = false) {
+ if (source.IsDisabled) return targetList;
+ if (source.ContentType.Equals("List") && !includeNestedTopics) return targetList;
+ targetList.Add(source);
+ source.Children.ToList().ForEach(t => PopulateChildTopics(t, targetList));
+ return targetList;
}
} //Class
-} //Namespace
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Properties/AssemblyInfo.cs b/Ignia.Topics/Properties/AssemblyInfo.cs
index f03124b3..cbd22887 100644
--- a/Ignia.Topics/Properties/AssemblyInfo.cs
+++ b/Ignia.Topics/Properties/AssemblyInfo.cs
@@ -1,47 +1,29 @@
-/*===========================================================================================================================
-| COPYRIGHT (C) 2004-2014 IGNIA, LLC. NOT LICENSED FOR REDISTRIBUTION.
-\--------------------------------------------------------------------------------------------------------------------------*/
-
-/*===========================================================================================================================
-| IGNIA TOPICS LIBRARY
-|
-
+/*==============================================================================================================================
| Author Ignia, LLC
-| Client Ignia
-| Project Topics
-|
-| Purpose A content management system (CMS) based on structured data.
-|
->============================================================================================================================
-| Revisions Date Author Comments
-| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-| 03.24.09 Casey Margell Created initial version.
-| 08.28.10 Jeremy Caney Released version 2.0.
-| 08.14.14 Katherine Trunkey Updated for version 3.0
-\--------------------------------------------------------------------------------------------------------------------------*/
-
-/*===========================================================================================================================
-| DECLARE NAMESPACE REFERENCES
-\--------------------------------------------------------------------------------------------------------------------------*/
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-/*===========================================================================================================================
+/*==============================================================================================================================
| DEFINE ASSEMBLY ATTRIBUTES
->============================================================================================================================
+>===============================================================================================================================
| Declare and define attributes used in the compiling of the finished assembly.
-\--------------------------------------------------------------------------------------------------------------------------*/
+\-----------------------------------------------------------------------------------------------------------------------------*/
[assembly: AssemblyCompany("Ignia, LLC")]
-[assembly: AssemblyCopyright("Ignia, LLC. All rights reserved.")]
-[assembly: AssemblyProduct("Ignia Topics Library")]
-[assembly: AssemblyTitle("Ignia Topics Library")]
+[assembly: AssemblyCopyright("Copyright © 2018 Ignia, LLC")]
+[assembly: AssemblyProduct("Ignia OnTopic Library")]
+[assembly: AssemblyTitle("Ignia OnTopic Library")]
[assembly: AssemblyDescription("Libraries for supporting Ignia Topics, a content management system (CMS) based on structured, hierarchical data.")]
+[assembly: AssemblyConfiguration("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
-[assembly: AssemblyVersion("3.1.0.0")]
+[assembly: ComVisible(false)]
+[assembly: AssemblyVersion("3.6.1762.0")]
+[assembly: AssemblyFileVersion("3.5.1794.0")]
[assembly: InternalsVisibleTo("Ignia.Topics.Tests")]
-[assembly: System.Runtime.InteropServices.ComVisible(false)]
[assembly: CLSCompliant(true)]
[assembly: GuidAttribute("3CA9F6CB-B45A-4E74-AAA4-0C87CAA2704F")]
diff --git a/Ignia.Topics/Querying/Topic.cs b/Ignia.Topics/Querying/Topic.cs
index 78eb6fdb..eefa01d9 100644
--- a/Ignia.Topics/Querying/Topic.cs
+++ b/Ignia.Topics/Querying/Topic.cs
@@ -18,54 +18,76 @@ namespace Ignia.Topics.Querying {
public static class Topic {
/*==========================================================================================================================
- | METHOD: FIND ALL BY ATTRIBUTE
- >===========================================================================================================================
- | ###TODO JJC080313: Consider adding an overload of the out-of-the-box FindAll() method that supports recursion, thus
- | allowing a search by any criteria - including attributes.
+ | METHOD: FIND FIRST
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Retrieves a collection of topics based on an attribute name and value.
+ /// Finds the first instance of a in the topic tree that satisfies the delegate.
///
/// The instance of the to operate against; populated automatically by .NET.
- /// The string identifier for the against which to be searched.
- /// The text value for the against which to be searched.
+ /// The function to validate whether a should be included in the output.
+ /// The first instance of the topic to be satisfied.
+ public static Target.Topic FindFirst(this Target.Topic topic, Func predicate) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Validate contracts
+ \---------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(topic != null, "The topic parameter must be specified.");
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Search attributes
+ \---------------------------------------------------------------------------------------------------------------------*/
+ if (predicate(topic)) {
+ return topic;
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Recurse over children
+ \---------------------------------------------------------------------------------------------------------------------*/
+ foreach (var child in topic.Children) {
+ var nestedResult = child.FindFirst(predicate);
+ if (nestedResult != null) {
+ return nestedResult;
+ }
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Indicate no results found
+ \---------------------------------------------------------------------------------------------------------------------*/
+ return null;
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: FIND ALL
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a collection of topics based on a supplied function.
+ ///
+ /// The instance of the to operate against; populated automatically by .NET.
+ /// The function to validate whether a should be included in the output.
/// A collection of topics matching the input parameters.
- ///
- /// !String.IsNullOrWhiteSpace(name)
- ///
- ///
- /// !name.Contains(" ")
- ///
- public static ReadOnlyTopicCollection FindAllByAttribute(this Target.Topic topic, string name, string value) {
+ public static ReadOnlyTopicCollection FindAll(this Target.Topic topic, Func predicate) {
/*----------------------------------------------------------------------------------------------------------------------
| Validate contracts
\---------------------------------------------------------------------------------------------------------------------*/
Contract.Requires(topic != null, "The topic parameter must be specified.");
- Contract.Requires(!String.IsNullOrWhiteSpace(name), "The attribute name must be specified.");
- Contract.Requires(!String.IsNullOrWhiteSpace(value), "The attribute value must be specified.");
Contract.Ensures(Contract.Result>() != null);
- TopicFactory.ValidateKey(name);
/*----------------------------------------------------------------------------------------------------------------------
| Search attributes
\---------------------------------------------------------------------------------------------------------------------*/
var results = new TopicCollection();
- if (
- !String.IsNullOrEmpty(topic.Attributes.GetValue(name)) &&
- topic.Attributes.GetValue(name).IndexOf(value, StringComparison.InvariantCultureIgnoreCase) >= 0
- ) {
+ if (predicate(topic)) {
results.Add(topic);
}
/*----------------------------------------------------------------------------------------------------------------------
- | Search children, if recursive
+ | Recurse over children
\---------------------------------------------------------------------------------------------------------------------*/
foreach (var child in topic.Children) {
- var nestedResults = child.FindAllByAttribute(name, value);
+ var nestedResults = child.FindAll(predicate);
foreach (var matchedTopic in nestedResults) {
if (!results.Contains(matchedTopic.Key)) {
results.Add(matchedTopic);
@@ -80,5 +102,44 @@ public static class Topic {
}
+ /*==========================================================================================================================
+ | METHOD: FIND ALL BY ATTRIBUTE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a collection of topics based on an attribute name and value.
+ ///
+ /// The instance of the to operate against; populated automatically by .NET.
+ /// The string identifier for the against which to be searched.
+ /// The text value for the against which to be searched.
+ /// A collection of topics matching the input parameters.
+ ///
+ /// !String.IsNullOrWhiteSpace(name)
+ ///
+ ///
+ /// !name.Contains(" ")
+ ///
+ public static ReadOnlyTopicCollection FindAllByAttribute(this Target.Topic topic, string name, string value) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Validate contracts
+ \---------------------------------------------------------------------------------------------------------------------*/
+ Contract.Requires(topic != null, "The topic parameter must be specified.");
+ Contract.Requires(!String.IsNullOrWhiteSpace(name), "The attribute name must be specified.");
+ Contract.Requires(!String.IsNullOrWhiteSpace(value), "The attribute value must be specified.");
+ Contract.Ensures(Contract.Result>() != null);
+ TopicFactory.ValidateKey(name);
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Return results
+ \---------------------------------------------------------------------------------------------------------------------*/
+ return topic.FindAll(t =>
+ !String.IsNullOrEmpty(t.Attributes.GetValue(name)) &&
+ t.Attributes.GetValue(name).IndexOf(value, StringComparison.InvariantCultureIgnoreCase) >= 0
+ );
+
+ }
+
}
}
diff --git a/Ignia.Topics/README.md b/Ignia.Topics/README.md
index faef3414..b0b9227c 100644
--- a/Ignia.Topics/README.md
+++ b/Ignia.Topics/README.md
@@ -15,18 +15,23 @@ Out of the box, the OnTopic library contains two specially derived topics for su
- **[`ITopicRoutingService`](ITopicRoutingService.cs)**: Given contextual information, such as a URL and routing information, will identify the current `Topic` instance. What contextual information is required is environment-specific; for instance, the `MvcTopicRoutingService` requires an `ITopicRepository`, `Uri`, and `RouteData` collection.
- **[`ITopicRepository`](Repositories/ITopicRepository.cs)**: Defines the data access layer interface, with `Load()`, `Save()`, `Delete()`, and `Move()` methods.
- **[`ITopicMappingService`](Mapping)**: Defines the interface for a service that can convert a `Topic` class into any arbitrary data transfer object based on predetermined conventions.
+- **[`ITypeLookupService`](ITypeLookupService.cs)**: Defines the interface that can identify `Type` objects based on a `GetType(typeName)` query. Used by e.g. `ITopicMappingService` to find corresponding `TopicViewModel` classes to map to.
## Implementations
- **[`TopicMappingService`](Mapping)**: A default implementation of the `ITopicMappingService`, with built-in conventions that should address that majority of mapping requirements. This also includes a number of attributes for annotating view models with hints that the `TopicMappingService` can use in populating target objects.
+- **[`StaticTypeLookupService`](StaticTypeLookupService.cs)**: A basic implementation of the `ITypeLookupService` interface that allows types to be explicitly registered; useful when a small number of types are expected.
+ - **[`DynamicTypeLookupService`](Reflection\DynamicTypeLookupService.cs)**: A reflection-based implementation of the `ITypeLookupService` interface that looks up types from all loaded assemblies based on a `Func` delegate.
+ - **[`DynamicTopicLookupService`](Reflection\DynamicTopicLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that derive from `Topic`; this is the default implementation for `TopicFactory`.
+ - **[`DynamicTopicViewModeLookupService`](Reflection\DynamicTopicViewModeLookupService.cs)**: A version of `DynamicTypeLookupService` that returns all classes that end with `TopicViewModel`; this is useful for the `TopicMappingService`.
## Extension Methods
-- **[`Querying`](Querying/Topic.cs)**: The `Topic` class exposes optional extension methods for querying a topic (and its descendants) based on attribute values.
+- **[`Querying`](Querying/Topic.cs)**: The `Topic` class exposes optional extension methods for querying a topic (and its descendants) based on attribute values. This includes the useful `Topic.FindAll(Func)` method for querying an entire topic graph and returning topics validated by a predicate.
## Collections
In addition to the above key classes, the `Ignia.Topics` assembly contains a number of specialized collections. These include:
- **[`TopicCollection{T}`](Collections/TopicCollection{T}.cs)**: A `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`.
- **[`TopicCollection`](Collections/TopicCollection.cs)**: A `KeyedCollection` of `Topic` keyed by `Id` and `Key`.
- - **[`NamedTopicCollection`](Collections/NamedTopicCollection.cs)**: Proviedes a unique name to a `TopicCollection` so it can be keyed as part of a collection-of-collections.
+ - **[`NamedTopicCollection`](Collections/NamedTopicCollection.cs)**: Provides a unique name to a `TopicCollection` so it can be keyed as part of a collection-of-collections.
- **[`ReadOnlyTopicCollection{T}`](Collections/ReadOnlyTopicCollection{T}.cs)**: A read-only `KeyedCollection` of a `Topic` (or derivative) keyed by `Id` and `Key`.
- **[`ReadOnlyTopicCollection`](Collections/ReadOnlyTopicCollection.cs)**: A read-only `KeyedCollection` of `Topic` keyed by `Id` and `Key`.
- **[`RelatedTopicCollection`](Collections/RelatedTopicCollection.cs)**: A `KeyedCollection` of `NamedTopicCollection` objects, keyed by `Name`, thus providing a collection-of-collections.
@@ -36,4 +41,10 @@ In addition to the above key classes, the `Ignia.Topics` assembly contains a num
The following are intended to provide support for the Editor domain objects, `ContentTypeDescriptor` and `AttributeDescriptor`.
- **[`ContentTypeDescriptorCollection`](Collections/ContentTypeDescriptorCollection.cs)**: A `KeyedCollection` of `ContentTypeDescriptor` objects keyed by `Id` and `Key`.
- **[`AttributeDescriptorCollection`](Collections/AttributeDescriptorCollection.cs)**: A `KeyedCollection` of `AttributeDescriptor` objects keyed by `Id` and `Key`.
-
\ No newline at end of file
+
+## View Models
+The core Topic library has been designed to be view model agnostic; i.e., view models should be defined for the specific presentation framework (e.g., ASP.NET MVC) and customer. That said, to facilitate reusability of features that work with view models, several interfaces are defined which can be applied as appropriate. These include:
+- **[`ITopicViewModel`](ViewModels/ITopicViewModel.cs)**: Includes universal properties such as `Key`, `Id`, and `ContentType`.
+- **[`IPageTopicViewModel`](ViewModels/IPageTopicViewModel.cs)**: Includes page-specific properties such as `Title`, `MetaKeywords`, and `WebPath`.
+- **[`INavigationTopicViewModel`](ViewModels/INavigationTopicViewModel{T}.cs)**: Includes `IPageTopicViewModel`, `Children`, and an `IsSelected()` view logic handler, for use with navigation menus.
+
\ No newline at end of file
diff --git a/Ignia.Topics/Reflection/DynamicTopicLookupService.cs b/Ignia.Topics/Reflection/DynamicTopicLookupService.cs
new file mode 100644
index 00000000..e56e0727
--- /dev/null
+++ b/Ignia.Topics/Reflection/DynamicTopicLookupService.cs
@@ -0,0 +1,31 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+
+namespace Ignia.Topics.Reflection {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC LOOKUP SERVICE
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The will search all assemblies for s that derive from .
+ ///
+ public class DynamicTopicLookupService : DynamicTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a .
+ ///
+ internal DynamicTopicLookupService() : base(
+ t => typeof(Topic).IsAssignableFrom(t),
+ typeof(Topic)
+ ) {}
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Reflection/DynamicTopicViewModelLookupService.cs b/Ignia.Topics/Reflection/DynamicTopicViewModelLookupService.cs
new file mode 100644
index 00000000..64d9c390
--- /dev/null
+++ b/Ignia.Topics/Reflection/DynamicTopicViewModelLookupService.cs
@@ -0,0 +1,31 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+
+namespace Ignia.Topics.Reflection {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC VIEW MODEL LOOKUP SERVICE
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The will search all assemblies for s that end with
+ /// "TopicViewModel"
+ ///
+ public class DynamicTopicViewModelLookupService : DynamicTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a .
+ ///
+ internal DynamicTopicViewModelLookupService() : base(
+ t => t.Name.EndsWith("TopicViewModel"),
+ typeof(object)
+ ) {}
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Reflection/DynamicTypeLookupService.cs b/Ignia.Topics/Reflection/DynamicTypeLookupService.cs
new file mode 100644
index 00000000..388df5d2
--- /dev/null
+++ b/Ignia.Topics/Reflection/DynamicTypeLookupService.cs
@@ -0,0 +1,54 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Linq;
+
+namespace Ignia.Topics.Reflection {
+
+ /*============================================================================================================================
+ | CLASS: TYPE INDEX
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The will search all assemblies for instances that match a
+ /// predicate.
+ ///
+ public class DynamicTypeLookupService : StaticTypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a based on a and,
+ /// optionally, a default object to return if none is specified.
+ ///
+ /// The search condition to use to identify target classes.
+ /// The default type to return if no match can be found. Defaults to object.
+ public DynamicTypeLookupService(Func predicate, Type defaultType = null) : base(null, defaultType) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Find target classes
+ \---------------------------------------------------------------------------------------------------------------------*/
+ var matchedTypes = AppDomain
+ .CurrentDomain
+ .GetAssemblies()
+ .SelectMany(t => t.GetTypes())
+ .Where(t => t.IsClass && predicate(t))
+ .OrderBy(t => t.Namespace.StartsWith("Ignia.Topics"))
+ .ToList();
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Populate collection
+ \---------------------------------------------------------------------------------------------------------------------*/
+ foreach (var type in matchedTypes) {
+ if (!Contains(type)) {
+ Add(type);
+ }
+ }
+
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Reflection/MemberInfoCollection.cs b/Ignia.Topics/Reflection/MemberInfoCollection.cs
new file mode 100644
index 00000000..f130f441
--- /dev/null
+++ b/Ignia.Topics/Reflection/MemberInfoCollection.cs
@@ -0,0 +1,44 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Ignia.Topics.Reflection {
+
+ /*============================================================================================================================
+ | CLASS: MEMBER INFO COLLECTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Provides keyed access to a collection of instances.
+ ///
+ internal class MemberInfoCollection : MemberInfoCollection {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance of the class associated with a
+ /// name.
+ ///
+ /// The associated with the collection.
+ internal MemberInfoCollection(Type type) : base(type) {
+ }
+
+ ///
+ /// Initializes a new instance of the class associated with a
+ /// name and prepopulates it with a predetermined set of instances.
+ ///
+ /// The associated with the collection.
+ ///
+ /// An of instances to populate the collection.
+ ///
+ internal MemberInfoCollection(Type type, IEnumerable members) : base(type, members) {
+ }
+
+ } //Class
+
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Collections/PropertyInfoCollection.cs b/Ignia.Topics/Reflection/MemberInfoCollection{T}.cs
similarity index 51%
rename from Ignia.Topics/Collections/PropertyInfoCollection.cs
rename to Ignia.Topics/Reflection/MemberInfoCollection{T}.cs
index 46a8caa3..68a9542d 100644
--- a/Ignia.Topics/Collections/PropertyInfoCollection.cs
+++ b/Ignia.Topics/Reflection/MemberInfoCollection{T}.cs
@@ -4,46 +4,81 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
+using System.Linq;
using System.Reflection;
-namespace Ignia.Topics.Collections {
+namespace Ignia.Topics.Reflection {
/*============================================================================================================================
- | CLASS: PROPERTY INFO COLLECTION
+ | CLASS: MEMBER INFO COLLECTION {T}
\---------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides keyed access to a collection of instances.
+ /// Provides keyed access to a collection of instances.
///
- public class PropertyInfoCollection : KeyedCollection {
-
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- Type _type = null;
+ internal class MemberInfoCollection : KeyedCollection where T : MemberInfo {
/*==========================================================================================================================
| CONSTRUCTOR
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Initializes a new instance of the class associated with a
+ /// Initializes a new instance of the class associated with a
/// name.
///
/// The associated with the collection.
- public PropertyInfoCollection(Type type) : base(StringComparer.OrdinalIgnoreCase) {
+ internal MemberInfoCollection(Type type) : base(StringComparer.OrdinalIgnoreCase) {
Contract.Requires(type != null);
- _type = type;
+ Type = type;
foreach (
- var property
- in type.GetProperties(
+ var member
+ in type.GetMembers(
BindingFlags.Instance |
BindingFlags.FlattenHierarchy |
BindingFlags.NonPublic |
BindingFlags.Public
- )
+ ).Where(m => typeof(T).IsAssignableFrom(m.GetType()))
) {
- Add(property);
+ Add((T)member);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class associated with a
+ /// name and prepopulates it with a predetermined set of instances.
+ ///
+ /// The associated with the collection.
+ ///
+ /// An of instances to populate the collection.
+ ///
+ internal MemberInfoCollection(Type type, IEnumerable members) : base(StringComparer.OrdinalIgnoreCase) {
+ Contract.Requires(type != null);
+ Contract.Requires(members != null);
+ Type = type;
+ foreach (var member in members) {
+ Add(member);
+ }
+ }
+
+ /*==========================================================================================================================
+ | OVERRIDE: INSERT ITEM
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ /// Fires any time an item is added to the collection.
+ ///
+ /// Compared to the base implementation, will throw a specific error if a duplicate key is
+ /// inserted. This conveniently provides the name of the and the 's , so it's clear what is being duplicated.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The instance to insert.
+ /// The Type '{Type.Name}' already contains the MemberInfo '{item.Name}'
+ protected override void InsertItem(int index, T item) {
+ if (!Contains(item.Name)) {
+ base.InsertItem(index, item);
+ }
+ else {
+ throw new ArgumentException($"The Type '{Type.Name}' already contains the MemberInfo '{item.Name}'");
}
}
@@ -53,7 +88,7 @@ in type.GetProperties(
///
/// Returns the type associated with this collection.
///
- internal Type Type => _type;
+ internal Type Type { get; }
/*==========================================================================================================================
| OVERRIDE: GET KEY FOR ITEM
@@ -63,7 +98,7 @@ in type.GetProperties(
///
/// The object from which to extract the key.
/// The key for the specified collection item.
- protected override string GetKeyForItem(PropertyInfo item) {
+ protected override string GetKeyForItem(T item) {
Contract.Assume(item != null, "Assumes the item is available when deriving its key.");
return item.Name;
}
diff --git a/Ignia.Topics/Reflection/TypeCollection.cs b/Ignia.Topics/Reflection/TypeCollection.cs
new file mode 100644
index 00000000..9ac7d692
--- /dev/null
+++ b/Ignia.Topics/Reflection/TypeCollection.cs
@@ -0,0 +1,424 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using System.Reflection;
+
+namespace Ignia.Topics.Reflection {
+
+ /*============================================================================================================================
+ | CLASS: TYPE COLLECTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// A collection of instances, each associated with a specific .
+ ///
+ internal class TypeCollection : KeyedCollection {
+
+ /*==========================================================================================================================
+ | PRIVATE VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ private readonly Type _attributeFlag = null;
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR (STATIC)
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes static properties on .
+ ///
+ static TypeCollection() {
+ SettableTypes = new List {
+ typeof(bool),
+ typeof(int),
+ typeof(string),
+ typeof(DateTime)
+ };
+ }
+
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// An optional which properties must have defined to be considered writable.
+ ///
+ internal TypeCollection(Type attributeFlag = null) : base() {
+ _attributeFlag = attributeFlag;
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET MEMBERS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Returns a collection of objects associated with a specific type.
+ ///
+ ///
+ /// If the collection cannot be found locally, it will be created.
+ ///
+ /// The type for which the members should be retrieved.
+ internal MemberInfoCollection GetMembers(Type type) {
+ if (!Contains(type)) {
+ Add(new MemberInfoCollection(type));
+ }
+ return this[type];
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET MEMBERS {T}
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Returns a collection of objects associated with a specific type.
+ ///
+ ///
+ /// If the collection cannot be found locally, it will be created.
+ ///
+ /// The type for which the members should be retrieved.
+ internal MemberInfoCollection GetMembers(Type type) where T: MemberInfo =>
+ new MemberInfoCollection(type, GetMembers(type).Where(m => typeof(T).IsAssignableFrom(m.GetType())).Cast());
+
+ /*==========================================================================================================================
+ | METHOD: GET MEMBER
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify a local member by a given name, and returns the associated
+ /// instance.
+ ///
+ internal MemberInfo GetMember(Type type, string name) {
+ var members = GetMembers(type);
+ if (members.Contains(name)) {
+ return members[name];
+ }
+ return null;
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET MEMBER {T}
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify a local member by a given name, and returns the associated
+ /// instance.
+ ///
+ internal T GetMember(Type type, string name) where T : MemberInfo {
+ var members = GetMembers(type);
+ if (members.Contains(name) && typeof(T).IsAssignableFrom(members[name].GetType())) {
+ return members[name] as T;
+ }
+ return null;
+ }
+
+ /*==========================================================================================================================
+ | METHOD: HAS MEMBER
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local member is available.
+ ///
+ internal bool HasMember(Type type, string name) => GetMember(type, name) != null;
+
+ /*==========================================================================================================================
+ | METHOD: HAS MEMBER {T}
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local member of type is available.
+ ///
+ internal bool HasMember(Type type, string name) where T: MemberInfo => GetMember(type, name) != null;
+
+ /*==========================================================================================================================
+ | METHOD: HAS SETTABLE PROPERTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local property is available and settable.
+ ///
+ ///
+ /// Will return false if the property is not available.
+ ///
+ /// The on which the property is defined.
+ /// The name of the property to assess.
+ internal bool HasSettableProperty(Type type, string name) {
+ var property = GetMember(type, name);
+ return (
+ property != null &&
+ property.CanWrite &&
+ IsSettableType(property.PropertyType) &&
+ (_attributeFlag == null || System.Attribute.IsDefined(property, _attributeFlag))
+ );
+ }
+
+ /*==========================================================================================================================
+ | METHOD: SET PROPERTY VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Uses reflection to call a property, assuming that it is a) writable, and b) of type ,
+ /// , or .
+ ///
+ /// The object on which the property is defined.
+ /// The name of the property to assess.
+ /// The value to set on the property.
+ internal bool SetPropertyValue(object target, string name, string value) {
+
+ if (!HasSettableProperty(target.GetType(), name)) {
+ return false;
+ }
+
+ var property = GetMember(target.GetType(), name);
+
+ Contract.Assume(property != null);
+
+ var valueObject = GetValueObject(property.PropertyType, value);
+
+ if (valueObject == null) {
+ return false;
+ }
+
+ property.SetValue(target, valueObject);
+ return true;
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: HAS GETTABLE PROPERTY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local property is available and gettable.
+ ///
+ ///
+ /// Will return false if the property is not available.
+ ///
+ /// The on which the property is defined.
+ /// The name of the property to assess.
+ /// Optional, the expected.
+ internal bool HasGettableProperty(Type type, string name, Type targetType = null) {
+ var property = GetMember(type, name);
+ return (
+ property != null &&
+ property.CanRead &&
+ IsSettableType(property.PropertyType, targetType) &&
+ (_attributeFlag == null || System.Attribute.IsDefined(property, _attributeFlag))
+ );
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET PROPERTY VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Uses reflection to call a property, assuming that it is a) readable, and b) of type ,
+ /// , or .
+ ///
+ /// The object instance on which the property is defined.
+ /// The name of the property to assess.
+ /// Optional, the expected.
+ internal object GetPropertyValue(object target, string name, Type targetType = null) {
+
+ if (!HasGettableProperty(target.GetType(), name, targetType)) {
+ return null;
+ }
+
+ var property = GetMember(target.GetType(), name);
+
+ Contract.Assume(property != null);
+
+ return property.GetValue(target);
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: HAS SETTABLE METHOD
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local method is available and settable.
+ ///
+ ///
+ /// Will return false if the method is not available. Methods are only considered settable if they have one parameter of
+ /// a settable type.
+ ///
+ /// The on which the method is defined.
+ /// The name of the method to assess.
+ internal bool HasSettableMethod(Type type, string name) {
+ var method = GetMember(type, name);
+ return (
+ method != null &&
+ method.GetParameters().Count().Equals(1) &&
+ IsSettableType(method.GetParameters().First().ParameterType) &&
+ (_attributeFlag == null || System.Attribute.IsDefined(method, _attributeFlag))
+ );
+ }
+
+ /*==========================================================================================================================
+ | METHOD: SET METHOD VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Uses reflection to call a method, assuming that it is a) writable, and b) of type ,
+ /// , or .
+ ///
+ /// The object instance on which the method is defined.
+ /// The name of the method to assess.
+ /// The value to set the method to.
+ internal bool SetMethodValue(object target, string name, string value) {
+
+ if (!HasSettableMethod(target.GetType(), name)) {
+ return false;
+ }
+
+ var method = GetMember(target.GetType(), name);
+
+ Contract.Assume(method != null);
+
+ var valueObject = GetValueObject(method.GetParameters().First().ParameterType, value);
+
+ if (valueObject == null) {
+ return false;
+ }
+
+ method.Invoke(target, new object[] {valueObject});
+
+ return true;
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: HAS GETTABLE METHOD
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Used reflection to identify if a local method is available and gettable.
+ ///
+ ///
+ /// Will return false if the method is not available. Methods are only considered gettable if they have no parameters and
+ /// their return value is a settable type.
+ ///
+ /// The on which the method is defined.
+ /// The name of the method to assess.
+ /// Optional, the expected.
+ internal bool HasGettableMethod(Type type, string name, Type targetType = null) {
+ var method = GetMember(type, name);
+ return (
+ method != null &&
+ method.GetParameters().Count().Equals(0) &&
+ IsSettableType(method.ReturnType, targetType) &&
+ (_attributeFlag == null || System.Attribute.IsDefined(method, _attributeFlag))
+ );
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET METHOD VALUE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Uses reflection to call a method, assuming that it has no parameters.
+ ///
+ /// The object instance on which the method is defined.
+ /// The name of the method to assess.
+ /// Optional, the expected.
+ internal object GetMethodValue(object target, string name, Type targetType = null) {
+
+ if (!HasGettableMethod(target.GetType(), name, targetType)) {
+ return null;
+ }
+
+ var method = GetMember(target.GetType(), name);
+
+ return method.Invoke(target, Array.Empty());
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: IS SETTABLE TYPE?
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Determines whether a given type is settable, either assuming the list of , or provided a
+ /// specific .
+ ///
+ private static bool IsSettableType(Type sourceType, Type targetType = null) {
+
+ if (targetType != null) {
+ return sourceType.Equals(targetType);
+ }
+ return SettableTypes.Contains(sourceType);
+
+ }
+
+ /*==========================================================================================================================
+ | METHOD: GET VALUE OBJECT
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Converts a string value to an object of the target type.
+ ///
+ private static object GetValueObject(Type type, string value) {
+
+ var valueObject = (object)null;
+
+ if (type.Equals(typeof(bool))) {
+ valueObject = value.Equals("1") || value.Equals("true", StringComparison.InvariantCultureIgnoreCase);
+ }
+ else if (type.Equals(typeof(int))) {
+ Int32.TryParse(value, out var intValue);
+ valueObject = intValue;
+ }
+ else if (type.Equals(typeof(string))) {
+ valueObject = value;
+ }
+ else if (type.Equals(typeof(DateTime))) {
+ if (DateTime.TryParse(value, out var date)) {
+ valueObject = date;
+ }
+ }
+
+ return valueObject;
+
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: SETTABLE TYPES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// A list of types that are allowed to be set using .
+ ///
+ static internal List SettableTypes { get; }
+
+ /*==========================================================================================================================
+ | OVERRIDE: INSERT ITEM
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Fires any time an item is added to the collection.
+ ///
+ ///
+ /// Compared to the base implementation, will throw a specific error if a duplicate key
+ /// is inserted. This conveniently provides the , so it's clear what is being
+ /// duplicated.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The instance to insert.
+ ///
+ /// The TypeCollection already contains the MemberInfoCollection of the Type '{item.Type}'.
+ ///
+ protected override void InsertItem(int index, MemberInfoCollection item) {
+ if (!Contains(item.Type)) {
+ base.InsertItem(index, item);
+ }
+ else {
+ throw new ArgumentException(
+ $"The '{nameof(TypeCollection)}' already contains the {nameof(MemberInfoCollection)} of the Type '{item.Type}'.");
+ }
+ }
+
+ /*==========================================================================================================================
+ | OVERRIDE: GET KEY FOR ITEM
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Method must be overridden for the EntityCollection to extract the keys from the items.
+ ///
+ /// The object from which to extract the key.
+ /// The key for the specified collection item.
+ protected override Type GetKeyForItem(MemberInfoCollection item) {
+ Contract.Assume(item != null, "Assumes the item is available when deriving its key.");
+ return item.Type;
+ }
+
+ } //Class
+
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Repositories/DeleteEventArgs.cs b/Ignia.Topics/Repositories/DeleteEventArgs.cs
index f2ca65c3..aa189f24 100644
--- a/Ignia.Topics/Repositories/DeleteEventArgs.cs
+++ b/Ignia.Topics/Repositories/DeleteEventArgs.cs
@@ -15,11 +15,6 @@ namespace Ignia.Topics.Repositories {
///
public class DeleteEventArgs : EventArgs {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private Topic _topic = null;
-
/*==========================================================================================================================
| CONSTRUCTOR: TAXONOMY DELETE EVENT ARGS
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -28,7 +23,7 @@ public class DeleteEventArgs : EventArgs {
///
/// The topic.
public DeleteEventArgs(Topic topic) : base() {
- _topic = topic;
+ Topic = topic;
}
/*==========================================================================================================================
@@ -37,10 +32,7 @@ public DeleteEventArgs(Topic topic) : base() {
///
/// Getter that returns the Topic object associated with the event
///
- public Topic Topic {
- get => _topic;
- set => _topic = value;
- }
+ public Topic Topic { get; set; }
} // Class
diff --git a/Ignia.Topics/Repositories/ITopicRepository.cs b/Ignia.Topics/Repositories/ITopicRepository.cs
index 4b1cce84..3e49111c 100644
--- a/Ignia.Topics/Repositories/ITopicRepository.cs
+++ b/Ignia.Topics/Repositories/ITopicRepository.cs
@@ -49,7 +49,7 @@ public interface ITopicRepository {
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic identifier.
/// Determines whether or not to recurse through and load a topic's children.
@@ -57,7 +57,7 @@ public interface ITopicRepository {
Topic Load(int topicId, bool isRecursive = true);
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified key name.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified key name.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
diff --git a/Ignia.Topics/Repositories/ITopicRepositoryContract.cs b/Ignia.Topics/Repositories/ITopicRepositoryContract.cs
index fe7c7d94..4e2c7b21 100644
--- a/Ignia.Topics/Repositories/ITopicRepositoryContract.cs
+++ b/Ignia.Topics/Repositories/ITopicRepositoryContract.cs
@@ -45,7 +45,7 @@ public abstract class ITopicRepositoryContract : ITopicRepository {
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic identifier.
/// Determines whether or not to recurse through and load a topic's children.
@@ -53,24 +53,12 @@ public abstract class ITopicRepositoryContract : ITopicRepository {
public Topic Load(int topicId, bool isRecursive = true) => null;
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified key name.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified key name.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
/// A topic object.
- public Topic Load(string topicKey = null, bool isRecursive = true) {
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Validate return value
- \-----------------------------------------------------------------------------------------------------------------------*/
- //TopicFactory.ValidateKey(topicKey, true);
-
- /*------------------------------------------------------------------------------------------------------------------------
- | Provide dummy return value
- \-----------------------------------------------------------------------------------------------------------------------*/
- return null;
-
- }
+ public Topic Load(string topicKey = null, bool isRecursive = true) => null;
///
/// Loads a specific version of a topic based on its version.
@@ -88,7 +76,10 @@ public Topic Load(int topicId, DateTime version) {
| Validate return value
\-----------------------------------------------------------------------------------------------------------------------*/
Contract.Requires(version.Date < DateTime.Now, "The version requested must be a valid historical date.");
- Contract.Requires(version.Date > new DateTime(2014, 12, 9), "The version is expected to have been created since version support was introduced into the topic library.");
+ Contract.Requires(
+ version.Date > new DateTime(2014, 12, 9),
+ "The version is expected to have been created since version support was introduced into the topic library."
+ );
/*------------------------------------------------------------------------------------------------------------------------
| Provide dummy return value
@@ -125,6 +116,7 @@ public int Save(Topic topic, bool isRecursive = false, bool isDraft = false) {
}
+
/*==========================================================================================================================
| METHOD: MOVE
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -138,11 +130,13 @@ public int Save(Topic topic, bool isRecursive = false, bool isDraft = false) {
///
/// topic != null
///
+ #pragma warning disable CA1822 // Mark members as static
public void Move(Topic topic, Topic target) {
Contract.Requires(target != topic);
Contract.Requires(topic != null, "topic");
Contract.Requires(target != null, "target");
}
+ #pragma warning restore CA1822 // Mark members as static
///
/// Interface method that supports moving a topic from one position to another.
@@ -176,6 +170,7 @@ public void Move(Topic topic, Topic target, Topic sibling) {
///
/// topic != null
/// topic
+ #pragma warning disable IDE0022 // Use expression body for methods
public void Delete(Topic topic, bool isRecursive) {
/*------------------------------------------------------------------------------------------------------------------------
@@ -184,6 +179,7 @@ public void Delete(Topic topic, bool isRecursive) {
Contract.Requires(topic != null, "topic");
}
+ #pragma warning restore IDE0022 // Use expression body for methods
/*==========================================================================================================================
| METHOD: ROLLBACK
diff --git a/Ignia.Topics/Repositories/MoveEventArgs.cs b/Ignia.Topics/Repositories/MoveEventArgs.cs
index d845565f..fcf99673 100644
--- a/Ignia.Topics/Repositories/MoveEventArgs.cs
+++ b/Ignia.Topics/Repositories/MoveEventArgs.cs
@@ -19,12 +19,6 @@ namespace Ignia.Topics.Repositories {
///
public class MoveEventArgs : EventArgs {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private Topic _topic = null;
- private Topic _target = null;
-
/*==========================================================================================================================
| CONSTRUCTOR: TAXONOMY MOVE EVENT ARGS
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -35,7 +29,7 @@ public MoveEventArgs() { }
///
/// Initializes a new instance of the class and sets the and
- /// propreties based on the specified objects.
+ /// properties based on the specified objects.
///
/// The topic object associated with the move event.
/// The parent topic object targeted by the move event.
@@ -49,8 +43,8 @@ public MoveEventArgs(Topic topic, Topic target) {
Contract.Requires(topic != null, "topic");
Contract.Requires(target != null, "target");
Contract.Requires(topic != target, "The topic cannot be its own parent.");
- _topic = topic;
- _target = target;
+ Topic = topic;
+ Target = target;
}
/*==========================================================================================================================
@@ -59,10 +53,7 @@ public MoveEventArgs(Topic topic, Topic target) {
///
/// Gets or sets the Topic object associated with the event.
///
- public Topic Topic {
- get => _topic;
- set => _topic = value;
- }
+ public Topic Topic { get; set; }
/*==========================================================================================================================
| PROPERTY: TARGET
@@ -70,10 +61,7 @@ public Topic Topic {
///
/// Gets or sets the new parent that the topic will be moved to.
///
- public Topic Target {
- get => _target;
- set => _target = value;
- }
+ public Topic Target { get; set; }
} // Class
diff --git a/Ignia.Topics/Repositories/RenameEventArgs.cs b/Ignia.Topics/Repositories/RenameEventArgs.cs
index cb8ae99b..22a74ae1 100644
--- a/Ignia.Topics/Repositories/RenameEventArgs.cs
+++ b/Ignia.Topics/Repositories/RenameEventArgs.cs
@@ -15,11 +15,6 @@ namespace Ignia.Topics.Repositories {
///
public class RenameEventArgs : EventArgs {
- /*==========================================================================================================================
- | PRIVATE VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- private Topic _topic = null;
-
/*==========================================================================================================================
| CONSTRUCTOR: TAXONOMY RENAME EVENT ARGS
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -37,7 +32,7 @@ public RenameEventArgs() { }
///
/// The topic object associated with the rename event.
public RenameEventArgs(Topic topic) {
- _topic = topic;
+ Topic = topic;
}
/*==========================================================================================================================
@@ -49,10 +44,7 @@ public RenameEventArgs(Topic topic) {
///
/// The topic.
///
- public Topic Topic {
- get => _topic;
- set => _topic = value;
- }
+ public Topic Topic { get; }
} // Class
diff --git a/Ignia.Topics/Repositories/TopicRepositoryBase.cs b/Ignia.Topics/Repositories/TopicRepositoryBase.cs
index 4adf4c13..b7984466 100644
--- a/Ignia.Topics/Repositories/TopicRepositoryBase.cs
+++ b/Ignia.Topics/Repositories/TopicRepositoryBase.cs
@@ -6,6 +6,7 @@
using System;
using System.Diagnostics.Contracts;
using Ignia.Topics.Collections;
+using Ignia.Topics.Querying;
namespace Ignia.Topics.Repositories {
@@ -17,6 +18,11 @@ namespace Ignia.Topics.Repositories {
///
public abstract class TopicRepositoryBase : ITopicRepository {
+ /*==========================================================================================================================
+ | PRIVATE VARIABLES
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ private ContentTypeDescriptorCollection _contentTypeDescriptors = null;
+
/*==========================================================================================================================
| EVENT HANDLERS
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -41,13 +47,56 @@ public abstract class TopicRepositoryBase : ITopicRepository {
///
/// Retrieves a collection of Content Type Descriptor objects from the configuration section of the data provider.
///
- public abstract ContentTypeDescriptorCollection GetContentTypeDescriptors();
+ public virtual ContentTypeDescriptorCollection GetContentTypeDescriptors() {
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Initialize content types
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (_contentTypeDescriptors == null) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Load configuration data
+ \---------------------------------------------------------------------------------------------------------------------*/
+ var configuration = Load("Configuration");
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Add available Content Types to the collection
+ \---------------------------------------------------------------------------------------------------------------------*/
+ _contentTypeDescriptors = new ContentTypeDescriptorCollection();
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Ensure the parent ContentTypes topic is available to iterate over
+ \---------------------------------------------------------------------------------------------------------------------*/
+ if (configuration.Children.GetTopic("ContentTypes") == null) {
+ throw new Exception("Unable to load section Configuration:ContentTypes.");
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Add available Content Types to the collection
+ \---------------------------------------------------------------------------------------------------------------------*/
+ foreach (var topic in configuration.Children.GetTopic("ContentTypes").FindAllByAttribute("ContentType", "ContentType")) {
+ // Ensure the Topic is used as the strongly-typed ContentType
+ // Add ContentType Topic to collection if not already added
+ if (
+ topic is ContentTypeDescriptor contentTypeDescriptor &&
+ !_contentTypeDescriptors.Contains(contentTypeDescriptor.Key)
+ ) {
+ _contentTypeDescriptors.Add(contentTypeDescriptor);
+ }
+ }
+
+ }
+
+ return _contentTypeDescriptors;
+
+ }
+
/*==========================================================================================================================
| METHOD: LOAD
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified unique identifier.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified unique identifier.
///
/// The topic identifier.
/// Determines whether or not to recurse through and load a topic's children.
@@ -55,7 +104,7 @@ public abstract class TopicRepositoryBase : ITopicRepository {
public abstract Topic Load(int topicId, bool isRecursive = true);
///
- /// Loads a topic (and, optionally, all of its descendents) based on the specified key name.
+ /// Loads a topic (and, optionally, all of its descendants) based on the specified key name.
///
/// The topic key.
/// Determines whether or not to recurse through and load a topic's children.
@@ -103,7 +152,7 @@ public static Topic Load(XmlNode node, ImportStrategy importStrategy = ImportStr
/// exception="T:System.ArgumentNullException">
/// !VersionHistory.Contains(version)
///
- public void Rollback(Topic topic, DateTime version) {
+ public virtual void Rollback(Topic topic, DateTime version) {
/*------------------------------------------------------------------------------------------------------------------------
| Retrieve topic from database
@@ -167,11 +216,29 @@ public void Rollback(Topic topic, DateTime version) {
/// topic
public virtual int Save(Topic topic, bool isRecursive = false, bool isDraft = false) {
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Validate content type
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var contentTypes = GetContentTypeDescriptors();
+ if (!contentTypes.Contains(topic.ContentType)) {
+ throw new ArgumentException(
+ $"The Content Type \"{topic.ContentType}\" referenced by \"{topic.Key}\" could not be found under " +
+ $"\"Configuration:ContentTypes\". There are currently {contentTypes.Count} ContentTypes in the Repository."
+ );
+ }
+
+ /*------------------------------------------------------------------------------------------------------------------------
+ | Update content types collection, if appropriate
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ if (topic is ContentTypeDescriptor && !contentTypes.Contains(topic.Key)) {
+ _contentTypeDescriptors.Add(topic as ContentTypeDescriptor);
+ }
+
/*------------------------------------------------------------------------------------------------------------------------
| Trigger event
\-----------------------------------------------------------------------------------------------------------------------*/
if (topic.OriginalKey != null && topic.OriginalKey != topic.Key) {
- var args = new RenameEventArgs(topic);
+ var args = new RenameEventArgs(topic);
RenameEvent?.Invoke(this, args);
}
diff --git a/Ignia.Topics/Repositories/TopicRepositoryException.cs b/Ignia.Topics/Repositories/TopicRepositoryException.cs
new file mode 100644
index 00000000..5184d9fc
--- /dev/null
+++ b/Ignia.Topics/Repositories/TopicRepositoryException.cs
@@ -0,0 +1,63 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Data.Common;
+using System.Diagnostics.Contracts;
+using System.Runtime.Serialization;
+
+namespace Ignia.Topics.Repositories {
+
+ /*============================================================================================================================
+ | CLASS: TOPIC REPOSITORY EXCEPTION
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The provides a general exception that can be thrown for any persistence errors
+ /// that arise from concrete implementations.
+ ///
+ ///
+ /// Microsoft provides a set of classes, such as , which are specific to
+ /// their target implementations. Since , however, is intended to be database agnostic, none
+ /// of these are appropriate to catch when implementing . Instead, the provides a database agnostic version of an exception that can provide a wrapper
+ /// around any of these more concrete exceptions.
+ ///
+ public class TopicRepositoryException : DbException {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR: TAXONOMY DELETE EVENT ARGS
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The message to display for this exception.
+ public TopicRepositoryException() : base() { }
+
+ ///
+ /// Initializes a new instance with a specific error message.
+ ///
+ /// The message to display for this exception.
+ public TopicRepositoryException(string message) : base(message) {}
+
+ ///
+ /// Initializes a new instance with a specific error message and nested exception.
+ ///
+ /// The message to display for this exception.
+ /// The reference to the original, underlying exception.
+ public TopicRepositoryException(string message, Exception innerException) : base(message, innerException) { }
+
+ ///
+ /// Instantiates a new instance for serialization.
+ ///
+ /// A instance with details about the serialization requirements.
+ /// A instance with details about the request context.
+ /// A new instance.
+ protected TopicRepositoryException(SerializationInfo info, StreamingContext context) : base(info, context) {
+ Contract.Requires(info != null);
+ }
+
+ } // Class
+
+} // Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/StaticTypeLookupService.cs b/Ignia.Topics/StaticTypeLookupService.cs
new file mode 100644
index 00000000..327751b7
--- /dev/null
+++ b/Ignia.Topics/StaticTypeLookupService.cs
@@ -0,0 +1,112 @@
+/*==============================================================================================================================
+| Author Ignia, LLC
+| Client Ignia, LLC
+| Project Topics Library
+\=============================================================================================================================*/
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics.Contracts;
+using System.Reflection;
+
+namespace Ignia.Topics {
+
+ /*============================================================================================================================
+ | CLASS: TYPE INDEX
+ \---------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The can be configured to provide a lookup of .
+ ///
+ public class StaticTypeLookupService: KeyedCollection, ITypeLookupService {
+
+ /*==========================================================================================================================
+ | CONSTRUCTOR
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Establishes a new instance of a . Optionally accepts a list of
+ /// instances and a default value.
+ ///
+ ///
+ /// Any instances submitted via should be unique by ; if they are not, they will be removed.
+ ///
+ /// The list of instances to expose as part of this service.
+ /// The default type to return if no match can be found. Defaults to object.
+ public StaticTypeLookupService(
+ IEnumerable types = null,
+ Type defaultType = null
+ ): base(StringComparer.InvariantCultureIgnoreCase) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Set default type
+ \---------------------------------------------------------------------------------------------------------------------*/
+ DefaultType = defaultType;
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Populate collection
+ \---------------------------------------------------------------------------------------------------------------------*/
+ if (types != null) {
+ foreach (var type in types) {
+ if (!Contains(type)) {
+ Add(type);
+ }
+ }
+ }
+
+ }
+
+ /*==========================================================================================================================
+ | PROPERTY: DEFAULT TYPE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// The default type to return in case cannot find a match.
+ ///
+ public Type DefaultType { get; }
+
+ /*==========================================================================================================================
+ | METHOD: GET TYPE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Retrieves a from the class based on its string representation.
+ ///
+ /// A string representing the type.
+ /// A class type corresponding to the specified string.
+ ///
+ /// !String.IsNullOrWhiteSpace(contentType)
+ ///
+ ///
+ /// !contentType.Contains(" ")
+ ///
+ public Type GetType(string typeName) {
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Return cached entry
+ \---------------------------------------------------------------------------------------------------------------------*/
+ if (Contains(typeName)) {
+ return this[typeName];
+ }
+
+ /*----------------------------------------------------------------------------------------------------------------------
+ | Return default
+ \---------------------------------------------------------------------------------------------------------------------*/
+ return DefaultType;
+
+ }
+
+ /*==========================================================================================================================
+ | 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 string GetKeyForItem(Type item) {
+ Contract.Assume(item != null, "Assumes the item is available when deriving its key.");
+ return item.Name;
+ }
+
+ } //Class
+} //Namespace
\ No newline at end of file
diff --git a/Ignia.Topics/Topic.cs b/Ignia.Topics/Topic.cs
index 2e5ba29d..f4a757aa 100644
--- a/Ignia.Topics/Topic.cs
+++ b/Ignia.Topics/Topic.cs
@@ -9,7 +9,6 @@
using System.Globalization;
using System.Linq;
using Ignia.Topics.Collections;
-using Ignia.Topics.Repositories;
namespace Ignia.Topics {
@@ -43,7 +42,7 @@ public class Topic {
/// cref="ContentType"/> will be set to , which is required in order to correctly save
/// new topics to the database. When the parameter is set, however, the property is set to falseon and , as
- /// it is assumed these are being set to the same values currently used in the persistance store.
+ /// it is assumed these are being set to the same values currently used in the persistence store.
///
/// A string representing the key for the new topic instance.
/// A string representing the key of the target content type.
@@ -93,15 +92,21 @@ public Topic(string key, string contentType, Topic parent, int id = -1) {
///
/// Gets or sets the topic's integer identifier according to the data provider.
///
+ ///
+ /// The unique identifier for the .
+ ///
+ ///
+ /// The value of this topic has already been set to {_id}; it cannot be changed.
+ ///
///
- /// value > 0
+ /// value > 0
///
public int Id {
get => _id;
set {
Contract.Requires(value > 0, "The id is expected to be a positive value.");
if (_id > 0 && !_id.Equals(value)) {
- throw new ArgumentException("The value of this topic has already been set to " + _id + "; it cannot be changed.");
+ throw new ArgumentException($"The value of this topic has already been set to {_id}; it cannot be changed.");
}
_id = value;
}
@@ -113,9 +118,12 @@ public int Id {
///
/// Reference to the parent topic of this node, allowing code to traverse topics as a linked list.
///
+ ///
+ /// The current 's parent .
+ ///
///
/// While topics may be represented as a network graph via relationships, they are physically stored and primarily
- /// represented via a hierarchy. As such, each topic may have at most a single parent. Note that the the root node will
+ /// represented via a hierarchy. As such, each topic may have at most a single parent. Note that the root node will
/// have a null parent.
///
///
@@ -137,8 +145,11 @@ public Topic Parent {
| PROPERTY: CHILDREN
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Provides a keyed collection of child instances associated with the current .
+ /// Provides a keyed collection of child instances associated with the current .
///
+ ///
+ /// The children of the current .
+ ///
public TopicCollection Children { get; }
/*==========================================================================================================================
@@ -149,10 +160,13 @@ public Topic Parent {
///
///
/// Each topic is associated with a content type. The content type determines which attributes are displayed in the Topics
- /// Editor (via the property). The content type also determines,
- /// by default, which view is rendered by the (assuming the value isn't
+ /// Editor (via the property). The content type also determines,
+ /// by default, which view is rendered by the (assuming the value isn't
/// overwritten down the pipe).
///
+ ///
+ /// The key of the current 's .
+ ///
public string ContentType {
get => Attributes.GetValue("ContentType");
set => SetAttributeValue("ContentType", value);
@@ -164,12 +178,16 @@ public string ContentType {
///
/// Gets or sets the topic's Key attribute, the primary text identifier for the topic.
///
+ ///
+ /// The current 's key, which is guaranteed to be unique among its siblings.
+ ///
///
/// value != null
///
///
+ /// exception="T:System.ArgumentException"
+ /// >
/// !value.Contains(" ")
///
[AttributeSetter]
@@ -196,14 +214,18 @@ public string Key {
/// Gets or sets the topic's original key.
///
///
- /// The original key is automatically set by when its value is updated (assuming the original key isn't
- /// already set). This is, in turn, used by the to represent the original value,
- /// and thus allow the (or derived providers) from updating the data store
- /// appropriately.
+ /// The original key is automatically set by when its value is updated (assuming the original key isn't
+ /// already set). This is, in turn, used by the to represent the original
+ /// value, and thus allow the (or derived providers) from updating the data
+ /// store appropriately.
///
+ ///
+ /// The key, as represented in the persistence layer.
+ ///
///
+ /// exception="T:System.ArgumentException"
+ /// >
/// !value?.Contains(" ")?? true
///
internal string OriginalKey {
@@ -225,15 +247,19 @@ internal string OriginalKey {
/// Gets or sets the View attribute, representing the default view to be used for the topic.
///
///
- /// This value can be set via the query string (via the class), via the Accepts header
- /// (also via the class), on the topic itself (via this property). By default, it will
- /// be set to the name of the ; e.g., if the Content Type is "Page", then the view will be
- /// "Page". This will cause the to look for a view at, for instance,
+ /// This value can be set via the query string (via the class), via the Accepts header
+ /// (also via the class), on the topic itself (via this property). By default, it will
+ /// be set to the name of the ; e.g., if the Content Type is "Page", then the view will be
+ /// "Page". This will cause the to look for a view at, for instance,
/// /Common/Templates/Page/Page.aspx.
///
+ ///
+ /// The view, as specified by the current .
+ ///
///
+ /// exception="T:System.ArgumentException"
+ /// >
/// !value?.Contains(" ")?? true
///
[AttributeSetter]
@@ -252,6 +278,9 @@ public string View {
///
/// Gets or sets whether the current topic is hidden.
///
+ ///
+ /// true if this instance is hidden; otherwise, false.
+ ///
[AttributeSetter]
public bool IsHidden {
get => Attributes.GetBoolean("IsHidden", false);
@@ -264,6 +293,9 @@ public bool IsHidden {
///
/// Gets or sets whether the current topic is disabled.
///
+ ///
+ /// true if this instance is disabled; otherwise, false.
+ ///
[AttributeSetter]
public bool IsDisabled {
get => Attributes.GetBoolean("IsDisabled", false);
@@ -281,6 +313,9 @@ public bool IsDisabled {
/// If an item is not marked as IsVisible, then the item will not be visible independent of whether showDisabled is set.
///
/// Determines whether or not items marked as IsDisabled should be displayed.
+ ///
+ /// true if the is visible; otherwise, false.
+ ///
public bool IsVisible(bool showDisabled = false) => !IsHidden && (showDisabled || !IsDisabled);
/*==========================================================================================================================
@@ -290,10 +325,13 @@ public bool IsDisabled {
/// Gets or sets the Title attribute, which represents the friendly name of the topic.
///
///
- /// While the may not contain, for instance, spaces or symbols, there are no restrictions on what
+ /// While the may not contain, for instance, spaces or symbols, there are no restrictions on what
/// characters can be used in the title. For this reason, it provides the default public value for referencing topics. If
- /// the title is not set, then this property falls back to the topic's .
+ /// the title is not set, then this property falls back to the topic's .
///
+ ///
+ /// The current 's title.
+ ///
///
/// !string.IsNullOrWhiteSpace(value)
///
@@ -312,6 +350,9 @@ public string Title {
/// The Description attribute is primarily used by the editor to display help content for an attribute topic, noting
/// how the attribute is used, what is the expected input format or value, etc.
///
+ ///
+ /// The current 's description.
+ ///
///
/// !string.IsNullOrWhiteSpace(value)
///
@@ -330,8 +371,11 @@ public string Description {
/// The value is stored in the database as a string (Attribute) value, but converted to DateTime for use in the system. It
/// is important to note that the last modified attribute is not tied to the system versioning (which operates at an
/// attribute level) nor is it guaranteed to be correct for auditing purposes; for example, the author may explicitly
- /// overwrite this value for various reasons (such as backdating a webpage).
+ /// overwrite this value for various reasons (such as backdating a web page).
///
+ ///
+ /// The date that the current was last modified.
+ ///
///
/// !string.IsNullOrWhiteSpace(value.ToString())
///
@@ -348,16 +392,21 @@ public DateTime LastModified {
| METHOD: SET PARENT
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Changes the current while simultenaously ensuring that the sort order of the topics is
- /// maintained, assuming a is set.
+ /// Changes the current while simultaneously ensuring that the sort order of the topics is
+ /// maintained, assuming a is set.
///
///
- /// If no is provided, then the item is added to the beginning of the collection. If
- /// the intent is to add it to the end of the collection, then set the to e.g.
+ /// If no is provided, then the item is added to the beginning of the collection. If
+ /// the intent is to add it to the end of the collection, then set the to e.g.
/// parent.Children.LastOrDefault().
///
- /// The to move this under.
- /// The to mvoe this to the right of.
+ /// The to move this under.
+ /// The to move this to the right of.
+ /// parent - A descendant cannot be its own parent.
+ ///
+ /// Duplicate key when setting Parent property: the topic with the name '{Key}' already exists in the '{parent.Key}'
+ /// topic.
+ ///
public void SetParent(Topic parent, Topic sibling = null) {
/*------------------------------------------------------------------------------------------------------------------------
@@ -378,9 +427,9 @@ public void SetParent(Topic parent, Topic sibling = null) {
\-----------------------------------------------------------------------------------------------------------------------*/
if (parent != _parent && parent.Children.Contains(Key)) {
throw new InvalidKeyException(
- "Duplicate key when setting Parent property: the topic with the name '" + Key +
- "' already exists in the '" + parent.Key + "' topic."
- );
+ $"Duplicate key when setting Parent property: the topic with the name '{Key}' already exists in the '{parent.Key}' " +
+ $"topic."
+ );
}
/*------------------------------------------------------------------------------------------------------------------------
@@ -413,6 +462,7 @@ public void SetParent(Topic parent, Topic sibling = null) {
/// The value for the UniqueKey property is a collated, colon-delimited representation of the topic and its parent(s).
/// Example: "Root:Configuration:ContentTypes:Page".
///
+ /// The unique key of the current .
public string GetUniqueKey() {
/*------------------------------------------------------------------------------------------------------------------------
@@ -427,7 +477,7 @@ public string GetUniqueKey() {
var topic = this;
for (var i = 0; i < 100; i++) {
- if (uniqueKey.Length > 0) uniqueKey = ":" + uniqueKey;
+ if (uniqueKey.Length > 0) uniqueKey = $":{uniqueKey}";
uniqueKey = topic.Key + uniqueKey;
topic = topic.Parent;
if (topic == null) break;
@@ -451,11 +501,12 @@ public string GetUniqueKey() {
/// Note: If the topic root is not bound to the root of the site, this needs to specifically accounted for in any views
/// that reference the web path (e.g., by providing a prefix).
///
+ /// The HTTP-based path to the current .
public string GetWebPath() {
Contract.Ensures(Contract.Result() != null);
var uniqueKey = GetUniqueKey().Replace("Root:", "/").Replace(":", "/") + "/";
if (!uniqueKey.StartsWith("/")) {
- uniqueKey = "/" + uniqueKey;
+ uniqueKey = $"/{uniqueKey}";
}
return uniqueKey;
}
@@ -474,13 +525,14 @@ public string GetWebPath() {
///
/// Derived topics allow attribute values to be inherited from another topic. When a derived topic is configured via the
/// TopicId attribute key, values from that topic are used when the method unable to find a local value for the attribute.
+ /// Boolean)" /> method unable to find a local value for the attribute.
///
///
/// Be aware that while multiple levels of derived topics can be configured, the method defaults to a maximum level of five "hops".
+ /// cref="AttributeValueCollection.GetValue(String, Boolean)" /> method defaults to a maximum level of five "hops".
///
///
+ /// The that values should be derived from, if not otherwise available.
///
/// value != this
///
@@ -512,11 +564,12 @@ public Topic DerivedTopic {
/// significant extensibility.
///
///
- /// Attributes are stored via an class which, in addition to the Attribute Key and Value,
- /// also track other metadata for the attribute, such as the version (via the
- /// property) and whether it has been persisted to the database or not (via the
+ /// Attributes are stored via an class which, in addition to the Attribute Key and Value,
+ /// also track other metadata for the attribute, such as the version (via the
+ /// property) and whether it has been persisted to the database or not (via the
/// property).
///
+ /// The current 's attributes.
public AttributeValueCollection Attributes { get; }
/*==========================================================================================================================
@@ -526,10 +579,11 @@ public Topic DerivedTopic {
/// A façade for accessing related topics based on a scope name; can be used for tags, related topics, etc.
///
///
- /// The relationships property exposes a with child topics representing named relationships (e.g.,
+ /// The relationships property exposes a with child topics representing named relationships (e.g.,
/// "Related" for related topics); those child topics in turn have child topics representing references to each related
/// topic, thus allowing the topic hierarchy to be represented as a network graph.
///
+ /// The current 's relationships.
public RelatedTopicCollection Relationships { get; }
/*===========================================================================================================================
@@ -539,11 +593,12 @@ public Topic DerivedTopic {
/// A façade for accessing related topics based on a scope name; can be used for tags, related topics, etc.
///
///
- /// The incoming relationships property provides a reverse index of the property, in order to
+ /// The incoming relationships property provides a reverse index of the property, in order to
/// indicate which topics point to the current topic. This can be useful for traversing the topic tree as a network graph.
/// This is of particular use for tags, where the current topic represents a tag, and the incoming relationships represents
/// all topics associated with that tag.
///
+ /// The current 's incoming relationships.
public RelatedTopicCollection IncomingRelationships { get; }
/*==========================================================================================================================
@@ -553,9 +608,10 @@ public Topic DerivedTopic {
/// Provides a collection of dates representing past versions of the topic, which can be rolled back to.
///
///
- /// It is expected that this collection will be populated by the (or one of
+ /// It is expected that this collection will be populated by the (or one of
/// its derived providers).
///
+ /// The current 's version history.
public List VersionHistory { get; }
#endregion
@@ -566,37 +622,41 @@ public Topic DerivedTopic {
| METHOD: SET ATTRIBUTE VALUE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Protected helper method that either adds a new object or updates the value of an existing
- /// one, depending on whether that value already exists.
+ /// Protected helper method that either adds a new object or updates the value of an
+ /// existing one, depending on whether that value already exists.
///
///
/// When an attribute value is set and a corresponding, writable property exists on the topic, that property will be
- /// called by the AttributeValueCollection.This is intended to enforce local business logic, and prevent callers from
- /// introducing invalid data.To prevent a redirect loop, however, local properties need to inform the
- /// AttributeValueCollection that the business logic has already been enforced.To do that, they must either call
- /// SetValue() with the enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this overload.
+ /// called by the . This is intended to enforce local business logic, and prevent
+ /// callers from introducing invalid data.To prevent a redirect loop, however, local properties need to inform the
+ /// that the business logic has already been enforced. To do that, they must either
+ /// call with the
+ /// enforceBusinessLogic flag set to false, or, if they're in a separate assembly, call this overload.
///
/// The string identifier for the AttributeValue.
/// The text value for the AttributeValue.
///
- /// Specified whether the value should be marked as . By default, it will be marked as
- /// dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is
+ /// Specified whether the value should be marked as . By default, it will be marked
+ /// as dirty if the value is new or has changed from a previous value. By setting this parameter, that behavior is
/// overwritten to accept whatever value is submitted. This can be used, for instance, to prevent an update from being
- /// persisted to the data store on .
+ /// persisted to the data store on .
///
///
+ /// exception="T:System.ArgumentNullException"
+ /// >
/// !String.IsNullOrWhiteSpace(key)
///
///
+ /// exception="T:System.ArgumentNullException"
+ /// >
/// !String.IsNullOrWhiteSpace(value)
///
///
+ /// exception="T:System.ArgumentException"
+ /// >
/// !value.Contains(" ")
///
protected void SetAttributeValue(string key, string value, bool? isDirty = null) {
diff --git a/Ignia.Topics/TopicFactory.cs b/Ignia.Topics/TopicFactory.cs
index 6ba777ef..bb782be2 100644
--- a/Ignia.Topics/TopicFactory.cs
+++ b/Ignia.Topics/TopicFactory.cs
@@ -4,9 +4,7 @@
| Project Topics Library
\=============================================================================================================================*/
using System;
-using System.Collections.Generic;
using System.Diagnostics.Contracts;
-using System.Linq;
using System.Text.RegularExpressions;
namespace Ignia.Topics {
@@ -21,78 +19,12 @@ namespace Ignia.Topics {
public static class TopicFactory {
/*==========================================================================================================================
- | STATIC VARIABLES
- \-------------------------------------------------------------------------------------------------------------------------*/
- static Dictionary _typeLookup = new Dictionary();
-
- /*==========================================================================================================================
- | METHOD: GET TOPIC TYPE
+ | PROPERTY: TYPE LOOKUP SERVICE
\-------------------------------------------------------------------------------------------------------------------------*/
///
- /// Static helper method for looking up a class type based on a string name.
+ /// Establishes static variables for the .
///
- ///
- /// Currently, this method uses , which can be non-performant. As such, this helper method
- /// caches its results in a static lookup table keyed by the string value.
- ///
- /// A string representing the key of the target content type.
- /// A class type corresponding to a derived class of .
- ///
- /// !String.IsNullOrWhiteSpace(contentType)
- ///
- ///
- /// !contentType.Contains(" ")
- ///
- private static Type GetTopicType(string contentType) {
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Validate contracts
- \---------------------------------------------------------------------------------------------------------------------*/
- Contract.Requires(!String.IsNullOrWhiteSpace(contentType));
- Contract.Ensures(Contract.Result() != null);
- TopicFactory.ValidateKey(contentType);
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Return cached entry
- \---------------------------------------------------------------------------------------------------------------------*/
- if (_typeLookup.Keys.Contains(contentType)) {
- return _typeLookup[contentType];
- }
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Determine if there is a matched type
- \---------------------------------------------------------------------------------------------------------------------*/
- var baseType = typeof(Topic);
- var targetType = Type.GetType("Ignia.Topics." + contentType);
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Validate type
- \---------------------------------------------------------------------------------------------------------------------*/
- if (targetType == null) {
- targetType = baseType;
- }
- else if (!targetType.IsSubclassOf(baseType)) {
- targetType = baseType;
- throw new ArgumentException("The topic \"Ignia.Topics." + contentType + "\" does not derive from \"Ignia.Topics.Topic\".");
- }
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Cache findings
- \---------------------------------------------------------------------------------------------------------------------*/
- lock (_typeLookup) {
- if (_typeLookup.Keys.Contains(contentType)) {
- _typeLookup.Add(contentType, targetType);
- }
- }
-
- /*----------------------------------------------------------------------------------------------------------------------
- | Return result
- \---------------------------------------------------------------------------------------------------------------------*/
- return targetType;
-
- }
+ public static ITypeLookupService TypeLookupService { get; set; } = new DefaultTopicLookupService();
/*==========================================================================================================================
| METHOD: CREATE
@@ -131,28 +63,28 @@ private static Type GetTopicType(string contentType) {
///
public static Topic Create(string key, string contentType, Topic parent = null) {
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Validate contracts
- \---------------------------------------------------------------------------------------------------------------------*/
+ \-----------------------------------------------------------------------------------------------------------------------*/
Contract.Requires(!String.IsNullOrWhiteSpace(key));
Contract.Requires(!String.IsNullOrWhiteSpace(contentType));
Contract.Ensures(Contract.Result() != null);
TopicFactory.ValidateKey(key);
TopicFactory.ValidateKey(contentType);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Determine target type
- \---------------------------------------------------------------------------------------------------------------------*/
- var targetType = TopicFactory.GetTopicType(contentType);
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var targetType = TypeLookupService.GetType(contentType);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Identify the appropriate topic
- \---------------------------------------------------------------------------------------------------------------------*/
+ \-----------------------------------------------------------------------------------------------------------------------*/
var topic = (Topic)Activator.CreateInstance(targetType, key, contentType, parent, -1);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Return the topic
- \---------------------------------------------------------------------------------------------------------------------*/
+ \-----------------------------------------------------------------------------------------------------------------------*/
return topic;
}
@@ -165,7 +97,7 @@ public static Topic Create(string key, string contentType, Topic parent = null)
///
/// When the parameter is set the property is set to
/// false on as well as on , since it is assumed these are
- /// being set to the same values currently used in the persistance store.
+ /// being set to the same values currently used in the persistence store.
///
/// A string representing the key for the new topic instance.
/// A string representing the key of the target content type.
@@ -177,9 +109,9 @@ public static Topic Create(string key, string contentType, Topic parent = null)
/// A strongly-typed instance of the class based on the target content type.
public static Topic Create(string key, string contentType, int id, Topic parent = null) {
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Validate input
- \---------------------------------------------------------------------------------------------------------------------*/
+ \-----------------------------------------------------------------------------------------------------------------------*/
Contract.Requires(!String.IsNullOrWhiteSpace(key));
Contract.Requires(!String.IsNullOrWhiteSpace(contentType));
Contract.Requires(id > 0);
@@ -187,19 +119,19 @@ public static Topic Create(string key, string contentType, int id, Topic parent
TopicFactory.ValidateKey(key);
TopicFactory.ValidateKey(contentType);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Determine target type
- \---------------------------------------------------------------------------------------------------------------------*/
- var targetType = TopicFactory.GetTopicType(contentType);
+ \-----------------------------------------------------------------------------------------------------------------------*/
+ var targetType = TypeLookupService.GetType(contentType);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Identify the appropriate topic
\---------------------------------------------------------------------------------------------------------------------*/
var topic = (Topic)Activator.CreateInstance(targetType, key, contentType, parent, id);
- /*----------------------------------------------------------------------------------------------------------------------
+ /*------------------------------------------------------------------------------------------------------------------------
| Return object
- \---------------------------------------------------------------------------------------------------------------------*/
+ \-----------------------------------------------------------------------------------------------------------------------*/
return topic;
}
@@ -221,7 +153,7 @@ public static Topic Create(string key, string contentType, int id, Topic parent
public static void ValidateKey(string topicKey, bool isOptional = false) {
Contract.Requires(isOptional || !String.IsNullOrEmpty(topicKey));
Contract.Requires(
- String.IsNullOrEmpty(topicKey) || Regex.IsMatch(topicKey?? "", @"^[a-zA-Z0-9\.\-_]+$"),
+ String.IsNullOrEmpty(topicKey) || Regex.IsMatch(topicKey ?? "", @"^[a-zA-Z0-9\.\-_]+$"),
"Key names should only contain letters, numbers, hyphens, and/or underscores."
);
}
diff --git a/Ignia.Topics/ViewModels/INavigationTopicViewModel{T}.cs b/Ignia.Topics/ViewModels/INavigationTopicViewModel{T}.cs
index 78e218c8..0023014c 100644
--- a/Ignia.Topics/ViewModels/INavigationTopicViewModel{T}.cs
+++ b/Ignia.Topics/ViewModels/INavigationTopicViewModel{T}.cs
@@ -17,7 +17,24 @@ namespace Ignia.Topics.ViewModels {
/// No topics are expected to have a Navigation content type. Instead, implementers of this view model are expected
/// to manually construct instances.
///
- public interface INavigationTopicViewModel : IPageTopicViewModel where T: INavigationTopicViewModel {
+ public interface INavigationTopicViewModel : ITopicViewModel where T: INavigationTopicViewModel {
+
+ /*==========================================================================================================================
+ | PROPERTY: WEBPATH
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Represents the HTTP routing path for the corresponding .
+ ///
+ string WebPath { get; set; }
+
+ /*==========================================================================================================================
+ | PROPERTY: SHORT TITLE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// In addition to the Title, a site may opt to define a Short Title used exclusively in the navigation. If present, this
+ /// value should be used instead of Title.
+ ///
+ string ShortTitle { get; set; }
/*==========================================================================================================================
| PROPERTY: CHILDREN
diff --git a/Ignia.Topics/ViewModels/IPageTopicViewModel.cs b/Ignia.Topics/ViewModels/IPageTopicViewModel.cs
index ba04d010..8b654ba4 100644
--- a/Ignia.Topics/ViewModels/IPageTopicViewModel.cs
+++ b/Ignia.Topics/ViewModels/IPageTopicViewModel.cs
@@ -31,22 +31,6 @@ public interface IPageTopicViewModel : ITopicViewModel {
///
string WebPath { get; set; }
- /*==========================================================================================================================
- | PROPERTY: TITLE
- \-------------------------------------------------------------------------------------------------------------------------*/
- ///
- /// Gets or sets the Title attribute, which represents the friendly name of the topic.
- ///
- ///
- /// While the may not contain, for instance, spaces or symbols, there are no
- /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for
- /// referencing topics.
- ///
- ///
- /// !string.IsNullOrWhiteSpace(value)
- ///
- string Title { get; set; }
-
/*==========================================================================================================================
| PROPERTY: META KEYWORDS
\-------------------------------------------------------------------------------------------------------------------------*/
diff --git a/Ignia.Topics/ViewModels/ITopicViewModel.cs b/Ignia.Topics/ViewModels/ITopicViewModel.cs
index 93543f70..d9e7c19d 100644
--- a/Ignia.Topics/ViewModels/ITopicViewModel.cs
+++ b/Ignia.Topics/ViewModels/ITopicViewModel.cs
@@ -45,6 +45,17 @@ public interface ITopicViewModel {
///
string Key { get; set; }
+ /*==========================================================================================================================
+ | PROPERTY: UNIQUE KEY
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Gets or sets the topic's attribute, the unique text identifier for the topic.
+ ///
+ ///
+ /// value != null
+ ///
+ string UniqueKey { get; set; }
+
/*==========================================================================================================================
| PROPERTY: CONTENT TYPE
\-------------------------------------------------------------------------------------------------------------------------*/
@@ -90,5 +101,21 @@ public interface ITopicViewModel {
///
bool IsHidden { get; set; }
+ /*==========================================================================================================================
+ | PROPERTY: TITLE
+ \-------------------------------------------------------------------------------------------------------------------------*/
+ ///
+ /// Gets or sets the Title attribute, which represents the friendly name of the topic.
+ ///
+ ///
+ /// While the may not contain, for instance, spaces or symbols, there are no
+ /// restrictions on what characters can be used in the title. For this reason, it provides the default public value for
+ /// referencing topics.
+ ///
+ ///
+ /// !string.IsNullOrWhiteSpace(value)
+ ///
+ string Title { get; set; }
+
} //Class
} //Namespace
diff --git a/README.md b/README.md
index e31ec932..20155146 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Specifically, it attempts to ensure that the responsibilities of the developer,
- **Backend developers** have access to *data repositories*, *services*, and a rich *domain model* in C# for consuming the structured data and implementing any *business logic* via code.
- **Designers and graphic producers** have access to light-weight *views* based on purpose-built *view models*, thus allowing them to focus exclusively on presentation concerns, without any platform-specific scaffolding.
-This is contrasted to most traditional CMSs, which attempt to coordinate all of these via an editor by exposing design responsibilities (via themes, templates, and layouts) as well as development responsibilities (via plugins or components). This works well for a small project without distinct design or development resources, but introduces a lot of complexity for larger teams with established responsibilities.
+This is contrasted to most traditional CMSs, which attempt to coordinate all of these via an editor by exposing design responsibilities (via themes, templates, and layouts) as well as development responsibilities (via plug-ins or components). This works well for a small project without distinct design or development resources, but introduces a lot of complexity for larger teams with established responsibilities.
### Multi-Device Optimized
In addition, OnTopic is optimized for multi-client/multi-device scenarios since the content editor focuses exclusively on structured data. This allows entirely distinct presentation layers to be established. For instance, the same content can be accessed by an iOS app, a website, and even a web-based API for third-party consumption. By contrast, most CMSs are designed for one client only: a website (which may be mobile-friendly via responsive templates.)