Skip to content

Release/5.0.0#28

Merged
JeremyCaney merged 887 commits intomasterfrom
release/5.0.0
Mar 3, 2021
Merged

Release/5.0.0#28
JeremyCaney merged 887 commits intomasterfrom
release/5.0.0

Conversation

@JeremyCaney
Copy link
Copy Markdown
Member

@JeremyCaney JeremyCaney commented Mar 2, 2021

OnTopic 5.0.0 is a major release primarily focused on reliability and tidying up the interface. It includes a significant number of breaking changes—though only a handful of them will affect most implementers.

Contents

  • New Features
    • Highlights
    • General
    • NuGet Packages
    • Topic Associations
    • Topic Repositories
    • Topic Mapping Service
    • ASP.NET Core
    • SQL
  • Bug Fixes
  • Breaking Changes
    • Highlights
    • General
    • Topic Associations
    • Topic Repositories
    • Topic Mapping Service
    • ASP.NET Core
    • SQL
  • Exceptions
    • General
    • Topic Repositories
    • Topic Mapping Service
  • Improvements
    • General
    • State Tracking
    • SQL

New Features

Highlights

  • Metapackage: Established OnTopic.All metapackage which includes a reference to all of the core packages and implementations (c15bf2e).
  • SourceLink: Configured SourceLink with references to GitHub commits so that packages can be properly debugged by implementers (f7fb979).
  • IntelliSense: The NuGet packages now include the XML documentation, thus allowing implementers to benefit from IntelliSense annotations in Visual Studio (219ece4).
  • Lookup Fallback Types: Allow multiple fallback types to be defined when calling ITypeLookupService.Lookup() (ab4253d).
  • Topic Referenes: Introduced support for keyed topic references via Topic.References, with full support for enforcing business logic (31144cc).
  • Full Versioning Support: Introduced versioning support for Relationships and TopicReferences, in addition to the previous support for versioning in Attributes and ExtendedAttributes (0e808ea).
  • Repository Caching: Established a ITopicRepository.Refresh() interface (3d51b14), SqlTopicRepository.Refresh() implementation (6dbf973), as well as a GetTopicUpdates stored procedure (0f36947) to allow occasional cache updates without needing to reload the entire topic graph (164de05).
  • Contextual Loading: When calling ITopicRepository.Load(), an optional referenceTopic can now be passed; this allows individual topics or topic branches to be loaded, while still being able to connect relationships and topic references to elsewhere in the topic graph (180526e). It can also be used to update topics already in the topic graph.
  • Resolve Associations: TopicRepository.Save() will now recurse over Children and attempt to resolve unresolved associations, such as relationships and topic references (fdd0025, e703de5); previously, this had to be implemented by each individual implementation.
  • Repository Events: ITopicRepository now supports the TopicLoaded and TopicSaved events, in addition to the previous TopicRenamed, TopicMoved and TopicDeleted events (37b38f5).
  • C# 9.0 Records: Introduced support for mapping C# 9.0 records with init setters in the topic mapping services (75dea66, 145c2a4) and updated all view models and binding models from read/write classes to C#'s new read-only, immutable record types (10d0652).
  • Core Attributes: TopicKey, ContentType, and ParentID are now first-class columns in the Topics table (c50f44d), greatly simplifying querying for the core attributes.
  • SQL Unit Tests: Introduced unit tests for stored procedures and functions for SQL Server database schema (91e3fe4) as well as for the SqlDataReader extension methods used by SqlTopicRepository (b91c292), thus offering far more assurances over the correctness and regression testing.

General

  • Lookup Fallback Types: The ITypeLookupService now accepts multiple type names to its Lookup() method, thus allowing callers to specify and control what alternatives or defaults they accept, instead of relying on the ITypeLookupService implementations to handle this (ab4253d). This offers more transparency, predictability, and control to the callers, and actually reduced repetitive code in many cases.
  • Unkeyed Collections: Introduced new TopicCollection and ReadOnlyTopicCollection types for composite collections—such as relationships—which do not need to be unique by key; replaced instances of Collection<Topic>, ReadOnlyCollection<Topic>, and some instances of IEnumerable<Topic> with these new types (826b93c).
  • Topic Index: Introduced a new TopicIndex collection (which indexes a list of topics by Topic.Id) (3af4c78) as well as a Topic.GetTopicIndex() extension method for generating a TopicIndex for a given topic and all of its descendants (3777b6b).
  • Model Interfaces: Introduced the new ICoreTopicViewModel (565273f, 3b5dda8, 5ea98c9, 9bc0d00, 527dee4) and INavigableTopicViewModel (adf7ff8, c41d989) interfaces (2f98c39).

NuGet Packages

  • Metapackage: Established OnTopic.All metapackage which includes a reference to all of the core packages and implementations (c15bf2e).
  • SourceLink: Configured SourceLink with references to GitHub commits so that packages can be properly debugged by implementers (f7fb979).
  • IntelliSense: The NuGet packages now include the XML documentation, thus allowing implementers to benefit from IntelliSense annotations in Visual Studio (219ece4).
  • Icon: Added icon to NuGet packages to make them easier to recognize, and consistently branded (7074bde).

Topic Associations

  • Topic References: Introduced support for keyed topic references via the [dbo].[TopicReferences] table and the Topic.References property (31144cc), including versioning and enforcement of business logic (3144235).
  • Full Versioning Support: Introduced versioning support for Relationships and TopicReferences, including the new RelationshipIndex and ReferencesIndex views (0e808ea).
  • Orphaned Association Tracking: Introduced an IsFullyLoaded property to both Topic.Relationships and Topic.References to track scenarios where target topics could not be mapped within the scoped topic graph during ITopicRepository.Load(); in these scenarios, the unmatched items won't be deleted from the collection on ITopicRepository.Save() in order to avoid potential data loss (4885958).
  • Relationship Object Model: Introduced a new TopicRelationshipMultiMap class (57ff3f9, 7129289) to replace the legacy RelatedTopicMultiMap. This offers a cleaner interface with no unexpected back doors, since the underlying data store is now hidden behind a façade (66ccd9d).

Topic Repositories

  • Repository Caching: Established a new ITopicRepository.Refresh() interface (3d51b14), SqlTopicRepository.Refresh() implementation (6dbf973), as well as a GetTopicUpdates stored procedure (0f36947) to allow occasional cache updates without needing to reload the entire topic graph (164de05).
  • Contextual Loading: When calling ITopicRepository.Load(), an optional referenceTopic can now be passed; this allows individual topics or topic branches to be loaded, while still being able to connect relationships and topic references to elsewhere in the topic graph (180526e). It can also be used to update topics already in the topic graph.
  • Event Bubbling: Added event bubbling to the CachedTopicRepository so that the DeleteEvent, MoveEvent, or RenameEvent delegates from the underlying ITopicRepisitory instance will notify any subscribers of the CachedTopicRepository's (c1ea208). Without this, these events weren't terribly useful in most real-world scenarios (which will almost always use the CachedTopicRepisitory decorator).
  • Observable Base Class: Introduced new ObservableTopicRepository base class for handling event logic of the ITopicRepository; it is expected that, at minimum, other ITopicRepository implementations will derive from this (fb663a2, e703de5).
  • Decorator Base Class: Introduced new TopicRepositoryDecorator base class for handling decorator logic, as used in e.g. CachedTopicRepository; this makes it easier to implement new decorators, including event bubbling (799e2b6, e703de5).
  • Resolve Associations: Introduced support for assigning a shared version, recursing over Children, and attempting to resolve unresolved relationships and topic references into TopicRepository.Save() (fdd0025, e703de5); previously, these had been handled by SqlTopicRepository.
  • Repository Events: Introduced new TopicLoaded and TopicSaved events, with corresponding TopicLoadEventArgs and TopicSaveEventArgs, to the ITopicRepository (37b38f5).
  • Raise Event Methods: Introduced new protected raise event methods to ObservableTopicRepository, such as OnTopicLoaded(), OnTopicSaved(), and OnTopicDeleted() (37b38f5).

