Streamfab.keepstreams.generic.hook-smeagol-ther... Review
// 2. Actual read from inner stream int bytesRead = await _inner.ReadAsync(destination, cancellationToken) .ConfigureAwait(false);
return bytesRead;
// 1. Pre‑hook (may adjust the requested length) _hook.BeforeReadAsync(_ctx, destination, cancellationToken);
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 StreamFab.KeepStreams.Generic.Hook-Smeagol-TheR...
public void BeforeWrite(IHookContext ctx, byte[] buffer, int offset, int count) /* … */ public void AfterWrite(IHookContext ctx, byte[] buffer, int offset, int count) /* … */
private readonly Stream _inner; private readonly THook _hook; private readonly IHookContext _ctx; // …
var inner = provider.GetRequiredService<FileStream>(); var factory = provider.GetRequiredService<IHookFactory<MyCustomHook>>(); return new HookSmeagol<MyCustomHook>(inner, factory.Create(provider)); ); HookSmeagol can be stacked : | Hook name | Typical use‑case | Sample
The pattern mirrors Read ; the hook receives the buffer before the inner write and again after the write completes. 3.4 Seek , SetLength , Flush All these methods follow the same pre‑hook → inner operation → post‑hook flow. The async variants are implemented using ValueTask when possible to avoid allocations. 3.5 Disposal protected override void Dispose(bool disposing)
// 2. The inner stream performs the real read int bytesRead = _inner.Read(buffer, offset, count);
public sealed class LoggingHook : IStreamHook { public void BeforeRead(IHookContext ctx, byte[] buffer, int offset, int count) => Console.WriteLine($"[LOG] About to read | Array
services.AddSingleton<IHookFactory<MyCustomHook>, MyCustomHookFactory>(); services.AddTransient(typeof(Stream), provider =>
// Then the inner stream is disposed (unless the hook says otherwise) _inner.Dispose(); base.Dispose(disposing);
// 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.
public override async ValueTask<int> ReadAsync( Memory<byte> destination, CancellationToken cancellationToken = default)
if (disposing) // Hook gets notified first – it can release its own resources _hook.Dispose(_ctx);