diff --git a/src/Http/Http.Abstractions/src/HttpContext.cs b/src/Http/Http.Abstractions/src/HttpContext.cs index 6aa00a5b7ff2..4842446b33be 100644 --- a/src/Http/Http.Abstractions/src/HttpContext.cs +++ b/src/Http/Http.Abstractions/src/HttpContext.cs @@ -72,6 +72,15 @@ public abstract class HttpContext /// public abstract ISession Session { get; set; } + /// + /// Gets or sets the for the current request. + /// + /// + /// The endpoint for a request is typically set by routing middleware. A request might not have + /// an endpoint if routing middleware hasn't run yet, or the request didn't match a route. + /// + public abstract Endpoint? Endpoint { get; set; } + /// /// Aborts the connection underlying this request. /// @@ -90,7 +99,7 @@ private sealed class HttpContextDebugView(HttpContext context) public HttpContextFeatureDebugView Features => new HttpContextFeatureDebugView(_context.Features); public HttpRequest Request => _context.Request; public HttpResponse Response => _context.Response; - public Endpoint? Endpoint => _context.GetEndpoint(); + public Endpoint? Endpoint => _context.Endpoint; public ConnectionInfo Connection => _context.Connection; public WebSocketManager WebSockets => _context.WebSockets; public ClaimsPrincipal User => _context.User; diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index 6ccd592237b8..2aa2ecebeabb 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +abstract Microsoft.AspNetCore.Http.HttpContext.Endpoint.get -> Microsoft.AspNetCore.Http.Endpoint? +abstract Microsoft.AspNetCore.Http.HttpContext.Endpoint.set -> void Microsoft.AspNetCore.Http.Metadata.IAllowCookieRedirectMetadata Microsoft.AspNetCore.Http.Metadata.IDisableCookieRedirectMetadata Microsoft.AspNetCore.Http.Metadata.IDisableValidationMetadata diff --git a/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs index 5c953912a379..f0edc35369aa 100644 --- a/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs @@ -142,6 +142,72 @@ public void SetAndGetEndpoint_Roundtrip_EndpointIsRoundtrip() Assert.Equal(initialEndpoint, endpoint); } + [Fact] + public void EndpointProperty_Get_ReturnsNullByDefault() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = context.Endpoint; + + // Assert + Assert.Null(endpoint); + } + + [Fact] + public void EndpointProperty_SetAndGet_Roundtrip() + { + // Arrange + var context = new DefaultHttpContext(); + var testEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + + // Act + context.Endpoint = testEndpoint; + var retrievedEndpoint = context.Endpoint; + + // Assert + Assert.Equal(testEndpoint, retrievedEndpoint); + } + + [Fact] + public void EndpointProperty_SetNull_ClearsEndpoint() + { + // Arrange + var context = new DefaultHttpContext(); + var testEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.Endpoint = testEndpoint; + + // Act + context.Endpoint = null; + + // Assert + Assert.Null(context.Endpoint); + } + + [Fact] + public void EndpointProperty_ConsistentWithExtensionMethods() + { + // Arrange + var context = new DefaultHttpContext(); + var testEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + + // Act - Set via property + context.Endpoint = testEndpoint; + + // Assert - Both property and extension method return the same value + Assert.Equal(testEndpoint, context.Endpoint); + Assert.Equal(testEndpoint, context.GetEndpoint()); + + // Act - Set via extension method + var newEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "New endpoint"); + context.SetEndpoint(newEndpoint); + + // Assert - Both property and extension method return the new value + Assert.Equal(newEndpoint, context.Endpoint); + Assert.Equal(newEndpoint, context.GetEndpoint()); + } + private class EndpointFeature : IEndpointFeature { public Endpoint? Endpoint { get; set; } diff --git a/src/Http/Http/src/DefaultHttpContext.cs b/src/Http/Http/src/DefaultHttpContext.cs index 2439fc48efe9..5a4092407764 100644 --- a/src/Http/Http/src/DefaultHttpContext.cs +++ b/src/Http/Http/src/DefaultHttpContext.cs @@ -213,6 +213,13 @@ public override ISession Session } } + /// + public override Endpoint? Endpoint + { + get { return this.GetEndpoint(); } + set { this.SetEndpoint(value); } + } + // This property exists because of backwards compatibility. // We send an anonymous object with an HttpContext property // via DiagnosticListener in various events throughout the pipeline. Instead