DependencyQueue 1.0.0-rc.5

This is a prerelease version of DependencyQueue.
There is a newer version of this package available.
See the version list below for details.
dotnet add package DependencyQueue --version 1.0.0-rc.5
                    
NuGet\Install-Package DependencyQueue -Version 1.0.0-rc.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DependencyQueue" Version="1.0.0-rc.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DependencyQueue" Version="1.0.0-rc.5" />
                    
Directory.Packages.props
<PackageReference Include="DependencyQueue" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DependencyQueue --version 1.0.0-rc.5
                    
#r "nuget: DependencyQueue, 1.0.0-rc.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DependencyQueue@1.0.0-rc.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DependencyQueue&version=1.0.0-rc.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DependencyQueue&version=1.0.0-rc.5&prerelease
                    
Install as a Cake Tool

DependencyQueue

A dependency queue for .NET: a thread-safe generic queue that dequeues elements in dependency order.

Status

Build NuGet NuGet

  • Tested: 100% coverage by automated tests.
  • Documented: IntelliSense on everything. Guide below.

Installation

Install this NuGet Package in your project.

Usage

DependencyQueue provides a specialized queue that works differently than a typical FIFO (first in, first out) queue. The queue accepts enqueued items in any order but yields dequeued items in an order that respects dependencies: an item is not dequeued until any items on which it depends have been dequeued. If one must have a catchy acronym for such a queue, a possibility is WIRDO — whatever in, reverse dependency out.

If an example would be helpful, skip to the Examples section.

Creation

The queue class is the generic DependencyQueue<T>, which supports simple creation via its constructor.

using var queue = new DependencyQueue<Step>();

Because the queue class implements IDisposable, make sure to guarantee eventual disposal of queue instances via a using block or other means.

Enqueueing Items

A DependencyQueue<T> instance accepts element values of type T. Each element value is wrapped in a DependencyQueueItem<T> object — an item — which also tracks how that item relates dependency-wise to the other items in the queue. DependencyQueue provides two ways to create and enqueue (add) items: an Enqueue() method and a builder pattern.

Enqueueing Items With a Builder

To obtain a builder object, call CreateItemBuilder().

var builder = queue.CreateItemBuilder();

The builder uses a fluent interface to build and enqueue an item incrementally. Each method returns the builder itself, enabling the developer to chain multiple method calls together.

To begin a new item, call NewItem() on the builder, passing both a name for the item and a value to store in the item. To add the item to the queue, call Enqueue() on the builder. The builder then becomes reusable for another item.

builder
    .NewItem("MyItem", theItem)
    .Enqueue();

To indicate that an item requires some other item to be dequeued first, call AddRequires() on the builder, passing one or more names on which the current item will depend.

builder
    .NewItem("MyItem", theItem)
    .AddRequires("ThingINeedA", "ThingINeedB")  // accepts multiple names
    .AddRequires("ThingINeedC")                 // can use multiple times
    .Enqueue();

Sometimes an item is part of some larger whole, and it is useful to give that whole a name so that other items can depend on it. At other times, it is useful for an item to have more than one name. To add more names to the current item, call AddProvides() on the builder, passing one or more extra names for the item.

builder
    .NewItem("MyItem", theItem)
    .AddProvides("BigThingIAmPartOf", "MyAlias")  // accepts multiple names
    .AddProvides("AnotherAlias")                  // can use multiple times
    .Enqueue();

CreateItemBuilder() is thread-safe, but the builder object it returns is not. To build and enqueue items in parallel, create a separate builder for each thread.

Enqueueing Items With the Enqueue Method

As an alternative to the builder pattern, DependencyQueue also provides an Enqueue() method that can create and enqueue an item in one call. The tradeoff is that all of the information about the item must be specified in that one call.

queue.Enqueue(
    name:     "MyItem",
    value:    theItem,
    requires: ["ThingINeedA", "ThingINeedB", "ThingINeedC"],
    provides: ["BigThingIAmPartOf", "MyAlias", "AnotherAlias"]
);

