Fix #629 - Add logging to DfaMatcther

Adds logging for the most common things that can prevent an endpoint
from matching.

Note that we already have good logging in other parts of the system, the
stuff here completes the story by providing details at the debug level.
This commit is contained in:
Ryan Nowak 2018-09-27 13:26:52 -07:00
parent b7939328b8
commit 3227de8c0b
14 changed files with 346 additions and 30 deletions

View File

@ -421,8 +421,8 @@ namespace Microsoft.AspNetCore.Routing
LogLevel.Debug,
EventIds.TemplateFailedExpansion,
"Failed to process the template {Template} for {Endpoint}. " +
"The failure occured while expanding the template with values {Values}. " +
"This is usually due to a missing or empty value in a complex segment.");
"The failure occured while expanding the template with values {Values} " +
"This is usually due to a missing or empty value in a complex segment");
private static readonly Action<ILogger, IEnumerable<string>, string, Exception> _linkGenerationSucceeded = LoggerMessage.Define<IEnumerable<string>, string>(
LogLevel.Debug,

View File

@ -57,12 +57,12 @@ namespace Microsoft.AspNetCore.Routing
private static readonly Action<ILogger, string, Exception> _executingEndpoint = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(0, "ExecutingEndpoint"),
"Executing endpoint '{EndpointName}'.");
"Executing endpoint '{EndpointName}'");
private static readonly Action<ILogger, string, Exception> _executedEndpoint = LoggerMessage.Define<string>(
LogLevel.Information,
new EventId(1, "ExecutedEndpoint"),
"Executed endpoint '{EndpointName}'.");
"Executed endpoint '{EndpointName}'");
public static void ExecutingEndpoint(ILogger logger, Endpoint endpoint)
{

View File

@ -119,12 +119,12 @@ namespace Microsoft.AspNetCore.Routing
private static readonly Action<ILogger, string, Exception> _matchSuccess = LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(1, "MatchSuccess"),
"Request matched endpoint '{EndpointName}'.");
"Request matched endpoint '{EndpointName}'");
private static readonly Action<ILogger, Exception> _matchFailure = LoggerMessage.Define(
LogLevel.Debug,
new EventId(2, "MatchFailure"),
"Request did not match any endpoints.");
"Request did not match any endpoints");
public static void MatchSuccess(ILogger logger, EndpointSelectorContext context)
{

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing.Logging
LogLevel.Debug,
1,
"Route value '{RouteValue}' with key '{RouteKey}' did not match " +
"the constraint '{RouteConstraint}'.");
"the constraint '{RouteConstraint}'");
}
public static void RouteValueDoesNotMatchConstraint(

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Routing.Logging
_requestDidNotMatchRoutes = LoggerMessage.Define(
LogLevel.Debug,
1,
"Request did not match any routes.");
"Request did not match any routes");
}
public static void RequestDidNotMatchRoutes(this ILogger logger)

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Routing.Logging
_matchedRoute = LoggerMessage.Define<string, string>(
LogLevel.Debug,
1,
"Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.");
"Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'");
}
public static void MatchedRoute(

View File

@ -6,17 +6,20 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.Matching
{
internal sealed class DfaMatcher : Matcher
{
private readonly ILogger _logger;
private readonly EndpointSelector _selector;
private readonly DfaState[] _states;
private readonly int _maxSegmentCount;
public DfaMatcher(EndpointSelector selector, DfaState[] states, int maxSegmentCount)
public DfaMatcher(ILogger<DfaMatcher> logger, EndpointSelector selector, DfaState[] states, int maxSegmentCount)
{
_logger = logger;
_selector = selector;
_states = states;
_maxSegmentCount = maxSegmentCount;
@ -34,6 +37,9 @@ namespace Microsoft.AspNetCore.Routing.Matching
throw new ArgumentNullException(nameof(context));
}
// All of the logging we do here is at level debug, so we can get away with doing a single check.
var log = _logger.IsEnabled(LogLevel.Debug);
// The sequence of actions we take is optimized to avoid doing expensive work
// like creating substrings, creating route value dictionaries, and calling
// into policies like versioning.
@ -49,9 +55,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
var candidates = FindCandidateSet(httpContext, path, segments);
if (candidates.Length == 0)
{
if (log)
{
Logger.CandidatesNotFound(_logger, path);
}
return Task.CompletedTask;
}
if (log)
{
Logger.CandidatesFound(_logger, path, candidates);
}
// At this point we have a candidate set, defined as a list of endpoints in
// priority order.
//
@ -117,21 +133,34 @@ namespace Microsoft.AspNetCore.Routing.Matching
// Now that we have the route values, we need to process complex segments.
// Complex segments go through an old API that requires a fully-materialized
// route value dictionary.
var isMatch = true;
if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0)
{
if (!ProcessComplexSegments(candidate.ComplexSegments, path, segments, values))
if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, values))
{
candidateSet.SetValidity(i, false);
continue;
isMatch = false;
}
}
if ((flags & Candidate.CandidateFlags.HasConstraints) != 0)
{
if (!ProcessConstraints(candidate.Constraints, httpContext, values))
if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, values))
{
candidateSet.SetValidity(i, false);
continue;
isMatch = false;
}
}
if (log)
{
if (isMatch)
{
Logger.CandidateValid(_logger, path, candidate.Endpoint);
}
else
{
Logger.CandidateNotValid(_logger, path, candidate.Endpoint);
}
}
}
@ -202,6 +231,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
private bool ProcessComplexSegments(
Endpoint endpoint,
(RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments,
string path,
ReadOnlySpan<PathSegment> segments,
@ -209,10 +239,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
for (var i = 0; i < complexSegments.Length; i++)
{
var segment = segments[complexSegments[i].segmentIndex];
(var complexSegment, var segmentIndex) = complexSegments[i];
var segment = segments[segmentIndex];
var text = path.Substring(segment.Start, segment.Length);
if (!RoutePatternMatcher.MatchComplexSegment(complexSegments[i].pathSegment, text, values))
if (!RoutePatternMatcher.MatchComplexSegment(complexSegment, text, values))
{
Logger.CandidateRejectedByComplexSegment(_logger, path, endpoint, complexSegment);
return false;
}
}
@ -221,6 +253,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
}
private bool ProcessConstraints(
Endpoint endpoint,
KeyValuePair<string, IRouteConstraint>[] constraints,
HttpContext httpContext,
RouteValueDictionary values)
@ -230,11 +263,112 @@ namespace Microsoft.AspNetCore.Routing.Matching
var constraint = constraints[i];
if (!constraint.Value.Match(httpContext, NullRouter.Instance, constraint.Key, values, RouteDirection.IncomingRequest))
{
Logger.CandidateRejectedByConstraint(_logger, httpContext.Request.Path, endpoint, constraint.Key, constraint.Value, values[constraint.Key]);
return false;
}
}
return true;
}
internal static class EventIds
{
public static readonly EventId CandidatesNotFound = new EventId(1000, "CandidatesNotFound");
public static readonly EventId CandidatesFound = new EventId(1001, "CandidatesFound");
public static readonly EventId CandidateRejectedByComplexSegment = new EventId(1002, "CandidateRejectedByComplexSegment");
public static readonly EventId CandidateRejectedByConstraint = new EventId(1003, "CandidateRejectedByConstraint");
public static readonly EventId CandidateNotValid = new EventId(1004, "CandiateNotValid");
public static readonly EventId CandidateValid = new EventId(1005, "CandiateValid");
}
private static class Logger
{
private static readonly Action<ILogger, string, Exception> _candidatesNotFound = LoggerMessage.Define<string>(
LogLevel.Debug,
EventIds.CandidatesNotFound,
"No candidates found for the request path '{Path}'");
private static readonly Action<ILogger, int, string, Exception> _candidatesFound = LoggerMessage.Define<int, string>(
LogLevel.Debug,
EventIds.CandidatesFound,
"{CandidateCount} candidate(s) found for the request path '{Path}'");
private static readonly Action<ILogger, string, string, string, string, Exception> _candidateRejectedByComplexSegment = LoggerMessage.Define<string, string, string, string>(
LogLevel.Debug,
EventIds.CandidateRejectedByComplexSegment,
"Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by complex segment '{Segment}' for the request path '{Path}'");
private static readonly Action<ILogger, string, string, string, string, object, string, Exception> _candidateRejectedByConstraint = LoggerMessage.Define<string, string, string, string, object, string>(
LogLevel.Debug,
EventIds.CandidateRejectedByConstraint,
"Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by constraint '{ConstraintName}':'{Constraint}' with value '{RouteValue}' for the request path '{Path}'");
private static readonly Action<ILogger, string, string, string, Exception> _candidateNotValid = LoggerMessage.Define<string, string, string>(
LogLevel.Debug,
EventIds.CandidateNotValid,
"Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is not valid for the request path '{Path}'");
private static readonly Action<ILogger, string, string, string, Exception> _candidateValid = LoggerMessage.Define<string, string, string>(
LogLevel.Debug,
EventIds.CandidateValid,
"Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is valid for the request path '{Path}'");
public static void CandidatesNotFound(ILogger logger, string path)
{
_candidatesNotFound(logger, path, null);
}
public static void CandidatesFound(ILogger logger, string path, Candidate[] candidates)
{
_candidatesFound(logger, candidates.Length, path, null);
}
public static void CandidateRejectedByComplexSegment(ILogger logger, string path, Endpoint endpoint, RoutePatternPathSegment segment)
{
// This should return a real pattern since we're processing complex segments.... but just in case.
if (logger.IsEnabled(LogLevel.Debug))
{
var routePattern = GetRoutePattern(endpoint);
_candidateRejectedByComplexSegment(logger, endpoint.DisplayName, routePattern, segment.DebuggerToString(), path, null);
}
}
public static void CandidateRejectedByConstraint(ILogger logger, string path, Endpoint endpoint, string constraintName, IRouteConstraint constraint, object value)
{
// This should return a real pattern since we're processing constraints.... but just in case.
if (logger.IsEnabled(LogLevel.Debug))
{
var routePattern = GetRoutePattern(endpoint);
_candidateRejectedByConstraint(logger, endpoint.DisplayName, routePattern, constraintName, constraint.ToString(), value, path, null);
}
}
public static void CandidateNotValid(ILogger logger, string path, Endpoint endpoint)
{
// This can be the fallback value because it really might not be a route endpoint
if (logger.IsEnabled(LogLevel.Debug))
{
var routePattern = GetRoutePattern(endpoint);
_candidateNotValid(logger, endpoint.DisplayName, routePattern, path, null);
}
}
public static void CandidateValid(ILogger logger, string path, Endpoint endpoint)
{
// This can be the fallback value because it really might not be a route endpoint
if (logger.IsEnabled(LogLevel.Debug))
{
var routePattern = GetRoutePattern(endpoint);
_candidateValid(logger, endpoint.DisplayName, routePattern, path, null);
}
}
private static string GetRoutePattern(Endpoint endpoint)
{
return (endpoint as RouteEndpoint)?.RoutePattern?.RawText ?? "(none)";
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Routing.Matching
{
@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
private readonly List<RouteEndpoint> _endpoints = new List<RouteEndpoint>();
private readonly ILoggerFactory _loggerFactory;
private readonly ParameterPolicyFactory _parameterPolicyFactory;
private readonly EndpointSelector _selector;
private readonly MatcherPolicy[] _policies;
@ -29,10 +31,12 @@ namespace Microsoft.AspNetCore.Routing.Matching
private int _stateIndex;
public DfaMatcherBuilder(
ILoggerFactory loggerFactory,
ParameterPolicyFactory parameterPolicyFactory,
EndpointSelector selector,
IEnumerable<MatcherPolicy> policies)
{
_loggerFactory = loggerFactory;
_parameterPolicyFactory = parameterPolicyFactory;
_selector = selector;
_policies = policies.OrderBy(p => p.Order).ToArray();
@ -304,7 +308,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
JumpTableBuilder.Build(exitDestination, exitDestination, null),
null);
return new DfaMatcher(_selector, states, maxSegmentCount);
return new DfaMatcher(_loggerFactory.CreateLogger<DfaMatcher>(), _selector, states, maxSegmentCount);
}
private int AddNode(

View File

@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Routing
};
var sink = SetUpMatch(constraints, loggerEnabled: true);
var expectedMessage = "Route value 'value' with key 'b' did not match the constraint " +
$"'{typeof(FailConstraint).FullName}'.";
$"'{typeof(FailConstraint).FullName}'";
// Assert
Assert.Empty(sink.Scopes);

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Routing
public async Task Invoke_OnCall_WritesToConfiguredLogger()
{
// Arrange
var expectedMessage = "Request matched endpoint 'Test endpoint'.";
var expectedMessage = "Request matched endpoint 'Test endpoint'";
var sink = new TestSink(
TestSink.EnableWithTypeName<EndpointRoutingMiddleware>,

View File

@ -5,6 +5,7 @@ using System;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
@ -117,6 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
return new DfaMatcherBuilder(
NullLoggerFactory.Instance,
Mock.Of<ParameterPolicyFactory>(),
Mock.Of<EndpointSelector>(),
policies);

View File

@ -7,6 +7,7 @@ using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@ -972,6 +973,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
{
var dataSource = new CompositeEndpointDataSource(Array.Empty<EndpointDataSource>());
return new DfaMatcherBuilder(
NullLoggerFactory.Instance,
new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), Mock.Of<IServiceProvider>()),
Mock.Of<EndpointSelector>(),
policies);

View File

@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
@ -26,7 +28,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
template);
}
private Matcher CreateDfaMatcher(EndpointDataSource dataSource, EndpointSelector endpointSelector = null)
private Matcher CreateDfaMatcher(EndpointDataSource dataSource, EndpointSelector endpointSelector = null, ILoggerFactory loggerFactory = null)
{
var serviceCollection = new ServiceCollection()
.AddLogging()
@ -38,6 +40,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
serviceCollection.AddSingleton<EndpointSelector>(endpointSelector);
}
if (loggerFactory != null)
{
serviceCollection.AddSingleton<ILoggerFactory>(loggerFactory);
}
var services = serviceCollection.BuildServiceProvider();
var factory = services.GetRequiredService<MatcherFactory>();
@ -76,14 +83,14 @@ namespace Microsoft.AspNetCore.Routing.Matching
var matcher = CreateDfaMatcher(endpointDataSource);
var (httpContext, endpointFeature) = CreateContext();
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/One";
// Act
await matcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Null(endpointFeature.Endpoint);
Assert.Null(context.Endpoint);
}
[Fact]
@ -101,14 +108,14 @@ namespace Microsoft.AspNetCore.Routing.Matching
var matcher = CreateDfaMatcher(endpointDataSource);
var (httpContext, endpointFeature) = CreateContext();
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/Teams";
// Act
await matcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Equal(lowerOrderEndpoint, endpointFeature.Endpoint);
Assert.Equal(lowerOrderEndpoint, context.Endpoint);
}
[Fact]
@ -147,14 +154,181 @@ namespace Microsoft.AspNetCore.Routing.Matching
var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector.Object);
var (httpContext, endpointFeature) = CreateContext();
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/Teams";
// Act
await matcher.MatchAsync(httpContext, endpointFeature);
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Equal(endpoint2, endpointFeature.Endpoint);
Assert.Equal(endpoint2, context.Endpoint);
}
[Fact]
public async Task MatchAsync_NoCandidates_Logging()
{
// Arrange
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
CreateEndpoint("/{p:int}", 0)
});
var sink = new TestSink();
var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true));
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Null(context.Endpoint);
Assert.Collection(
sink.Writes,
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidatesNotFound, log.EventId);
Assert.Equal("No candidates found for the request path '/'", log.Message);
});
}
[Fact]
public async Task MatchAsync_ConstraintRejectsEndpoint_Logging()
{
// Arrange
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
CreateEndpoint("/{p:int}", 0)
});
var sink = new TestSink();
var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true));
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/One";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Null(context.Endpoint);
Assert.Collection(
sink.Writes,
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidatesFound, log.EventId);
Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateRejectedByConstraint, log.EventId);
Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateNotValid, log.EventId);
Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message);
});
}
[Fact]
public async Task MatchAsync_ComplexSegmentRejectsEndpoint_Logging()
{
// Arrange
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
CreateEndpoint("/x-{id}-y", 0)
});
var sink = new TestSink();
var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true));
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/One";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Null(context.Endpoint);
Assert.Collection(
sink.Writes,
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidatesFound, log.EventId);
Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateRejectedByComplexSegment, log.EventId);
Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateNotValid, log.EventId);
Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message);
});
}
[Fact]
public async Task MatchAsync_MultipleCandidates_Logging()
{
// Arrange
var endpointDataSource = new DefaultEndpointDataSource(new List<Endpoint>
{
CreateEndpoint("/One", 0),
CreateEndpoint("/{p:int}", 0),
CreateEndpoint("/x-{id}-y", 0),
});
var sink = new TestSink();
var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true));
var (httpContext, context) = CreateContext();
httpContext.Request.Path = "/One";
// Act
await matcher.MatchAsync(httpContext, context);
// Assert
Assert.Same(endpointDataSource.Endpoints[0], context.Endpoint);
Assert.Collection(
sink.Writes,
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidatesFound, log.EventId);
Assert.Equal("3 candidate(s) found for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateValid, log.EventId);
Assert.Equal("Endpoint '/One' with route pattern '/One' is valid for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateRejectedByConstraint, log.EventId);
Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateNotValid, log.EventId);
Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateRejectedByComplexSegment, log.EventId);
Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message);
},
(log) =>
{
Assert.Equal(DfaMatcher.EventIds.CandidateNotValid, log.EventId);
Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message);
});
}
private (HttpContext httpContext, EndpointSelectorContext context) CreateContext()

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing
public async Task Invoke_LogsCorrectValues_WhenNotHandled()
{
// Arrange
var expectedMessage = "Request did not match any routes.";
var expectedMessage = "Request did not match any routes";
var isHandled = false;
var sink = new TestSink(