Skip to content

Commit 1c63bf6

Browse files
JamesNKTratcher
andauthored
Write log message for matched fallback routes (#47798)
Co-authored-by: Chris Ross <[email protected]>
1 parent 0c42c03 commit 1c63bf6

9 files changed

+139
-10
lines changed

src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public static IEndpointConventionBuilder MapFallback(
7777
var conventionBuilder = endpoints.Map(pattern, requestDelegate);
7878
conventionBuilder.WithDisplayName("Fallback " + pattern);
7979
conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue);
80+
conventionBuilder.WithMetadata(FallbackMetadata.Instance);
8081
return conventionBuilder;
8182
}
8283
}

src/Http/Routing/src/EndpointRoutingMiddleware.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ private Task SetRoutingAndContinue(HttpContext httpContext)
109109

110110
Log.MatchSuccess(_logger, endpoint);
111111

112+
if (_logger.IsEnabled(LogLevel.Debug)
113+
&& endpoint.Metadata.GetMetadata<FallbackMetadata>() is not null)
114+
{
115+
Log.FallbackMatch(_logger, endpoint);
116+
}
117+
112118
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
113119
if (shortCircuitMetadata is not null)
114120
{
@@ -283,5 +289,8 @@ public static void MatchSkipped(ILogger logger, Endpoint endpoint)
283289

284290
[LoggerMessage(6, LogLevel.Information, "The endpoint '{EndpointName}' is being short circuited without running additional middleware or producing a response.", EventName = "ShortCircuitedEndpoint")]
285291
public static partial void ShortCircuitedEndpoint(ILogger logger, Endpoint endpointName);
292+
293+
[LoggerMessage(7, LogLevel.Debug, "Matched endpoint '{EndpointName}' is a fallback endpoint.", EventName = "FallbackMatch", SkipEnabledCheck = true)]
294+
public static partial void FallbackMatch(ILogger logger, Endpoint endpointName);
286295
}
287296
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Routing;
5+
6+
internal sealed class FallbackMetadata
7+
{
8+
public static readonly FallbackMetadata Instance = new FallbackMetadata();
9+
10+
private FallbackMetadata()
11+
{
12+
}
13+
}

src/Http/Routing/src/RouteEndpointDataSource.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder(
127127
// The Map methods don't support customizing the order apart from using int.MaxValue to give MapFallback the lowest priority.
128128
// Otherwise, we always use the default of 0 unless a convention changes it later.
129129
var order = isFallback ? int.MaxValue : 0;
130-
var displayName = pattern.RawText ?? pattern.DebuggerToString();
130+
var displayName = pattern.DebuggerToString();
131131

132132
// Don't include the method name for non-route-handlers because the name is just "Invoke" when built from
133133
// ApplicationBuilder.Build(). This was observed in MapSignalRTests and is not very useful. Maybe if we come up
@@ -173,6 +173,11 @@ private RouteEndpointBuilder CreateRouteEndpointBuilder(
173173
ApplicationServices = _applicationServices,
174174
};
175175

176+
if (isFallback)
177+
{
178+
builder.Metadata.Add(FallbackMetadata.Instance);
179+
}
180+
176181
if (isRouteHandler)
177182
{
178183
builder.Metadata.Add(handler.Method);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#nullable enable
5+
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Routing;
8+
9+
namespace Microsoft.AspNetCore.Builder;
10+
11+
public class FallbackEndpointRouteBuilderExtensionsTest
12+
{
13+
private EndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) =>
14+
Assert.Single(endpointRouteBuilder.DataSources);
15+
16+
[Fact]
17+
public void MapFallback_AddFallbackMetadata()
18+
{
19+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(EmptyServiceProvider.Instance));
20+
21+
RequestDelegate initialRequestDelegate = static (context) => Task.CompletedTask;
22+
23+
builder.MapFallback(initialRequestDelegate);
24+
25+
var dataSource = GetBuilderEndpointDataSource(builder);
26+
var endpoint = Assert.Single(dataSource.Endpoints);
27+
28+
Assert.Contains(FallbackMetadata.Instance, endpoint.Metadata);
29+
Assert.Equal(int.MaxValue, ((RouteEndpoint)endpoint).Order);
30+
}
31+
32+
[Fact]
33+
public void MapFallback_Pattern_AddFallbackMetadata()
34+
{
35+
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(EmptyServiceProvider.Instance));
36+
37+
RequestDelegate initialRequestDelegate = static (context) => Task.CompletedTask;
38+
39+
builder.MapFallback("/", initialRequestDelegate);
40+
41+
var dataSource = GetBuilderEndpointDataSource(builder);
42+
var endpoint = Assert.Single(dataSource.Endpoints);
43+
44+
Assert.Contains(FallbackMetadata.Instance, endpoint.Metadata);
45+
Assert.Equal(int.MaxValue, ((RouteEndpoint)endpoint).Order);
46+
}
47+
48+
private sealed class EmptyServiceProvider : IServiceProvider
49+
{
50+
public static EmptyServiceProvider Instance { get; } = new EmptyServiceProvider();
51+
public object? GetService(Type serviceType) => null;
52+
}
53+
}

src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder()
765765
Assert.Single(routeEndpointBuilder.RoutePattern.Parameters);
766766
Assert.True(routeEndpointBuilder.RoutePattern.Parameters[0].IsCatchAll);
767767
Assert.Equal(int.MaxValue, routeEndpointBuilder.Order);
768+
Assert.Contains(FallbackMetadata.Instance, routeEndpointBuilder.Metadata);
768769
}
769770

770771
[Fact]

src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,7 @@ public async Task Invoke_OnCall_WritesToConfiguredLogger()
5858
var expectedMessage = "Request matched endpoint 'Test endpoint'";
5959
bool eventFired = false;
6060

61-
var sink = new TestSink(
62-
TestSink.EnableWithTypeName<EndpointRoutingMiddleware>,
63-
TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
61+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
6462
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
6563
var listener = new DiagnosticListener("TestListener");
6664

@@ -224,6 +222,46 @@ public async Task ThrowIfSecurityMetadataPresent(int? statusCode, bool hasAuthMe
224222
await Assert.ThrowsAsync<InvalidOperationException>(() => middleware.Invoke(httpContext));
225223
}
226224

225+
[Theory]
226+
[InlineData(true)]
227+
[InlineData(false)]
228+
public async Task Invoke_CheckForFallbackMetadata_LogIfPresent(bool hasFallbackMetadata)
229+
{
230+
// Arrange
231+
var sink = new TestSink(TestSink.EnableWithTypeName<EndpointRoutingMiddleware>);
232+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
233+
var logger = new Logger<EndpointRoutingMiddleware>(loggerFactory);
234+
235+
var metadata = new List<object>();
236+
if (hasFallbackMetadata)
237+
{
238+
metadata.Add(FallbackMetadata.Instance);
239+
}
240+
241+
var httpContext = CreateHttpContext();
242+
243+
var middleware = CreateMiddleware(
244+
logger: logger,
245+
matcherFactory: new TestMatcherFactory(isHandled: true, setEndpointCallback: c =>
246+
{
247+
c.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(metadata), "myapp"));
248+
}));
249+
250+
// Act
251+
await middleware.Invoke(httpContext);
252+
253+
// Assert
254+
if (hasFallbackMetadata)
255+
{
256+
var write = Assert.Single(sink.Writes.Where(w => w.EventId.Name == "FallbackMatch"));
257+
Assert.Equal("Matched endpoint 'myapp' is a fallback endpoint.", write.Message);
258+
}
259+
else
260+
{
261+
Assert.DoesNotContain(sink.Writes, w => w.EventId.Name == "FallbackMatch");
262+
}
263+
}
264+
227265
private HttpContext CreateHttpContext()
228266
{
229267
var httpContext = new DefaultHttpContext
@@ -235,7 +273,7 @@ private HttpContext CreateHttpContext()
235273
}
236274

237275
private EndpointRoutingMiddleware CreateMiddleware(
238-
Logger<EndpointRoutingMiddleware> logger = null,
276+
ILogger<EndpointRoutingMiddleware> logger = null,
239277
MatcherFactory matcherFactory = null,
240278
DiagnosticListener listener = null,
241279
RequestDelegate next = null)

src/Http/Routing/test/UnitTests/TestObjects/TestMatcher.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@ namespace Microsoft.AspNetCore.Routing.TestObjects;
99
internal class TestMatcher : Matcher
1010
{
1111
private readonly bool _isHandled;
12+
private readonly Action<HttpContext> _setEndpointCallback;
1213

13-
public TestMatcher(bool isHandled)
14+
public TestMatcher(bool isHandled, Action<HttpContext> setEndpointCallback = null)
1415
{
1516
_isHandled = isHandled;
17+
18+
setEndpointCallback ??= static c =>
19+
{
20+
c.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" });
21+
c.SetEndpoint(new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "Test endpoint"));
22+
};
23+
_setEndpointCallback = setEndpointCallback;
1624
}
1725

1826
public override Task MatchAsync(HttpContext httpContext)
1927
{
2028
if (_isHandled)
2129
{
22-
httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" });
23-
httpContext.SetEndpoint(new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "Test endpoint"));
30+
_setEndpointCallback(httpContext);
2431
}
2532

2633
return Task.CompletedTask;

src/Http/Routing/test/UnitTests/TestObjects/TestMatcherFactory.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ namespace Microsoft.AspNetCore.Routing.TestObjects;
1212
internal class TestMatcherFactory : MatcherFactory
1313
{
1414
private readonly bool _isHandled;
15+
private readonly Action<HttpContext> _setEndpointCallback;
1516

16-
public TestMatcherFactory(bool isHandled)
17+
public TestMatcherFactory(bool isHandled, Action<HttpContext> setEndpointCallback = null)
1718
{
1819
_isHandled = isHandled;
20+
_setEndpointCallback = setEndpointCallback;
1921
}
2022

2123
public override Matcher CreateMatcher(EndpointDataSource dataSource)
2224
{
23-
return new TestMatcher(_isHandled);
25+
return new TestMatcher(_isHandled, _setEndpointCallback);
2426
}
2527
}
2628

0 commit comments

Comments
 (0)