Table of Contents

Custom Blocks

DataflowBuilder is designed to be fully extensible. If your workflow requires specialized queuing, deduplication, sliding windows, or custom routing, you can create custom propagator blocks using BasePropagatorBlock<TInput, TOutput>.


1. The BasePropagatorBlock Pattern

An IPropagatorBlock<TInput, TOutput> acts simultaneously as an ITargetBlock<TInput> (receiving data) and an ISourceBlock<TOutput> (emitting data).

BasePropagatorBlock<TInput, TOutput> encapsulates the boiler-plate coordination by requiring you to implement only two properties:

public abstract class BasePropagatorBlock<TInput, TOutput> : IPropagatorBlock<TInput, TOutput>
{
    public abstract ITargetBlock<TInput> Target { get; }
    public abstract ISourceBlock<TOutput> Source { get; }
    public IPropagatorBlock<TInput, TOutput> Pipeline => DataflowBlock.Encapsulate(Target, Source);
    // Automatic pass-through for LinkTo, OfferMessage, ConsumeMessage, Complete, Fault, etc.
}

2. Walkthrough: The Built-in GroupBlock

To see how BasePropagatorBlock works in practice, examine built-in blocks such as TimeBatchBlock<T> (which uses timer-driven flushes) and GroupBlock<TGroupKey, TInput>:

using DotnetKit.DataflowBuilder.Abstractions;
using System.Threading.Tasks.Dataflow;

public class GroupBlock<TGroupKey, TInput> : BasePropagatorBlock<TInput, TInput[]>
    where TGroupKey : notnull
{
    private readonly ITargetBlock<TInput> _inputBlock;
    private readonly BufferBlock<TInput[]> _outputBlock;
    private readonly Dictionary<TGroupKey, List<TInput>> _groups = new();

    public GroupBlock(
        Func<TInput, TGroupKey> groupingSelector,
        int boundedCapacity = 1,
        int maxDegreeOfParallelism = 1,
        CancellationToken cancellationToken = default)
    {
        _outputBlock = new BufferBlock<TInput[]>(new DataflowBlockOptions
        {
            BoundedCapacity = boundedCapacity,
            CancellationToken = cancellationToken
        });

        _inputBlock = new ActionBlock<TInput>(async input =>
        {
            var key = groupingSelector(input);
            if (!_groups.TryGetValue(key, out var list))
            {
                list = new List<TInput>(boundedCapacity);
                _groups[key] = list;
            }
            list.Add(input);

            if (list.Count >= boundedCapacity)
            {
                await _outputBlock.SendAsync(list.ToArray());
                list.Clear();
                _groups.Remove(key);
            }
        }, new ExecutionDataflowBlockOptions
        {
            BoundedCapacity = boundedCapacity,
            MaxDegreeOfParallelism = maxDegreeOfParallelism,
            CancellationToken = cancellationToken
        });
    }

    public override ITargetBlock<TInput> Target => _inputBlock;
    public override ISourceBlock<TInput[]> Source => _outputBlock;
}

3. Creating a Custom Filtering Block Example

Suppose you want to create a FilterBlock<T> that only allows items passing a predicate to continue downstream:

using DotnetKit.DataflowBuilder.Abstractions;
using System.Threading.Tasks.Dataflow;

public class FilterBlock<T> : BasePropagatorBlock<T, T>
{
    private readonly ITargetBlock<T> _target;
    private readonly BufferBlock<T> _source;

    public FilterBlock(Predicate<T> predicate, CancellationToken cancellationToken = default)
    {
        _source = new BufferBlock<T>(new DataflowBlockOptions { CancellationToken = cancellationToken });

        _target = new ActionBlock<T>(async item =>
        {
            if (predicate(item))
            {
                await _source.SendAsync(item);
            }
        }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
    }

    public override ITargetBlock<T> Target => _target;
    public override ISourceBlock<T> Source => _source;
}

4. Exposing via Fluent Extension Methods

To integrate your custom block into DataflowBuilder's fluent syntax, define an extension method targeting IFluentBuilder<ISourceBlock<TInput>, IPipelineBuilder<TSource>>:

using DotnetKit.DataflowBuilder;
using DotnetKit.DataflowBuilder.Abstractions;
using DotnetKit.DataflowBuilder.Core;
using System.Threading.Tasks.Dataflow;

public static class CustomPipelineExtensions
{
    public static IFluentBuilder<ISourceBlock<TInput>, IPipelineBuilder<TSource>> Filter<TSource, TInput>(
        this IFluentBuilder<ISourceBlock<TInput>, IPipelineBuilder<TSource>> builder,
        Predicate<TInput> predicate)
    {
        var filterBlock = new FilterBlock<TInput>(predicate, builder.Current.CancellationToken);
        builder.Current.AddPropagatorBlock(filterBlock);
        return new FluentBuilder<ISourceBlock<TInput>, IPipelineBuilder<TSource>>(builder.Current);
    }
}

Now, developers can use .Filter(...) directly within their pipelines:

var pipeline = DataFlowPipelineBuilder.FromSource<int>()
    .Filter(x => x % 2 == 0) // Custom block!
    .Process(x => x * 10)
    .ToTarget(Console.WriteLine)
    .Build();