[Design] Set the endpoint feature only on success
This commit is contained in:
parent
8395ad8340
commit
945e798ca7
|
|
@ -32,26 +32,18 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var feature = httpContext.Features.Get<IEndpointFeature>();
|
var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
|
||||||
if (feature == null)
|
if (endpoint?.RequestDelegate != null)
|
||||||
{
|
{
|
||||||
var message = $"Unable to execute an endpoint because the {nameof(EndpointRoutingMiddleware)} was not run for this request. " +
|
Log.ExecutingEndpoint(_logger, endpoint);
|
||||||
$"Ensure {nameof(EndpointRoutingMiddleware)} is added to the request execution pipeline before {nameof(EndpointMiddleware)} in application startup code.";
|
|
||||||
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feature.Endpoint?.RequestDelegate != null)
|
|
||||||
{
|
|
||||||
Log.ExecutingEndpoint(_logger, feature.Endpoint);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await feature.Endpoint.RequestDelegate(httpContext);
|
await endpoint.RequestDelegate(httpContext);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Log.ExecutedEndpoint(_logger, feature.Endpoint);
|
Log.ExecutedEndpoint(_logger, endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
public async Task Invoke(HttpContext httpContext)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
// For back-compat EndpointRouteValuesFeature implements IEndpointFeature, IRouteValuesFeature and IRoutingFeature
|
|
||||||
var feature = new EndpointFeature();
|
var feature = new EndpointFeature();
|
||||||
|
|
||||||
SetEndpointFeature(httpContext, feature);
|
|
||||||
|
|
||||||
// There's an inherent race condition between waiting for init and accessing the matcher
|
// There's an inherent race condition between waiting for init and accessing the matcher
|
||||||
// this is OK because once `_matcher` is initialized, it will not be set to null again.
|
// this is OK because once `_matcher` is initialized, it will not be set to null again.
|
||||||
var matcher = await InitializeAsync();
|
var matcher = await InitializeAsync();
|
||||||
|
|
@ -67,6 +64,10 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
await matcher.MatchAsync(httpContext, feature);
|
await matcher.MatchAsync(httpContext, feature);
|
||||||
if (feature.Endpoint != null)
|
if (feature.Endpoint != null)
|
||||||
{
|
{
|
||||||
|
// Set the endpoint feature only on success. This means we won't overwrite any
|
||||||
|
// existing state for related features unless we did something.
|
||||||
|
SetEndpointFeature(httpContext, feature);
|
||||||
|
|
||||||
Log.MatchSuccess(_logger, feature);
|
Log.MatchSuccess(_logger, feature);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -79,11 +80,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
private static void SetEndpointFeature(HttpContext httpContext, EndpointFeature feature)
|
private static void SetEndpointFeature(HttpContext httpContext, EndpointFeature feature)
|
||||||
{
|
{
|
||||||
// An IRouteValuesFeature might have already been set
|
// For back-compat EndpointRouteValuesFeature implements IEndpointFeature,
|
||||||
// Copy its RouteValues collection if present
|
// IRouteValuesFeature and IRoutingFeature
|
||||||
var currentRouteValuesFeature = httpContext.Features.Get<IRouteValuesFeature>();
|
|
||||||
feature.RouteValues = currentRouteValuesFeature?.RouteValues;
|
|
||||||
|
|
||||||
httpContext.Features.Set<IRoutingFeature>(feature);
|
httpContext.Features.Set<IRoutingFeature>(feature);
|
||||||
httpContext.Features.Set<IRouteValuesFeature>(feature);
|
httpContext.Features.Set<IRouteValuesFeature>(feature);
|
||||||
httpContext.Features.Set<IEndpointFeature>(feature);
|
httpContext.Features.Set<IEndpointFeature>(feature);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Internal;
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
using Microsoft.AspNetCore.Routing.Patterns;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -51,7 +52,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UseEndpointRouting_ServicesRegistered_SetsFeature()
|
public async Task UseEndpointRouting_ServicesRegistered_NoMatch_DoesNotSetFeature()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var services = CreateServices();
|
var services = CreateServices();
|
||||||
|
|
@ -67,7 +68,36 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
await appFunc(httpContext);
|
await appFunc(httpContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(httpContext.Features.Get<IEndpointFeature>());
|
Assert.Null(httpContext.Features.Get<IEndpointFeature>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UseEndpointRouting_ServicesRegistered_Match_DoesNotSetsFeature()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint = new RouteEndpoint(
|
||||||
|
TestConstants.EmptyRequestDelegate,
|
||||||
|
RoutePatternFactory.Parse("{*p}"),
|
||||||
|
0,
|
||||||
|
EndpointMetadataCollection.Empty,
|
||||||
|
"Test");
|
||||||
|
|
||||||
|
var services = CreateServices(endpoint);
|
||||||
|
|
||||||
|
var app = new ApplicationBuilder(services);
|
||||||
|
|
||||||
|
app.UseEndpointRouting();
|
||||||
|
|
||||||
|
var appFunc = app.Build();
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await appFunc(httpContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var feature = httpContext.Features.Get<IEndpointFeature>();
|
||||||
|
Assert.NotNull(feature);
|
||||||
|
Assert.Same(endpoint, feature.Endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -90,7 +120,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UseEndpoint_ServicesRegisteredAndEndpointRoutingRegistered_SetsFeature()
|
public async Task UseEndpoint_ServicesRegisteredAndEndpointRoutingRegistered_NoMatch_DoesNotSetFeature()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var services = CreateServices();
|
var services = CreateServices();
|
||||||
|
|
@ -107,10 +137,10 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
await appFunc(httpContext);
|
await appFunc(httpContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(httpContext.Features.Get<IEndpointFeature>());
|
Assert.Null(httpContext.Features.Get<IEndpointFeature>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private IServiceProvider CreateServices()
|
private IServiceProvider CreateServices(params Endpoint[] endpoints)
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
|
@ -118,6 +148,8 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
services.AddOptions();
|
services.AddOptions();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
|
||||||
|
services.AddSingleton<EndpointDataSource>(new DefaultEndpointDataSource(endpoints));
|
||||||
|
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -15,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
public class EndpointMiddlewareTest
|
public class EndpointMiddlewareTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Invoke_NoFeature_ThrowFriendlyErrorMessage()
|
public async Task Invoke_NoFeature_NoOps()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
|
|
@ -23,22 +21,73 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
RequestDelegate next = (c) =>
|
RequestDelegate next = (c) =>
|
||||||
{
|
{
|
||||||
return Task.FromResult<object>(null);
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
|
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var invokeTask = middleware.Invoke(httpContext);
|
await middleware.Invoke(httpContext);
|
||||||
|
|
||||||
|
// Assert - does not throw
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_NoEndpoint_NoOps()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.RequestServices = new ServiceProvider();
|
||||||
|
|
||||||
|
httpContext.Features.Set<IEndpointFeature>(new EndpointFeature()
|
||||||
|
{
|
||||||
|
Endpoint = null,
|
||||||
|
});
|
||||||
|
|
||||||
|
RequestDelegate next = (c) =>
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middleware.Invoke(httpContext);
|
||||||
|
|
||||||
|
// Assert - does not throw
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Invoke_WithEndpoint_InvokesDelegate()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
httpContext.RequestServices = new ServiceProvider();
|
||||||
|
|
||||||
|
var invoked = false;
|
||||||
|
RequestDelegate endpointFunc = (c) =>
|
||||||
|
{
|
||||||
|
invoked = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
httpContext.Features.Set<IEndpointFeature>(new EndpointFeature()
|
||||||
|
{
|
||||||
|
Endpoint = new Endpoint(endpointFunc, EndpointMetadataCollection.Empty, "Test"),
|
||||||
|
});
|
||||||
|
|
||||||
|
RequestDelegate next = (c) =>
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
var middleware = new EndpointMiddleware(NullLogger<EndpointMiddleware>.Instance, next);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await middleware.Invoke(httpContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await invokeTask);
|
Assert.True(invoked);
|
||||||
|
|
||||||
Assert.Equal(
|
|
||||||
"Unable to execute an endpoint because the EndpointRoutingMiddleware was not run for this request. " +
|
|
||||||
"Ensure EndpointRoutingMiddleware is added to the request execution pipeline before EndpointMiddleware " +
|
|
||||||
"in application startup code.",
|
|
||||||
ex.Message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ServiceProvider : IServiceProvider
|
private class ServiceProvider : IServiceProvider
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue