Streamfab.keepstreams.generic.hook-smeagol-ther... Apr 2026
var encrypted = new HookSmeagol<EncryptionHook>(baseStream, encryptionHook); var logged = new HookSmeagol<LoggingHook>(encrypted, loggingHook); var throttled = new HookSmeagol<ThrottlingHook>(logged, throttlingHook); The order matters: the outermost hook sees data all inner hooks have processed it. In the example above, the logger records encrypted bytes, then the throttler sees the same encrypted payload. 6. Performance considerations | Aspect | Guidance | |--------|----------| | Allocation avoidance | Prefer the ReadAsync(Memory<byte>) / WriteAsync(ReadOnlyMemory<byte>) overloads to avoid array rentals. HookSmeagol forwards the exact Memory instance to the hook. | | Buffer reuse | If a hook needs a temporary buffer (e.g., for decryption), allocate it once in the hook’s constructor and reuse it across calls. | | Async‑over‑sync | Never call .Result or .Wait() inside a hook; it can dead‑lock the caller. Use await all the way. | | Seek support | Some inner streams are non‑seekable (e.g., network sockets). The hook must check inner.CanSeek before forwarding Seek . A typical pattern is to throw NotSupportedException if the underlying stream can’t seek. | | Cancellation | Pass the caller’s CancellationToken straight to inner async calls and to any async hook work. This keeps the whole pipeline responsive. | | Thread‑safety | HookSmeagol itself is not thread‑safe – it mirrors the underlying stream’s contract. If you need concurrent reads/writes, wrap the whole pipeline in a SemaphoreSlim or expose a thread‑safe façade. |
// 3. Post‑hook (e.g., logging, decryption, metrics) await _hook.AfterReadAsync(_ctx, destination.Slice(0, bytesRead), cancellationToken) .ConfigureAwait(false);
| Responsibility | Why it matters | |----------------|----------------| | inbound/outbound data flowing through any System.IO.Stream ‑derived object without breaking the original contract. | Enables logging, diagnostics, transformation, or throttling of data pipelines (e.g., network sockets, file streams, compression streams). | | Preserve the original stream’s semantics (async/sync, seeking, length, timeouts). | Guarantees drop‑in replacement – callers do not need to change their code. | | Compose multiple hooks (e.g., logging + encryption + compression) in a deterministic order. | Keeps the pipeline modular and testable. | | Dispose safely – the hook forwards Dispose / DisposeAsync while also releasing its own resources (buffers, diagnostic listeners). | Prevents resource leaks in long‑running services. |
public void BeforeRead(IHookContext ctx, byte[] buffer, int offset, int count) /* … */ public void AfterRead(IHookContext ctx, byte[] buffer, int offset, int bytesRead) /* … */ StreamFab.KeepStreams.Generic.Hook-Smeagol-TheR...
Typical overhead for a (i.e., a hook that just forwards everything) is ≈ 30–50 ns per call on modern .NET runtimes – negligible for most I/O‑bound workloads. Real‑world hooks (logging, encryption, compression) dominate the cost, not the wrapper. 7. Debugging & diagnostics HookSmeagol ships with a built‑in diagnostic source ( System.Diagnostics.DiagnosticListener ) named "StreamFab.KeepStreams.HookSmeagol" . It emits the following events:
The async version ( DisposeAsync ) follows the same order with await . | Hook name | Typical use‑case | Sample code fragment | |-----------|------------------|----------------------| | LoggingHook | Write a line‑by‑line trace of every read/write, optionally throttling large payloads. | await logger.LogAsync($"bytesRead bytes read from ctx.StreamId"); | | CompressionHook | Transparent GZip/Deflate compression on the fly. | var compressor = new GZipStream(_inner, CompressionMode.Compress, leaveOpen:true); | | EncryptionHook | Apply AES‑CTR or ChaCha20 encryption per‑chunk. | Array.Copy(_cipher.TransformBlock(buffer, offset, count), 0, buffer, offset, count); | | MetricsHook | Emit Prometheus counters or OpenTelemetry spans for each operation. | meter.CreateHistogram<long>("stream.read.bytes").Record(bytesRead); | | ThrottlingHook | Enforce a max‑bytes‑per‑second quota. | await _rateLimiter.WaitAsync(bytesRead, cancellationToken); | Why the name “Smeagol”? In the original open‑source demo the author likened the hook to Smeagol – it “follows” the stream everywhere, silently observing and occasionally meddling. The name stuck and became part of the public API. 5. Extending the hook – writing your own THook 5.1 Minimal stub public sealed class MyCustomHook : IStreamHook
public void Dispose(IHookContext ctx) /* free any unmanaged resources */ | | Async‑over‑sync | Never call
return bytesRead;
var listener = new DiagnosticListener("StreamFab.KeepStreams.HookSmeagol"); listener.Subscribe(new MyObserver()); These events are invaluable when you need to without modifying the hook code itself. 8. Common pitfalls & how to avoid them | Pitfall | Symptom | Fix | |---------|---------|-----| | Double‑dispose | ObjectDisposedException on later reads/writes. | Ensure the hook does not call Dispose on the inner stream unless it owns it. The wrapper already disposes the inner stream once. | | Blocking async hooks | Thread‑pool starvation, deadlocks. | Never use .Result / .Wait() inside async hook methods; always await . | | Changing CanSeek | Consumer thinks the stream is seekable but it isn’t. | Propagate CanSeek from the inner stream unchanged; if you need to add seeking (e.g., buffering), expose a new wrapper type rather than HookSmeagol . | | Unbounded memory growth | Hook buffers grow without limit (e.g., a logging hook that stores every payload). | Use bounded buffers or stream the data to a file/DB as it arrives. | | Incorrect async signature | ValueTask returned but not awaited → lost exceptions. | Always await the returned ValueTask inside the wrapper (the library already does this). | 9. Sample end‑to‑end usage Below is a short, self‑contained console demo that composes three hooks:
// Then the inner stream is disposed (unless the hook says otherwise) _inner.Dispose(); base.Dispose(disposing); public override async ValueTask<
public override async ValueTask<int> ReadAsync( Memory<byte> destination, CancellationToken cancellationToken = default)
// 2. Actual read from inner stream int bytesRead = await _inner.ReadAsync(destination, cancellationToken) .ConfigureAwait(false);
public void BeforeWrite(IHookContext ctx, byte[] buffer, int offset, int count) /* … */ public void AfterWrite(IHookContext ctx, byte[] buffer, int offset, int count) /* … */
return bytesRead;
// Async overloads (optional but recommended) public ValueTask BeforeReadAsync(IHookContext ctx, Memory<byte> destination, CancellationToken ct) => default; public ValueTask AfterReadAsync(IHookContext ctx, ReadOnlyMemory<byte> data, CancellationToken ct) => default; // … similar for Write, Seek, etc.


































