File: ActionSelectorBenchmark.cs
Web Access
Project: src\src\Mvc\perf\Microbenchmarks\Microsoft.AspNetCore.Mvc\Microsoft.AspNetCore.Mvc.Microbenchmarks.csproj (Microsoft.AspNetCore.Mvc.Microbenchmarks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
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.Routing;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Microsoft.AspNetCore.Mvc.Microbenchmarks;
 
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<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>[] _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 = NaiveSelectCandidates(_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<ActionDescriptor> NaiveSelectCandidates(ActionDescriptor[] actions, RouteValueDictionary routeValues)
    {
        var results = new List<ActionDescriptor>();
        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], CultureInfo.InvariantCulture) ??
                    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<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var kvp in new RouteValueDictionary(obj))
        {
            routeValues.Add(kvp.Key, Convert.ToString(kvp.Value, CultureInfo.InvariantCulture) ?? string.Empty);
        }
 
        return new ActionDescriptor()
        {
            RouteValues = routeValues,
        };
    }
 
    private static KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>[] GetDataSet(ActionDescriptor[] actions)
    {
        var random = new Random(Seed);
 
        var data = new List<KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>>();
 
        for (var i = 0; i < actions.Length; i += 2)
        {
            var action = actions[i];
            var routeValues = new RouteValueDictionary(action.RouteValues);
            var matches = NaiveSelectCandidates(actions, routeValues);
            if (matches.Count == 0)
            {
                throw new InvalidOperationException("This should have at least one match.");
            }
 
            data.Add(new KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>(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 = NaiveSelectCandidates(actions, routeValues);
            if (matches.Count != 0)
            {
                throw new InvalidOperationException("This should have 0 matches.");
            }
 
            data.Add(new KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>(routeValues, matches));
        }
 
        return data.ToArray();
    }
 
    private static void Verify(IReadOnlyList<ActionDescriptor> expected, IReadOnlyList<ActionDescriptor> 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<IActionConstraintProvider>()),
            NullLoggerFactory.Instance);
    }
 
    private sealed class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
    {
        public MockActionDescriptorCollectionProvider(ActionDescriptor[] actions)
        {
            ActionDescriptors = new ActionDescriptorCollection(actions, 0);
        }
 
        public ActionDescriptorCollection ActionDescriptors { get; }
    }
 
    private sealed 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();
        }
    }
}