Skip to content

Commit 96f1473

Browse files
committed
Introduced support for resolving orphaned relationships
When doing an import, an issue can occur where `topic.Relationships` and `topic.DerivedTopic`s point to new topics that haven't yet been `Import()`ed as part of the tree traversal and, therefore, cannot be resolved. Traditionally, this has required calling `Import()` a _second_ time to ensure that these are resolved. This shouldn't be necessary. This commit introduces internal handling of this scenario as part of `Import()` by creating a cache of unresolved relationships which is then evaluated after the initial `Import()` is complete. This ensures that any relationships that were initially missed due to the topics being `Import()`ed later are resolved. This should address the majority of scenarios. It's worth noting that there are two scenarios where this will still fail. First, if the relationship is outside of the `ExportOptions.ExportScope` and the target relationship doesn't already exist in the target repository. Second, if the relationship is an implicit topic pointer (i.e., an attribute ending with `Id`), since that necessitates that the target topic be saved in order to persist the relationship as a meaningful attribute value. The latter scenario still mandates the `Import()` be called twice. But as this must happen _after_ it has been `Save()`d, it's not practical for us to handle that scenario internally. As such, when importing topic graphs that (might) contain implicit topic pointers, callers will still need to call: 1. `Import()` (to establish the topic graph) 2. `Save()` (to populate `topic.Id`s) 3. `Import()` (to resolve implicit topic pointers to newly imported topics) 4. `Save()` (to save those newly resolved topic pointers) That remains an unfortunate necessity, but at least this won't be necessary for explicit topic pointers in the form of relationships and `DerivedTopic`s. Finally, this will mitigate an issue where attribute deriving from later attribute references (e.g., in `Root:Configuration:Attributes`) might be saved in the wrong location due to an unresolved inheritance of `StoreInBlob`, and then immediately moved on the second `Save()`. Since that `DerivedTopic` will not be resolved during the initial `Import()`, it will have immediate access to the inherited `StoreInBlob` value. This occurred, notably, with the `Title` attribute when importing the reference configuration. This includes a unit test to validate this functionality.
1 parent b32304b commit 96f1473

File tree

2 files changed

+114
-2
lines changed

2 files changed

+114
-2
lines changed

OnTopic.Data.Transfer.Tests/TopicExtensionsTest.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,41 @@ public void Import_DerivedTopicKey_MapsDerivedTopic() {
332332

333333
}
334334

335+
/*==========================================================================================================================
336+
| TEST: IMPORT: DERIVED TOPIC KEY: MAPS NEWLY DERIVED TOPIC
337+
\-------------------------------------------------------------------------------------------------------------------------*/
338+
/// <summary>
339+
/// Creates a <see cref="TopicData"/> with a <see cref="TopicData.DerivedTopicKey"/> that points to a newly imported
340+
/// topic that is later in the tree traversal, and ensures that the <see cref="Topic.DerivedTopic"/> is set correctly.
341+
/// </summary>
342+
[TestMethod]
343+
public void Import_DerivedTopicKey_MapsNewlyDerivedTopic() {
344+
345+
var rootTopic = TopicFactory.Create("Root", "Container");
346+
var topic = TopicFactory.Create("Test", "Container", rootTopic);
347+
348+
var topicData = new TopicData() {
349+
Key = topic.Key,
350+
UniqueKey = topic.GetUniqueKey(),
351+
ContentType = topic.ContentType,
352+
DerivedTopicKey = "Root:Test:Child"
353+
};
354+
355+
var childTopicData = new TopicData() {
356+
Key = "Child",
357+
UniqueKey = $"{topicData.UniqueKey}:Child",
358+
ContentType = "Container"
359+
};
360+
361+
topicData.Children.Add(childTopicData);
362+
363+
topic.Import(topicData);
364+
365+
Assert.IsNotNull(topic.DerivedTopic);
366+
Assert.AreEqual<string>(childTopicData.Key, topic.DerivedTopic?.Key);
367+
368+
}
369+
335370
/*==========================================================================================================================
336371
| TEST: IMPORT: INVALID DERIVED TOPIC KEY: MAINTAINS EXISTING VALUE
337372
\-------------------------------------------------------------------------------------------------------------------------*/

OnTopic.Data.Transfer/Interchange/TopicExtensions.cs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
| Project Topics Library
55
\=============================================================================================================================*/
66
using System;
7+
using System.Collections.Generic;
78
using System.Diagnostics.CodeAnalysis;
89
using System.Globalization;
910
using System.Linq;
1011
using OnTopic.Attributes;
1112
using OnTopic.Internal.Diagnostics;
13+
using OnTopic.Mapping.Annotations;
1214
using OnTopic.Querying;
1315