Topic Mapping Service

  • C# 9.0 Records: Introduced support for mapping C# 9.0 records with init setters in the topic mapping services (75dea66, 145c2a4) and updated all view models and binding models from read/write classes to C#'s new read-only, immutable record types (10d0652).
  • Map Missing Associations: Introduced ability for the TopicMappingService to fill in any missing associations on a view model that it previously mapped and cached the results of (5cf13bb).
  • Filter by Content Type: Introduced the [FilterByContentType()] attribute for use by the ITopicMappingService as a more specialized version of the original [FilterByAttribute()] attribute (105cfdd).
  • Non-Generic Collections: Introduced support for compatible-type mapping in non-generic collections using the TopicMappingService; previously, this was only supported on non-collections, or collections that directly implemented a generic type (560aaca).

ASP.NET Core

  • Sitemap Customization: The attributes and content types excluded or skipped by the SitemapController can now be customized by adjusting the static ExcludedContentTypes, SkippedContentTypes, and ExcludedAttributes collections (a9f04ab).
  • Topic References: Topic.References are now exposed to the SitemapController's Extended() action (dcd1b4c).

SQL

  • Core Attributes: Added TopicKey, ContentType, and ParentID to the Topics table (c50f44d), and @Key and @ContentType to the CreateTopic and UpdateTopic stored procedures (a74389d)
  • Create Topic Transaction: Added transaction and appropriate lock to the CreateTopic stored procedure to further protect the integrity of the nested set during concurrent operations (282f8a9).
  • Unit Testing: Introduced unit tests for stored procedures and functions for SQL Server database schema (91e3fe4).
  • Version Precision: Updated database to use new DATETIME2(7) data type for Version columns in order to increase the precision to match the .NET CLR's DateTime type (5d0ea80).

Bug Fixes

  • Post-Exception Business Logic Enforcement: If an exception occurs while enforcing business logic on Topic.Attributes, the request will be de-registered, thus ensuring any subsequent attempts to set an item with the same key will have business logic enforced (1b5dd5b). Previously, the exception could prevent that process from being completed, thus introducing a potential loophole during subsequent actions.
  • Track Deleted Attributes: Ensured that AttributeCollection.Clear() correctly marked the collection as IsDirty(), if appropriate, by accounting any DeletedAttributes (ce3cc43).
  • Track Attribute Reversions: Ensured that all modified AttributeRecord instances in a TopicRepositoryBase.Rollback() operation were correctly marked as IsDirty, thus ensuring they would be properly Save()d (e77d8eb).
  • Allow Duplicate Topic Keys in Relationships: The Topic.Relationships collection now allows relationships to different topics with the same Topic.Key (66ccd9d). Previously, this used a KeyedTopicCollection, which artificially restricted it to relationships with a unique Key.
  • Allow Duplication Topic Keys in FindAll(): Fixed a bug in the Topic.FindAll() extension methods which prevented it from returning multiple topics which the same Key, even if they represented different entities (75f6369).
  • Track Version in Reversion: Fixed bug in the GetTopicVersion stored procedure which resulted in the Version column not being returned for Attributes or ExtendedAttributes, and thus the AttributeRecord.LastModified not being properly updated during e.g. a Rollback() (622d575). In practice, this didn't harm anything, but it prevented removal of legacy schema validation which explicitly looked for the Version column.
  • Map Overriden Members and Overloaded Methods: Resolved an issue where the MemberInfoCollection<T>—used by the topic mapping services—would throw an exception if an overloaded or overridden member was present on a type (75dea66). Now, it will simply defer to the first or most derived instance of a member, and ignore all others. This enables support for C# 9.0 records, which have an implicit compiler-injected override, and is also a logical default for most scenarios where this might occur.
  • Allow Multiple Roots: Fixed bug where calling the CreateTopic or MoveTopic stored procedures with a null @ParentID would result in a corrupted hierarchy (500ca09). This isn't an expected use case, but we obviously want to make sure there's no risk of corrupting the hierarchy if it does occur.
  • Stored Procedure Errors: Fixed the RAISERROR() calls in the MoveTopic stored procedure; these weren't working correctly due to a formatting bug (bfdec1c).

Breaking changes

For a quick summary of all renamed and removed types, members, and methods, see #27.

Highlights

  • .NET Framework Support: Removed support for .NET Standard 2.0 and, thus, .NET Framework; this effectively ends support for OnTopic-WebForms, OnTopic-MVC, and OnTopic-Editor-WebForms (b3ba1a0).
  • Topic.DerivedTopic: Renamed Topic.DerivedTopic to the more semantically accurate Topic.BaseTopic (2486ab2). This breaks not only calls to Topic.DerivedTopic, but also database records for e.g. ReferenceKeys storing the DerivedTopic value. The OnTopic 5.x database migration script addresses the data aspects of the migrations.
  • Immutable View Models: Changed all view models and binding models from read/write classes to C#'s new read-only, immutable record types (10d0652). This necessitates that all adopters that rely on these migrate any derived types from classes to records.
  • Keyed Topic Collections: Renamed TopicCollection classes (such as TopicCollection<T>, ReadOnlyTopicCollection) to KeyedTopicCollection (e.g., KeyedTopicCollection<T>, ReadOnlyKeyedTopicCollection) to better communicate that the items are indexed and, thus, must have a unique key, even if they represent different entities (826b93c).
  • Attribute Collection: Renamed AttributeValueCollection to AttributeCollection, and AttributeValue to AttributeRecord (f51407a).
  • TopicRelationshipMultiMap: The RelatedTopicCollection and NamedTopicCollection used by Topic.Relationships have been replaced with TopicRelationshipMultiMap (57ff3f9, 7129289), ReadOnlyTopicMultiMap (eb5c1c3), and TopicMultiMap (d049ea9). Care was taken to maintain the overall interface. The interface for calls to the underlying collections, however, will have changed (66ccd9d). Most notably, all write operations must now go through SetValue(), Remove(), or Clear(). In addition, for callers iterating over the relationships, the relationship key is now Key not Name, the topics are now stored under a Values property, and the methods now require a full Topic reference, instead of just a Topic.Key (0d9dcd5).
  • GetValue() Syntax: Renamed (ReadOnly)KeyedTopicCollection<T>.GetTopic() to GetValue() (6674c7d); on TopicRelationshipMultiMap (i.e., Topic.Relationships), renamed GetAllTopics(), GetTopics(), RemoveTopic(), and ClearTopics() to GetAllValues(), GetValues(), Remove(), and Clear() for consistency with other collections, and especially the TrackedRecordCollection used by AttributeCollection and TopicRelationshipCollection (34a8c52).
  • OnTopic.Lookup namespace: Consolidated all ITypeLookupService implementations into a new OnTopic.Lookup namespace (f80e084).
  • Load() by Unique Key: Modified Load(topicKey) to instead expect a fully-qualified UniqueKey (e.g., Root:Web:Products); as part of that, renamed the topicKey parameter to uniqueKey to disambiguate usage (4c3d30f).
  • Mapping Attributes: Renamed [Relationship()] to [Collection()] (f84ae66), [Follow()] to [Include()] (7c150d8), RelationshipType to CollectionType (1daf799), and Relationships to AssociationTypes (78600fd) as part of a broader effort to establish a more consistent and unified nomenclature that distinguishes between topic associations and specific types of associations, such as Topic.Relationships, Topic.References, or Topic.BaseTopic (b01216e).
  • Filter by Content Type: The [FilterByAttribute()] attribute continues to operate as it previously did, but can no longer be used to filter by ContentType since content type is no longer stored in Topic.Attributes (4a9f5b7); instead, use the new [FilterByContentType()] attribute (105cfdd). If ContentType is used with [FilterByAttribute()], an ArgumentException will be thrown (c68040a).
  • IRouteBuilder Support: Removed (deprecated) support for IRouteBuilder extension methods in ASP.NET Core; implementers should use end point routing via IEndpointRouteBuilder instead (6be993b, 19612ac).
  • NavigationViewModel: Replaced CurrentKey with CurrentWebPath; updated the INavigationTopicViewModel<T>'s IsSelected() method to expect a webPath parameter instead of a uniqueKey parameter (3e68b16).
  • Core Attributes: Added @Key and @ContentType to the CreateTopic and UpdateTopic stored procedures (a74389d); removed TopicKey, ContentType, and ParentID from the Attributes table; they are now in the Topics table (c50f44d).

