diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs index 3ab14b9342..b0b2ba10ae 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs @@ -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; + } + } } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/ISubscriptionToken.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/ISubscriptionToken.cs new file mode 100644 index 0000000000..b2e6db99ab --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/ISubscriptionToken.cs @@ -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 +{ + /// + /// Returns a value stating whether the subscription is currently active + /// + bool IsActive { get; } + + /// + /// Unsubscribes from the event + /// + void Unsubscribe(); +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs index 7bb2b7beaf..e2cf7fb30c 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Ref.Standard.cs @@ -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 @@ -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, diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/TypeForwards.Framework.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/TypeForwards.Framework.cs index 919c7584dc..9e87563e61 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/TypeForwards.Framework.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/TypeForwards.Framework.cs @@ -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))] diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs index 5c31f86038..5f7b0fa117 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.Tests/HttpContextTests.cs @@ -152,5 +152,93 @@ public void CacheFromServices() // Assert Assert.Same(cache, result); } + + [Fact] + public void DisposeOnPipelineCompleted() + { + // Arrange + var coreContext = new Mock(); + var coreResponse = new Mock(); + + coreContext.Setup(c => c.Response).Returns(coreResponse.Object); + + var context = new HttpContext(coreContext.Object); + var disposable = new Mock(); + + // Act + var token = context.DisposeOnPipelineCompleted(disposable.Object); + + // Assert + Assert.True(token.IsActive); + } + + [Fact] + public void DisposeOnPipelineCompletedUnsubscribed() + { + // Arrange + var coreContext = new Mock(); + var coreResponse = new Mock(); + + coreContext.Setup(c => c.Response).Returns(coreResponse.Object); + + var context = new HttpContext(coreContext.Object); + var disposable = new Mock(); + + // 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(); + var coreResponse = new Mock(); + coreResponse.Setup(c => c.RegisterForDispose(It.IsAny())) + .Callback((IDisposable disposable) => registeredDisposable = disposable); + + coreContext.Setup(c => c.Response).Returns(coreResponse.Object); + + var context = new HttpContext(coreContext.Object); + var disposable = new Mock(); + + // 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(); + var coreResponse = new Mock(); + coreResponse.Setup(c => c.RegisterForDispose(It.IsAny())) + .Callback((IDisposable disposable) => disposable.Dispose()); + + coreContext.Setup(c => c.Response).Returns(coreResponse.Object); + + var context = new HttpContext(coreContext.Object); + var disposable = new Mock(); + + // Act + var token = context.DisposeOnPipelineCompleted(disposable.Object); + + // Assert + Assert.False(token.IsActive); + disposable.Verify(d => d.Dispose(), Times.Once); + } } }