Enqueue() is thread-safe.

Other Details About Enqueueing

Enqueue() and NewItem() place no restriction on the element value passed as the value argument.

Names passed to Enqueue(), NewItem(), AddRequires(), and AddProvides() can be any non-null, non-empty strings. Duplicate names are allowed and in fact are often useful.

Each name defines a topic. For each topic, a DependencyQueue<T> tracks:

  • which items provide that topic, and
  • which items require that topic.

Each item always provides the topic defined by the item's own name.

By default, topic names use case-sensitive ordinal comparison. To use different comparison rules, pass a StringComparer instance to the queue constructor.

Dequeueing Items

To dequeue (remove) an item, call Dequeue() or DequeueAsync(). Both methods yield the next item in the queue, or null if the queue is empty.

var item = queue.Dequeue();
var item = await queue.DequeueAsync(cancellation: cancellationToken);

The element value is available from the Value property of the returned item.

When the code that dequeued the item is done with the item, the code must call Complete() to inform the queue.

queue.Complete(item);

Once an item is completed, any other items that depended on it become available to be dequeued if those items have no other outstanding dependencies.

The dequeue methods support an optional predicate parameter. If the caller provides a predicate, the queue tests the Value of each ready-to-dequeue item against the predicate and yields the first item for which the predicate returns true. If the predicate does not return true for any ready-to-dequeue item, then the dequeue method waits until an item becomes available that does satisfy the predicate.

var item = await queue.DequeueAsync(
    value => MyCustomPredicate(value),
    cancellationToken
);

To remove all items from a queue, call Clear().

queue.Clear();

Dequeue(), DequeueAsync(), Complete(), and Clear() are thread-safe.

Validation

A DependencyQueue<T> can be invalid. Specifically, the web of dependencies between items — the dependency graph — can be invalid in two ways:

  1. A topic is required but not provided by any item. This can happen due to a typo in a topic name or because an expected item was never enqueued.

  2. The dependency graph contains a cycle. This occurs when an item requires itself, either directly or indirectly.

If a queue is invalid, Dequeue() and DequeueAsync() will throw an InvalidDependencyQueueException. The exception's Errors property lists the reasons why the queue is invalid.

To validate a queue explicitly without dequeuing any items, call Validate(), which returns a list of errors found in the queue. if the list is empty, then the queue is valid. Validate() is thread-safe.

var errors = queue.Validate();

foreach (var error in errors)
{
    switch (error)
    {
        case DependencyQueueUnprovidedTopicError<Step> unprovided:
            // Available properties:
            _ = unprovided.Topic;       // The topic required but not provided
            break;

        case DependencyQueueCycleError<Step> cycle:
            // Available properties:
            _ = cycle.RequiringItem;    // The item that caused the cycle
            _ = cycle.RequiredTopic;    // What it required, causing the cycle
            break;
    }
}

Inspection

To peek into a queue, the DependencyQueue<T> class provides the Inspect() and InspectAsync() methods. These methods acquire an exclusive lock on the queue and return a read-only view of the queue. Because the view holds the exclusive lock until disposed, queue inspection is thread-safe.

using var view = queue.Inspect();

_ = view.ReadyItems;                  // Items ready to be dequeued
_ = view.ReadyItems.First();          // First ready item
_ = view.ReadyItems.First().Provides; // Names of topics it provides
_ = view.ReadyItems.First().Requires; // Names of topics it requires
_ = view.Topics;                      // Dictionary of topics keyed by name
_ = view.Topics["Foo"];               // Topic "foo"
_ = view.Topics["Foo"].ProvidedBy;    // Items that provide topic "Foo"
_ = view.Topics["Foo"].RequiredBy;    // Items that require topic "Foo"

Thread Safety

