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);
+ }
}
}