From 30094736fe48e850cfb2bab41fc48f6389a4e0d7 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 4 Sep 2026 08:11:25 -0400 Subject: [PATCH 1/4] feat(core): add opt-in buffering for raw invocation event logging Middleware wanting to log the raw request payload previously had no safe way to read IInvocationDataFeature.EventStream: it's read once, lazily, by event deserialization, and isn't guaranteed to be seekable. Reading it directly in middleware would starve deserialization. Add EnableBuffering() (mirrors ASP.NET Core's HttpRequest.EnableBuffering) which buffers the stream into a seekable MemoryStream only when needed, plus an ILambdaInvocationContext.EnableEventBuffering() convenience extension. Opt-in per invocation to avoid the copy when nobody is logging. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qn9kagNWsCubTrnzN6NEqV --- .../Features/IInvocationDataFeature.cs | 13 +++ ...eatureLambdaInvocationContextExtensions.cs | 14 +++ .../Core/Features/InvocationDataFeature.cs | 23 +++- src/MinimalLambda/README.md | 21 ++++ ...reLambdaInvocationContextExtensionsTest.cs | 29 +++++ .../Features/InvocationDataFeatureTests.cs | 103 ++++++++++++++++++ 6 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 tests/MinimalLambda.UnitTests/Core/Features/InvocationDataFeatureTests.cs diff --git a/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs b/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs index b8b11f93..984475bc 100644 --- a/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs +++ b/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs @@ -20,4 +20,17 @@ public interface IInvocationDataFeature : IDisposable /// needed to redirect response data to a different destination. /// Stream ResponseStream { get; set; } + + /// + /// Ensures is seekable, buffering it into memory first if + /// necessary. Enables middleware to read the raw event payload (for example, to log it) + /// without consuming the stream that event deserialization depends on. + /// + /// + /// Call before reading . After reading, reset + /// .Position to 0 so downstream event deserialization + /// can still consume it. Opt in per-invocation, since it buffers the event payload into + /// memory even when it is already seekable. + /// + void EnableBuffering(); } diff --git a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs index d9f1c539..5f4c1a10 100644 --- a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs +++ b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs @@ -105,5 +105,19 @@ public T GetRequiredResponse() return responseT; } + + /// + /// Ensures the raw Lambda event stream is seekable, buffering it into memory first if + /// necessary. Call from middleware before reading + /// directly (for example, to log the raw + /// payload), then reset its Position to 0 afterward so event deserialization + /// can still consume it. + /// + public void EnableEventBuffering() + { + ArgumentNullException.ThrowIfNull(context); + + context.Features.GetRequired().EnableBuffering(); + } } } diff --git a/src/MinimalLambda/Core/Features/InvocationDataFeature.cs b/src/MinimalLambda/Core/Features/InvocationDataFeature.cs index faa8fc03..bacbb398 100644 --- a/src/MinimalLambda/Core/Features/InvocationDataFeature.cs +++ b/src/MinimalLambda/Core/Features/InvocationDataFeature.cs @@ -2,12 +2,31 @@ namespace MinimalLambda; internal sealed class InvocationDataFeature : IInvocationDataFeature { - public required Stream EventStream { get; init; } + private Stream _eventStream = null!; + + public required Stream EventStream + { + get => _eventStream; + init => _eventStream = value; + } + public Stream ResponseStream { get; set; } = new MemoryStream(); + public void EnableBuffering() + { + if (_eventStream.CanSeek) + return; + + var buffered = new MemoryStream(); + _eventStream.CopyTo(buffered); + _eventStream.Dispose(); + buffered.Position = 0L; + _eventStream = buffered; + } + /// /// Dispose the underlying stream. We only dispose of the event stream, not the response /// stream as the Lambda bootstrap will dispose of it. /// - public void Dispose() => EventStream.Dispose(); + public void Dispose() => _eventStream.Dispose(); } diff --git a/src/MinimalLambda/README.md b/src/MinimalLambda/README.md index 47bd1bda..b9c45660 100644 --- a/src/MinimalLambda/README.md +++ b/src/MinimalLambda/README.md @@ -166,6 +166,27 @@ lambda.UseMiddleware(async (context, next) => }); ``` +To log the raw event payload from middleware, call `EnableEventBuffering()` first—by default the +underlying stream may not be seekable and is meant to be read once by event deserialization: + +```csharp +lambda.UseMiddleware(async (context, next) => +{ + context.EnableEventBuffering(); + var invocationData = context.Features.GetRequired(); + + using (var reader = new StreamReader(invocationData.EventStream, leaveOpen: true)) + logger.LogInformation("Request: {Raw}", await reader.ReadToEndAsync()); + invocationData.EventStream.Position = 0; // reset for event deserialization + + await next(context); +}); +``` + +The response stream can always be replaced (for example, with a `Stream` wrapper that tees writes +to a logger) since `IInvocationDataFeature.ResponseStream` is serialized to after the middleware +pipeline completes. + ### Lambda Lifecycle The framework manages initialization and shutdown phases automatically. Add as many callbacks as diff --git a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs index 67b0a7dc..67122707 100644 --- a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs +++ b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs @@ -557,6 +557,27 @@ public void GetRequiredResponse_WorksWithDifferentResponseTypes( #endregion + #region EnableEventBuffering Tests + + [Theory] + [AutoNSubstituteData] + public void EnableEventBuffering_CallsEnableBufferingOnInvocationDataFeature( + [Frozen] IFeatureCollection features, + ILambdaInvocationContext context, + IInvocationDataFeature invocationDataFeature) + { + // Arrange + features.Get().Returns(invocationDataFeature); + + // Act + context.EnableEventBuffering(); + + // Assert + invocationDataFeature.Received(1).EnableBuffering(); + } + + #endregion + #region Null Context Tests [Fact] @@ -607,6 +628,14 @@ public void GetRequiredResponse_ThrowsArgumentNullExceptionWhenContextIsNull() act.Should().ThrowExactly(); } + [Fact] + public void EnableEventBuffering_ThrowsArgumentNullExceptionWhenContextIsNull() + { + // Act & Assert + var act = () => ((ILambdaInvocationContext?)null)!.EnableEventBuffering(); + act.Should().ThrowExactly(); + } + #endregion #region Test Fixtures diff --git a/tests/MinimalLambda.UnitTests/Core/Features/InvocationDataFeatureTests.cs b/tests/MinimalLambda.UnitTests/Core/Features/InvocationDataFeatureTests.cs new file mode 100644 index 00000000..690085ed --- /dev/null +++ b/tests/MinimalLambda.UnitTests/Core/Features/InvocationDataFeatureTests.cs @@ -0,0 +1,103 @@ +using System.Text; + +namespace MinimalLambda.UnitTests.Core.Features; + +[TestSubject(typeof(InvocationDataFeature))] +public class InvocationDataFeatureTests +{ + [Fact] + public void EnableBuffering_WhenEventStreamAlreadySeekable_DoesNotReplaceStream() + { + // Arrange + var eventStream = new MemoryStream("payload"u8.ToArray()); + var feature = new InvocationDataFeature { EventStream = eventStream }; + + // Act + feature.EnableBuffering(); + + // Assert + feature.EventStream.Should().BeSameAs(eventStream); + } + + [Fact] + public void EnableBuffering_WhenEventStreamNotSeekable_ReplacesWithSeekableCopy() + { + // Arrange + var payload = "payload"u8.ToArray(); + var eventStream = new NonSeekableStream(payload); + var feature = new InvocationDataFeature { EventStream = eventStream }; + + // Act + feature.EnableBuffering(); + + // Assert + feature.EventStream.CanSeek.Should().BeTrue(); + } + + [Fact] + public void EnableBuffering_WhenEventStreamNotSeekable_PreservesContentAndResetsPosition() + { + // Arrange + var payload = "payload"u8.ToArray(); + var eventStream = new NonSeekableStream(payload); + var feature = new InvocationDataFeature { EventStream = eventStream }; + + // Act + feature.EnableBuffering(); + + // Assert + feature.EventStream.Position.Should().Be(0L); + using var reader = new StreamReader(feature.EventStream, Encoding.UTF8); + reader.ReadToEnd().Should().Be("payload"); + } + + [Fact] + public void + EnableBuffering_AfterReadingBufferedStream_AllowsResettingPositionForRedeserialization() + { + // Arrange + var payload = "payload"u8.ToArray(); + var eventStream = new NonSeekableStream(payload); + var feature = new InvocationDataFeature { EventStream = eventStream }; + feature.EnableBuffering(); + + using (var reader = new StreamReader(feature.EventStream, Encoding.UTF8, leaveOpen: true)) + reader.ReadToEnd(); + + // Act + feature.EventStream.Position = 0L; + + // Assert + using var reader2 = new StreamReader(feature.EventStream, Encoding.UTF8); + reader2.ReadToEnd().Should().Be("payload"); + } + + private sealed class NonSeekableStream(byte[] data) : Stream + { + private readonly MemoryStream _inner = new(data); + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _inner.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _inner.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + } +} From af010c0d55e8a140385a4b271bcb341e05067585 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 4 Sep 2026 08:23:36 -0400 Subject: [PATCH 2/4] chore(deps): update NuGet package versions Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qn9kagNWsCubTrnzN6NEqV --- Directory.Packages.props | 54 ++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 142b28ea..81dbe99e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,61 +13,61 @@ - + - - - + + + - - - + + + - + - - - + + + - + - - - - - + + + + + - - + + - - + + - - + + - + - - - + + + From 4e42345cb1b30b21e2a093e5e8fc725e63cc0956 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 4 Sep 2026 08:36:29 -0400 Subject: [PATCH 3/4] refactor(core): split event buffering into an optional feature interface Adding EnableBuffering() directly to the public IInvocationDataFeature interface source-breaks anyone who implements it themselves (custom test hosts, hand-rolled fakes). Split it into a separate IInvocationDataBufferingFeature, probed for via the feature collection like ASP.NET Core's optional HTTP features, so IInvocationDataFeature stays untouched and existing implementations keep compiling. Addresses review feedback from chatgpt-codex-connector on PR #398. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qn9kagNWsCubTrnzN6NEqV --- .../IInvocationDataBufferingFeature.cs | 28 +++++++++++++++++++ .../Features/IInvocationDataFeature.cs | 13 --------- ...eatureLambdaInvocationContextExtensions.cs | 6 +++- .../Core/Features/InvocationDataFeature.cs | 3 +- .../Runtime/LambdaHandlerComposer.cs | 3 ++ ...reLambdaInvocationContextExtensionsTest.cs | 28 ++++++++++++++++--- 6 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs diff --git a/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs b/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs new file mode 100644 index 00000000..971eb138 --- /dev/null +++ b/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs @@ -0,0 +1,28 @@ +namespace MinimalLambda; + +/// +/// Optional capability of that allows buffering the +/// invocation event stream into memory so it can be read outside of event deserialization. +/// +/// +/// Registered in alongside +/// by implementations that support it. Probe for it with +/// context.Features.Get<IInvocationDataBufferingFeature>() (or the +/// context.EnableEventBuffering() convenience extension) rather than assuming every +/// implementation supports buffering. +/// +public interface IInvocationDataBufferingFeature +{ + /// + /// Ensures is seekable, buffering it into + /// memory first if necessary. Enables middleware to read the raw event payload (for example, + /// to log it) without consuming the stream that event deserialization depends on. + /// + /// + /// Call before reading . After reading, reset + /// EventStream.Position to 0 so downstream event deserialization can still + /// consume it. Opt in per-invocation, since it buffers the event payload into memory even + /// when it is already seekable. + /// + void EnableBuffering(); +} diff --git a/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs b/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs index 984475bc..b8b11f93 100644 --- a/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs +++ b/src/MinimalLambda.Abstractions/Features/IInvocationDataFeature.cs @@ -20,17 +20,4 @@ public interface IInvocationDataFeature : IDisposable /// needed to redirect response data to a different destination. /// Stream ResponseStream { get; set; } - - /// - /// Ensures is seekable, buffering it into memory first if - /// necessary. Enables middleware to read the raw event payload (for example, to log it) - /// without consuming the stream that event deserialization depends on. - /// - /// - /// Call before reading . After reading, reset - /// .Position to 0 so downstream event deserialization - /// can still consume it. Opt in per-invocation, since it buffers the event payload into - /// memory even when it is already seekable. - /// - void EnableBuffering(); } diff --git a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs index 5f4c1a10..51b82ac7 100644 --- a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs +++ b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs @@ -113,11 +113,15 @@ public T GetRequiredResponse() /// payload), then reset its Position to 0 afterward so event deserialization /// can still consume it. /// + /// + /// Thrown when no is available in the + /// context. + /// public void EnableEventBuffering() { ArgumentNullException.ThrowIfNull(context); - context.Features.GetRequired().EnableBuffering(); + context.Features.GetRequired().EnableBuffering(); } } } diff --git a/src/MinimalLambda/Core/Features/InvocationDataFeature.cs b/src/MinimalLambda/Core/Features/InvocationDataFeature.cs index bacbb398..e7e41687 100644 --- a/src/MinimalLambda/Core/Features/InvocationDataFeature.cs +++ b/src/MinimalLambda/Core/Features/InvocationDataFeature.cs @@ -1,6 +1,7 @@ namespace MinimalLambda; -internal sealed class InvocationDataFeature : IInvocationDataFeature +internal sealed class InvocationDataFeature + : IInvocationDataFeature, IInvocationDataBufferingFeature { private Stream _eventStream = null!; diff --git a/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs b/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs index 2ea86788..f5380f35 100644 --- a/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs +++ b/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs @@ -70,6 +70,9 @@ async Task CreateRequestHandler(Stream inputStream, ILambdaContext lambd using var invocationDataFeature = _invocationDataFeatureFactory.Create(inputStream); lambdaInvocationContext.Features.Set(invocationDataFeature); + if (invocationDataFeature is IInvocationDataBufferingFeature bufferingFeature) + lambdaInvocationContext.Features.Set(bufferingFeature); + // Invoke the handler wrapped in the middleware pipeline. await handler.Invoke(lambdaInvocationContext).ConfigureAwait(false); diff --git a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs index 67122707..be8ec35a 100644 --- a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs +++ b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs @@ -561,19 +561,39 @@ public void GetRequiredResponse_WorksWithDifferentResponseTypes( [Theory] [AutoNSubstituteData] - public void EnableEventBuffering_CallsEnableBufferingOnInvocationDataFeature( + public void EnableEventBuffering_CallsEnableBufferingOnInvocationDataBufferingFeature( [Frozen] IFeatureCollection features, ILambdaInvocationContext context, - IInvocationDataFeature invocationDataFeature) + IInvocationDataBufferingFeature bufferingFeature) { // Arrange - features.Get().Returns(invocationDataFeature); + features.Get().Returns(bufferingFeature); // Act context.EnableEventBuffering(); // Assert - invocationDataFeature.Received(1).EnableBuffering(); + bufferingFeature.Received(1).EnableBuffering(); + } + + [Theory] + [AutoNSubstituteData] + public void EnableEventBuffering_ThrowsInvalidOperationExceptionWhenFeatureNotFound( + [Frozen] IFeatureCollection features, + ILambdaInvocationContext context) + { + // Arrange + features + .Get() + .Returns((IInvocationDataBufferingFeature?)null); + + // Act & Assert + var act = () => context.EnableEventBuffering(); + act + .Should() + .ThrowExactly() + .WithMessage( + $"Feature of type '{typeof(IInvocationDataBufferingFeature).FullName}' is not available in the collection."); } #endregion From 986bc80eb38db37d4ca60433a990bc22d6ce1cc7 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Fri, 4 Sep 2026 08:50:31 -0400 Subject: [PATCH 4/4] fix(core): resolve buffering capability from the active event feature The prior fix registered IInvocationDataBufferingFeature under its own key in the feature collection, alongside IInvocationDataFeature. Since Features.Set keys strictly by typeof(T), those became two independent slots pointing at the same instance. Middleware replacing IInvocationDataFeature (Features.Set(...)) would leave the buffering slot pointing at the stale instance, so EnableEventBuffering() would silently buffer the wrong stream while the actually-active one still gets consumed once by deserialization - the exact starvation bug this feature exists to prevent. Drop the separate registration. EnableEventBuffering() now resolves whatever IInvocationDataFeature is currently active and probes it for the capability, so there's no second slot to fall out of sync. Addresses further review feedback from chatgpt-codex-connector on PR #398. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qn9kagNWsCubTrnzN6NEqV --- .../IInvocationDataBufferingFeature.cs | 15 ++++--- ...eatureLambdaInvocationContextExtensions.cs | 11 +++++- .../Runtime/LambdaHandlerComposer.cs | 3 -- ...reLambdaInvocationContextExtensionsTest.cs | 39 ++++++++++++++----- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs b/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs index 971eb138..b9768995 100644 --- a/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs +++ b/src/MinimalLambda.Abstractions/Features/IInvocationDataBufferingFeature.cs @@ -5,11 +5,16 @@ namespace MinimalLambda; /// invocation event stream into memory so it can be read outside of event deserialization. /// /// -/// Registered in alongside -/// by implementations that support it. Probe for it with -/// context.Features.Get<IInvocationDataBufferingFeature>() (or the -/// context.EnableEventBuffering() convenience extension) rather than assuming every -/// implementation supports buffering. +/// Implemented alongside by implementations that support +/// it, rather than registered as its own entry in . Probe the +/// currently active for this capability (for example, +/// context.Features.Get<IInvocationDataFeature>() is IInvocationDataBufferingFeature, +/// or the context.EnableEventBuffering() convenience extension) rather than looking this +/// type up in the feature collection directly or assuming every +/// implementation supports buffering. Looking it up +/// separately in the feature collection risks it becoming out of sync with whichever +/// is currently registered, if middleware replaces that +/// registration. /// public interface IInvocationDataBufferingFeature { diff --git a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs index 51b82ac7..06f2ac58 100644 --- a/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs +++ b/src/MinimalLambda/Core/Features/FeatureLambdaInvocationContextExtensions.cs @@ -121,7 +121,16 @@ public void EnableEventBuffering() { ArgumentNullException.ThrowIfNull(context); - context.Features.GetRequired().EnableBuffering(); + if (context.Features.Get() is IInvocationDataBufferingFeature + bufferingFeature) + { + bufferingFeature.EnableBuffering(); + return; + } + + throw new InvalidOperationException( + $"Feature of type '{typeof(IInvocationDataBufferingFeature).FullName}' is not " + + "available in the context."); } } } diff --git a/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs b/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs index f5380f35..2ea86788 100644 --- a/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs +++ b/src/MinimalLambda/Runtime/LambdaHandlerComposer.cs @@ -70,9 +70,6 @@ async Task CreateRequestHandler(Stream inputStream, ILambdaContext lambd using var invocationDataFeature = _invocationDataFeatureFactory.Create(inputStream); lambdaInvocationContext.Features.Set(invocationDataFeature); - if (invocationDataFeature is IInvocationDataBufferingFeature bufferingFeature) - lambdaInvocationContext.Features.Set(bufferingFeature); - // Invoke the handler wrapped in the middleware pipeline. await handler.Invoke(lambdaInvocationContext).ConfigureAwait(false); diff --git a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs index be8ec35a..c163ccec 100644 --- a/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs +++ b/tests/MinimalLambda.UnitTests/Core/Features/FeatureLambdaInvocationContextExtensionsTest.cs @@ -561,31 +561,50 @@ public void GetRequiredResponse_WorksWithDifferentResponseTypes( [Theory] [AutoNSubstituteData] - public void EnableEventBuffering_CallsEnableBufferingOnInvocationDataBufferingFeature( + public void EnableEventBuffering_CallsEnableBufferingWhenActiveFeatureSupportsBuffering( [Frozen] IFeatureCollection features, - ILambdaInvocationContext context, - IInvocationDataBufferingFeature bufferingFeature) + ILambdaInvocationContext context) { // Arrange - features.Get().Returns(bufferingFeature); + var invocationDataFeature = + Substitute.For(); + features.Get().Returns(invocationDataFeature); // Act context.EnableEventBuffering(); // Assert - bufferingFeature.Received(1).EnableBuffering(); + ((IInvocationDataBufferingFeature)invocationDataFeature).Received(1).EnableBuffering(); } [Theory] [AutoNSubstituteData] - public void EnableEventBuffering_ThrowsInvalidOperationExceptionWhenFeatureNotFound( + public void EnableEventBuffering_ThrowsInvalidOperationExceptionWhenNoInvocationDataFeature( [Frozen] IFeatureCollection features, ILambdaInvocationContext context) { // Arrange - features - .Get() - .Returns((IInvocationDataBufferingFeature?)null); + features.Get().Returns((IInvocationDataFeature?)null); + + // Act & Assert + var act = () => context.EnableEventBuffering(); + act + .Should() + .ThrowExactly() + .WithMessage( + $"Feature of type '{typeof(IInvocationDataBufferingFeature).FullName}' is not available in the context."); + } + + [Theory] + [AutoNSubstituteData] + public void + EnableEventBuffering_ThrowsInvalidOperationExceptionWhenActiveFeatureDoesNotSupportBuffering( + [Frozen] IFeatureCollection features, + ILambdaInvocationContext context, + IInvocationDataFeature invocationDataFeature) + { + // Arrange + features.Get().Returns(invocationDataFeature); // Act & Assert var act = () => context.EnableEventBuffering(); @@ -593,7 +612,7 @@ public void EnableEventBuffering_ThrowsInvalidOperationExceptionWhenFeatureNotFo .Should() .ThrowExactly() .WithMessage( - $"Feature of type '{typeof(IInvocationDataBufferingFeature).FullName}' is not available in the collection."); + $"Feature of type '{typeof(IInvocationDataBufferingFeature).FullName}' is not available in the context."); } #endregion