Open.ChannelExtensions

repository·master·Indexed 19 days ago

https://github.com/open-net-libraries/open.channelextensions

A set of extension methods for System.Threading.Channels designed to simplify and optimize common patterns. It provides functionality for batching, filtering, transforming, and concurrent processing, as well as tools to build asynchronous pipelines using methods such as SourceAsync, Pipe, PipeAsync, and ReadAllAsync.

Tokens
3.1K
Snippets
11
Records
11
Agent score
18%

What's inside Open.ChannelExtensions

  1. Build an asynchronous pipeline with Open.ChannelExtensions

    master

    You can define expressive, asynchronous processing pipelines by chaining extensions on a System.Threading.Channels.Channel. This allows you to source data, apply transformations with controlled concurrency and capacity, and consume the final results in a single fluent statement.

    Common pipeline steps include:

    • SourceAsync: Populates a channel from an IEnumerable<Task<T>>.
    • PipeAsync: Performs asynchronous transformations with maxConcurrency and capacity settings.
    • Pipe: Performs synchronous transformations with capacity settings.
    • ReadAllAsync: Consumes the final values from the pipeline.
    await Channel
        .CreateBounded<T>(10)
        .SourceAsync(source /* IEnumerable<Task<T>> */)
        .PipeAsync(
            maxConcurrency: 2,
            capacity: 5,
            transform: asyncTransform01)
        .Pipe(transform02, /* capacity */ 3)
        .ReadAllAsync(finalTransformedValue => {
            // Do something async with each final value.
        });
  2. Build an asynchronous pipeline

    master

    You can chain multiple operations to create an expressive asynchronous pipeline using SourceAsync, PipeAsync, Pipe, and ReadAllAsync.

    await Channel
        .CreateBounded<T>(10)
        .SourceAsync(source /* IEnumerable<Task<T>> */)
        .PipeAsync(
            maxConcurrency: 2,
            capacity: 5,
            transform: asyncTransform01)
        .Pipe(transform02, /* capacity */ 3)
        .ReadAllAsync(finalTransformedValue => {
            // Do something async with each final value.
        });
  3. Batch and Join channel entries

    master

    Batching

    Group items into a List<T> before they are available for reading. You can use .WithTimeout(int milliseconds) to ensure non-empty batches are flushed periodically.

    Joining

    The inverse of batching. Use .Join() to combine multiple batches back into a single channel of individual items.

    // Batching
    values.Reader
        .Batch(10 /*batch size*/)
        .WithTimeout(1000) // Flush every second
        .ReadAllAsync(async batch => {/*...*/});
    
    // Joining
    batches.Reader
        .Join()
        .ReadAllAsync(async value => {/*...*/});
  4. Pipe and Transform channels

    master

    The Pipe method allows you to transform values from a source channel into a new channel. You can control the capacity (bounding) and the concurrency of the transformation.

    • Unbounded: channel.Pipe(async value => ...)
    • Bounded (Capacity): channel.Pipe(async value => ..., capacity)
    • Bounded with Concurrency: channel.Pipe(maxConcurrency, async value => ..., capacity)
    // Transform to new unbounded channel
    var transformed = channel.Pipe(async value => /* transformation */);
    
    // Transform to new unbounded channel with max concurrency X
    var transformed = channel.Pipe(X, async value => /* transformation */);
    
    // Transform to new bounded channel with capacity N
    var transformed = channel.Pipe(async value => /* transformation */, N);
    
    // Transform to new bounded channel with capacity N and max concurrency X
    var transformed = channel.Pipe(X, async value => /* transformation */, N);
    
    // Using named arguments
    var transformed = channel.Pipe(
        maxConcurrency: X,
        capacity: N,
        transform: async value => /* transformation */
    );
  5. Read entries from a channel

    master

    Open.ChannelExtensions provides several ways to consume entries from a channel until it is closed. You can read entries sequentially (one by one) or concurrently.

    Sequential Reading

    Use ReadAll for synchronous processing or ReadAllAsync for asynchronous processing. Both support providing the entry alone or the entry along with its index.

    Concurrent Reading

    Use ReadAllConcurrently or ReadAllConcurrentlyAsync to process multiple entries from the channel simultaneously. You must specify a maxConcurrency value.

    | Method | Processing Type | Supports Index |

    // One by one (Sync)
    await channel.ReadAll(entry => { /* ... */ });
    await channel.ReadAll((entry, index) => { /* ... */ });
    
    // One by one (Async)
    await channel.ReadAllAsync(async entry => { await /* ... */ });
    await channel.ReadAllAsync(async (entry, index) => { await /* ... */ });
    
    // Concurrent (Sync)
    await channel.ReadAllConcurrently(maxConcurrency, entry => { /* ... */ });
    
    // Concurrent (Async)
    await channel.ReadAllConcurrentlyAsync(maxConcurrency, async entry => { await /* ... */ });
  6. Read all entries from a channel

    master

    Use ReadAll or ReadAllAsync to consume all entries from a channel until it is closed. You can process entries one by one or with their index.

    Available methods:

    • ReadAll(Action<T> action)
    • ReadAll((T entry, int index) => action)
    • ReadAllAsync(Func<T, ValueTask> action)
    • ReadAllAsync(Func<T, int, ValueTask> action)
    // One by one
    await channel.ReadAll(entry => { /* Processing Code */ });
    
    // With index
    await channel.ReadAll((entry, index) => { /* Processing Code */ });
    
    // Async
    await channel.ReadAllAsync(async entry => { await /* Processing Code */ });
    
    // Async with index
    await channel.ReadAllAsync(async (entry, index) => { await /* Processing Code */ });
  7. Read entries concurrently from a channel

    master

    Use ReadAllConcurrently or ReadAllConcurrentlyAsync to process multiple entries from a channel in parallel, controlled by a maxConcurrency parameter.

    // Synchronous processing with concurrency
    await channel.ReadAllConcurrently(maxConcurrency, entry => { /* Processing Code */ });
    
    // Asynchronous processing with concurrency
    await channel.ReadAllConcurrentlyAsync(maxConcurrency, async entry => { await /* Processing Code */ });
  8. Transform and buffer entries using Pipe

    master

    The Pipe and PipeAsync methods allow you to create a new channel by transforming the values from an existing channel. This is useful for creating processing stages in a pipeline.

    Configuration Options

    • Transformation: A function that maps the input value to a new value.
    • maxConcurrency: Limits how many transformations are running at the same time. (Available in PipeAsync).
    • capacity: Defines the bound of the resulting channel (the number of entries it can hold before applying backpressure).

    Usage Patterns

    • Unbounded Transformation: channel.Pipe(async value => ...) creates a new unbounded channel.
    • Bounded Transformation: channel.Pipe(async value => ..., N) creates a new channel with capacity N.
    • Controlled Concurrency: channel.Pipe(X, async value => ..., N) creates a channel with capacity N and a maximum concurrency of X.
    // Unbounded
    var transformed = channel.Pipe(async value => /* transformation */);
    
    // Bounded with max concurrency X
    const X = 4;
    var transformed = channel.Pipe(X, async value => /* transformation */);
    
    // Bounded with capacity N
    const N = 5;
    var transformed = channel.Pipe(async value => /* transformation */, N);
    
    // Bounded with capacity N and max concurrency X
    const X = 4;
    const N = 5;
    var transformed = channel.Pipe(X, async value => /* transformation */, N);
    
    // Using named arguments for clarity
    transformed = channel.Pipe(
        maxConcurrency: X,
        capacity: N,
        transform: async value => /* transformation */);
  9. Write entries to a channel

    master

    You can populate a channel from an existing enumeration using WriteAll or WriteAllAsync. If the complete parameter is set to true, the channel will be automatically closed once the source enumeration is exhausted.

    Writing Methods

    • WriteAll: Dumps an IEnumerable<T> into the channel.
    • WriteAllAsync: Dumps an IEnumerable<Task<T>> or IEnumerable<ValueTask<T>> into the channel.
    • WriteAllConcurrentlyAsync: Synchronizes reading from the source and processes the results concurrently using a specified maxConcurrency.
    // Dump IEnumerable<T>
    await channel.WriteAll(source, complete: true);
    
    // Dump IEnumerable<Task<T>> or IEnumerable<ValueTask<T>>
    await channel.WriteAllAsync(source, complete: true);
    
    // Concurrent write/process
    await channel.WriteAllConcurrentlyAsync(maxConcurrency, source, complete: true);
  10. Filter and Transform ChannelReader

    master

    You can apply synchronous filtering and transformation directly on the ChannelReader.

    Warning: Any predicate or selector function must trap errors; otherwise, downstream reads will fail and data may not be recoverable.

    • Filter(Predicate<T>): Acts like .Where().
    • Transform(Selector<T, TResult>): Acts like .Select().
    // Filter and transform when reading.
    channel.Reader
        .Filter(predicate) // .Where()
        .Transform(selector) // .Select()
        .ReadAllAsync(async value => {/*...*/});