General

  • .NET Framework Support: Removed support for .NET Standard 2.0 and, thus, .NET Framework; this effectively ends support for OnTopic-WebForms, OnTopic-MVC, and OnTopic-Editor-WebForms (b3ba1a0).
  • Topic.DerivedTopic: Renamed Topic.DerivedTopic to the more semantically accurate Topic.BaseTopic (2486ab2). This breaks not only calls to Topic.DerivedTopic, but also database records for e.g. ReferenceKeys storing the DerivedTopic value. The OnTopic 5.x database migration script addresses the data aspects of the migrations.
  • Immutable View Models: Changed all view models and binding models from read/write classes to C#'s new read-only, immutable record types (10d0652). This necessitates that all adopters that rely on these migrate any derived types from classes to records.
  • Keyed Topic Collections: Renamed TopicCollection classes (such as TopicCollection<T>, ReadOnlyTopicCollection) to KeyedTopicCollection (e.g., KeyedTopicCollection<T>, ReadOnlyKeyedTopicCollection) to better communicate that the items are indexed and, thus, must have a unique key, even if they represent different entities (826b93c).
  • Attribute Collection: Renamed AttributeValueCollection to AttributeCollection, and AttributeValue to AttributeRecord (f51407a).
  • GetValue() Syntax: Renamed (ReadOnly)KeyedTopicCollection<T>.GetTopic() to GetValue() (6674c7d); on TopicRelationshipMultiMap (i.e., Topic.Relationships), renamed GetAllTopics(), GetTopics(), RemoveTopic(), and ClearTopics() to GetAllValues(), GetValues(), Remove(), and Clear() for consistency with other collections, and especially the TrackedRecordCollection used by AttributeCollection and TopicRelationshipCollection (34a8c52).
  • Model Interfaces: Deprecated the IPageTopicViewModel interface, and removed IsHidden from the ITopicViewModel interface (2f98c39), and marked core properties as [Required] (1bf25be).
  • OnTopic.Lookup namespace: Consolidated all ITypeLookupService implementations into a new OnTopic.Lookup namespace (f80e084).
  • ITypeLookupService.Lookup(): The ITypeLookupService's Lookup() call now accepts a params string[] typeNames argument, instead of string typeName (ab4253d). Implementations which override Lookup() will need to update their implementation.
  • Immutable Attributes: AttributeRecord (previously AttributeValue) is now a read-only, immutable record; previously writable properties such as IsDirty and LastModified can no longer be modified without making a copy of the object. Prefer using AttributeCollection.SetValue() over working directly with AttributeRecord instances (bba59bb).
  • Topic Factory Signature: The parameter order on TopicFactory.Create() was updated to match the parameter order of the Topic constructor (69caaa2). This change isn't expected to impact most implementations, as it only affects calls that specify an id—which is typically only done in test code or concrete ITopicRepository implementations.
  • Topic.Description: The Topic.Description property is now obsolete (6c3a2e9, bbfd9ff). Instead, either use GetValue() or SetValue() on AttributeCollection (via Topic.Attributes) or, better yet, use the ITopicMappingService to map view models with a Description property.
  • ReadOnlyTopicCollection<T>FromList(): The ReadOnlyTopicCollection<T>.FromList() factory method is now obsolete (b013e38); use the ReadOnlyTopicCollection(IList<T> innerCollection) overload instead, which does the same thing without creating a redundant instance of the object.
  • List<T> Return Types: Changed public List<T> collections to Collection<T> (86fd5c4); notably, this includes Topic.VersionHistory and TypeMemberInfoCollection.SettableTypes.
  • AttributeTypeDescriptor: Eliminated AttributeTypeDescriptor; since we no longer support OnTopic-WebForms, and its complex EditorType and ModelType logic, we can merge the simplified AttributeTypeDescriptor into AttributeDescriptor, and use that directly as the base class for each of attribute type descriptors (7b00274).
  • Namespace Updates: Moved AttributeCollection to OnTopic.Attributes; MappedTopicCache to OnTopic.Internal.Mapping; and TopicRelationshipMultiMap, and TopicReferenceCollection to the new OnTopic.Associations namespaces (7e99f41, 213a46b).
  • Reflection Classes: Marked OnTopic.Internal.Reflection classes—most notably, TypeMemberInfoCollection—as internal, so they're no longer accessibly publicly (a38d619).
  • SetValue(…, isDirty) parameter: Renamed the isDirty parameter on SetValue() methods to markDirty across the code base (a6d1801). This helps avoid some confusion when comparing the value to local isDirty or _isDirty values, which vary slightly in their semantic.
  • Protected Override Members: Marked most protected override members as sealed to prevent unintended further modification of the business logic (46cc506). This particularly applied to KeyedCollection implementations.

Topic Associations

  • TopicRelationshipMultiMap: The RelatedTopicCollection and NamedTopicCollection used by Topic.Relationships have been replaced with TopicRelationshipMultiMap (57ff3f9, 7129289), ReadOnlyTopicMultiMap (eb5c1c3), and TopicMultiMap (d049ea9). Care was taken to maintain the overall interface. The interface for calls to the underlying collections, however, will have changed (66ccd9d). Most notably, all write operations must now go through SetValue(), Remove(), or Clear(). In addition, for callers iterating over the relationships, the relationship key is now Key not Name, the topics are now stored under a Values property, and the methods now require a full Topic reference, instead of just a Topic.Key (0d9dcd5).
  • SetValue(…, isIncoming): Moved isIncoming overloads on the TopicRelationshipMultiMap (previously RelatedTopicCollection) to internal since they are intended for managing the internal integrity of the topic graph (5e40470, c780f4e). As part of this, exposed the optional isDirty parameter to the public SetTopic() method.
  • KeyValuesPair<T>.IsDirty: Converted IsDirty property on the KeyValuesPair<T> (previously NamedTopicCollection) to a method for consistency with other collections (7a9bbaf).

