44| Project Topics Library
55\=============================================================================================================================*/
66using System ;
7+ using System . Collections . Generic ;
78using System . Data ;
89using System . Data . SqlTypes ;
910using System . Diagnostics . CodeAnalysis ;
@@ -264,6 +265,68 @@ public override Topic Load(int topicId, DateTime version) {
264265 /// <inheritdoc />
265266 public override int Save ( [ NotNull ] Topic topic , bool isRecursive = false , bool isDraft = false ) {
266267
268+ /*------------------------------------------------------------------------------------------------------------------------
269+ | Establish dependencies
270+ \-----------------------------------------------------------------------------------------------------------------------*/
271+ var unresolvedTopics = new List < Topic > ( ) ;
272+ var connection = new SqlConnection ( _connectionString ) ;
273+
274+ connection . Open ( ) ;
275+
276+ /*------------------------------------------------------------------------------------------------------------------------
277+ | Handle first pass
278+ \-----------------------------------------------------------------------------------------------------------------------*/
279+ var topicId = Save ( topic , isRecursive , isDraft , connection , unresolvedTopics ) ;
280+
281+ /*------------------------------------------------------------------------------------------------------------------------
282+ | Attempt to resolve outstanding relationships
283+ \-----------------------------------------------------------------------------------------------------------------------*/
284+ foreach ( var unresolvedTopic in unresolvedTopics ) {
285+ Save ( unresolvedTopic , false , isDraft , connection , new List < Topic > ( ) ) ;
286+ }
287+
288+ /*------------------------------------------------------------------------------------------------------------------------
289+ | Return value
290+ \-----------------------------------------------------------------------------------------------------------------------*/
291+ connection . Close ( ) ;
292+ connection . Dispose ( ) ;
293+ return topicId ;
294+
295+ }
296+
297+ /// <summary>
298+ /// The private overload of the <see cref="Save"/> method provides support for sharing the <see cref="SqlConnection"/>
299+ /// between multiple requests, and maintaining a list of <paramref name="unresolvedRelationships"/>.
300+ /// </summary>
301+ /// <remarks>
302+ /// <para>
303+ /// When recursively saving a topic graph, it is conceivable that references to other topics—such as <see
304+ /// cref="Topic.Relationships"/> or <see cref="Topic.DerivedTopic"/>—can't yet be persisted because the target <see
305+ /// cref="Topic"/> hasn't yet been saved, and thus the <see cref="Topic.Id"/> is still set to <c>-1</c>. To mitigate
306+ /// this, the <paramref name="unresolvedRelationships"/> allows this private overload to keep track of unresolved
307+ /// relationships. The public <see cref="Save(Topic, Boolean, Boolean)"/> overload uses this list to resave any topics
308+ /// that include such references. This adds some overhead due to the duplicate <see cref="Save"/>, but helps avoid
309+ /// potential data loss when working with complex topic graphs.
310+ /// </para>
311+ /// <para>
312+ /// The connection sharing probably doesn't provide that much of a gain in that .NET does a good job of connection
313+ /// pooling. Nevertheless, there is some overhead to opening a new connection, so sharing an open connection when we
314+ /// doing a recursive save could potentially provide some performance benefit.
315+ /// </para>
316+ /// </remarks>
317+ /// <param name="topic">The source <see cref="Topic"/> to save.</param>
318+ /// <param name="isRecursive">Determines whether or not to recursively save <see cref="Topic.Children"/>.</param>
319+ /// <param name="isDraft">Determines if the <see cref="Topic"/> should be saved as a draft version.</param>
320+ /// <param name="connection">The open <see cref="SqlConnection"/> to use for executing <see cref="SqlCommand"/>s.</param>
321+ /// <param name="unresolvedRelationships">A list of <see cref="Topic"/>s with unresolved topic references.</param>
322+ private int Save (
323+ [ NotNull ] Topic topic ,
324+ bool isRecursive ,
325+ bool isDraft ,
326+ SqlConnection connection ,
327+ List < Topic > unresolvedRelationships
328+ ) {
329+
267330 /*------------------------------------------------------------------------------------------------------------------------
268331 | Call base method - will trigger any events associated with the save
269332 \-----------------------------------------------------------------------------------------------------------------------*/
@@ -355,19 +418,31 @@ void addUnmatchedAttribute(string key) {
355418 | Establish database connection
356419 \-----------------------------------------------------------------------------------------------------------------------*/
357420 var isNew = topic . Id == - 1 ;
358- var connection = new SqlConnection ( _connectionString ) ;
359421 var procedureName = isNew ? "CreateTopic" : "UpdateTopic" ;
360422 var command = new SqlCommand ( procedureName , connection ) {
361423 CommandType = CommandType . StoredProcedure
362424 } ;
363425 var version = new SqlDateTime ( DateTime . Now ) ;
426+ var areReferencesResolved = true ;
427+
428+ /*------------------------------------------------------------------------------------------------------------------------
429+ | Handle unresolved references
430+ >-------------------------------------------------------------------------------------------------------------------------
431+ | If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been
432+ | saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The
433+ | same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship.
434+ \-----------------------------------------------------------------------------------------------------------------------*/
435+ if ( isRecursive && ( topic . DerivedTopic ? . Id < 0 || topic . Relationships . Any ( r => r . Any ( t => t . Id < 0 ) ) ) ) {
436+ unresolvedRelationships . Add ( topic ) ;
437+ areReferencesResolved = false ;
438+ }
364439
365440 /*------------------------------------------------------------------------------------------------------------------------
366441 | Establish query parameters
367442 \-----------------------------------------------------------------------------------------------------------------------*/
368443 if ( ! isNew ) {
369444 command . AddParameter ( "TopicID" , topic . Id ) ;
370- command . AddParameter ( "DeleteRelationships" , true ) ;
445+ command . AddParameter ( "DeleteRelationships" , areReferencesResolved ) ;
371446 }
372447 else if ( topic . Parent != null ) {
373448 command . AddParameter ( "ParentID" , topic . Parent . Id ) ;
@@ -382,7 +457,6 @@ void addUnmatchedAttribute(string key) {
382457 \-----------------------------------------------------------------------------------------------------------------------*/
383458 try {
384459
385- connection . Open ( ) ;
386460 command . ExecuteNonQuery ( ) ;
387461
388462 topic . Id = command . GetReturnCode ( ) ;
@@ -392,7 +466,9 @@ void addUnmatchedAttribute(string key) {
392466 "The call to the CreateTopic stored procedure did not return the expected 'Id' parameter."
393467 ) ;
394468
395- PersistRelations ( topic , connection , true ) ;
469+ if ( areReferencesResolved ) {
470+ PersistRelations ( topic , connection , true ) ;
471+ }
396472
397473 topic . VersionHistory . Insert ( 0 , version . Value ) ;
398474
@@ -402,6 +478,7 @@ void addUnmatchedAttribute(string key) {
402478 | Catch exception
403479 \-----------------------------------------------------------------------------------------------------------------------*/
404480 catch ( SqlException exception ) {
481+ connection ? . Dispose ( ) ;
405482 throw new TopicRepositoryException (
406483 $ "Failed to save Topic '{ topic . Key } ' ({ topic . Id } ) via '{ _connectionString } ': '{ exception . Message } '",
407484 exception
@@ -413,7 +490,6 @@ void addUnmatchedAttribute(string key) {
413490 \-----------------------------------------------------------------------------------------------------------------------*/
414491 finally {
415492 command ? . Dispose ( ) ;
416- connection ? . Dispose ( ) ;
417493 attributes . Dispose ( ) ;
418494 }
419495
@@ -423,7 +499,7 @@ void addUnmatchedAttribute(string key) {
423499 if ( isRecursive ) {
424500 foreach ( var childTopic in topic . Children ) {
425501 childTopic . Attributes . SetValue ( "ParentID" , topic . Id . ToString ( CultureInfo . InvariantCulture ) ) ;
426- Save ( childTopic , isRecursive , isDraft ) ;
502+ Save ( childTopic , isRecursive , isDraft , connection , unresolvedRelationships ) ;
427503 }
428504 }
429505
@@ -633,14 +709,13 @@ private static string PersistRelations(Topic topic, SqlConnection connection, bo
633709 finally {
634710 command ? . Dispose ( ) ;
635711 targetIds . Dispose ( ) ;
636- //Since the SQL connection is being passed in, do not close connection; this allows command pooling.
712+ //Since the SQL connection is being passed in, do not close connection; this allows connection pooling.
637713 }
638714
639715 /*------------------------------------------------------------------------------------------------------------------------
640716 | Return the relationship attributes to append to the XML attributes (unless skipXml is set to true)
641717 \-----------------------------------------------------------------------------------------------------------------------*/
642- if ( skipXml ) return "" ;
643- else return CreateRelationshipsXml ( topic ) ;
718+ return skipXml ? "" : CreateRelationshipsXml ( topic ) ;
644719
645720 }
646721
0 commit comments