Message Channels & Yielding
Garyon provides utilities for message-based communication and efficient value generation through MessageRequestChannel, Yielder, and related types.
MessageRequestChannel
A message request channel allows signaling requests for a single operation using channels.
Framework Support: Available only when
System.Threading.Channelsis supported (typically .NET Core 3.1+).
Basic Usage
using Garyon.Objects;
using System.Threading;
using System.Threading.Channels;
var channelOptions = new UnboundedChannelOptions
{
SingleWriter = true,
SingleReader = true
};
var channel = MessageRequestChannel.Create(channelOptions);
// Producer: Request an operation
await channel.WriteOne(CancellationToken.None);
// Consumer: Check and consume requests
if (channel.ConsumeAllRequests())
{
// At least one request was pending
await PerformRequestedOperation();
}
Request Pattern
The channel is designed for scenarios where:
- Multiple requests should be coalesced into one
- Only the presence of a request matters, not the count
- Consumers process requests in batches
// Drop mode: Additional writes while processing are dropped
var channel = MessageRequestChannel.Create(new UnboundedChannelOptions
{
SingleWriter = false,
SingleReader = true
});
// Multiple concurrent requests
await Task.WhenAll(
channel.WriteOne(),
channel.WriteOne(),
channel.WriteOne()
);
// ConsumeAllRequests returns true if ANY request was pending
bool hadRequests = channel.ConsumeAllRequests();
// All requests consumed in one call
Example: Refresh Trigger
using Garyon.Objects;
using System.Threading;
using System.Threading.Tasks;
public class DataRefresher
{
private readonly MessageRequestChannel _refreshChannel;
private readonly CancellationTokenSource _cts = new();
public DataRefresher()
{
_refreshChannel = MessageRequestChannel.Create(
new UnboundedChannelOptions
{
SingleWriter = false,
SingleReader = true
}
);
_ = Task.Run(RefreshLoop);
}
public async Task RequestRefreshAsync()
{
await _refreshChannel.WriteOne(_cts.Token);
}
private async Task RefreshLoop()
{
while (!_cts.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token);
if (_refreshChannel.ConsumeAllRequests())
{
await PerformRefresh();
}
}
}
private async Task PerformRefresh()
{
Console.WriteLine("Refreshing data...");
await Task.Delay(100); // Simulate work
}
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
Yielder
The Yielder<T> pattern provides mechanisms for convenient value generation from a factory function.
Basic Usage
using Garyon.Objects;
var random = new Random();
var yielder = Yielding.For(() => random.Next(100));
// Generate array
int[] numbers = yielder.YieldArray(10);
// Generate list
List<int> list = yielder.YieldList(20);
// Generate set (duplicates automatically handled)
HashSet<int> set = yielder.YieldSet(15);
Factory Function
The factory is called for each value:
int counter = 0;
var yielder = Yielding.For(() => counter++);
int[] sequence = yielder.YieldArray(5);
// sequence: [0, 1, 2, 3, 4]
Collection Types
Generate different collection types:
using Garyon.Objects;
var guidFactory = Yielding.For(() => Guid.NewGuid());
// Array
Guid[] guids = guidFactory.YieldArray(10);
// List
List<Guid> guidList = guidFactory.YieldList(10);
// HashSet
HashSet<Guid> guidSet = guidFactory.YieldSet(10);
// SortedSet
SortedSet<int> sortedNumbers = Yielding.For(() => random.Next(100))
.YieldSortedSet(20);
// ImmutableArray (when available)
#if HAS_IMMUTABLE
ImmutableArray<int> immutable = yielder.YieldImmutableArray(10);
#endif
Yielding Into Existing Collections
Add generated values to existing collections:
var yielder = Yielding.For(() => random.Next());
var existingList = new List<int> { 1, 2, 3 };
yielder.YieldInto(5, existingList);
// existingList now has 8 elements (3 original + 5 generated)
var existingSet = new HashSet<int>();
yielder.YieldInto(10, existingSet);
// existingSet has up to 10 elements (duplicates ignored)
Lazy Enumeration
Generate values without materializing:
var yielder = Yielding.For(() => random.NextDouble());
// Lazy enumeration
IEnumerable<double> values = yielder.Yield(1000);
// Only generates values as enumerated
foreach (var value in values.Take(5))
{
Console.WriteLine(value); // Only 5 values generated
}
SpanYielder
Similar to Yielder but works with spans for zero-allocation scenarios.
Basic Usage
using Garyon.Objects;
var yielder = new SpanYielder<int>(() => random.Next(100));
// Yield into a span
Span<int> buffer = stackalloc int[10];
yielder.YieldIntoSpan(buffer);
Ref Struct Factory
For maximum performance with ref structs:
var yielder = new SpanYielder<int>(static (ref int value) =>
{
value = Random.Shared.Next(100);
});
Span<int> numbers = stackalloc int[20];
yielder.YieldIntoSpan(numbers);
Examples
Test Data Generation
using Garyon.Objects;
public class TestDataGenerator
{
private static readonly Random _random = new();
public static List<User> GenerateUsers(int count)
{
var userYielder = Yielding.For(() => new User
{
Id = Guid.NewGuid(),
Name = GenerateRandomName(),
Age = _random.Next(18, 80),
Email = $"user{_random.Next(10000)}@example.com"
});
return userYielder.YieldList(count);
}
private static string GenerateRandomName()
{
var names = new[] { "Alice", "Bob", "Charlie", "Diana", "Eve" };
return names[_random.Next(names.Length)];
}
}
Unique ID Generation
using Garyon.Objects;
public class IdGenerator
{
private long _nextId = 1;
private readonly object _lock = new();
public List<long> GenerateIds(int count)
{
var yielder = Yielding.For(() =>
{
lock (_lock)
{
return _nextId++;
}
});
return yielder.YieldList(count);
}
}
Random Sample Generator
using Garyon.Objects;
public class SampleGenerator<T>
{
private readonly List<T> _population;
private readonly Random _random;
public SampleGenerator(List<T> population)
{
_population = population;
_random = new Random();
}
public HashSet<T> GenerateUniqueSample(int sampleSize)
{
if (sampleSize > _population.Count)
throw new ArgumentException("Sample size exceeds population");
var yielder = Yielding.For(() =>
_population[_random.Next(_population.Count)]);
// HashSet automatically handles uniqueness
var sample = yielder.YieldSet(sampleSize * 2); // Generate extra to account for duplicates
// Trim to desired size
return sample.Take(sampleSize).ToHashSet();
}
}
Batch Processing with MessageRequestChannel
using Garyon.Objects;
using System.Threading.Channels;
public class BatchProcessor<T>
{
private readonly MessageRequestChannel _processChannel;
private readonly Queue<T> _queue = new();
private readonly object _queueLock = new();
private readonly CancellationTokenSource _cts = new();
public BatchProcessor()
{
_processChannel = MessageRequestChannel.Create(
new UnboundedChannelOptions
{
SingleWriter = false,
SingleReader = true
}
);
_ = Task.Run(ProcessLoop);
}
public async Task AddItemAsync(T item)
{
lock (_queueLock)
{
_queue.Enqueue(item);
}
await _processChannel.WriteOne(_cts.Token);
}
private async Task ProcessLoop()
{
while (!_cts.Token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token);
if (_processChannel.ConsumeAllRequests())
{
List<T> batch;
lock (_queueLock)
{
batch = new List<T>(_queue);
_queue.Clear();
}
if (batch.Count > 0)
{
await ProcessBatch(batch);
}
}
}
}
private async Task ProcessBatch(List<T> items)
{
Console.WriteLine($"Processing batch of {items.Count} items");
await Task.Delay(100); // Simulate work
}
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
}
Performance Test Data
using Garyon.Objects;
public class PerformanceTestHelper
{
public static List<int> GenerateSortedData(int count)
{
var yielder = Yielding.For(() => Random.Shared.Next(1000000));
var data = yielder.YieldList(count);
data.Sort();
return data;
}
public static int[] GenerateRandomArray(int count, int min, int max)
{
var yielder = Yielding.For(() => Random.Shared.Next(min, max));
return yielder.YieldArray(count);
}
public static HashSet<string> GenerateUniqueStrings(int count, int length = 10)
{
var yielder = Yielding.For(() => GenerateRandomString(length));
return yielder.YieldSet(count * 2); // Generate extra for uniqueness
}
private static string GenerateRandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var buffer = new char[length];
for (int i = 0; i < length; i++)
{
buffer[i] = chars[Random.Shared.Next(chars.Length)];
}
return new string(buffer);
}
}
Best Practices
Yielder
- Factory purity: Keep factory functions simple and side-effect free when possible
- Set generation: When using
YieldSet, generate extra to account for duplicates - Lazy vs. Materialized: Use
Yield()for lazy, orYieldArray/Listfor materialized - Reuse yielder: Create once, use multiple times with different counts
MessageRequestChannel
- Single purpose: Use for coalescing multiple requests into one operation
- Channel options: Configure for your specific read/write patterns
- Consume pattern: Always use
ConsumeAllRequests()to clear all pending - Cleanup: Properly dispose of cancellation tokens
Performance
Yielder Performance
// ❌ Traditional: Verbose and primitive
var list = new List<int>();
for (int i = 0; i < 1000; i++)
{
list.Add(factory());
}
// ✅ Yielder: Convenient, concise and frictionless
var yielder = Yielding.For(factory);
var list = yielder.YieldList(1000);
SpanYielder for Zero Allocation
// ✅ Zero heap allocation
Span<int> buffer = stackalloc int[100];
var yielder = new SpanYielder<int>(() => Random.Shared.Next());
yielder.YieldIntoSpan(buffer);
API Reference
See the following API references: