Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion samples/Modules/ModulesLibrary/BaseModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void Dispose()
{
}

public void Init(HttpApplication application)
public virtual void Init(HttpApplication application)
{
if (application is null)
{
Expand All @@ -38,6 +38,7 @@ public void Init(HttpApplication application)
application.PostUpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.PostUpdateRequestCache));
application.PreRequestHandlerExecute += (s, e) => WriteDetails(s, nameof(application.PreRequestHandlerExecute));
application.PreSendRequestHeaders += (s, e) => WriteDetails(s, nameof(application.PreSendRequestHeaders));
application.PreSendRequestContent += (s, e) => WriteDetails(s, nameof(application.PreSendRequestContent));
application.ReleaseRequestState += (s, e) => WriteDetails(s, nameof(application.ReleaseRequestState));
application.ResolveRequestCache += (s, e) => WriteDetails(s, nameof(application.ResolveRequestCache));
application.UpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.UpdateRequestCache));
Expand Down
27 changes: 22 additions & 5 deletions samples/Modules/ModulesLibrary/EventsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,35 @@ public class EventsModule : BaseModule
public const string Complete = "complete";
public const string Throw = "throw";

public override void Init(HttpApplication application)
{
if (application is { })
{
application.BeginRequest += (s, o) => ((HttpApplication)s!).Context.Response.ContentType = "text/plain";

base.Init(application);
}
}

protected override void InvokeEvent(HttpContext context, string name)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.CurrentNotification == RequestNotification.BeginRequest)
var action = context.Request.QueryString["action"];

var writeOutputBefore = action != Throw;

if (writeOutputBefore)
{
context.Response.ContentType = "text/plain";
context.Response.Output.WriteLine(name);
}

context.Response.Output.WriteLine(name);

if (string.Equals(name, context.Request.QueryString["notification"], StringComparison.OrdinalIgnoreCase))
{
switch (context.Request.QueryString["action"])
switch (action)
{
case End:
context.Response.End();
Expand All @@ -38,6 +50,11 @@ protected override void InvokeEvent(HttpContext context, string name)
throw new InvalidOperationException();
}
}

if (!writeOutputBefore)
{
context.Response.Output.WriteLine(name);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.ObjectPool;
Expand All @@ -16,7 +17,6 @@ internal sealed class HttpApplicationFeature : IHttpApplicationFeature, IHttpRes
ApplicationEvent.LogRequest,
ApplicationEvent.PostLogRequest,
ApplicationEvent.EndRequest,
ApplicationEvent.PreSendRequestHeaders,
];

private readonly IHttpResponseEndFeature _previous;
Expand All @@ -30,13 +30,6 @@ public HttpApplicationFeature(HttpContextCore context, IHttpResponseEndFeature p
_contextOrApplication = context;
_pool = pool;
_previous = previousEnd;

context.Response.OnStarting(static state =>
{
var context = (HttpContextCore)state;

return context.Features.GetRequired<IHttpApplicationFeature>().RaiseEventAsync(ApplicationEvent.PreSendRequestHeaders).AsTask();
}, context);
}

public RequestNotification CurrentNotification { get; set; }
Expand Down Expand Up @@ -102,7 +95,7 @@ private void RaiseEvent(ApplicationEvent appEvent)
{
Application.InvokeEvent(appEvent);
}
catch (Exception ex)
catch (Exception ex) when (!IsReentrant(ex))
{
AddError(ex);
Application.InvokeEvent(ApplicationEvent.Error);
Expand All @@ -114,6 +107,24 @@ private void RaiseEvent(ApplicationEvent appEvent)
}
}

/// <summary>
/// Checks to see if we're attempting to invoke the Error event when we're currently throwing the existing errors.
/// This would imply a nested event invocation and we don't need to rethrow in that case.
/// </summary>
private bool IsReentrant(Exception e)
{
if (_exceptions is [{ } exception])
{
return ReferenceEquals(e, exception);
}
else if (e is AggregateException a && _exceptions is { } exceptions)
{
return a.InnerExceptions.SequenceEqual(exceptions);
}

return false;
}

private void ThrowIfErrors()
{
if (_exceptions is [{ } exception])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

namespace Microsoft.AspNetCore.SystemWebAdapters.Features;

/// <summary>
/// Used to intercept calls to Flush so we can raise <see cref="ApplicationEvent.PreSendRequestHeaders"/> and <see cref="ApplicationEvent.PreSendRequestContent"/> events
/// </summary>
internal sealed class HttpApplicationPreSendEventsResponseBodyFeature : PipeWriter, IHttpResponseBodyFeature
{
private State _state;
private int _byteCount;

private readonly PipeWriter _pipe;
private readonly IHttpResponseBodyFeature _other;
private readonly HttpContextCore _context;

private enum State
{
NotStarted,
RaisingPreHeader,
ReadyForContent,
RaisingPreContent,
}

public HttpApplicationPreSendEventsResponseBodyFeature(HttpContextCore context, IHttpResponseBodyFeature other)
{
_other = other;
_pipe = _other.Writer;
_context = context;
}

public Stream Stream => Writer.AsStream();

public PipeWriter Writer => this;

Task IHttpResponseBodyFeature.CompleteAsync() => _other.CompleteAsync();

void IHttpResponseBodyFeature.DisableBuffering() => _other.DisableBuffering();

public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default)
=> SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellationToken);