Topic Repositories

  • SqlClient Dependency: SqlTopicRepository now requires, at minimum, SqlClient 2.0.0 or above.
  • Load() by Unique Key: Modified Load(topicKey) to instead expect a fully-qualified UniqueKey (e.g., Root:Web:Products); as part of that, renamed the topicKey parameter to uniqueKey to disambiguate usage (4c3d30f).
  • Topic Repository Event Names: Renamed the ITopicRepository events from e.g. MoveEvent to TopicMoved, and the arguments from e.g. MoveEventArgs to TopicMoveEventArgs (37b38f5).
  • Save Draft Feature: The optional ITopicRepository.Save(isDraft) parameter has been removed (59a7716). This was never wired up, and has always been optional, so we don't anticipate that anyone has used it, except as part of calls meant to relay the optional parameter.
  • Get Content Type Descriptors: Removed (deprecated) TopicRepository.GetContentTypeDescriptors(ContentTypeDescriptor) overload; this is replaced by the more appropriately named SetContentTypeDescriptors(), since it potentially updates the content type descriptor cache (a078fc8, 100588e).
  • Non-Recursive Deletions: Set default value of ITopicRepository.Delete(…, isRecursive) to false, such that an exception will be thrown on attempts to delete a topic with children (704c8de). This is how this method was originally intended, but a bug prevented validation (a0cc21c) and, to avoid a breaking change outside of a major release, the default had been set to true (1773113).
  • Save() Return Type: Changed return type of ITopicRepository.Save() method from int (representing the Topic.Id) to void (c2a8fe8).
  • Contextual Load: The signature for the ITopicRepository.Load() methods has been updated to include a new, optional referenceTopic parameter. This will break calls to Load() which included the optional isRecursive parameter, as the new parameter has been added before that (180526e).
  • TopicRepositoryBase: Renamed TopicRepositoryBase to TopicRepository (f0d9d44, e703de5); this will break any derived implementations.
  • Sealed Methods: Marked TopicRepository.Save(), Move(), and Delete() as sealed so they cannot be derived; instead, implementers must now override the new abstract methods SaveTopic(), MoveTopic(), and DeleteTopic() (116c8eb, e703de5); it is no longer necessary to call the base methods.

Topic Mapping Service

  • Mapping Attributes: Renamed [Relationship()] to [Collection()] (f84ae66), [Follow()] to [Include()] (7c150d8), RelationshipType to CollectionType (1daf799), and Relationships to AssociationTypes (78600fd) as part of a broader effort to establish a more consistent and unified nomenclature that distinguishes between topic associations and specific types of associations, such as Topic.Relationships, Topic.References, or Topic.BaseTopic (b01216e).
  • StaticTypeLookupService.DefaultType: The StaticTypeLookupService no longer defines or accepts a DefaultType (ab4253d). Implementations which call the StaticTypeLookupService with a defaultType parameter, or callers which rely on a StaticTypeLookupService derivative to return a default will need to be updated to pass the default through as a call to Lookup() instead.
  • Filter by Content Type: The [FilterByAttribute()] attribute continues to operate as it previously did, but can no longer be used to filter by ContentType since content type is no longer stored in Topic.Attributes (4a9f5b7); instead, use the new [FilterByContentType()] attribute (105cfdd). If ContentType is used with [FilterByAttribute()], an ArgumentException will be thrown (c68040a).
  • AttributeKeyAttribute.Value: Renamed [AttributeKey()]'s Value property to Key (64120a9); as this is normally set via the constructor and accessed via PropertyConfiguration, this won't likely break most implementations.
  • AssociatedTopicBindingModel: Moved AssociatedTopicBindingModel to the OnTopic.ViewModels library, as it is not needed by the core OnTopic library, and it will likely be needed by implementations relying on the main view model library, which is where most concrete models are stored (ac01bbf).
  • Binding Model Lookup Service: Updated DynamicTopicBindingModelLookupService to include types that implement ITopicBindingModel, regardless of whether or not they end in TopicBindingModel (5c8bd06). This allows e.g. TopicViewModels to double as binding models.
  • Mapping Internals: Moved PropertyConfiguration and MappedTopicCache to internal; they were already in the OnTopic.Mapping.Internal namespace and intended for internal use, but as they're no longer leaked via protected methods in TopicMappingService, they can be made truly internal (179c664). Moved all classes from the OnTopic.Internal.Mapping namespace to the new OnTopic.Mapping.Internal namespace (3c4dc3f).
  • Protected Methods: Marked the protected helper functions in TopicMappingService and ReverseTopicMappingService as private, since these classes aren't otherwise setup for derivatives, and there isn't a compelling case for how exposing these would aid in that process currently (52488e5).

ASP.NET Core

  • IRouteBuilder Support: Removed (deprecated) support for IRouteBuilder extension methods in ASP.NET Core; implementers should use end point routing via IEndpointRouteBuilder instead (6be993b, 19612ac).
  • NavigationViewModel: Replaced CurrentKey with CurrentWebPath; updated the INavigationTopicViewModel<T>'s IsSelected() method to expect a webPath parameter instead of a uniqueKey parameter (3e68b16).
  • Extended Sitemap Schema: The SitemapController's Extended() action now includes a DataObject named Attributes, instead of a DataObject named after the ContentType; this provides more flexibility in querying with Google Custom Search (f79f1ff).
  • Sitemap Internals: The GenerateSitemap() and AddTopic() methods on the SitemapController are now private (c669055); these were never expected to be used externally, and are not expected to cause any problems.
  • Virtual Actions: Removed virtual from most ASP.NET Controller actions—such as on SitemapController (da5e81c) and RedirectController (e77c7d5)—as well as the TopicRepository.SetContentTypeDescriptors() (d802bcd).

SQL

  • Core Attributes: Added @Key and @ContentType to the CreateTopic and UpdateTopic stored procedures (a74389d); removed TopicKey, ContentType, and ParentID from the Attributes table; they are now in the Topics table (c50f44d).
  • Topic Index: Removed the unnecessary TopicIndex view; that information can now be pulled directly from Topics without the need for a complex query (a4a28d6)
  • @DeleteRelationships: Removed the legacy @DeleteRelationships parameter from the UpdateTopic stored procedure (06b2cd9).
  • GetTopicID Function: Replaced GetTopicID with logic from the newer GetTopicIDByUniqueKey (a0f1a32, ff58a71, 10a9c0c).
  • Legacy Columns: Deleted legacy Topics.Stack_Top (8ba3514), Attributes.LastModified (2b981f2), and ExtendedAttributes.LastModified (242593c) columns (b5b4829).

Exceptions

General

  • Exception Bubbling: If an exception occurs while enforcing business logic on either Topic.Attributes or Topic.References, the original exception thrown by the property will now bubble up, instead of being wrapped in a TargetInvocationException (bf7782c).
  • Contract Exceptions: Updated Contract.Requires(bool) and Contract.Assumes(bool) to throw an InvalidOperationException instead of a generic Exception (7548771, 7e6cb4c, db32581). Since InvalidOperationException derives from Exception this should be backward compatible for most scenarios, while offering implementors to catch a more specific exception. Implementors that requires more flexibility should use e.g. Contract.Requires<T>(bool) instead in order to define what Exception should be thrown.
  • Change Topic.Id: Throw InvalidOperationException when attempting to change a Topic.Id, instead of a generic ArgumentException (25bdeb4, 2f75e8b).
  • Topic.Relationships.SetValue(): Updated the TopicRelationshipMultiMap.SetValue() method (previously RelatedTopicCollection.SetTopic()) to throw an InvalidOperationException instead of an ArgumentNullException for scenarios where isIncoming is set on a TopicRelationshipMultiMap that isn't set to _isIncoming (9391a5d). This should never occur, as this is an internal overload and managed by the OnTopic library; if it does, however, it suggests a design flaw in the code.
  • Parameter Names: Ensured all cases where an ArgumentNullException or ArgumentException was thrown included the parameter name that was null, using the nameof(parameter) syntax to ensure it's always kept up to date (24e79fb, 8ac9d24).
  • ArgumentOutOfRangeException: When a method parameter is being validated against a range—e.g., to determine if it is a positive number, or falls within a particular range of numbers—prefer ArgumentOutOfRangeException over the more generic ArgumentException (9069f45). As ArgumentOutOfRangeException derives from ArgumentException, this should not be a breaking change, but will provide implementors to capture a more specific exception, should they choose.
  • ITypeLookupService.Lookup(): Throw a TypeLoadException for cases where the ITypeLookupService can't find an appropriate model for a content type (3859d5e).

Topic Repositories

  • Unresolved Association: TopicRepository.Save() will now throw a ReferentialIntegrityException is any relationships or topic references cannot be resolved during the scope of the Save() operation (6f55b2a, e703de5).
  • Content Type Validation: Throw an ReferentialIntegrityException instead of a generic ArgumentException when ITopicRepository.Save() can't validate the ContentTypeDescriptor referred to by Topic.ContentType (8ac9d24). As ReferentialIntegrityException derives from TopicRepositoryException, this simplifies error handling, while still allowing a more specific exception to be handled.

Topic Mapping Service

  • TopicMappingException: Introduced the new TopicMappingException to enable improved exception handling for the topic mapping services—and, most notably, for model validation errors thrown by ReverseTopicMappingService (9b3836e).
  • InvalidTypeException: Swapped out InvalidOperationException in the mapping service implementations with a new InvalidTypeException (2d0047e, d1a17cc), which derives from the new TopicMappingException (9b3836e).
  • MappingModelValidationException: Swapped out the InvalidOperationException in the ReverseTopicMappingService (9c2baf9, 7a6b2f3), which derives from the new TopicMappingException (9b3836e).
  • Hierarchical Mapping Validation: In addition, implemented an ArgumentNullException and ArgumentOutOfRangeException when validating the HierarchicalTopicMappingService<T>'s defaultRoot parameter (911ace8).
  • Reverse Mapping Validation: Updated ReverseTopicMappingService.MapAsync() to throw InvalidOperationException instead of the less appropriate InvalidEnumArgumentException which was used before (1f49495, b4ceb75).

Improvements

General

  • Default Attribute Values: The AttributeCollection extension methods, such as GetBoolean() and GetDateTime(), no longer require that a defaultValue parameter be defined; if it is omitted, the default value for each datatype (e.g., false for GetBoolean) will be used (a79a0b1).
  • TrackedRecordCollection<>: Established new TrackedRecordCollection<> and base TrackedCollection<> to handle IsDirty tracking and enforcement of business logic in keyed collections associated with a particular topic; this is now the base class for both AttributeCollection and TopicReferenceCollection (8b9ae46).
  • TopicPropertyDispatch: Centralized the code from TrackedRecordCollection<> for enforcing the business logic by calling any corresponding properties on Topic to a new TopicPropertyDispatch class (d7b63a8).
  • .NET 5 Hosts: Updated unit tests and sample host to use .NET 5, which is the runtime we expect most implementers to utilize (87add5f).
  • Organizational Folders: Established _camelCase folders for organizing classes without incrementing their namespaces (214e1bb). E.g., the ITopicRepository event arguments are in the /OnTopic/Repositories/_eventArgs folder, but in the OnTopic.Repositories namespace (d945366).
  • MappedTopicCache: Private methods on TopicMappingService now accept a MappedTopicCache object instead of a ConcurrentDictionary<int, object>; this serves a similar purpose, but allows us to track additional information about what, specifically, has been cached (d8e3c68).

State Tracking

  • Topic.IsDirty(): Introduced Topic.IsDirty() method to aid in state tracking; optionally, this can optionally call AttributeCollection.IsDirty() and RelatedTopicCollection.IsDirty() via the checkCollections parameter (a8feb6d, f7d36f0).
  • Topic.MarkClean(): Introduced MarkClean() method to Topic (28ea6ea, 48e8ed4) and RelatedTopicCollection (c37934b).
  • ITrackDirtyKeys: Introduced a new ITrackDirtyKeys interface to standardize the implementation of IsDirty() and MarkClean() between Topic (f7d36f0), Topic.Attributes, Topic.Relationships, and Topic.References (c757dd3).

SQL

  • Update(Extended)Attributes Stored Procedure: Moved logic from the UpdateTopic stored procedure into the new UpdateAttributes (634857b) and UpdateExtendedAttributes (3664b2b) stored procedures (08a57db).
  • SqlDataReader Unit Tests: While we're still not able to easily unit test the SqlTopicRepository, we now have unit tests for the SqlDataReader extension methods, which handle much of the logic for Load(), Rollback(), and Refresh(), as well as most of the complex logic outside of the Save() method (b91c292).

It's not entirely clear why this was working previously, as several unit tests worked with mapping derived types. Regardless, this is an easy and obvious fix that ensures that derived types can be set to properties of their parent type.
Introduced a new `TrackedCollection<>` type (e77d6e9) which centralizes the logic for both `AttributeValueCollection` (beb3d1f) and `TopicReferenceCollection` (previously `TopicReferenceDictionary`) (74d5907). This allows the two classes to share the following features:
- Lookup of actual values by `key` via `GetValue()`, with inheritance from `BaseTopic` and, optionally, `Parent`.
- Tracking of state via `IsDirty` and `Version` on a unified `TrackedItem<T>` metadata container.
- Enforcement of business logic by calling appropriate properties on `Topic` via `SetValue()`.

This also unifies the semantics and the syntax for working with these two collections, thus acknowledging that topic references are, ultimately, just topic-valued attributes, and should thus otherwise operate the same. This changes the semantics and syntax from the initial design of `TopicReferenceDictionary` which was, instead, modeled more off of `TopicRelationshipMultiMap`. Outside of returning `Topic` types, though, `TopicReferenceCollection` has more in common with `AttributeValueCollection`.

As a result of this, the `TopicPropertyDispatcher` is now _exclusively_ used by the new `TrackedCollection<>`, which exclusively operates off of `TrackedItem<T>`. As such, I updated `TopicPropertyDispatcher` to operate off of that assumption, which thus eliminated the one-off hacks for both `AttributeValueCollection` and `TopicReferenceCollection` (11506c3). This also necessitated some bug fixes in the underlying `MemberDispatcher` to better handle generic type parameters (13d43d1, edc9f4b).

Because of the change from `IDictionary<>` to `KeyedCollection<>`, the `TopicReferenceCollection` has some breaking changes. As it was first developed for OnTopic 5.x, which hasn't yet released, this won't impact any customers. It will require updating the OnTopic Editor, however, which has been developing its 5.x release in parallel based on pre-release packages. These changes are mostly trivial, pertaining to slight differences in interface. The most notable changes, however, include:
1. The `GetTopic()` and `SetTopic()` methods have been replaced with the more generically named `GetValue()` and `SetValue()` (0544353).
2. The `TryGetValue()` (4b00642) and `Add(string, Topic)` (956c9f4) methods no longer exist, and should be replaced with `GetValue()` and `SetValue()` respectively.
3. The `Value` is now nullable, which can be an issue when iterating over the collection (e7a5520).

In exchange, the `TopicReferenceCollection` now enjoys nearly identical semantics and syntax as `AttributeValueCollection`, including some new features such as `inheritFromParent`, which it didn't previously support.
Originally, the `[Relationship()]` attribute was intended to disambiguate references to `Topic.Relationships` and `Topic.IncomingRelationships`. Almost as soon as it was conceived of, however, it was expanded to include e.g. `Topic.Children`, as well as mapped collections (i.e., strongly typed collections with compatible types), and nested topics (a specialized type of `Topic.Children`).

Technically, we can argue that all of these are types of relationships. But the name remains ambiguous and unintuitive, since it sounds specific to `Topic.Relationships`. To mitigate that, it's being renamed to `[Collection()]`, which better articulates that it's clarifying the mapping between the source collection and the target collection, independent of whether or not it's a relationship attribute.
This corresponds to the rename of the `[Relationship()]` attribute to `[Collection()]`. This also has a large downstream effect, since `PropertyConfiguration` has a `RelationshipType` property, and there are a lot of related `relationshipType` variables throughout the code base.
With the renaming of `[Relationship()]` to `[Collection()]` (f84ae66), it makes sense to rename the `PropertyConfiguration`'s `RelationshipKey` property to `CollectionKey`, since it maps to the `CollectionAttribute.Key` property.
The name `Relationships` has always been ambiguous, since it can refer both to `Topic.Relationships`, as well as other types of topic associations, such as `Topic.Parent` or `Topic.References`. To avoid that ambiguity, we're moving toward the more general name `Associations`.

Ideally, the `OnTopic.References` namespace—which provides types associated with topic associations—will be renamed to `OnTopic.Associations`. To avoid a conflict between the `Associations` namespace and an `Associations` enum, the enum is being named `AssociationTypes`. I don't love this name, but it's better than having two different labels for the same concept in the library.

This has a lot of downstream impacts, such as properties on `MappedTopicCacheEntry`, the parameters on `ITopicMappingService`, the `RelationshipMap` class, and properties on the `PropertyConfiguration` class. Those downstream impacts will be addressed in subsequent updates.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I've renamed the `MappedTopicCacheEntry`'s members to `Associations`, `GetMissingAssociations()` and `AddMissingAssociations()`. While I was at it, I made some minor cleanup to the inline documentation.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I've renamed the `ITopicMappingService`'s `MapAsync()` methods to use the parameter `associations` instead of `relationships`. This change was applied to all concrete implementations of `ITopicMappingService`, as well as their internal variable references.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I renamed the (private) `SetPropertyAsync()` method's `mapRelationshipsOnly` to `mapAssociationsOnly`.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a) as well as the previous rename of `RelationshipType` to the more specific `CollectionType` (1daf799), I renamed the (local) `GetRelationship()` method to `getCollection()` , the `relationshipKey` variable to `collectionKey`, and the `targetRelationships` variable to `targetAssociations`.
The `RelationshipMap` is an `internal` class that provides a mapping between the `Relationships` enum and the `RelationshipType` enum. Those enums have been renamed to `AssociationTypes` (191262a) and `CollectionType` (1daf799) respectively, and so the `RelationshipMap` identifier no longer makes sense. As such, it's been renamed to `AssociationMap`.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a), I renamed the `[Follow()]` attribute's parameter from `relationships` to `associations`, and its property from `Relationships` to `Associations`.
The `[Follow()]` attribute name isn't bad, but it's potentially misleading in that it isn't a recusive instruction. For this reason, it's being renamed to `[Include()]`. Not only does this better communicate that this exclusively impacts the models in the associated property, but not their associations, but it is also more consistent with the Entity Framework's nomenclature for navigation properties—which are effectively synonymous with OnTopic's associations.

While we're not generally striving to maintain feature or identifier parity with Entity Framework, it's useful to leverage familiar terminology when the concepts cleanly map, as is the case here.
To correspond with the rename of the `Relationships` enum to the more general `Associations` (191262a) as well as the rename of the `[Follow()]` attribute to `[Include()]` (7c150d8), I renamed the `CrawlRelationships` property to `IncludeAssociations`.
To correspond with the rename of the `[Follow()]` attribute to `[Include()]` (7c150d8), I reworded references to "follow" to use the "include" nomenclature instead—while being careful to only reword those usages that pertained to mapping associations.
As we've worked to disambiguate the term "relationships" from "collections" (1daf799) and "relationships" (191262a) by establishing the more general term "associations", we should update the XML Docs to maintain this nomenclature. Most of these are not references to e.g. the `AssociationTypes` enum, but rather discuss the concept in more abstract terms. By consistently using "associations" here instead of "relationships", we help reinforce the preferred vocabulary, and reduce ambiguities.

This update required care to differentiate between references to "relationship" that referred to the broader concept, and references to "relationship" that referred, specifically, to e.g. `Topic.Relationships`, as the latter should be retained.
The `RelatedTopicBindingModel` is intended to ensure that topic binding models can refer to related topics and have those relationships persisted. With the introduction of topic references, however, we've reevaluated the "relationship" nomenclature, since a related topic could be a relationship or a reference. The preferred terminology for referring to _both_ of these is "associations".

Given that, we've introduced a new `IAssociatedTopicBindingModel` interface and a new `AssociatedTopicBindingModel` implementation.
In the previous commit, we renamed `IRelatedTopicBindingModel` to `IAssociatedTopicBindingModel` to ensure that it accounted for both relationships and topic references, thus using the preferred "associations" nomenclature.

As `IRelatedTopicBindingModel` hadn't been marked as deprecated, this could caused confusion. To help mitigate that, the `IRelatedTopicBindingModel` is being reintroduced as a deprecated so that consumers will be giving instructions on how to migrate their implementations.

These will be removed with OnTopic 5.1.0.
The introduction of topic references has caused confusion and ambiguity in the naming convention for relationships. Previously, the only relationships to other topics we had were `Topic.Relationships`. But, now, we also have `Topic.References` as well. Referring to both of these as "relationships" is thus confusing since it _could_ refer to `Topic.Relationships` specifically, but could _also_ refer to `Topic.References`.

An early attempt to mitigate this had resulted in the `OnTopic.References` nomenclature. That's no better, as it _could_ refer to `Topic.References` specifically, but could _also_ refer to `Topic.Relationships`.

To avoid confusion, those are now being referred to collectively as topic _associations_. We've already updated e.g. the `Relationships` enum to `AssociationTypes` (191262a), along with a number of related changes. We're now renaming `OnTopic.References` to `OnTopic.Associations`.
Previously, the `TopicRepository`'s base `Delete()` class accounted for `Topic.Relationships` by removing any outgoing or incoming relationships that would be affected by the operation, thus ensuring that no orphaned topic references were persisted in memory from other parts of the topic graph. This helps ensure there aren't any legacy references maintained in the cache after a delete.

While this accounted for `Topic.Relationships`, however, it didn't account for the new `Topic.References`. This is corrected in this update.
Previously, `GetUnmatchedAttributes()` ignored `AttributeDescriptor`s on the source `ContentTypeDescriptor` with a `ModelType` of `Relationship` or `NestedTopic` since those aren't actually stored as attributes. This failed to include `Reference` types, however.

As a result, this meant that any topics with a topic reference attribute would always pass those topic references as null attributes to ensure they were deleted. Technically, this doesn't hurt anything—but it's also entirely unnecessary.

To mitigate this, the `ModelType.Reference` is included as part of this condition.

