Merged
Conversation
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 `[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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
OnTopic.Allmetapackage which includes a reference to all of the core packages and implementations (c15bf2e).SourceLinkwith references to GitHub commits so that packages can be properly debugged by implementers (f7fb979).ITypeLookupService.Lookup()(ab4253d).Topic.References, with full support for enforcing business logic (31144cc).RelationshipsandTopicReferences, in addition to the previous support for versioning inAttributesandExtendedAttributes(0e808ea).ITopicRepository.Refresh()interface (3d51b14),SqlTopicRepository.Refresh()implementation (6dbf973), as well as aGetTopicUpdatesstored procedure (0f36947) to allow occasional cache updates without needing to reload the entire topic graph (164de05).ITopicRepository.Load(), an optionalreferenceTopiccan 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.TopicRepository.Save()will now recurse overChildrenand attempt to resolve unresolved associations, such as relationships and topic references (fdd0025, e703de5); previously, this had to be implemented by each individual implementation.ITopicRepositorynow supports theTopicLoadedandTopicSavedevents, in addition to the previousTopicRenamed,TopicMovedandTopicDeletedevents (37b38f5).initsetters 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, immutablerecordtypes (10d0652).TopicKey,ContentType, andParentIDare now first-class columns in theTopicstable (c50f44d), greatly simplifying querying for the core attributes.SqlDataReaderextension methods used bySqlTopicRepository(b91c292), thus offering far more assurances over the correctness and regression testing.General
ITypeLookupServicenow accepts multiple type names to itsLookup()method, thus allowing callers to specify and control what alternatives or defaults they accept, instead of relying on theITypeLookupServiceimplementations to handle this (ab4253d). This offers more transparency, predictability, and control to the callers, and actually reduced repetitive code in many cases.TopicCollectionandReadOnlyTopicCollectiontypes for composite collections—such as relationships—which do not need to be unique by key; replaced instances ofCollection<Topic>,ReadOnlyCollection<Topic>, and some instances ofIEnumerable<Topic>with these new types (826b93c).TopicIndexcollection (which indexes a list of topics byTopic.Id) (3af4c78) as well as aTopic.GetTopicIndex()extension method for generating aTopicIndexfor a given topic and all of its descendants (3777b6b).ICoreTopicViewModel(565273f, 3b5dda8, 5ea98c9, 9bc0d00, 527dee4) andINavigableTopicViewModel(adf7ff8, c41d989) interfaces (2f98c39).NuGet Packages
OnTopic.Allmetapackage which includes a reference to all of the core packages and implementations (c15bf2e).SourceLinkwith references to GitHub commits so that packages can be properly debugged by implementers (f7fb979).Topic Associations
[dbo].[TopicReferences]table and theTopic.Referencesproperty (31144cc), including versioning and enforcement of business logic (3144235).RelationshipsandTopicReferences, including the newRelationshipIndexandReferencesIndexviews (0e808ea).IsFullyLoadedproperty to bothTopic.RelationshipsandTopic.Referencesto track scenarios where target topics could not be mapped within the scoped topic graph duringITopicRepository.Load(); in these scenarios, the unmatched items won't be deleted from the collection onITopicRepository.Save()in order to avoid potential data loss (4885958).TopicRelationshipMultiMapclass (57ff3f9, 7129289) to replace the legacyRelatedTopicMultiMap. 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
ITopicRepository.Refresh()interface (3d51b14),SqlTopicRepository.Refresh()implementation (6dbf973), as well as aGetTopicUpdatesstored procedure (0f36947) to allow occasional cache updates without needing to reload the entire topic graph (164de05).ITopicRepository.Load(), an optionalreferenceTopiccan 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.CachedTopicRepositoryso that theDeleteEvent,MoveEvent, orRenameEventdelegates from the underlyingITopicRepisitoryinstance will notify any subscribers of theCachedTopicRepository's (c1ea208). Without this, these events weren't terribly useful in most real-world scenarios (which will almost always use theCachedTopicRepisitorydecorator).ObservableTopicRepositorybase class for handling event logic of theITopicRepository; it is expected that, at minimum, otherITopicRepositoryimplementations will derive from this (fb663a2, e703de5).TopicRepositoryDecoratorbase class for handling decorator logic, as used in e.g.CachedTopicRepository; this makes it easier to implement new decorators, including event bubbling (799e2b6, e703de5).version, recursing overChildren, and attempting to resolve unresolved relationships and topic references intoTopicRepository.Save()(fdd0025, e703de5); previously, these had been handled bySqlTopicRepository.TopicLoadedandTopicSavedevents, with correspondingTopicLoadEventArgsandTopicSaveEventArgs, to theITopicRepository(37b38f5).protectedraise event methods toObservableTopicRepository, such asOnTopicLoaded(),OnTopicSaved(), andOnTopicDeleted()(37b38f5).Topic Mapping Service
initsetters 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, immutablerecordtypes (10d0652).TopicMappingServiceto fill in any missing associations on a view model that it previously mapped and cached the results of (5cf13bb).[FilterByContentType()]attribute for use by theITopicMappingServiceas a more specialized version of the original[FilterByAttribute()]attribute (105cfdd).TopicMappingService; previously, this was only supported on non-collections, or collections that directly implemented a generic type (560aaca).ASP.NET Core
SitemapControllercan now be customized by adjusting the staticExcludedContentTypes,SkippedContentTypes, andExcludedAttributescollections (a9f04ab).Topic.Referencesare now exposed to theSitemapController'sExtended()action (dcd1b4c).SQL
TopicKey,ContentType, andParentIDto theTopicstable (c50f44d), and@Keyand@ContentTypeto theCreateTopicandUpdateTopicstored procedures (a74389d)CreateTopicstored procedure to further protect the integrity of the nested set during concurrent operations (282f8a9).DATETIME2(7)data type forVersioncolumns in order to increase the precision to match the .NET CLR'sDateTimetype (5d0ea80).Bug Fixes
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.AttributeCollection.Clear()correctly marked the collection asIsDirty(), if appropriate, by accounting anyDeletedAttributes(ce3cc43).AttributeRecordinstances in aTopicRepositoryBase.Rollback()operation were correctly marked asIsDirty, thus ensuring they would be properlySave()d (e77d8eb).Topic.Relationshipscollection now allows relationships to different topics with the sameTopic.Key(66ccd9d). Previously, this used aKeyedTopicCollection, which artificially restricted it to relationships with a uniqueKey.FindAll(): Fixed a bug in theTopic.FindAll()extension methods which prevented it from returning multiple topics which the sameKey, even if they represented different entities (75f6369).GetTopicVersionstored procedure which resulted in theVersioncolumn not being returned forAttributesorExtendedAttributes, and thus theAttributeRecord.LastModifiednot being properly updated during e.g. aRollback()(622d575). In practice, this didn't harm anything, but it prevented removal of legacy schema validation which explicitly looked for theVersioncolumn.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.CreateTopicorMoveTopicstored procedures with anull@ParentIDwould 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.RAISERROR()calls in theMoveTopicstored 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
Topic.DerivedTopic: RenamedTopic.DerivedTopicto the more semantically accurateTopic.BaseTopic(2486ab2). This breaks not only calls toTopic.DerivedTopic, but also database records for e.g.ReferenceKeys storing theDerivedTopicvalue. The OnTopic 5.x database migration script addresses the data aspects of the migrations.recordtypes (10d0652). This necessitates that all adopters that rely on these migrate any derived types from classes to records.TopicCollectionclasses (such asTopicCollection<T>,ReadOnlyTopicCollection) toKeyedTopicCollection(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).AttributeValueCollectiontoAttributeCollection, andAttributeValuetoAttributeRecord(f51407a).TopicRelationshipMultiMap: TheRelatedTopicCollectionandNamedTopicCollectionused byTopic.Relationshipshave been replaced withTopicRelationshipMultiMap(57ff3f9, 7129289),ReadOnlyTopicMultiMap(eb5c1c3), andTopicMultiMap(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 throughSetValue(),Remove(), orClear(). In addition, for callers iterating over the relationships, the relationship key is nowKeynotName, the topics are now stored under aValuesproperty, and the methods now require a fullTopicreference, instead of just aTopic.Key(0d9dcd5).GetValue()Syntax: Renamed(ReadOnly)KeyedTopicCollection<T>.GetTopic()toGetValue()(6674c7d); onTopicRelationshipMultiMap(i.e.,Topic.Relationships), renamedGetAllTopics(),GetTopics(),RemoveTopic(), andClearTopics()toGetAllValues(),GetValues(),Remove(), andClear()for consistency with other collections, and especially theTrackedRecordCollectionused byAttributeCollectionandTopicRelationshipCollection(34a8c52).OnTopic.Lookupnamespace: Consolidated allITypeLookupServiceimplementations into a newOnTopic.Lookupnamespace (f80e084).Load()by Unique Key: ModifiedLoad(topicKey)to instead expect a fully-qualifiedUniqueKey(e.g.,Root:Web:Products); as part of that, renamed thetopicKeyparameter touniqueKeyto disambiguate usage (4c3d30f).[Relationship()]to[Collection()](f84ae66),[Follow()]to[Include()](7c150d8),RelationshipTypetoCollectionType(1daf799), andRelationshipstoAssociationTypes(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 asTopic.Relationships,Topic.References, orTopic.BaseTopic(b01216e).[FilterByAttribute()]attribute continues to operate as it previously did, but can no longer be used to filter byContentTypesince content type is no longer stored inTopic.Attributes(4a9f5b7); instead, use the new[FilterByContentType()]attribute (105cfdd). IfContentTypeis used with[FilterByAttribute()], anArgumentExceptionwill be thrown (c68040a).IRouteBuilderSupport: Removed (deprecated) support forIRouteBuilderextension methods in ASP.NET Core; implementers should use end point routing viaIEndpointRouteBuilderinstead (6be993b, 19612ac).NavigationViewModel: ReplacedCurrentKeywithCurrentWebPath; updated theINavigationTopicViewModel<T>'sIsSelected()method to expect awebPathparameter instead of auniqueKeyparameter (3e68b16).@Keyand@ContentTypeto theCreateTopicandUpdateTopicstored procedures (a74389d); removedTopicKey,ContentType, andParentIDfrom theAttributestable; they are now in theTopicstable (c50f44d).General
Topic.DerivedTopic: RenamedTopic.DerivedTopicto the more semantically accurateTopic.BaseTopic(2486ab2). This breaks not only calls toTopic.DerivedTopic, but also database records for e.g.ReferenceKeys storing theDerivedTopicvalue. The OnTopic 5.x database migration script addresses the data aspects of the migrations.recordtypes (10d0652). This necessitates that all adopters that rely on these migrate any derived types from classes to records.TopicCollectionclasses (such asTopicCollection<T>,ReadOnlyTopicCollection) toKeyedTopicCollection(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).AttributeValueCollectiontoAttributeCollection, andAttributeValuetoAttributeRecord(f51407a).GetValue()Syntax: Renamed(ReadOnly)KeyedTopicCollection<T>.GetTopic()toGetValue()(6674c7d); onTopicRelationshipMultiMap(i.e.,Topic.Relationships), renamedGetAllTopics(),GetTopics(),RemoveTopic(), andClearTopics()toGetAllValues(),GetValues(),Remove(), andClear()for consistency with other collections, and especially theTrackedRecordCollectionused byAttributeCollectionandTopicRelationshipCollection(34a8c52).IPageTopicViewModelinterface, and removedIsHiddenfrom theITopicViewModelinterface (2f98c39), and marked core properties as[Required](1bf25be).OnTopic.Lookupnamespace: Consolidated allITypeLookupServiceimplementations into a newOnTopic.Lookupnamespace (f80e084).ITypeLookupService.Lookup(): TheITypeLookupService'sLookup()call now accepts aparams string[] typeNamesargument, instead ofstring typeName(ab4253d). Implementations which overrideLookup()will need to update their implementation.AttributeRecord(previouslyAttributeValue) is now a read-only, immutable record; previously writable properties such asIsDirtyandLastModifiedcan no longer be modified without making a copy of the object. Prefer usingAttributeCollection.SetValue()over working directly withAttributeRecordinstances (bba59bb).TopicFactory.Create()was updated to match the parameter order of theTopicconstructor (69caaa2). This change isn't expected to impact most implementations, as it only affects calls that specify anid—which is typically only done in test code or concreteITopicRepositoryimplementations.Topic.Description: TheTopic.Descriptionproperty is now obsolete (6c3a2e9, bbfd9ff). Instead, either useGetValue()orSetValue()onAttributeCollection(viaTopic.Attributes) or, better yet, use theITopicMappingServiceto map view models with aDescriptionproperty.ReadOnlyTopicCollection<T>FromList(): TheReadOnlyTopicCollection<T>.FromList()factory method is now obsolete (b013e38); use theReadOnlyTopicCollection(IList<T> innerCollection)overload instead, which does the same thing without creating a redundant instance of the object.List<T>Return Types: Changed publicList<T>collections toCollection<T>(86fd5c4); notably, this includesTopic.VersionHistoryandTypeMemberInfoCollection.SettableTypes.AttributeTypeDescriptor: EliminatedAttributeTypeDescriptor; since we no longer support OnTopic-WebForms, and its complexEditorTypeandModelTypelogic, we can merge the simplifiedAttributeTypeDescriptorintoAttributeDescriptor, and use that directly as the base class for each of attribute type descriptors (7b00274).AttributeCollectiontoOnTopic.Attributes;MappedTopicCachetoOnTopic.Internal.Mapping; andTopicRelationshipMultiMap, andTopicReferenceCollectionto the newOnTopic.Associationsnamespaces (7e99f41, 213a46b).OnTopic.Internal.Reflectionclasses—most notably,TypeMemberInfoCollection—asinternal, so they're no longer accessibly publicly (a38d619).SetValue(…, isDirty)parameter: Renamed theisDirtyparameter onSetValue()methods tomarkDirtyacross the code base (a6d1801). This helps avoid some confusion when comparing the value to localisDirtyor_isDirtyvalues, which vary slightly in their semantic.protected overridemembers assealedto prevent unintended further modification of the business logic (46cc506). This particularly applied toKeyedCollectionimplementations.Topic Associations
TopicRelationshipMultiMap: TheRelatedTopicCollectionandNamedTopicCollectionused byTopic.Relationshipshave been replaced withTopicRelationshipMultiMap(57ff3f9, 7129289),ReadOnlyTopicMultiMap(eb5c1c3), andTopicMultiMap(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 throughSetValue(),Remove(), orClear(). In addition, for callers iterating over the relationships, the relationship key is nowKeynotName, the topics are now stored under aValuesproperty, and the methods now require a fullTopicreference, instead of just aTopic.Key(0d9dcd5).SetValue(…, isIncoming): MovedisIncomingoverloads on theTopicRelationshipMultiMap(previouslyRelatedTopicCollection) tointernalsince they are intended for managing the internal integrity of the topic graph (5e40470, c780f4e). As part of this, exposed the optionalisDirtyparameter to the publicSetTopic()method.KeyValuesPair<T>.IsDirty: ConvertedIsDirtyproperty on theKeyValuesPair<T>(previouslyNamedTopicCollection) to a method for consistency with other collections (7a9bbaf).Topic Repositories
SqlClientDependency:SqlTopicRepositorynow requires, at minimum,SqlClient2.0.0 or above.Load()by Unique Key: ModifiedLoad(topicKey)to instead expect a fully-qualifiedUniqueKey(e.g.,Root:Web:Products); as part of that, renamed thetopicKeyparameter touniqueKeyto disambiguate usage (4c3d30f).ITopicRepositoryevents from e.g.MoveEventtoTopicMoved, and the arguments from e.g.MoveEventArgstoTopicMoveEventArgs(37b38f5).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.TopicRepository.GetContentTypeDescriptors(ContentTypeDescriptor)overload; this is replaced by the more appropriately namedSetContentTypeDescriptors(), since it potentially updates the content type descriptor cache (a078fc8, 100588e).ITopicRepository.Delete(…, isRecursive)tofalse, 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 totrue(1773113).Save()Return Type: Changed return type ofITopicRepository.Save()method fromint(representing theTopic.Id) tovoid(c2a8fe8).ITopicRepository.Load()methods has been updated to include a new, optionalreferenceTopicparameter. This will break calls toLoad()which included the optionalisRecursiveparameter, as the new parameter has been added before that (180526e).TopicRepositoryBase: RenamedTopicRepositoryBasetoTopicRepository(f0d9d44, e703de5); this will break any derived implementations.TopicRepository.Save(),Move(), andDelete()assealedso they cannot be derived; instead, implementers must now override the new abstract methodsSaveTopic(),MoveTopic(), andDeleteTopic()(116c8eb, e703de5); it is no longer necessary to call the base methods.Topic Mapping Service
[Relationship()]to[Collection()](f84ae66),[Follow()]to[Include()](7c150d8),RelationshipTypetoCollectionType(1daf799), andRelationshipstoAssociationTypes(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 asTopic.Relationships,Topic.References, orTopic.BaseTopic(b01216e).StaticTypeLookupService.DefaultType: TheStaticTypeLookupServiceno longer defines or accepts aDefaultType(ab4253d). Implementations which call theStaticTypeLookupServicewith adefaultTypeparameter, or callers which rely on aStaticTypeLookupServicederivative to return a default will need to be updated to pass the default through as a call toLookup()instead.[FilterByAttribute()]attribute continues to operate as it previously did, but can no longer be used to filter byContentTypesince content type is no longer stored inTopic.Attributes(4a9f5b7); instead, use the new[FilterByContentType()]attribute (105cfdd). IfContentTypeis used with[FilterByAttribute()], anArgumentExceptionwill be thrown (c68040a).AttributeKeyAttribute.Value: Renamed[AttributeKey()]'sValueproperty toKey(64120a9); as this is normally set via the constructor and accessed viaPropertyConfiguration, this won't likely break most implementations.AssociatedTopicBindingModel: MovedAssociatedTopicBindingModelto theOnTopic.ViewModelslibrary, as it is not needed by the coreOnTopiclibrary, and it will likely be needed by implementations relying on the main view model library, which is where most concrete models are stored (ac01bbf).DynamicTopicBindingModelLookupServiceto include types that implementITopicBindingModel, regardless of whether or not they end inTopicBindingModel(5c8bd06). This allows e.g.TopicViewModels to double as binding models.PropertyConfigurationandMappedTopicCachetointernal; they were already in theOnTopic.Mapping.Internalnamespace and intended for internal use, but as they're no longer leaked viaprotectedmethods inTopicMappingService, they can be made truly internal (179c664). Moved all classes from theOnTopic.Internal.Mappingnamespace to the newOnTopic.Mapping.Internalnamespace (3c4dc3f).protectedhelper functions inTopicMappingServiceandReverseTopicMappingServiceasprivate, 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
IRouteBuilderSupport: Removed (deprecated) support forIRouteBuilderextension methods in ASP.NET Core; implementers should use end point routing viaIEndpointRouteBuilderinstead (6be993b, 19612ac).NavigationViewModel: ReplacedCurrentKeywithCurrentWebPath; updated theINavigationTopicViewModel<T>'sIsSelected()method to expect awebPathparameter instead of auniqueKeyparameter (3e68b16).SitemapController'sExtended()action now includes aDataObjectnamedAttributes, instead of aDataObjectnamed after theContentType; this provides more flexibility in querying with Google Custom Search (f79f1ff).GenerateSitemap()andAddTopic()methods on theSitemapControllerare nowprivate(c669055); these were never expected to be used externally, and are not expected to cause any problems.virtualfrom most ASP.NET Controller actions—such as onSitemapController(da5e81c) andRedirectController(e77c7d5)—as well as theTopicRepository.SetContentTypeDescriptors()(d802bcd).SQL
@Keyand@ContentTypeto theCreateTopicandUpdateTopicstored procedures (a74389d); removedTopicKey,ContentType, andParentIDfrom theAttributestable; they are now in theTopicstable (c50f44d).TopicIndexview; that information can now be pulled directly fromTopicswithout the need for a complex query (a4a28d6)@DeleteRelationships: Removed the legacy@DeleteRelationshipsparameter from theUpdateTopicstored procedure (06b2cd9).GetTopicIDFunction: ReplacedGetTopicIDwith logic from the newerGetTopicIDByUniqueKey(a0f1a32, ff58a71, 10a9c0c).Topics.Stack_Top(8ba3514),Attributes.LastModified(2b981f2), andExtendedAttributes.LastModified(242593c) columns (b5b4829).Exceptions
General
Topic.AttributesorTopic.References, the original exception thrown by the property will now bubble up, instead of being wrapped in aTargetInvocationException(bf7782c).Contract.Requires(bool)andContract.Assumes(bool)to throw anInvalidOperationExceptioninstead of a genericException(7548771, 7e6cb4c, db32581). SinceInvalidOperationExceptionderives fromExceptionthis 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 whatExceptionshould be thrown.Topic.Id: ThrowInvalidOperationExceptionwhen attempting to change aTopic.Id, instead of a genericArgumentException(25bdeb4, 2f75e8b).Topic.Relationships.SetValue(): Updated theTopicRelationshipMultiMap.SetValue()method (previouslyRelatedTopicCollection.SetTopic()) to throw anInvalidOperationExceptioninstead of anArgumentNullExceptionfor scenarios whereisIncomingis set on aTopicRelationshipMultiMapthat isn't set to_isIncoming(9391a5d). This should never occur, as this is aninternaloverload and managed by the OnTopic library; if it does, however, it suggests a design flaw in the code.ArgumentNullExceptionorArgumentExceptionwas thrown included the parameter name that was null, using thenameof(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—preferArgumentOutOfRangeExceptionover the more genericArgumentException(9069f45). AsArgumentOutOfRangeExceptionderives fromArgumentException, this should not be a breaking change, but will provide implementors to capture a more specific exception, should they choose.ITypeLookupService.Lookup(): Throw aTypeLoadExceptionfor cases where theITypeLookupServicecan't find an appropriate model for a content type (3859d5e).Topic Repositories
TopicRepository.Save()will now throw aReferentialIntegrityExceptionis any relationships or topic references cannot be resolved during the scope of theSave()operation (6f55b2a, e703de5).ReferentialIntegrityExceptioninstead of a genericArgumentExceptionwhenITopicRepository.Save()can't validate theContentTypeDescriptorreferred to byTopic.ContentType(8ac9d24). AsReferentialIntegrityExceptionderives fromTopicRepositoryException, this simplifies error handling, while still allowing a more specific exception to be handled.Topic Mapping Service
TopicMappingException: Introduced the newTopicMappingExceptionto enable improved exception handling for the topic mapping services—and, most notably, for model validation errors thrown byReverseTopicMappingService(9b3836e).InvalidTypeException: Swapped outInvalidOperationExceptionin the mapping service implementations with a newInvalidTypeException(2d0047e, d1a17cc), which derives from the newTopicMappingException(9b3836e).MappingModelValidationException: Swapped out theInvalidOperationExceptionin theReverseTopicMappingService(9c2baf9, 7a6b2f3), which derives from the newTopicMappingException(9b3836e).ArgumentNullExceptionandArgumentOutOfRangeExceptionwhen validating theHierarchicalTopicMappingService<T>'sdefaultRootparameter (911ace8).ReverseTopicMappingService.MapAsync()to throwInvalidOperationExceptioninstead of the less appropriateInvalidEnumArgumentExceptionwhich was used before (1f49495, b4ceb75).Improvements
General
AttributeCollectionextension methods, such asGetBoolean()andGetDateTime(), no longer require that adefaultValueparameter be defined; if it is omitted, the default value for each datatype (e.g.,falseforGetBoolean) will be used (a79a0b1).TrackedRecordCollection<>: Established newTrackedRecordCollection<>and baseTrackedCollection<>to handleIsDirtytracking and enforcement of business logic in keyed collections associated with a particular topic; this is now the base class for bothAttributeCollectionandTopicReferenceCollection(8b9ae46).TopicPropertyDispatch: Centralized the code fromTrackedRecordCollection<>for enforcing the business logic by calling any corresponding properties onTopicto a newTopicPropertyDispatchclass (d7b63a8)._camelCasefolders for organizing classes without incrementing their namespaces (214e1bb). E.g., theITopicRepositoryevent arguments are in the/OnTopic/Repositories/_eventArgsfolder, but in theOnTopic.Repositoriesnamespace (d945366).MappedTopicCache: Private methods onTopicMappingServicenow accept aMappedTopicCacheobject instead of aConcurrentDictionary<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(): IntroducedTopic.IsDirty()method to aid in state tracking; optionally, this can optionally callAttributeCollection.IsDirty()andRelatedTopicCollection.IsDirty()via thecheckCollectionsparameter (a8feb6d, f7d36f0).Topic.MarkClean(): IntroducedMarkClean()method toTopic(28ea6ea, 48e8ed4) andRelatedTopicCollection(c37934b).ITrackDirtyKeys: Introduced a newITrackDirtyKeysinterface to standardize the implementation ofIsDirty()andMarkClean()betweenTopic(f7d36f0),Topic.Attributes,Topic.Relationships, andTopic.References(c757dd3).SQL
Update(Extended)AttributesStored Procedure: Moved logic from theUpdateTopicstored procedure into the newUpdateAttributes(634857b) andUpdateExtendedAttributes(3664b2b) stored procedures (08a57db).SqlDataReaderUnit Tests: While we're still not able to easily unit test theSqlTopicRepository, we now have unit tests for theSqlDataReaderextension methods, which handle much of the logic forLoad(),Rollback(), andRefresh(), as well as most of the complex logic outside of theSave()method (b91c292).