Report when input to first dataflow block finishes all linked blocks

You could consider using the TaskCompletionSource as the base class for all your sub-entities. For example:

class Attachment : TaskCompletionSource
{
}

class Conversation : TaskCompletionSource
{
}

Then every time you insert a sub-entity in the database, you mark it as completed:

attachment.SetResult();

...or if the insert fails, mark it as faulted:

attachment.SetException(ex);

Finally you can combine all the asynchronous completions in one, with the method Task.WhenAll:

Task ticketCompletion = Task.WhenAll(Enumerable.Empty<Task>()
    .Append(ticket.Task)
    .Concat(attachments.Select(e => e.Task))
    .Concat(conversations.Select(e => e.Task)));

If I am tracking progress in Dataflow, usually I will set up the last block as a notify the UI of progress type block. To be able to track the progress of your inputs, you will need to keep the context of the original input in all the objects you are passing around, so in this case you need to be able to tell that you are working on ticket 1 all the way through your pipeline, and if one of your transforms removes the context that it is working on ticket 1, then you will need to rethink the object types that you are passing through your pipeline so you can keep that context.

A simple example of what I'm talking about is laid out below with a broadcast block going to three transform blocks, and then all three transform blocks going back to an action block that notifies about the progress of the pipelines.

When combining back into the single action block you need to make sure not to propagate completion at that point because as soon as one block propagates completion to the action block, that action block will stop accepting input, so you will still wait for the last block of each pipeline to complete, and then after that manually complete your final notify the UI action block.

using System;
using System.Threading.Tasks.Dataflow;
using System.Threading.Tasks;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        var broadcastBlock = new BroadcastBlock<string>(x => x);
        
        var transformBlockA = new TransformBlock<string, string>(x =>
        {
            return x + "A";
        });
        
        var transformBlockB = new TransformBlock<string, string>(x =>
        {
            return x + "B";
        });
        
        var transformBlockC = new TransformBlock<string, string>(x =>
        {
            return x + "C";
        });
        
        var ticketTracking = new Dictionary<int, List<string>>();
        var notifyUiBlock = new ActionBlock<string>(x =>
        {
            var ticketNumber = int.Parse(x.Substring(5,1));
            var taskLetter = x.Substring(7,1);
            var success = ticketTracking.TryGetValue(ticketNumber, out var tasksComplete);
            if (!success)
            {
                tasksComplete = new List<string>();
                ticketTracking[ticketNumber] = tasksComplete;
            }
            tasksComplete.Add(taskLetter);
            
            if (tasksComplete.Count == 3)
            {
                Console.WriteLine($"Ticket {ticketNumber} is complete");
            }
        });
        
        DataflowLinkOptions linkOptions = new DataflowLinkOptions() {PropagateCompletion = true};
        
        broadcastBlock.LinkTo(transformBlockA, linkOptions);
        broadcastBlock.LinkTo(transformBlockB, linkOptions);
        broadcastBlock.LinkTo(transformBlockC, linkOptions);
        transformBlockA.LinkTo(notifyUiBlock);
        transformBlockB.LinkTo(notifyUiBlock);
        transformBlockC.LinkTo(notifyUiBlock);
        
        for(var i = 0; i < 5; i++)
        {
            broadcastBlock.Post($"Task {i} ");
        }
        
        broadcastBlock.Complete();
        
        Task.WhenAll(transformBlockA.Completion, transformBlockB.Completion, transformBlockC.Completion).Wait();
        notifyUiBlock.Complete();
        notifyUiBlock.Completion.Wait();
        
        Console.WriteLine("Done");
    }
}

This will give an output similar to this

Ticket 0 is complete
Ticket 1 is complete
Ticket 2 is complete
Ticket 3 is complete
Ticket 4 is complete
Done