This breaks a unit test. This isn't because the underlying functionality stopped working, but because the content type being evaluated—`ContentTypeDescriptor`—only had one actual attribute—`Title`—and that was deliberately being set in the unit test. Previously, this failed to be recognized, however, because `ContentTypeDescriptor` had a `BaseTopic` reference. With the exclusion of `ModelType.Reference`, that was no longer being returned. This is easily fixed by updating the unit test to evaluate a `Page` content type instead, which has additional attributes defined.
Updated documentation to refer to e.g. `[Include(AssociationTypes)]` and `[Collection(CollectionType)]` instead of the legacy `[Follow(Relationships)]` and `[Relationships(RelationshipType)]`.
Previously, we've used the term "Relationships" to describe both `Topic.Relationships` as well as any collection of topics on either a `Topic` or a `TopicViewModel`—which could include children, nested topics, or relationships (obviously). This became even more confused when `Topic.References` were introduced, and the `OnTopic.References` namespace was established to refer to classes associated with both `Topic.References` (as expected) _and_ `Topic.Relationships`.

To mitigate that, I've renamed a number of classes, members, and arguments to better distinguish between these concepts. Most significantly, the term "associations" has been established in order to refer collectively to `Topic.References`, `Topic.Relationships`, `Topic.Parent`, or any other scenario where a `Topic` or `TopicViewModel` refers to other topics in the topic graph. Further, where these associations are more interested in their behavior as a _collection_ instead of as an _association_, the references have been updated to use the term "collection".

In addition to renaming classes, I've also updated the documentation to adhere to these concepts, thus helping establish a more uniform vocabulary around how we discuss these ideas. Arguably, terms like "references" and "relationships" and "associations" remain ambiguous, but by using them in a consistent way we help reaffirm their meaning in context of OnTopic. E.g., "references" are 1:1 associations, "relationships" are 1:n associations, and "associations" are any link between topics in the topic graph.

This includes renaming the following classes and enums:
- `[Relationship(RelationshipType)]` to `[Collection[CollectionType]]` (f84ae66, 1daf799).
- `Relationships` enum to `AssociationTypes` enum (191262a).
- `RelationshipMap` to `AssociationMap` (f96f7d6).
- `[Follow(Relationships)]` to `[Include(AssociationTypes)]` (7c150d8, 78600fd).
- `IRelatedTopicBindingModel` and `RelatedTopicBindingModel` to `IAssociatedTopicBindingModel` and `AssociatedTopicBindingModel` (7435889, cf7badf).
- `OnTopic.References` namespace to `OnTopic.Associations` (213a46b)

- `PropertyConfiguration`'s `RelationshipKey' and `RelationshipType` to `CollectionKey` and `CollectionType` (44dc3eb).
- `PropertyConfigurations`'s `CrawlRelationships` to `IncludeAssociations` (c16c0a8).
- `MappedTopicCacheEntry`'s `Relationships`, `GetMissingRelationships()`, and `AddMissingRelationships()` to `Associations`, `GetMissingAssociations()`, and `AddMissingAssociations()' (a993214).
- `ITopicMappingService`'s `MapAsync()` overload's `relationships` parameter to `associations` (2fd7470).

In addition, a lot of the XML documentation and inline comments were updated to adhere to the "include" (acb64e9) and "associations" (0e32098).

While I was at it, I also updated `TopicRepository`'s `Delete()` (f77cf59) and `GetUnmatchedAttributes()` (80c9017) to fully account for `Topic.References`.
A `Topic` instance that hasn't been saved can _only_ be dirty, by definition. As such, any efforts to mark a collection associated with a new `Topic` as clean, or to modify the collection with an `!markDirty`  parameter should not be honored.

There's no need to check this condition from `IsDirty`; instead, we can just make sure it's checked during any entry points, such as e.g. `MarkClean()`, `SetValue()` or `SetTopic()`, `InsertItem()`, and/or `SetItem()`.
With the previous update, I prevented attributes, references, and relationships from being marked as clean if the topic was new, since a new topic is, by definition, dirty. (The one exception to this being the removal of an item.) This means that the unit tests for evaluating `IsDirty` needed to be updated to be "saved" entities—i.e., topics with an `Id`.
Previously, `Topic` implemented an incredibly basic `IsDirty()` tracking via a single `_isDirty` boolean field. This has been improved to use the `DirtyKeyCollection`, thus allowing it to more intelligently track _which_ fields are dirty, if any. This will allow for more granular checking of dirty status in future commits, if needed.
To help enforce consistency between classes, the `ITrackDirtyKeys` interface has been applied to `Topic`, thus ensuring it can be interacted with in the same ways as other classes that provide tracking of dirty keys, such as `TopicReferenceCollection`, `AttributeValueCollection`, and `TopicRelationshipMultiMap`.

This required some juggling of the overloads to ensure that the contract was adhered to while still supporting extended scenarios not covered by the interface.
As with the collections (85c386f), a `Topic` that `IsNew` is, by definition, dirty—and, therefore, should not be permitted to be marked clean via e.g. `MarkClean()`.

Technically, there isn't any need to check `IsNew` on `IsDirty()`, since the topic cannot be `MarkClean()'ed if it `IsNew`. That said, this is a bit faster than doing a lookup against the `DirtyKeyCollection` if it's guaranteed to be dirty.

Note that this shortcut _only_ applies to `Topic`, and not its collections, since a new `Topic` _will_ have dirty attributes, but a `Topic`'s collections may be empty—and, thus, clean.
Previously, there was no tracking of `IsDirty` for resetting the `Parent`. This was intentional, since changes to the parent are handled separated from changes to other properties—i.e., via `ITopicRepository.Move()`, not `ITopicRepository.Save()`—and, as such, we didn't want _moving_ a `Topic` to mark it as needing to be _saved_.

This prevented `Save()`, however, from detecting if a `Topic` needed to _also_ be `Move()`d as part of the operation, a feature we've long supported. This is, admittedly, a rare use case, but can come in handy when programmatically working with the topic graph—e.g., when making multiple changes, then doing a recursive `Save()`.

The actual implementation of this tracking will be implemented in a subsequent commit.
Since `Key` is now defined on both `ITopicBindingModel` and the new `IKeyedTopicViewModel` (565273f), and there's no other "pollution" from any otherwise unnecessary interface requirements on `IKeyedTopicViewModel`, we can derive `ITopicBindingModel` from `IKeyedTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces.
Since `Key` is now defined on both `ITopicBindingModel` and the new `IKeyedTopicViewModel` (565273f), and there's no other "pollution" from any otherwise unnecessary interface requirements on `IKeyedTopicViewModel`, we can derive `ITopicBindingModel` from `IKeyedTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces.
Since `UniqueKey` is defined on both `ITopicBindingModel` and `IAssociatedTopicBindingModel`, and there's no other "pollution" from any otherwise unnecessary interface requirements on `IAssociatedTopicBindingModel`, we can derive `ITopicBindingModel` from `IAssociatedTopicBindingModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. This can also be applied to `TopicViewModel`.
Since `ContentType` is defined on both `ITopicBindingModel` and `ITopicBindingModel`, and there's no other "pollution" from any otherwise unnecessary interface requirements on `ITopicBindingModel`, we can derive `ITopicBindingModel` from `ITopicBindingModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces. This can also be applied to `TopicViewModel`.
In binding models, the types used for topic references must implement `ITopicBindingModel` and the types used for relationships must implement `IAssociatedTopicBindingModel`. If they don't, an exception is thrown.

Previously, this was validated via unit tests that used `TopicViewModel` as the type for each of those, thus failing validation. We now derive `ITopicViewModel` from `ITopicBindingModel` (ffe775e) and `IAssociatedTopicBindingModel` (8ad42b9), however, and thus that actually satisfies the condition. That makes view models more flexible by allowing them to double as binding models. But it breaks our unit tests.

