From fca9831115de6e5b4898314af83f702a9d8dd243 Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Thu, 17 Jul 2014 12:44:53 -0700 Subject: [PATCH] Routing Logging Added scoped logging to RouterMiddleware, RouteCollection, and TemplateRoute. --- .../Logging/LogFormatter.cs | 37 +++ .../Logging/LoggerExtensions.cs | 20 ++ .../RouteCollectionRouteAsyncValues.cs | 60 ++++ .../RouteConstraintMatcherMatchValues.cs | 71 ++++ .../Logging/RouterMiddlewareInvokeValues.cs | 51 +++ .../Logging/StringBuilderHelpers.cs | 44 +++ .../Logging/TemplateRouteRouteAsyncValues.cs | 122 +++++++ .../Microsoft.AspNet.Routing.kproj | 7 + .../RouteCollection.cs | 41 ++- .../RouteConstraintMatcher.cs | 27 +- .../RouterMiddleware.cs | 36 +- .../Template/TemplateRoute.cs | 128 +++++-- src/Microsoft.AspNet.Routing/project.json | 3 +- .../ConstraintMatcherTest.cs | 313 ++++++++++++++++++ .../ConstraintMatcherTests.cs | 157 --------- .../Logging/BeginScopeContext.cs | 12 + .../Logging/NullDisposable.cs | 17 + .../Logging/NullLogger.cs | 23 ++ .../Logging/NullLoggerFactory.cs | 17 + .../Logging/TestLogger.cs | 52 +++ .../Logging/TestLoggerFactory.cs | 22 ++ .../Logging/TestSink.cs | 56 ++++ .../Logging/WriteCoreContext.cs | 25 ++ .../Microsoft.AspNet.Routing.Tests.kproj | 13 +- .../RouteCollectionTest.cs | 97 +++++- .../RouterMiddlewareTest.cs | 139 ++++++++ ...lateRouteTests.cs => TemplateRouteTest.cs} | 162 ++++++++- .../project.json | 1 + 28 files changed, 1545 insertions(+), 208 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/Logging/LogFormatter.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/LoggerExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/RouteCollectionRouteAsyncValues.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/RouteConstraintMatcherMatchValues.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/RouterMiddlewareInvokeValues.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/StringBuilderHelpers.cs create mode 100644 src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTest.cs delete mode 100644 test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/BeginScopeContext.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/NullDisposable.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/NullLogger.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/NullLoggerFactory.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/TestLogger.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/TestLoggerFactory.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/TestSink.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Logging/WriteCoreContext.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs rename test/Microsoft.AspNet.Routing.Tests/Template/{TemplateRouteTests.cs => TemplateRouteTest.cs} (79%) diff --git a/src/Microsoft.AspNet.Routing/Logging/LogFormatter.cs b/src/Microsoft.AspNet.Routing/Logging/LogFormatter.cs new file mode 100644 index 0000000000..63971503df --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/LogFormatter.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Routing.Logging +{ + public static class LogFormatter + { + /// + /// A formatter for use with . + /// + public static string Formatter(object o, Exception e) + { + if (o != null && e != null) + { + return o + Environment.NewLine + e; + } + + if (o != null) + { + return o.ToString(); + } + + if (e != null) + { + return e.ToString(); + } + + return ""; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/LoggerExtensions.cs b/src/Microsoft.AspNet.Routing/Logging/LoggerExtensions.cs new file mode 100644 index 0000000000..ebcdca6744 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/LoggerExtensions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing.Logging +{ + internal static class LoggerExtensions + { + public static bool WriteValues([NotNull] this ILogger logger, object values) + { + return logger.WriteCore( + eventType: TraceType.Information, + eventId: 0, + state: values, + exception: null, + formatter: LogFormatter.Formatter); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/RouteCollectionRouteAsyncValues.cs b/src/Microsoft.AspNet.Routing/Logging/RouteCollectionRouteAsyncValues.cs new file mode 100644 index 0000000000..f8303bb91a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/RouteCollectionRouteAsyncValues.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNet.Routing.Logging +{ + /// + /// Describes the state of + /// . + /// + public class RouteCollectionRouteAsyncValues + { + /// + /// The name of the state. + /// + public string Name + { + get + { + return "RouteCollection.RouteAsync"; + } + } + + /// + /// The available routes. + /// + public IList Routes { get; set; } + + /// + /// True if the request is handled. + /// + public bool Handled { get; set; } + + /// + /// A summary of the data for display. + /// + public string Summary + { + get + { + var builder = new StringBuilder(); + builder.AppendLine(Name); + builder.Append("\tRoutes: "); + StringBuilderHelpers.Append(builder, Routes); + builder.AppendLine(); + builder.Append("\tHandled? "); + builder.Append(Handled); + return builder.ToString(); + } + } + + /// + public override string ToString() + { + return Summary; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/RouteConstraintMatcherMatchValues.cs b/src/Microsoft.AspNet.Routing/Logging/RouteConstraintMatcherMatchValues.cs new file mode 100644 index 0000000000..4f6272eb70 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/RouteConstraintMatcherMatchValues.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; + +namespace Microsoft.AspNet.Routing.Logging +{ + /// + /// Describes the state of . + /// + public class RouteConstraintMatcherMatchValues + { + /// + /// The name of the state. + /// + public string Name + { + get + { + return "RouteConstraintMatcher.Match"; + } + } + + /// + /// The key of the constraint. + /// + public string ConstraintKey { get; set; } + + /// + /// The constraint. + /// + public IRouteConstraint Constraint { get; set; } + + /// + /// True if the matched. + /// + public bool Matched { get; set; } + + /// + /// A summary of the data for display. + /// + public string Summary + { + get + { + var builder = new StringBuilder(); + builder.AppendLine(Name); + builder.Append("\tConstraint key: "); + builder.AppendLine(ConstraintKey); + builder.Append("\tConstraint: "); + builder.Append(Constraint); + builder.AppendLine(); + builder.Append("\tMatched? "); + builder.Append(Matched); + return builder.ToString(); + } + } + + /// + public override string ToString() + { + return Summary; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/RouterMiddlewareInvokeValues.cs b/src/Microsoft.AspNet.Routing/Logging/RouterMiddlewareInvokeValues.cs new file mode 100644 index 0000000000..7a474b1a9d --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/RouterMiddlewareInvokeValues.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text; + +namespace Microsoft.AspNet.Routing.Logging +{ + /// + /// Describes the state of + /// . + /// + public class RouterMiddlewareInvokeValues + { + /// + /// The name of the state. + /// + public string Name + { + get + { + return "RouterMiddleware.Invoke"; + } + } + + /// + /// True if the request is handled. + /// + public bool Handled { get; set; } + + /// + /// A summary of the data for display. + /// + public string Summary + { + get + { + var builder = new StringBuilder(); + builder.AppendLine(Name); + builder.Append("\tHandled? "); + builder.Append(Handled); + return builder.ToString(); + } + } + + /// + public override string ToString() + { + return Summary; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/StringBuilderHelpers.cs b/src/Microsoft.AspNet.Routing/Logging/StringBuilderHelpers.cs new file mode 100644 index 0000000000..218ff36f71 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/StringBuilderHelpers.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNet.Routing.Logging +{ + internal static class StringBuilderHelpers + { + public static void Append(StringBuilder builder, IEnumerable items) + { + if (items == null) + { + return; + } + + foreach (var item in items) + { + builder.Append(Environment.NewLine); + builder.Append("\t\t"); + builder.Append(item != null ? item.ToString() : "null"); + } + } + + public static void Append(StringBuilder builder, IDictionary dict) + { + if (dict == null) + { + return; + } + + foreach (var kvp in dict) + { + builder.Append(Environment.NewLine); + builder.Append("\t\t"); + builder.Append(kvp.Key != null ? kvp.Key.ToString() : "null"); + builder.Append(" : "); + builder.Append(kvp.Value != null ? kvp.Value.ToString() : "null"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs b/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs new file mode 100644 index 0000000000..4163f88391 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Logging/TemplateRouteRouteAsyncValues.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNet.Routing.Logging +{ + /// + /// Describes the state of + /// . + /// + public class TemplateRouteRouteAsyncValues + { + /// + /// The name of the state. + /// + public string Name + { + get + { + return "TemplateRoute.RouteAsync"; + } + } + + /// + /// The target. + /// + public IRouter Target { get; set; } + + /// + /// The template. + /// + public string Template { get; set; } + + /// + /// The request path. + /// + public string RequestPath { get; set; } + + /// + /// The values produced by default. + /// + public IDictionary DefaultValues { get; set; } + + /// + /// The values produced from the request. + /// + public IDictionary ProducedValues { get; set; } + + /// + /// The constraints matched on the produced values. + /// + public IDictionary Constraints { get; set; } + + /// + /// True if the matched. + /// + public bool MatchedTemplate { get; set; } + + /// + /// True if the matched. + /// + public bool MatchedConstraints { get; set; } + + /// + /// True if this route matched. + /// + public bool Matched { get; set; } + + /// + /// True if the request is handled. + /// + public bool Handled { get; set; } + + /// + /// A summary of the data for display. + /// + public string Summary + { + get + { + var builder = new StringBuilder(); + builder.AppendLine(Name); + builder.Append("\tTarget: "); + builder.Append(Target); + builder.AppendLine(); + builder.Append("\tTemplate: "); + builder.AppendLine(Template); + builder.Append("\tRequest path: "); + builder.AppendLine(RequestPath); + builder.Append("\tDefault values: "); + StringBuilderHelpers.Append(builder, DefaultValues); + builder.AppendLine(); + builder.Append("\tProduced values: "); + StringBuilderHelpers.Append(builder, ProducedValues); + builder.AppendLine(); + builder.Append("\tConstraints: "); + StringBuilderHelpers.Append(builder, Constraints); + builder.AppendLine(); + builder.Append("\tMatched template? "); + builder.Append(MatchedTemplate); + builder.AppendLine(); + builder.Append("\tMatched constraints? "); + builder.Append(MatchedConstraints); + builder.AppendLine(); + builder.Append("\tMatched? "); + builder.Append(Matched); + builder.AppendLine(); + builder.Append("\tHandled? "); + builder.Append(Handled); + return builder.ToString(); + } + } + + /// + public override string ToString() + { + return Summary; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj index 6b0e870438..2168cb71e0 100644 --- a/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj +++ b/src/Microsoft.AspNet.Routing/Microsoft.AspNet.Routing.kproj @@ -47,10 +47,17 @@ + + + + + + + diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index 682a96649b..e232347929 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -4,6 +4,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Routing { @@ -11,8 +15,9 @@ namespace Microsoft.AspNet.Routing { private readonly List _routes = new List(); private readonly List _unnamedRoutes = new List(); - private readonly Dictionary _namedRoutes = + private readonly Dictionary _namedRoutes = new Dictionary(StringComparer.OrdinalIgnoreCase); + private ILogger _logger; public IRouter this[int index] { @@ -44,14 +49,27 @@ namespace Microsoft.AspNet.Routing public async virtual Task RouteAsync(RouteContext context) { - for (var i = 0; i < Count; i++) + EnsureLogger(context.HttpContext); + using (_logger.BeginScope("RouteCollection.RouteAsync")) { - var route = this[i]; - - await route.RouteAsync(context); - if (context.IsHandled) + for (var i = 0; i < Count; i++) { - return; + var route = this[i]; + + await route.RouteAsync(context); + if (context.IsHandled) + { + break; + } + } + + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(new RouteCollectionRouteAsyncValues() + { + Handled = context.IsHandled, + Routes = _routes + }); } } } @@ -98,5 +116,14 @@ namespace Microsoft.AspNet.Routing return null; } + + private void EnsureLogger(HttpContext context) + { + if (_logger == null) + { + var factory = context.RequestServices.GetService(); + _logger = factory.Create(); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs index 64c3341e79..c85f934fd0 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Routing { @@ -12,7 +14,8 @@ namespace Microsoft.AspNet.Routing [NotNull] IDictionary routeValues, [NotNull] HttpContext httpContext, [NotNull] IRouter route, - [NotNull] RouteDirection routeDirection) + [NotNull] RouteDirection routeDirection, + [NotNull] ILogger logger) { if (constraints == null) { @@ -24,8 +27,30 @@ namespace Microsoft.AspNet.Routing var constraint = kvp.Value; if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection)) { + if (routeDirection.Equals(RouteDirection.IncomingRequest) + && logger.IsEnabled(TraceType.Information)) + { + logger.WriteValues(new RouteConstraintMatcherMatchValues() + { + ConstraintKey = kvp.Key, + Constraint = kvp.Value, + Matched = false + }); + } + return false; } + + if (routeDirection.Equals(RouteDirection.IncomingRequest) + && logger.IsEnabled(TraceType.Information)) + { + logger.WriteValues(new RouteConstraintMatcherMatchValues() + { + ConstraintKey = kvp.Key, + Constraint = kvp.Value, + Matched = true + }); + } } return true; diff --git a/src/Microsoft.AspNet.Routing/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/RouterMiddleware.cs index 50e609b609..c249537dca 100644 --- a/src/Microsoft.AspNet.Routing/RouterMiddleware.cs +++ b/src/Microsoft.AspNet.Routing/RouterMiddleware.cs @@ -4,11 +4,16 @@ using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Builder { public class RouterMiddleware { + private ILogger _logger; + public RouterMiddleware(RequestDelegate next, IRouter router) { Next = next; @@ -29,13 +34,32 @@ namespace Microsoft.AspNet.Builder public async Task Invoke(HttpContext httpContext) { - var context = new RouteContext(httpContext); - context.RouteData.Routers.Add(Router); - - await Router.RouteAsync(context); - if (!context.IsHandled) + EnsureLogger(httpContext); + using (_logger.BeginScope("RouterMiddleware.Invoke")) { - await Next.Invoke(httpContext); + var context = new RouteContext(httpContext); + context.RouteData.Routers.Add(Router); + + await Router.RouteAsync(context); + + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(new RouterMiddlewareInvokeValues() { Handled = context.IsHandled }); + } + + if (!context.IsHandled) + { + await Next.Invoke(httpContext); + } + } + } + + private void EnsureLogger(HttpContext context) + { + if (_logger == null) + { + var factory = context.RequestServices.GetService(); + _logger = factory.Create(); } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index e6c774789d..6a73d0d97f 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -4,7 +4,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNet.Routing.Constraints; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Routing.Template { @@ -17,6 +20,8 @@ namespace Microsoft.AspNet.Routing.Template private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; + private ILogger _logger; + private ILogger _constraintLogger; public TemplateRoute(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : this(target, routeTemplate, null, null, inlineConstraintResolver) @@ -73,30 +78,69 @@ namespace Microsoft.AspNet.Routing.Template public async virtual Task RouteAsync([NotNull] RouteContext context) { - var requestPath = context.HttpContext.Request.Path.Value; - if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/') + EnsureLoggers(context.HttpContext); + using (_logger.BeginScope("TemplateRoute.RouteAsync")) { - requestPath = requestPath.Substring(1); - } + var requestPath = context.HttpContext.Request.Path.Value; - var values = _matcher.Match(requestPath, Defaults); - if (values == null) - { - // If we got back a null value set, that means the URI did not match - return; - } - else - { - // Not currently doing anything to clean this up if it's not a match. Consider hardening this. - context.RouteData.Values = values; - - if (RouteConstraintMatcher.Match(Constraints, - values, - context.HttpContext, - this, - RouteDirection.IncomingRequest)) + if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/') { - await _target.RouteAsync(context); + requestPath = requestPath.Substring(1); + } + + var values = _matcher.Match(requestPath, Defaults); + + if (values == null) + { + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(CreateRouteAsyncValues( + requestPath, + values, + matchedValues: false, + matchedConstraints: false, + handled: context.IsHandled)); + } + + // If we got back a null value set, that means the URI did not match + return; + } + else + { + // Not currently doing anything to clean this up if it's not a match. Consider hardening this. + context.RouteData.Values = values; + + if (RouteConstraintMatcher.Match(Constraints, + values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) + { + await _target.RouteAsync(context); + + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(CreateRouteAsyncValues( + requestPath, + values, + matchedValues: true, + matchedConstraints: true, + handled: context.IsHandled)); + } + } + else + { + if (_logger.IsEnabled(TraceType.Information)) + { + _logger.WriteValues(CreateRouteAsyncValues( + requestPath, + values, + matchedValues: true, + matchedConstraints: false, + handled: context.IsHandled)); + } + } } } } @@ -110,11 +154,13 @@ namespace Microsoft.AspNet.Routing.Template return null; } + EnsureLoggers(context.Context); if (!RouteConstraintMatcher.Match(Constraints, values.CombinedValues, context.Context, this, - RouteDirection.UrlGeneration)) + RouteDirection.UrlGeneration, + _constraintLogger)) { return null; } @@ -208,5 +254,41 @@ namespace Microsoft.AspNet.Routing.Template } } } + + private TemplateRouteRouteAsyncValues CreateRouteAsyncValues( + string requestPath, + IDictionary producedValues, + bool matchedValues, + bool matchedConstraints, + bool handled) + { + var values = new TemplateRouteRouteAsyncValues(); + values.Template = _routeTemplate; + values.RequestPath = requestPath; + values.DefaultValues = Defaults; + values.ProducedValues = producedValues; + values.Constraints = _constraints; + values.Target = _target; + values.MatchedTemplate = matchedValues; + values.MatchedConstraints = matchedConstraints; + values.Matched = matchedValues && matchedConstraints; + values.Handled = handled; + return values; + } + + private void EnsureLoggers(HttpContext context) + { + if (_logger == null) + { + var factory = context.RequestServices.GetService(); + _logger = factory.Create(); + _constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName); + } + } + + public override string ToString() + { + return _routeTemplate; + } } } diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 196d83ee36..24690c6e95 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -5,8 +5,9 @@ }, "dependencies": { "Microsoft.AspNet.Http": "1.0.0-*", - "Microsoft.Framework.DependencyInjection" : "1.0.0-*", "Microsoft.AspNet.RequestContainer": "1.0.0-*", + "Microsoft.Framework.DependencyInjection" : "1.0.0-*", + "Microsoft.Framework.Logging": "1.0.0-*", "Microsoft.Framework.OptionsModel": "1.0.0-*" }, "frameworks": { diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTest.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTest.cs new file mode 100644 index 0000000000..3a88c816dc --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTest.cs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +#if NET45 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Routing +{ + public class ConstraintMatcherTest + { +#if NET45 + [Fact] + public void MatchUrlGeneration_DoesNotLogData() + { + // Arrange + var name = "name"; + + var sink = new TestSink(); + var logger = new TestLogger(name, sink); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.UrlGeneration, + logger: logger); + + // Assert + // There are no BeginScopes called. + Assert.Equal(0, sink.Scopes.Count); + + // There are no WriteCores called. + Assert.Equal(0, sink.Writes.Count); + } + + [Fact] + public void MatchFail_LogsCorrectData() + { + // Arrange + var name = "name"; + + var sink = new TestSink(); + var logger = new TestLogger(name, sink); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: logger); + + // Assert + // There are no begin scopes called. + Assert.Equal(0, sink.Scopes.Count); + + // There are two records for IsEnabled and two for WriteCore. + Assert.Equal(4, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(name, enabled.LoggerName); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(name, write.LoggerName); + var values = Assert.IsType(write.State); + Assert.Equal("RouteConstraintMatcher.Match", values.Name); + Assert.Equal("a", values.ConstraintKey); + Assert.Equal(constraints["a"], values.Constraint); + Assert.Equal(true, values.Matched); + + enabled = sink.Writes[2]; + Assert.Equal(name, enabled.LoggerName); + Assert.Null(enabled.State); + + write = sink.Writes[3]; + Assert.Equal(name, write.LoggerName); + values = Assert.IsType(write.State); + Assert.Equal("RouteConstraintMatcher.Match", values.Name); + Assert.Equal("b", values.ConstraintKey); + Assert.Equal(constraints["b"], values.Constraint); + Assert.Equal(false, values.Matched); + } + + [Fact] + public void MatchSuccess_LogsCorrectData() + { + // Arrange + var name = "name"; + + var sink = new TestSink(); + var logger = new TestLogger(name, sink); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new PassConstraint()} + }; + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: logger); + + // Assert + // There are no begin scopes called. + Assert.Equal(0, sink.Scopes.Count); + + // There are two records for IsEnabled and two for WriteCore. + Assert.Equal(4, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(name, enabled.LoggerName); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(name, write.LoggerName); + var values = Assert.IsType(write.State); + Assert.Equal("RouteConstraintMatcher.Match", values.Name); + Assert.Equal("a", values.ConstraintKey); + Assert.Equal(constraints["a"], values.Constraint); + Assert.Equal(true, values.Matched); + + enabled = sink.Writes[2]; + Assert.Equal(name, enabled.LoggerName); + Assert.Null(enabled.State); + + write = sink.Writes[3]; + Assert.Equal(name, write.LoggerName); + values = Assert.IsType(write.State); + Assert.Equal("RouteConstraintMatcher.Match", values.Name); + Assert.Equal("b", values.ConstraintKey); + Assert.Equal(constraints["b"], values.Constraint); + Assert.Equal(true, values.Matched); + } + + [Fact] + public void ReturnsTrueOnValidConstraints() + { + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new PassConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ConstraintsGetTheRightKey() + { + var constraints = new Dictionary + { + {"a", new PassConstraint("a")}, + {"b", new PassConstraint("b")} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatDontMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch() + { + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } + + [Fact] + public void ReturnsTrueOnNullInput() + { + Assert.True(RouteConstraintMatcher.Match( + constraints: null, + routeValues: new RouteValueDictionary(), + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } +#endif + + private class PassConstraint : IRouteConstraint + { + private readonly string _expectedKey; + + public PassConstraint(string expectedKey = null) + { + _expectedKey = expectedKey; + } + + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + if (_expectedKey != null) + { + Assert.Equal(_expectedKey, routeKey); + } + + return true; + } + } + + private class FailConstraint : IRouteConstraint + { + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + return false; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs deleted file mode 100644 index 0a3ff5926b..0000000000 --- a/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#if NET45 -using System.Collections.Generic; -using Microsoft.AspNet.Http; -using Moq; -using Xunit; - -namespace Microsoft.AspNet.Routing.Tests -{ - public class ConstraintMatcherTests - { - [Fact] - public void ReturnsTrueOnValidConstraints() - { - var constraints = new Dictionary - { - {"a", new PassConstraint()}, - {"b", new PassConstraint()} - }; - - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - - Assert.True(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - [Fact] - public void ConstraintsGetTheRightKey() - { - var constraints = new Dictionary - { - {"a", new PassConstraint("a")}, - {"b", new PassConstraint("b")} - }; - - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - - Assert.True(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - [Fact] - public void ReturnsFalseOnInvalidConstraintsThatDontMatch() - { - var constraints = new Dictionary - { - {"a", new FailConstraint()}, - {"b", new FailConstraint()} - }; - - var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); - - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - [Fact] - public void ReturnsFalseOnInvalidConstraintsThatMatch() - { - var constraints = new Dictionary - { - {"a", new FailConstraint()}, - {"b", new FailConstraint()} - }; - - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - [Fact] - public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch() - { - var constraints = new Dictionary - { - {"a", new PassConstraint()}, - {"b", new FailConstraint()} - }; - - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - [Fact] - public void ReturnsTrueOnNullInput() - { - Assert.True(RouteConstraintMatcher.Match( - constraints: null, - routeValues: new RouteValueDictionary(), - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest)); - } - - private class PassConstraint : IRouteConstraint - { - private readonly string _expectedKey; - - public PassConstraint(string expectedKey = null) - { - _expectedKey = expectedKey; - } - - public bool Match(HttpContext httpContext, - IRouter route, - string routeKey, - IDictionary values, - RouteDirection routeDirection) - { - if (_expectedKey != null) - { - Assert.Equal(_expectedKey, routeKey); - } - - return true; - } - } - - private class FailConstraint : IRouteConstraint - { - public bool Match(HttpContext httpContext, - IRouter route, - string routeKey, - IDictionary values, - RouteDirection routeDirection) - { - return false; - } - } - } -} -#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/BeginScopeContext.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/BeginScopeContext.cs new file mode 100644 index 0000000000..456d1c1a6d --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/BeginScopeContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public class BeginScopeContext + { + public object Scope { get; set; } + + public string LoggerName { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/NullDisposable.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/NullDisposable.cs new file mode 100644 index 0000000000..ad687d1dbe --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/NullDisposable.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Routing +{ + public class NullDisposable : IDisposable + { + public static NullDisposable Instance = new NullDisposable(); + + public void Dispose() + { + // intentionally does nothing + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/NullLogger.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/NullLogger.cs new file mode 100644 index 0000000000..f92dc40c34 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/NullLogger.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing +{ + public class NullLogger : ILogger + { + public static NullLogger Instance = new NullLogger(); + + public IDisposable BeginScope(object state) + { + return NullDisposable.Instance; + } + + public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + { + return false; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/NullLoggerFactory.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/NullLoggerFactory.cs new file mode 100644 index 0000000000..e012631af7 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/NullLoggerFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing +{ + public class NullLoggerFactory : ILoggerFactory + { + public static NullLoggerFactory Instance = new NullLoggerFactory(); + + public ILogger Create(string name) + { + return NullLogger.Instance; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/TestLogger.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/TestLogger.cs new file mode 100644 index 0000000000..2627d8463b --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/TestLogger.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing +{ + public class TestLogger : ILogger + { + private object _scope; + private TestSink _sink; + private string _name; + + public TestLogger(string name, TestSink sink) + { + _sink = sink; + _name = name; + } + + public string Name { get; set; } + + public IDisposable BeginScope(object state) + { + _scope = state; + + _sink.Begin(new BeginScopeContext() + { + LoggerName = _name, + Scope = state, + }); + + return NullDisposable.Instance; + } + + public bool WriteCore(TraceType eventType, int eventId, object state, Exception exception, Func formatter) + { + _sink.Write(new WriteCoreContext() + { + EventType = eventType, + EventId = eventId, + State = state, + Exception = exception, + Formatter = formatter, + LoggerName = _name, + Scope = _scope + }); + + return true; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/TestLoggerFactory.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/TestLoggerFactory.cs new file mode 100644 index 0000000000..e30540ec12 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/TestLoggerFactory.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing +{ + public class TestLoggerFactory : ILoggerFactory + { + private TestSink _sink; + + public TestLoggerFactory(TestSink sink) + { + _sink = sink; + } + + public ILogger Create(string name) + { + return new TestLogger(name, _sink); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/TestSink.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/TestSink.cs new file mode 100644 index 0000000000..4c50f01caa --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/TestSink.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class TestSink + { + public TestSink( + Func writeEnabled = null, + Func beginEnabled = null) + { + WriteEnabled = writeEnabled; + BeginEnabled = beginEnabled; + + Scopes = new List(); + Writes = new List(); + } + + public Func WriteEnabled { get; set; } + + public Func BeginEnabled { get; set; } + + public List Scopes { get; set; } + + public List Writes { get; set; } + + public void Write(WriteCoreContext context) + { + if (WriteEnabled == null || WriteEnabled(context)) + { + Writes.Add(context); + } + } + + public void Begin(BeginScopeContext context) + { + if (BeginEnabled == null || BeginEnabled(context)) + { + Scopes.Add(context); + } + } + + public static bool EnableWithTypeName(WriteCoreContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + + public static bool EnableWithTypeName(BeginScopeContext context) + { + return context.LoggerName.Equals(typeof(T).FullName); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Logging/WriteCoreContext.cs b/test/Microsoft.AspNet.Routing.Tests/Logging/WriteCoreContext.cs new file mode 100644 index 0000000000..ddab5afb47 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Logging/WriteCoreContext.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Routing +{ + public class WriteCoreContext + { + public TraceType EventType { get; set; } + + public int EventId { get; set; } + + public object State { get; set; } + + public Exception Exception { get; set; } + + public Func Formatter { get; set; } + + public object Scope { get; set; } + + public string LoggerName { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj index 2d852ad36b..1e746e3946 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj +++ b/test/Microsoft.AspNet.Routing.Tests/Microsoft.AspNet.Routing.Tests.kproj @@ -20,7 +20,7 @@ - + @@ -42,15 +42,24 @@ + + + + + + + + + - + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs index 0809d1823a..b2fbe40256 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs @@ -2,19 +2,102 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #if NET45 - using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.Logging; using Moq; using Xunit; -namespace Microsoft.AspNet.Routing.Tests +namespace Microsoft.AspNet.Routing { public class RouteCollectionTest { + [Fact] + public async Task RouteAsync_LogsCorrectValuesWhenHandled() + { + // Arrange + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var routes = new RouteCollection(); + var route = CreateRoute(accept: true); + routes.Add(route.Object); + + var context = CreateRouteContext("/Cool", loggerFactory); + + // Act + await routes.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(RouteCollection).FullName, scope.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(RouteCollection).FullName, enabled.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(RouteCollection).FullName, write.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("RouteCollection.RouteAsync", values.Name); + Assert.NotNull(values.Routes); + Assert.Equal(true, values.Handled); + } + + [Fact] + public async Task RouteAsync_LogsCorrectValuesWhenNotHandled() + { + // Arrange + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var routes = new RouteCollection(); + var route = CreateRoute(accept: false); + routes.Add(route.Object); + + var context = CreateRouteContext("/Cool", loggerFactory); + + // Act + await routes.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(RouteCollection).FullName, scope.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(RouteCollection).FullName, enabled.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(RouteCollection).FullName, write.LoggerName); + Assert.Equal("RouteCollection.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("RouteCollection.RouteAsync", values.Name); + Assert.NotNull(values.Routes); + Assert.Equal(false, values.Handled); + } + [Fact] public async Task RouteAsync_FirstMatches() { @@ -214,12 +297,19 @@ namespace Microsoft.AspNet.Routing.Tests return new VirtualPathContext(null, null, null, routeName); } - private static RouteContext CreateRouteContext(string requestPath) + private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) { + if (factory == null) + { + factory = NullLoggerFactory.Instance; + } + var request = new Mock(MockBehavior.Strict); request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(factory); context.SetupGet(c => c.Request).Returns(request.Object); return new RouteContext(context.Object); @@ -244,5 +334,4 @@ namespace Microsoft.AspNet.Routing.Tests } } } - #endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs new file mode 100644 index 0000000000..0482bbe6f1 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/RouterMiddlewareTest.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Logging; +using Microsoft.Framework.Logging; +#if NET45 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Routing +{ + public class RouterMiddlewareTest + { +#if NET45 + [Fact] + public async void Invoke_LogsCorrectValuesWhenNotHandled() + { + // Arrange + var isHandled = false; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var mockContext = new Mock(MockBehavior.Strict); + mockContext.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(loggerFactory); + + RequestDelegate next = (c) => + { + return Task.FromResult(null); + }; + + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, router); + + // Act + await middleware.Invoke(mockContext.Object); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(RouterMiddleware).FullName, scope.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(RouterMiddleware).FullName, enabled.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(RouterMiddleware).FullName, write.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("RouterMiddleware.Invoke", values.Name); + Assert.Equal(false, values.Handled); + } + + [Fact] + public async void Invoke_LogsCorrectValuesWhenHandled() + { + // Arrange + var isHandled = true; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var mockContext = new Mock(MockBehavior.Strict); + mockContext.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(loggerFactory); + + RequestDelegate next = (c) => + { + return Task.FromResult(null); + }; + + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, router); + + // Act + await middleware.Invoke(mockContext.Object); + + // Assert + // exists a BeginScope, verify contents + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(RouterMiddleware).FullName, scope.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(RouterMiddleware).FullName, enabled.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(RouterMiddleware).FullName, write.LoggerName); + Assert.Equal("RouterMiddleware.Invoke", write.Scope); + Assert.Equal(typeof(RouterMiddlewareInvokeValues), write.State.GetType()); + var values = (RouterMiddlewareInvokeValues)write.State; + Assert.Equal("RouterMiddleware.Invoke", values.Name); + Assert.Equal(true, values.Handled); + } +#endif + + private class TestRouter : IRouter + { + private bool _isHandled; + + public TestRouter(bool isHandled) + { + _isHandled = isHandled; + } + + public string GetVirtualPath(VirtualPathContext context) + { + return ""; + } + + public Task RouteAsync(RouteContext context) + { + context.IsHandled = _isHandled; + return Task.FromResult(null); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs similarity index 79% rename from test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs rename to test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs index d2d29f23c7..44717645a0 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs @@ -8,17 +8,158 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Constraints; +using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Testing; -using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; using Moq; using Xunit; -namespace Microsoft.AspNet.Routing.Template.Tests +namespace Microsoft.AspNet.Routing.Template { - public class TemplateRouteTests + public class TemplateRouteTest { private static IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + [Fact] + public async Task RouteAsync_MatchSuccess_LogsCorrectValues() + { + // Arrange + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var template = "{controller}/{action}"; + + var route = CreateRoute(template); + var context = CreateRouteContext("/Home/Index", loggerFactory); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, scope.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, enabled.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(TemplateRoute).FullName, write.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", write.Scope); + + // verify WriteCore state contents + var values = Assert.IsType(write.State); + Assert.Equal("TemplateRoute.RouteAsync", values.Name); + Assert.Equal("Home/Index", values.RequestPath); + Assert.Equal(template, values.Template); + Assert.NotNull(values.DefaultValues); + Assert.NotNull(values.ProducedValues); + Assert.Equal(true, values.MatchedTemplate); + Assert.Equal(true, values.MatchedConstraints); + Assert.Equal(true, values.Matched); + Assert.Equal(context.IsHandled, values.Handled); + } + + [Fact] + public async Task RouteAsync_MatchFailOnValues_LogsCorrectValues() + { + // Arrange + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var template = "{controller}/{action}"; + + var route = CreateRoute(template); + var context = CreateRouteContext("/Home/Index/Failure", loggerFactory); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, scope.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, enabled.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(TemplateRoute).FullName, write.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("TemplateRoute.RouteAsync", values.Name); + Assert.Equal("Home/Index/Failure", values.RequestPath); + Assert.Equal(template, values.Template); + Assert.NotNull(values.DefaultValues); + Assert.Null(values.ProducedValues); + Assert.Equal(false, values.MatchedTemplate); + Assert.Equal(false, values.MatchedConstraints); + Assert.Equal(false, values.Matched); + Assert.Equal(context.IsHandled, values.Handled); + } + + [Fact] + public async Task RouteAsync_MatchFailOnConstraints_LogsCorrectValues() + { + // Arrange + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink); + + var template = "{controller}/{action}/{id:int}"; + + var route = CreateRoute(template); + var context = CreateRouteContext("/Home/Index/Failure", loggerFactory); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, scope.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", scope.Scope); + + // There is a record for IsEnabled and one for WriteCore. + Assert.Equal(2, sink.Writes.Count); + + var enabled = sink.Writes[0]; + Assert.Equal(typeof(TemplateRoute).FullName, enabled.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", enabled.Scope); + Assert.Null(enabled.State); + + var write = sink.Writes[1]; + Assert.Equal(typeof(TemplateRoute).FullName, write.LoggerName); + Assert.Equal("TemplateRoute.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("TemplateRoute.RouteAsync", values.Name); + Assert.Equal("Home/Index/Failure", values.RequestPath); + Assert.Equal(template, values.Template); + Assert.NotNull(values.DefaultValues); + Assert.NotNull(values.ProducedValues); + Assert.Equal(true, values.MatchedTemplate); + Assert.Equal(false, values.MatchedConstraints); + Assert.Equal(false, values.Matched); + Assert.Equal(context.IsHandled, values.Handled); + } + #region Route Matching // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases. @@ -116,20 +257,26 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Null(context.RouteData.Values["1controller"]); } - private static RouteContext CreateRouteContext(string requestPath) + private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) { + if (factory == null) + { + factory = NullLoggerFactory.Instance; + } + var request = new Mock(MockBehavior.Strict); request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(factory); context.SetupGet(c => c.Request).Returns(request.Object); return new RouteContext(context.Object); } - #endregion -#region Route Binding + #region Route Binding [Fact] public void GetVirtualPath_Success() @@ -502,6 +649,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests private static VirtualPathContext CreateVirtualPathContext(IDictionary values, IDictionary ambientValues) { var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); return new VirtualPathContext(context.Object, ambientValues, values); } @@ -712,5 +861,4 @@ namespace Microsoft.AspNet.Routing.Template.Tests } } } - #endif diff --git a/test/Microsoft.AspNet.Routing.Tests/project.json b/test/Microsoft.AspNet.Routing.Tests/project.json index 6f997da88d..ce70d799ec 100644 --- a/test/Microsoft.AspNet.Routing.Tests/project.json +++ b/test/Microsoft.AspNet.Routing.Tests/project.json @@ -6,6 +6,7 @@ "Microsoft.AspNet.Http" : "1.0.0-*", "Microsoft.AspNet.Routing" : "", "Microsoft.AspNet.Testing" : "1.0.0-*", + "Microsoft.Framework.Logging" : "1.0.0-*", "Xunit.KRunner": "1.0.0-*" }, "frameworks": {