Skip to content

Commit

Permalink
Enable HttpContext.DisposeOnPipelineCompleted
Browse files Browse the repository at this point in the history
This is similar to HttpResponse.RegisterForDispose, but allows someone to unsubscribe and track if it is still active.
  • Loading branch information
twsouthwick committed Jul 6, 2022
1 parent 8f8ca9c commit 61dac29
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,33 @@ public IPrincipal User
return null;
}

public ISubscriptionToken DisposeOnPipelineCompleted(IDisposable target)
{
var token = new DisposeOnPipelineSubscriptionToken(target);
_context.Response.RegisterForDispose(token);
return token;
}

[return: NotNullIfNotNull("context")]
public static implicit operator HttpContext?(HttpContextCore? context) => context?.GetAdapter();

[return: NotNullIfNotNull("context")]
public static implicit operator HttpContextCore?(HttpContext? context) => context?._context;

private sealed class DisposeOnPipelineSubscriptionToken : ISubscriptionToken, IDisposable
{
private IDisposable? _other;

public DisposeOnPipelineSubscriptionToken(IDisposable other) => _other = other;

bool ISubscriptionToken.IsActive => _other is not null;

void ISubscriptionToken.Unsubscribe() => _other = null;

void IDisposable.Dispose()
{
_other?.Dispose();
_other = null;
}
}
}
17 changes: 17 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/ISubscriptionToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace System.Web;

public interface ISubscriptionToken
{
/// <summary>
/// Returns a value stating whether the subscription is currently active
/// </summary>
bool IsActive { get; }

/// <summary>
/// Unsubscribes from the event
/// </summary>
void Unsubscribe();
}
6 changes: 6 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal HttpContext() { }
public System.Web.HttpServerUtility Server { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} }
public System.Web.SessionState.HttpSessionState Session { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} }
public System.Security.Principal.IPrincipal User { get { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");} }
public System.Web.ISubscriptionToken DisposeOnPipelineCompleted(System.IDisposable target) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");}
object System.IServiceProvider.GetService(System.Type service) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");}
}
public partial class HttpContextBase : System.IServiceProvider
Expand Down Expand Up @@ -346,6 +347,11 @@ public sealed partial class HttpUnhandledException : System.Web.HttpException
public HttpUnhandledException(string message) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");}
public HttpUnhandledException(string message, System.Exception innerException) { throw new System.PlatformNotSupportedException("Only support when running on ASP.NET Core or System.Web");}
}
public partial interface ISubscriptionToken
{
bool IsActive { get; }
void Unsubscribe();
}
public enum SameSiteMode
{
Lax = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpSessionStateBase))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpSessionStateWrapper))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpUnhandledException))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.ISubscriptionToken))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.SameSiteMode))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.Cache))]
[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.CacheDependency))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,93 @@ public void CacheFromServices()
// Assert
Assert.Same(cache, result);
}

[Fact]
public void DisposeOnPipelineCompleted()
{
// Arrange
var coreContext = new Mock<HttpContextCore>();
var coreResponse = new Mock<HttpResponseCore>();

coreContext.Setup(c => c.Response).Returns(coreResponse.Object);

var context = new HttpContext(coreContext.Object);
var disposable = new Mock<IDisposable>();

// Act
var token = context.DisposeOnPipelineCompleted(disposable.Object);

// Assert
Assert.True(token.IsActive);
}

[Fact]
public void DisposeOnPipelineCompletedUnsubscribed()
{
// Arrange
var coreContext = new Mock<HttpContextCore>();
var coreResponse = new Mock<HttpResponseCore>();

coreContext.Setup(c => c.Response).Returns(coreResponse.Object);

var context = new HttpContext(coreContext.Object);
var disposable = new Mock<IDisposable>();

// Act
var token = context.DisposeOnPipelineCompleted(disposable.Object);

token.Unsubscribe();

// Assert
Assert.False(token.IsActive);
}

[Fact]
public void DisposeOnPipelineCompletedUnsubscribedDisposed()
{
// Arrange
IDisposable registeredDisposable = null!;

var coreContext = new Mock<HttpContextCore>();
var coreResponse = new Mock<HttpResponseCore>();
coreResponse.Setup(c => c.RegisterForDispose(It.IsAny<IDisposable>()))
.Callback((IDisposable disposable) => registeredDisposable = disposable);

coreContext.Setup(c => c.Response).Returns(coreResponse.Object);

var context = new HttpContext(coreContext.Object);
var disposable = new Mock<IDisposable>();

// Act
var token = context.DisposeOnPipelineCompleted(disposable.Object);
token.Unsubscribe();
registeredDisposable.Dispose();

// Assert
Assert.False(token.IsActive);
disposable.Verify(d => d.Dispose(), Times.Never);
}

[Fact]
public void DisposeOnPipelineCompletedDisposed()
{
// Arrange
var coreContext = new Mock<HttpContextCore>();
var coreResponse = new Mock<HttpResponseCore>();
coreResponse.Setup(c => c.RegisterForDispose(It.IsAny<IDisposable>()))
.Callback((IDisposable disposable) => disposable.Dispose());

coreContext.Setup(c => c.Response).Returns(coreResponse.Object);

var context = new HttpContext(coreContext.Object);
var disposable = new Mock<IDisposable>();

// Act
var token = context.DisposeOnPipelineCompleted(disposable.Object);

// Assert
Assert.False(token.IsActive);
disposable.Verify(d => d.Dispose(), Times.Once);
}
}
}

0 comments on commit 61dac29

Please sign in to comment.