diff --git a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs index 951d6b645c..8e81202fdc 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/LinkGenerationDecisionTree.cs @@ -3,12 +3,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; using Microsoft.AspNetCore.Routing.DecisionTree; using Microsoft.AspNetCore.Routing.Tree; namespace Microsoft.AspNetCore.Routing.Internal { // A decision tree that matches link generation entries based on route data. + [DebuggerDisplay("{DebuggerDisplayString,nq}")] public class LinkGenerationDecisionTree { private readonly DecisionTreeNode _root; @@ -160,5 +164,51 @@ namespace Microsoft.AspNetCore.Routing.Internal y.Match.Entry.RouteTemplate.TemplateText); } } + + // Example output: + // + // => action: Buy => controller: Store => version: V1(Matches: Store/Buy/V1) + // => action: Buy => controller: Store => version: V2(Matches: Store/Buy/V2) + // => action: Buy => controller: Store => area: Admin(Matches: Admin/Store/Buy) + // => action: Buy => controller: Products(Matches: Products/Buy) + // => action: Cart => controller: Store(Matches: Store/Cart) + internal string DebuggerDisplayString + { + get + { + var sb = new StringBuilder(); + var branchStack = new Stack(); + branchStack.Push(string.Empty); + FlattenTree(_root, branchStack, sb); + return sb.ToString(); + } + } + + private void FlattenTree(DecisionTreeNode node, Stack branchStack, StringBuilder sb) + { + // leaf node + if (node.Criteria.Count == 0) + { + var temp = new StringBuilder(); + foreach (var branch in branchStack) + { + temp.Insert(0, branch); + } + sb.Append(temp.ToString()); + sb.Append(" (Matches: "); + sb.Append(string.Join(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText))); + sb.AppendLine(")"); + } + + foreach (var criterion in node.Criteria) + { + foreach (var branch in criterion.Branches) + { + branchStack.Push($" => {criterion.Key}: {branch.Key}"); + FlattenTree(branch.Value, branchStack, sb); + branchStack.Pop(); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs index c8c34b09d4..f07296faba 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/LinkGenerationDecisionTreeTest.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation. 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.Linq; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Xunit; @@ -318,11 +320,45 @@ namespace Microsoft.AspNetCore.Routing.Internal.Routing Assert.Equal(entries, matches); } - private OutboundMatch CreateMatch(object requiredValues) + [Fact] + public void ToDebuggerDisplayString_GivesAFlattenedTree() + { + // Arrange + var entries = new List(); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V1" }, "Store/Buy/V1")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", area = "Admin" }, "Admin/Store/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Products" }, "Products/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V2" }, "Store/Buy/V2")); + entries.Add(CreateMatch(new { action = "Cart", controller = "Store" }, "Store/Cart")); + entries.Add(CreateMatch(new { action = "Index", controller = "Home" }, "Home/Index/{id?}")); + var tree = new LinkGenerationDecisionTree(entries); + var newLine = Environment.NewLine; + var expected = + " => action: Buy => controller: Store => version: V1 (Matches: Store/Buy/V1)" + newLine + + " => action: Buy => controller: Store => version: V2 (Matches: Store/Buy/V2)" + newLine + + " => action: Buy => controller: Store => area: Admin (Matches: Admin/Store/Buy)" + newLine + + " => action: Buy => controller: Products (Matches: Products/Buy)" + newLine + + " => action: Cart => controller: Store (Matches: Store/Cart)" + newLine + + " => action: Index => controller: Home (Matches: Home/Index/{id?})" + newLine; + + // Act + var flattenedTree = tree.DebuggerDisplayString; + + // Assert + Assert.Equal(expected, flattenedTree); + } + + private OutboundMatch CreateMatch(object requiredValues, string routeTemplate = null) { var match = new OutboundMatch(); match.Entry = new OutboundRouteEntry(); match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + + if (!string.IsNullOrEmpty(routeTemplate)) + { + match.Entry.RouteTemplate = new RouteTemplate(RoutePatternFactory.Parse(routeTemplate)); + } + return match; }