public Task StartAsync(CancellationToken cancellationToken = default) => _other.StartAsync(cancellationToken);

public override void Advance(int bytes)
{
// Don't track additional bytes written when events are being raised or we end up with some recursion
if (_state is not State.RaisingPreContent or State.RaisingPreHeader)
{
_byteCount += bytes;
}

_pipe.Advance(bytes);
}

public override void CancelPendingFlush() => _pipe.CancelPendingFlush();

public override void Complete(Exception? exception = null) => _pipe.Complete(exception);

public override async ValueTask<FlushResult> FlushAsync(CancellationToken cancellationToken = default)
{
// Only need to raise events if data will be flushed and the feature is available
if (_byteCount > 0 && _context.Features.Get<IHttpApplicationFeature>() is { } httpApplication)
{
_byteCount = 0;

if (_state is State.NotStarted)
{
_state = State.RaisingPreHeader;
await _context.Features.GetRequired<IHttpApplicationFeature>().RaiseEventAsync(ApplicationEvent.PreSendRequestHeaders);
_state = State.ReadyForContent;
}

if (_state is State.ReadyForContent)
{
_state = State.RaisingPreContent;
await httpApplication.RaiseEventAsync(ApplicationEvent.PreSendRequestContent);
_state = State.ReadyForContent;
}
}

return await _pipe.FlushAsync(cancellationToken);
}

public override Memory<byte> GetMemory(int sizeHint = 0) => _pipe.GetMemory(sizeHint);

public override Span<byte> GetSpan(int sizeHint = 0) => _pipe.GetSpan(sizeHint);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Web;

using Microsoft.AspNetCore.SystemWebAdapters.Features;
using static System.FormattableString;

namespace Microsoft.AspNetCore.SystemWebAdapters;
Expand Down Expand Up @@ -48,6 +48,11 @@ public Type ApplicationType

public IDictionary<string, Type> Modules => ModuleCollection;

/// <summary>
/// Gets or sets whether <see cref="HttpApplication.PreSendRequestHeaders"/> and <see cref="HttpApplication.PreSendRequestContent"/> is supported
/// </summary>
public bool ArePreSendEventsEnabled { get; set; }

/// <summary>
/// Gets or sets the number of <see cref="HttpApplication"/> retained for reuse. In order to support modules and applications that may contain state,
/// a unique instance is required for each request. This type should be set to the average number of concurrent requests expected to be seen.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SystemWebAdapters.Features;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal sealed class RegisterHttpApplicationPreSendEventsMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContextCore context)
{
var previous = context.Features.GetRequired<IHttpResponseBodyFeature>();
var feature = new HttpApplicationPreSendEventsResponseBodyFeature(context, previous);

context.Features.Set<IHttpResponseBodyFeature>(feature);

await next(context);

context.Features.Set<IHttpResponseBodyFeature>(previous);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.SystemWebAdapters.Features;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -145,6 +146,13 @@ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> builder =>
{
builder.UseMiddleware<SetHttpContextTimestampMiddleware>();

if (builder.AreHttpApplicationEventsRequired() && builder.ApplicationServices.GetRequiredService<IOptions<HttpApplicationOptions>>().Value.ArePreSendEventsEnabled)
{
// Must be registered first in order to intercept each flush to the client
builder.UseMiddleware<RegisterHttpApplicationPreSendEventsMiddleware>();
}

builder.UseMiddleware<RegisterAdapterFeaturesMiddleware>();
builder.UseMiddleware<SessionStateMiddleware>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public enum ApplicationEvent

PreSendRequestHeaders,

PreSendRequestContent,

Error,

SessionStart,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public event System.EventHandler PostRequestHandlerExecute { add { } remove { }
public event System.EventHandler PostResolveRequestCache { add { } remove { } }
public event System.EventHandler PostUpdateRequestCache { add { } remove { } }
public event System.EventHandler PreRequestHandlerExecute { add { } remove { } }
public event System.EventHandler PreSendRequestContent { add { } remove { } }
public event System.EventHandler PreSendRequestHeaders { add { } remove { } }
public event System.EventHandler ReleaseRequestState { add { } remove { } }
public event System.EventHandler ResolveRequestCache { add { } remove { } }
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ public event EventHandler? PreSendRequestHeaders
remove => RemoveEvent(value);
}

public event EventHandler? PreSendRequestContent
{
add => AddEvent(value);
remove => RemoveEvent(value);
}

private void AddEvent(EventHandler? handler, [CallerMemberName] string? name = null)
{
if (handler is null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests;

public class BufferedModuleTests : ModuleTests
{
public BufferedModuleTests()
: base(true)
{
}
}
Loading