To fix this, I've introduced a new `EmptyViewModel` which implements no interfaces, and used it as the return type for the `InvalidreferenceTypeTopicBindingModel` as well as the `ContentTypes` collection type on the `InvalidRelationshipBaseTypeTopicBindingModel`. This effectively satisfies the expectations of those unit tests, and correctly returns them to throwing the expected exception.
These were already defined on `INavigationTopicViewModel<T>`, but that _also_ implemented `IHierarchicalTopicViewModel<T>`.

Sometimes, there are topics that should be evaluated for navigation, but which _aren't_ hierarchical, and which _don't_ necessitate the use of e.g. the `IHierarchicalTopicMappingService` or `NavigationViewComponentBase<T>`. For example, offering a list of child topics on a page. In this case, these _may_ implement `IHierarchicalTopicViewModel<T>`, but they only _need_ `Title`, `ShortTitle`, and `WebPath`.

To support this, the `INavigableTopicViewModel` extracts these from the `INavigationTopicViewModel<T>` so they can, optionally, be applied independently. Then, the `INavigationTopicViewModel<T>` effectively becomes a composite of `INavigableTopicViewModel` and `IHierarchicalTopicViewModel<T>`, adding only `IsSelected()`.

This is useful since the view models for the `NavigationTopicViewComponent<T>` are likely going to be independent from other view models, since most view models don't want or need a `Children` collection. But many basic view models will satisfy the `INavigableTopicViewModel` interface, and should be usable for that purpose. This allows different view models types from different chains (e.g., `OnTopic.ViewModels` and a separate customer implementation) to be handled by e.g. the same partial control.
Since `Title`, `ShortTitle`, and `WebPath` are now defined on both `IPageTopicViewModel` and the new `INavigableTopicViewModel` (adf7ff8), and there's no other "pollution" from any otherwise unnecessary interface requirements on `INavigableTopicViewModel`, we can derive `IPageTopicBindingModel` from `INavigableTopicViewModel`, thus allowing them to share this definition, while also allowing implementations to satisfy both interfaces.
In addition to cluttering an already overpopulated interface, the `IsHidden` property doesn't make much in the current version of OnTopic, as hidden view models are explicitly excluded from the the `TopicMappingService`.

The one exception to this is the top-level topic—i.e., the one sent directly to the `ITopicMappingService`, as opposed to referenced from its properties. But in those cases, we expect callers, such as `TopicController`, to explicitly determine if `IsHidden is appropriate or not. Given that, there isn't much benefit to exposing `IsHidden` to the view model—either the interface or the implementation.

As part of this, I marked the interface as obsolete, and marked the implementation as both obsolete and disabled mapping of the property. In addition, I removed references to it from the unit tests. These weren't strictly necessary, and can be safely removed while still satisfying the basic criteria of the unit tests.
There's no expected use case for the `IPageTopicViewModel`. We foresee the limited but potential need for e.g. shared view models that will handle navigation, but these can be handled via `INavigableTopicViewModel` and `INavigationTopicViewModel<T>`. Others _may_ need additional details, which are available via `ITopicViewModel`. But we don't expect reusable _layouts_, which is where the additional requirements of e.g. `IPageTopicViewModel` comes in.

On an assessment of OnTopic 4.x implementations, it doesn't appear anyone is utilizing `IPageTopicViewModel`, except possibly on model definitions, so it should be safe to remove. Marking it as obsolete.
In a typical use case, all properties will be set to their defaults when a view model is initialized. Given this, the return type of otherwise required properties must be nullable. The mapping service will then immediately populate these, however, and thus we never expect them to _actually_ be null. As such, we can annotate them with the `[NotNull]` attribute to assure callers that, in practice, they're not null, and the `[DisallowNull]` attribute to dissuade callers from setting them to null.

In addition, there are no scenarios where we expect most of these values to even have empty values. As such, we can mark them as `[Required]` to help enforce their use. This is critical for binding models, where we want to ensure that the interface isn't simply satisfied, but also that data is populated. For instance, a `ITopicBindingModel` without `Key` or `ContentType` values defined isn't especially useful.

Given that, most required properties have been annotated as `[Required, NotNull, DisallowNull]`.
This is just an organizational change, and has no functional impact.
This is similar to the updates made previously to core properties (1bf25be). `VideoUrl` is a core property of the `VideoTopicViewModel`, and always expected to have a value. If it doesn't, a validation error should be thrown.
This will allow e.g. `TopicViewModelCollection<T>` to filter by content type, which is a common feature of topic (view model) collections.
With the introduction of `ContentType` (9bc0d00), the new name is more fitting. In addition, this is more consistent with our familiar phrasing of "core attributes" to refer to `Id`, `Key`, `ContentType`, and `Parent`. (Though we don't need `Id` or `Parent` here, as they're rarely needed in view models.)
This greatly reduces the interface requirements of the `TItem` generic type argument. In practice, view models will likely have more properties—but they're not needed as far as `TopicViewModelCollection` is concerned.
Reevaluated the view model interfaces.

- Introduced new `ICoreTopicViewModel` interface with `Key` and `ContentType` (565273f, 527dee4, eef391c)
- Introduced new `INavigableTopicViewModel` interface with `Title`, `ShorTitle`, and `WebPath` (adf7ff8)
- Cross-applied interfaces that satisfied subsets of other interfaces; this includes applying:
  - `ICoreTopicViewModel` to `ITopicBindingModel` (3b5dda8)
  - `IAssociatedTopicBindingModel` to `ITopicViewModel` (8ad42b9)
  - `ITopicBindingModel` to `ITopicViewModel` (ffe775e)
  - `INavigableTopicViewModel` to `INavigationTopicViewModel<T>` (adf7ff8)
  - `INaviableTopicViewModel` to `IPageTopicViewModel` (c41d989)
- Removed `IsHidden` from `ITopicViewModel`, `TopicViewModel`; hidden models are excluded by the `TopicMappingService`, so this doesn't provide value (68a4e83).
- Marked the `IPageTopicViewModel` as obsolete; narrow applications are expected to use the `InavigableTopicViewModel` instead (745e562).
- Marked core properties (e.g., `Key`, `ContentType`, &c.) as non-nullable and required to reflect the fact that these should always be present (1bf25be, bfa64f1).
- Updated `TopicViewModelCollection` to use the much narrower (new) `ICoreTopicViewModel`, thus reducing the interface requirements of its `TItem` (eef391c).
This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version (though the latter is admittedly unlikely in this case).
This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version (though the latter is admittedly unlikely in this case).
This better articulates the class defined by the file, and helps avoid naming conflicts should we ever introduce a non-generic version.
This should have been made prior to the previous merge of `improvement/model-interfaces` (2f98c39). Apologies, future readers, for the clumsy git history.
Ensured that the file names reflected the generic type parameters, where appropriate. This was typically used, but not universally. It's a best practice for avoiding ambiguity, and clearly reflecting the class contained within a file.
@JeremyCaney JeremyCaney self-assigned this Mar 2, 2021
NuGet displays line breaks and white space, instead of compressing it. As such, the `<Description />` needs to be implemented on a single line.
Fixed display of description for NuGet package by ensuring it's all contained on one line, without any line breaks or extraneous spaces due to indentation.
This only affects the unit tests, and has no downstream impact on implementers. Ensured all unit tests continue to operate correctly.
Updated to latest version of Microsoft's Test SDK and Framework. Reran unit tests to confirm everything continues to operate correctly. As this only impacts the unit test projects, this has no impact on consumers of the NuGet packages.
@JeremyCaney JeremyCaney merged commit c27e6a2 into master Mar 3, 2021
@JeremyCaney JeremyCaney added this to the OnTopic 5.0.0 milestone Mar 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant