Skip to content

Commit 01c0ecb

Browse files
committed
Add support for HttpApplication.PreSendRequestContent
1 parent 6cc8452 commit 01c0ecb

File tree

12 files changed

+315
-110
lines changed

12 files changed

+315
-110
lines changed

samples/Modules/ModulesLibrary/BaseModule.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void Dispose()
1212
{
1313
}
1414

15-
public void Init(HttpApplication application)
15+
public virtual void Init(HttpApplication application)
1616
{
1717
if (application is null)
1818
{
@@ -38,6 +38,7 @@ public void Init(HttpApplication application)
3838
application.PostUpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.PostUpdateRequestCache));
3939
application.PreRequestHandlerExecute += (s, e) => WriteDetails(s, nameof(application.PreRequestHandlerExecute));
4040
application.PreSendRequestHeaders += (s, e) => WriteDetails(s, nameof(application.PreSendRequestHeaders));
41+
application.PreSendRequestContent += (s, e) => WriteDetails(s, nameof(application.PreSendRequestContent));
4142
application.ReleaseRequestState += (s, e) => WriteDetails(s, nameof(application.ReleaseRequestState));
4243
application.ResolveRequestCache += (s, e) => WriteDetails(s, nameof(application.ResolveRequestCache));
4344
application.UpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.UpdateRequestCache));

samples/Modules/ModulesLibrary/EventsModule.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,31 @@ public class EventsModule : BaseModule
1010
public const string Complete = "complete";
1111
public const string Throw = "throw";
1212

13+
public override void Init(HttpApplication application)
14+
{
15+
application!.BeginRequest += (s, o) => ((HttpApplication)s!).Context.Response.ContentType = "text/plain";
16+
base.Init(application);
17+
}
18+
1319
protected override void InvokeEvent(HttpContext context, string name)
1420
{
1521
if (context is null)
1622
{
1723
throw new ArgumentNullException(nameof(context));
1824
}
1925

20-
if (context.CurrentNotification == RequestNotification.BeginRequest)
26+
var action = context.Request.QueryString["action"];
27+
28+
var writeOutputBefore = action != Throw;
29+
30+
if (writeOutputBefore)
2131
{
22-
context.Response.ContentType = "text/plain";
32+
context.Response.Output.WriteLine(name);
2333
}
2434

25-
context.Response.Output.WriteLine(name);
26-
2735
if (string.Equals(name, context.Request.QueryString["notification"], StringComparison.OrdinalIgnoreCase))
2836
{
29-
switch (context.Request.QueryString["action"])
37+
switch (action)
3038
{
3139
case End:
3240
context.Response.End();
@@ -38,6 +46,11 @@ protected override void InvokeEvent(HttpContext context, string name)
3846
throw new InvalidOperationException();
3947
}
4048
}
49+
50+
if (!writeOutputBefore)
51+
{
52+
context.Response.Output.WriteLine(name);
53+
}
4154
}
4255
}
4356
}

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/HttpApplicationFeature.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Threading.Tasks;
78
using System.Web;
89
using Microsoft.Extensions.ObjectPool;
@@ -16,7 +17,6 @@ internal sealed class HttpApplicationFeature : IHttpApplicationFeature, IHttpRes
1617
ApplicationEvent.LogRequest,
1718
ApplicationEvent.PostLogRequest,
1819
ApplicationEvent.EndRequest,
19-
ApplicationEvent.PreSendRequestHeaders,
2020
];
2121

2222
private readonly IHttpResponseEndFeature _previous;
@@ -30,13 +30,6 @@ public HttpApplicationFeature(HttpContextCore context, IHttpResponseEndFeature p
3030
_contextOrApplication = context;
3131
_pool = pool;
3232
_previous = previousEnd;
33-
34-
context.Response.OnStarting(static state =>
35-
{
36-
var context = (HttpContextCore)state;
37-
38-
return context.Features.GetRequired<IHttpApplicationFeature>().RaiseEventAsync(ApplicationEvent.PreSendRequestHeaders).AsTask();
39-
}, context);
4033
}
4134

4235
public RequestNotification CurrentNotification { get; set; }
@@ -102,7 +95,7 @@ private void RaiseEvent(ApplicationEvent appEvent)
10295
{
10396
Application.InvokeEvent(appEvent);
10497
}
105-
catch (Exception ex)
98+
catch (Exception ex) when (!IsReentrant(ex))
10699
{
107100
AddError(ex);
108101
Application.InvokeEvent(ApplicationEvent.Error);
@@ -114,6 +107,24 @@ private void RaiseEvent(ApplicationEvent appEvent)
114107
}
115108
}
116109

110+
/// <summary>
111+
/// Checks to see if we're attempting to invoke the Error event when we're currently throwing the existing errors.
112+
/// This would imply a nested event invocation and we don't need to handle rethrow
113+
/// </summary>
114+
private bool IsReentrant(Exception e)
115+
{
116+
if (_exceptions is [{ } exception])
117+
{
118+
return ReferenceEquals(e, exception);
119+
}
120+
else if (e is AggregateException a && _exceptions is { } exceptions)
121+
{
122+
return a.InnerExceptions.SequenceEqual(exceptions);
123+
}
124+
125+
return false;
126+
}
127+
117128
private void ThrowIfErrors()
118129
{
119130
if (_exceptions is [{ } exception])
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Buffers;
6+
using System.IO;
7+
using System.IO.Pipelines;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Http.Features;
12+
13+
namespace Microsoft.AspNetCore.SystemWebAdapters.Features;
14+
15+
/// <summary>
16+
/// Used to intercept calls to Flush so we can raise <see cref="ApplicationEvent.PreSendRequestHeaders"/> and <see cref="ApplicationEvent.PreSendRequestContent"/> events
17+
/// </summary>
18+
internal sealed class HttpApplicationPreSendEventsResponseBodyFeature : PipeWriter, IHttpResponseBodyFeature
19+
{
20+
private State _state;
21+
private int _byteCount;
22+
private ArrayBufferWriter<byte>? _buffer;
23+
24+
private readonly PipeWriter _pipe;
25+
private readonly IHttpResponseBodyFeature _other;
26+
private readonly HttpContextCore _context;
27+
28+
private enum State
29+
{
30+
NotStarted,
31+
RaisingPreHeader,
32+
ReadyForContent,
33+
RaisingPreContent,
34+
}
35+
36+
public HttpApplicationPreSendEventsResponseBodyFeature(HttpContextCore context, IHttpResponseBodyFeature other)
37+
{
38+
_other = other;
39+
_pipe = _other.Writer;
40+
_context = context;
41+
}
42+
43+
public Stream Stream => Writer.AsStream();
44+
45+
public PipeWriter Writer => this;
46+
47+
Task IHttpResponseBodyFeature.CompleteAsync() => _other.CompleteAsync();
48+
49+
void IHttpResponseBodyFeature.DisableBuffering() => _other.DisableBuffering();
50+
51+
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
52+
=> SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellationToken);
53+
54+
public Task StartAsync(CancellationToken cancellationToken = default) => _other.StartAsync(cancellationToken);
55+
56+
public override void Advance(int bytes)
57+
{
58+
// Don't track additional bytes written when events are being raised or we end up with some recursion
59+
if (_state is not State.RaisingPreContent or State.RaisingPreHeader)
60+
{
61+
_byteCount += bytes;
62+
}
63+
64+
_pipe.Advance(bytes);
65+
}
66+
67+
public override void CancelPendingFlush() => _pipe.CancelPendingFlush();
68+
69+
public override void Complete(Exception? exception = null) => _pipe.Complete(exception);
70+
71+
public override async ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
72+
{
73+
// Only need to raise events if data will be flushed and the feature is available
74+
if (_byteCount > 0 && _context.Features.Get<IHttpApplicationFeature>() is { } httpApplication)
75+
{
76+
_byteCount = 0;
77+
78+
if (_state is State.NotStarted)
79+
{
80+
_state = State.RaisingPreHeader;
81+
await _context.Features.GetRequired<IHttpApplicationFeature>().RaiseEventAsync(ApplicationEvent.PreSendRequestHeaders);
82+
_state = State.ReadyForContent;
83+
}
84+
85+
if (_state is State.ReadyForContent)
86+
{
87+
_state = State.RaisingPreContent;
88+
await httpApplication.RaiseEventAsync(ApplicationEvent.PreSendRequestContent);
89+
_state = State.ReadyForContent;
90+
}
91+
}
92+
93+
return await _pipe.FlushAsync(cancellationToken);
94+
}
95+
96+
public override Memory<byte> GetMemory(int sizeHint = 0) => _pipe.GetMemory(sizeHint);
97+
98+
public override Span<byte> GetSpan(int sizeHint = 0) => _pipe.GetSpan(sizeHint);
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Features;
7+
using Microsoft.AspNetCore.SystemWebAdapters.Features;
8+
9+
namespace Microsoft.AspNetCore.SystemWebAdapters;
10+
11+
internal sealed class RegisterHttpApplicationPreSendEventsMiddleware(RequestDelegate next)
12+
{
13+
public async Task InvokeAsync(HttpContextCore context)
14+
{
15+
var previous = context.Features.GetRequired<IHttpResponseBodyFeature>();
16+
var feature = new HttpApplicationPreSendEventsResponseBodyFeature(context, previous);
17+
18+
context.Features.Set<IHttpResponseBodyFeature>(feature);
19+
20+
await next(context);
21+
22+
context.Features.Set<IHttpResponseBodyFeature>(previous);
23+
}
24+
}

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
145145
=> builder =>
146146
{
147147
builder.UseMiddleware<SetHttpContextTimestampMiddleware>();
148+
149+
if (builder.AreHttpApplicationEventsRequired())
150+
{
151+
// Must be registered first in order to intercept each flush to the client
152+
builder.UseMiddleware<RegisterHttpApplicationPreSendEventsMiddleware>();
153+
}
154+
148155
builder.UseMiddleware<RegisterAdapterFeaturesMiddleware>();
149156
builder.UseMiddleware<SessionStateMiddleware>();
150157

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/ApplicationEvent.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public enum ApplicationEvent
5757

5858
PreSendRequestHeaders,
5959

60+
PreSendRequestContent,
61+
6062
Error,
6163

6264
SessionStart,

src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public event System.EventHandler PostRequestHandlerExecute { add { } remove { }
5252
public event System.EventHandler PostResolveRequestCache { add { } remove { } }
5353
public event System.EventHandler PostUpdateRequestCache { add { } remove { } }
5454
public event System.EventHandler PreRequestHandlerExecute { add { } remove { } }
55+
public event System.EventHandler PreSendRequestContent { add { } remove { } }
5556
public event System.EventHandler PreSendRequestHeaders { add { } remove { } }
5657
public event System.EventHandler ReleaseRequestState { add { } remove { } }
5758
public event System.EventHandler ResolveRequestCache { add { } remove { } }

src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ public event EventHandler? PreSendRequestHeaders
251251
remove => RemoveEvent(value);
252252
}
253253

254+
public event EventHandler? PreSendRequestContent
255+
{
256+
add => AddEvent(value);
257+
remove => RemoveEvent(value);
258+
}
259+
254260
private void AddEvent(EventHandler? handler, [CallerMemberName] string? name = null)
255261
{
256262
if (handler is null)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests;
5+
6+
public class BufferedModuleTests : ModuleTests
7+
{
8+
public BufferedModuleTests()
9+
: base(true)
10+
{
11+
}
12+
}

0 commit comments

Comments
 (0)