Most members of DependencyQueue<T> are thread-safe. Specifically:

  • The Comparer and Count properties are thread-safe.

  • The Enqueue() method is thread-safe.

  • ⚠ The CreateItemBuilder() method is thread-safe, but the object it returns is not thread-safe. Multiple threads can each use their own builder instance to enqueue items in parallel.

  • The dequeue methods (Dequeue(), DequeueAsync(), and Complete()) are thread-safe.

  • The Validate() method is thread-safe.

  • The Clear() method is thread-safe.

  • The inspection methods (Inspect() and InspectAsync()) are thread-safe. The view objects they return also are thread-safe.

  • ⚠ The Dispose() method is <strong>not</strong> thread-safe.

Examples

Basic Usage

Imagine a program that cooks a basic hamburger. The program can add steps to a dependency queue in any order, and the queue will yield back the steps in the correct order to prepare a burger.

The values in the queue are Step objects. For the sake of the example, it is not important what a step is. Just imagine that the Step class has an Execute() method that performs the step.

// Create a queue
using var queue = new DependencyQueue<Step>();

// Create a builder for queue items
var builder = queue.CreateItemBuilder();

// Add items in any order
// First, we know we have to assemble the burger
builder
    .NewItem("Assembly", burgerAssembler)
    .AddRequires("GrilledPatty", "ToastedBun", "Condiments", "Sauce")
    .Enqueue();

// Gotta cook the patty
builder
    .NewItem("Grilling", griller)
    .AddRequires("Patty")
    .AddProvides("GrilledPatty")
    .Enqueue();

// Gotta toast the bun, too
builder
    .NewItem("Toasting", toaster)
    .AddRequires("Bun")
    .AddProvides("ToastedBun")
    .Enqueue();

// We have to get the ingredients somewhere
builder
    .NewItem("Gathering", fridgeRaider)
    .AddProvides("Patty", "Bun", "Condiments", "Sauce")
    .Enqueue();

// Validate the queue
var errors = queue.Validate();
if (errors.Any())
    throw new InvalidBurgerException(errors);

// Now build the burger
while (queue.Dequeue() is { } item)
{
    Console.WriteLine($"Executing: {item.Name}");

    // Execute the burger-making step
    item.Value.Execute();

    // Tell the queue that the step is done
    queue.Complete(item);
}

Output:

Executing: Gathering
Executing: Toasting
Executing: Grilling
Executing: Assembly

Asynchronous Code and Concurrency

DependencyQueue supports async code and concurrent processing. Imagine that the Step class from the example above has an ExecuteAsync() method that performs the step asynchronously.

async Task ProcessAsync(DependencyQueue<Step> queue, CancellationToken cancellation)
{
    // Spin up three workers and wait for them to finish
    await Task.WhenAll(
        WorkAsync(queue, 1, cancellation),
        WorkAsync(queue, 2, cancellation),
        WorkAsync(queue, 3, cancellation)
    );
}

async Task WorkAsync(DependencyQueue<Step> queue, int n, CancellationToken cancellation)
{
    // This yield causes the worker to hop onto another thread
    // so that the caller can continue creating more workers
    await Task.Yield();

    while (await queue.DequeueAsync(cancellation) is { } item)
    {
        Console.WriteLine($"Worker {n} executing: {item.Name}");

        // Execute the burger-making step
        await item.Value.ExecuteAsync(cancellation);

        // Tell the queue that the step is done
        queue.Complete(item);
    }
}

Output might be:

Worker 1 executing: Gathering
Worker 3 executing: Grilling
Worker 2 executing: Toasting
Worker 1 executing: Assembly
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 199 10/12/2025
1.0.0-rc.5 129 10/9/2025
1.0.0-rc.4 163 9/1/2025
1.0.0-rc.3 161 8/29/2025
1.0.0-rc.2 158 8/12/2025
1.0.0-rc.1 482 5/7/2022
1.0.0-pre.4 336 10/30/2021
1.0.0-pre.3 309 10/23/2021
1.0.0-pre.2 290 10/11/2021
1.0.0-pre.1 383 10/2/2021