// 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 System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.AspNetCore.Mvc.Performance { public class ActionSelectorBenchmark { private const int Seed = 1000; // About 35 or so plausible sounding conventional routing actions. // // We include some duplicates here, because that's what happens when you have one method that handles // GET and one that handles POST. private static readonly ActionDescriptor[] _actions = new ActionDescriptor[] { CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "AddUser" }), CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "AddUser" }), CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "DeleteUser" }), CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "DeleteUser" }), CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "Details" }), CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "List" }), CreateActionDescriptor(new { area = "Admin", controller = "Diagnostics", action = "Stats" }), CreateActionDescriptor(new { area = "Admin", controller = "Diagnostics", action = "Performance" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "CreateProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "CreateProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "DeleteProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "DeleteProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "EditProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "EditProduct" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "Index" }), CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "Inventory" }), CreateActionDescriptor(new { area = "Store", controller = "Search", action = "FindProduct" }), CreateActionDescriptor(new { area = "Store", controller = "Search", action = "ShowCategory" }), CreateActionDescriptor(new { area = "Store", controller = "Search", action = "HotItems" }), CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Index" }), CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Details" }), CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Buy" }), CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "ViewCart" }), CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Billing" }), CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Confim" }), CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Confim" }), CreateActionDescriptor(new { area = "", controller = "Blog", action = "Index" }), CreateActionDescriptor(new { area = "", controller = "Blog", action = "Search" }), CreateActionDescriptor(new { area = "", controller = "Blog", action = "ViewPost" }), CreateActionDescriptor(new { area = "", controller = "Blog", action = "PostComment" }), CreateActionDescriptor(new { area = "", controller = "Home", action = "Index" }), CreateActionDescriptor(new { area = "", controller = "Home", action = "Search" }), CreateActionDescriptor(new { area = "", controller = "Home", action = "About" }), CreateActionDescriptor(new { area = "", controller = "Home", action = "Contact" }), CreateActionDescriptor(new { area = "", controller = "Home", action = "Support" }), }; private static readonly KeyValuePair>[] _dataSet = GetDataSet(_actions); private static readonly IActionSelector _actionSelector = CreateActionSelector(_actions); [Benchmark(Description = "conventional action selection implementation")] public void SelectCandidates_MatchRouteData() { var routeContext = new RouteContext(new DefaultHttpContext()); for (var i = 0; i < _dataSet.Length; i++) { var routeValues = _dataSet[i].Key; var expected = _dataSet[i].Value; var state = routeContext.RouteData.PushState(MockRouter.Instance, routeValues, null); var actual = _actionSelector.SelectCandidates(routeContext); Verify(expected, actual); state.Restore(); } } [Benchmark(Baseline = true, Description = "conventional action selection baseline")] public void SelectCandidates_Baseline() { var routeContext = new RouteContext(new DefaultHttpContext()); for (var i = 0; i < _dataSet.Length; i++) { var routeValues = _dataSet[i].Key; var expected = _dataSet[i].Value; var state = routeContext.RouteData.PushState(MockRouter.Instance, routeValues, null); var actual = NaiveSelectCandiates(_actions, routeContext.RouteData.Values); Verify(expected, actual); state.Restore(); } } // A naive implementation we can use to generate match data for inputs, and for a baseline. private static IReadOnlyList NaiveSelectCandiates(ActionDescriptor[] actions, RouteValueDictionary routeValues) { var results = new List(); for (var i = 0; i < actions.Length; i++) { var action = actions[i]; var isMatch = true; foreach (var kvp in action.RouteValues) { var routeValue = Convert.ToString(routeValues[kvp.Key]) ?? string.Empty; if (string.IsNullOrEmpty(kvp.Value) && string.IsNullOrEmpty(routeValue)) { // Match } else if (string.Equals(kvp.Value, routeValue, StringComparison.OrdinalIgnoreCase)) { // Match; } else { isMatch = false; break; } } if (isMatch) { results.Add(action); } } return results; } private static ActionDescriptor CreateActionDescriptor(object obj) { // Our real ActionDescriptors don't use RVD, they use a regular old dictionary. // Just using RVD here to understand the anonymous object for brevity. var routeValues = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kvp in new RouteValueDictionary(obj)) { routeValues.Add(kvp.Key, Convert.ToString(kvp.Value) ?? string.Empty); } return new ActionDescriptor() { RouteValues = routeValues, }; } private static KeyValuePair>[] GetDataSet(ActionDescriptor[] actions) { var random = new Random(Seed); var data = new List>>(); for (var i = 0; i < actions.Length; i += 2) { var action = actions[i]; var routeValues = new RouteValueDictionary(action.RouteValues); var matches = NaiveSelectCandiates(actions, routeValues); if (matches.Count == 0) { throw new InvalidOperationException("This should have at least one match."); } data.Add(new KeyValuePair>(routeValues, matches)); } for (var i = 1; i < actions.Length; i += 3) { var action = actions[i]; var routeValues = new RouteValueDictionary(action.RouteValues); // Make one of the route values not match. routeValues[routeValues.First().Key] = ((string)routeValues.First().Value) + "fkdkfdkkf"; var matches = NaiveSelectCandiates(actions, routeValues); if (matches.Count != 0) { throw new InvalidOperationException("This should have 0 matches."); } data.Add(new KeyValuePair>(routeValues, matches)); } return data.ToArray(); } private static void Verify(IReadOnlyList expected, IReadOnlyList actual) { if (expected.Count == 0 && actual == null) { return; } if (expected.Count != actual.Count) { throw new InvalidOperationException("The count is different."); } for (var i = 0; i < actual.Count; i++) { if (!object.ReferenceEquals(expected[i], actual[i])) { throw new InvalidOperationException("The actions don't match."); } } } private static IActionSelector CreateActionSelector(ActionDescriptor[] actions) { var actionCollection = new MockActionDescriptorCollectionProvider(actions); return new ActionSelector( actionCollection, new ActionConstraintCache(actionCollection, Enumerable.Empty()), NullLoggerFactory.Instance); } private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider { public MockActionDescriptorCollectionProvider(ActionDescriptor[] actions) { ActionDescriptors = new ActionDescriptorCollection(actions, 0); } public ActionDescriptorCollection ActionDescriptors { get; } } private class MockRouter : IRouter { public static readonly IRouter Instance = new MockRouter(); public VirtualPathData GetVirtualPath(VirtualPathContext context) { throw new NotImplementedException(); } public Task RouteAsync(RouteContext context) { throw new NotImplementedException(); } } } }