1416
namespace OnTopic.Data.Transfer.Interchange {
@@ -161,6 +163,72 @@ public static TopicData Export(this Topic topic, [NotNull]ExportOptions? options
161163
/// <param name="options">An optional <see cref="ImportOptions"/> object to specify import settings.</param>
162164
public static void Import(this Topic topic, TopicData topicData, [NotNull]ImportOptions? options = null) {
163165

166+
/*------------------------------------------------------------------------------------------------------------------------
167+
| Establish cache
168+
\-----------------------------------------------------------------------------------------------------------------------*/
169+
var unresolvedRelationships = new List<Tuple<Topic, string, string>>();
170+
171+
/*------------------------------------------------------------------------------------------------------------------------
172+
| Handle first pass
173+
\-----------------------------------------------------------------------------------------------------------------------*/
174+
topic.Import(topicData, options, unresolvedRelationships);
175+
176+
/*------------------------------------------------------------------------------------------------------------------------
177+
| Attempt to resolve outstanding relationships
178+
\-----------------------------------------------------------------------------------------------------------------------*/
179+
foreach (var relationship in unresolvedRelationships) {
180+
181+
//Attempt to find the target relationship
182+
var source = relationship.Item1;
183+
var target = topic.GetByUniqueKey(relationship.Item3);
184+
var key = relationship.Item2;
185+
186+
//If the relationship STILL can't be resolved, skip it
187+
if (target == null) {
188+
continue;
189+
}
190+
191+
//Wire up derived topics
192+
if (key.Equals("DerivedTopic")) {
193+
source.DerivedTopic = target;
194+
}
195+
196+
//Wire up relationships
197+
else {
198+
source.Relationships.SetTopic(key, target);
199+
}
200+
201+
}
202+
203+
}
204+
205+
/// <summary>
206+
/// Imports a <see cref="TopicData"/> data transfer object—and, potentially, its descendants—into an existing <see
207+
/// cref="Topic"/> entity. Track unmatched relationships.
208+
/// </summary>
209+
/// <remarks>
210+
/// <para>
211+
/// While traversing a topic graph with many new topics, scenarios emerge where the topic graph cannot be fully
212+
/// reconstructed since the relationships and derived topics may refer to new topics that haven't yet been imported. To
213+
/// mitigate that, this overload accepts and populates a cache of such relationships, so that they can be recreated
214+
/// afterwards.
215+
/// </para>
216+
/// <para>
217+
/// This does <i>not</i> address the scenario where implicit topic pointers (i.e., attributes ending in <c>Id</c>)
218+
/// cannot be resolved because the target topics haven't yet been saved—and, therefore, the <see
219+
/// cref="Topic.GetUniqueKey"/> cannot be translated to a <see cref="Topic.Id"/>. There isn't any obvious way to address
220+
/// this via <see cref="Import"/> directly.
221+
/// </para>
222+
/// </remarks>
223+
/// <param name="topic">The source <see cref="Topic"/> to operate off of.</param>
224+
/// <param name="options">An optional <see cref="ImportOptions"/> object to specify import settings.</param>
225+
private static void Import(
226+
this Topic topic,
227+
TopicData topicData,
228+
[NotNull]ImportOptions? options,
229+
List<Tuple<Topic, string, string>> unresolvedRelationships
230+
) {
231+
164232
/*------------------------------------------------------------------------------------------------------------------------
165233
| Validate topic
166234
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -194,7 +262,13 @@ public static void Import(this Topic topic, TopicData topicData, [NotNull]Import
194262
}
195263

196264
if (topicData.DerivedTopicKey?.Length > 0) {
197-
topic.DerivedTopic = topic.GetByUniqueKey(topicData.DerivedTopicKey)?? topic.DerivedTopic;
265+
var target = topic.GetByUniqueKey(topicData.DerivedTopicKey);
266+
if (target != null) {
267+
topic.DerivedTopic = target;
268+
}
269+
else {
270+
unresolvedRelationships.Add(new Tuple<Topic, string, string>(topic, "DerivedTopic", topicData.DerivedTopicKey));
271+
}
198272
}
199273

200274
/*------------------------------------------------------------------------------------------------------------------------
@@ -272,7 +346,10 @@ public static void Import(this Topic topic, TopicData topicData, [NotNull]Import
272346
var relatedTopic = topic.GetByUniqueKey(relatedTopicKey);
273347
if (relationship.Key != null && relatedTopic != null) {
274348
topic.Relationships.SetTopic(relationship.Key, relatedTopic);
275-
};
349+
}
350+
else {
351+
unresolvedRelationships.Add(new Tuple<Topic, string, string>(topic, relationship.Key!, relatedTopicKey));
352+
}
276353
}
277354
}
278355

0 commit comments

Comments
 (0)