implement a decision-tree-based action selector
This commit is contained in:
parent
28092d975b
commit
8bfb6eb8d5
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Mvc.Core;
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
using Microsoft.AspNet.Mvc.Logging;
|
using Microsoft.AspNet.Mvc.Logging;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
|
||||||
|
|
@ -16,14 +17,18 @@ namespace Microsoft.AspNet.Mvc
|
||||||
public class DefaultActionSelector : IActionSelector
|
public class DefaultActionSelector : IActionSelector
|
||||||
{
|
{
|
||||||
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
||||||
|
private readonly IActionSelectorDecisionTreeProvider _decisionTreeProvider;
|
||||||
private readonly IActionBindingContextProvider _bindingProvider;
|
private readonly IActionBindingContextProvider _bindingProvider;
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
public DefaultActionSelector(IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
|
public DefaultActionSelector(
|
||||||
IActionBindingContextProvider bindingProvider,
|
[NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
|
||||||
[NotNull] ILoggerFactory loggerFactory)
|
[NotNull] IActionSelectorDecisionTreeProvider decisionTreeProvider,
|
||||||
|
[NotNull] IActionBindingContextProvider bindingProvider,
|
||||||
|
[NotNull] ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
|
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
|
||||||
|
_decisionTreeProvider = decisionTreeProvider;
|
||||||
_bindingProvider = bindingProvider;
|
_bindingProvider = bindingProvider;
|
||||||
_logger = loggerFactory.Create<DefaultActionSelector>();
|
_logger = loggerFactory.Create<DefaultActionSelector>();
|
||||||
}
|
}
|
||||||
|
|
@ -32,11 +37,8 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
using (_logger.BeginScope("DefaultActionSelector.SelectAsync"))
|
using (_logger.BeginScope("DefaultActionSelector.SelectAsync"))
|
||||||
{
|
{
|
||||||
var allDescriptors = GetActions();
|
var tree = _decisionTreeProvider.DecisionTree;
|
||||||
|
var matchingRouteConstraints = tree.Select(context.RouteData.Values);
|
||||||
var matchingRouteConstraints =
|
|
||||||
allDescriptors.Where(ad =>
|
|
||||||
MatchRouteConstraints(ad, context)).ToList();
|
|
||||||
|
|
||||||
var matchingRouteAndMethodConstraints =
|
var matchingRouteAndMethodConstraints =
|
||||||
matchingRouteConstraints.Where(ad =>
|
matchingRouteConstraints.Where(ad =>
|
||||||
|
|
@ -103,12 +105,6 @@ namespace Microsoft.AspNet.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MatchRouteConstraints(ActionDescriptor descriptor, RouteContext context)
|
|
||||||
{
|
|
||||||
return descriptor.RouteConstraints == null ||
|
|
||||||
descriptor.RouteConstraints.All(c => c.Accept(context));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context)
|
private bool MatchMethodConstraints(ActionDescriptor descriptor, RouteContext context)
|
||||||
{
|
{
|
||||||
return descriptor.MethodConstraints == null ||
|
return descriptor.MethodConstraints == null ||
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public class DecisionCriterion<TItem>
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<object, DecisionTreeNode<TItem>> Branches { get; set; }
|
||||||
|
|
||||||
|
public DecisionTreeNode<TItem> Fallback { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public struct DecisionCriterionValue
|
||||||
|
{
|
||||||
|
private readonly bool _isCatchAll;
|
||||||
|
private readonly object _value;
|
||||||
|
|
||||||
|
public DecisionCriterionValue(object value, bool isCatchAll)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
_isCatchAll = isCatchAll;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCatchAll
|
||||||
|
{
|
||||||
|
get { return _isCatchAll; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Value
|
||||||
|
{
|
||||||
|
get { return _value; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public class DecisionCriterionValueEqualityComparer : IEqualityComparer<DecisionCriterionValue>
|
||||||
|
{
|
||||||
|
public DecisionCriterionValueEqualityComparer(IEqualityComparer<object> innerComparer)
|
||||||
|
{
|
||||||
|
InnerComparer = innerComparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEqualityComparer<object> InnerComparer { get; private set; }
|
||||||
|
|
||||||
|
public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y)
|
||||||
|
{
|
||||||
|
return x.IsCatchAll == y.IsCatchAll || InnerComparer.Equals(x.Value, y.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(DecisionCriterionValue obj)
|
||||||
|
{
|
||||||
|
if (obj.IsCatchAll)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return InnerComparer.GetHashCode(obj.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
// 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.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
// This code generates a minimal tree of decision criteria that map known categorical data
|
||||||
|
// (key-value-pairs) to a set of inputs. Action Selection is the best example of how this
|
||||||
|
// can be used, so the comments here will describe the process from the point-of-view,
|
||||||
|
// though the decision tree is generally applicable to like-problems.
|
||||||
|
//
|
||||||
|
// Care has been taken here to keep the performance of building the data-structure at a
|
||||||
|
// reasonable level, as this has an impact on startup cost for action selection. Additionally
|
||||||
|
// we want to hold on to the minimal amount of memory needed once we've built the tree.
|
||||||
|
//
|
||||||
|
// Ex:
|
||||||
|
// Given actions like the following, create a decision tree that will help action
|
||||||
|
// selection work efficiently.
|
||||||
|
//
|
||||||
|
// Given any set of route data it should be possible to traverse the tree using the
|
||||||
|
// presence our route data keys (like action), and whether or not they match any of
|
||||||
|
// the known values for that route data key, to find the set of actions that match
|
||||||
|
// the route data.
|
||||||
|
//
|
||||||
|
// Actions:
|
||||||
|
//
|
||||||
|
// { controller = "Home", action = "Index" }
|
||||||
|
// { controller = "Products", action = "Index" }
|
||||||
|
// { controller = "Products", action = "Buy" }
|
||||||
|
// { area = "Admin", controller = "Users", action = "AddUser" }
|
||||||
|
//
|
||||||
|
// The generated tree looks like this (json-like-notation):
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// action : {
|
||||||
|
// "AddUser" : {
|
||||||
|
// controller : {
|
||||||
|
// "Users" : {
|
||||||
|
// area : {
|
||||||
|
// "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "Buy" : {
|
||||||
|
// controller : {
|
||||||
|
// "Products" : {
|
||||||
|
// area : {
|
||||||
|
// null : match { controller = "Products", action = "Buy" }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "Index" : {
|
||||||
|
// controller : {
|
||||||
|
// "Home" : {
|
||||||
|
// area : {
|
||||||
|
// null : match { controller = "Home", action = "Index" }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "Products" : {
|
||||||
|
// area : {
|
||||||
|
// "null" : match { controller = "Products", action = "Index" }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
public static class DecisionTreeBuilder<TItem>
|
||||||
|
{
|
||||||
|
public static DecisionTreeNode<TItem> GenerateTree(IReadOnlyList<TItem> items, IClassifier<TItem> classifier)
|
||||||
|
{
|
||||||
|
var itemDescriptors = new List<ItemDescriptor<TItem>>();
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
itemDescriptors.Add(new ItemDescriptor<TItem>()
|
||||||
|
{
|
||||||
|
Criteria = classifier.GetCriteria(items[i]),
|
||||||
|
Index = i,
|
||||||
|
Item = items[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer);
|
||||||
|
return GenerateNode(
|
||||||
|
new TreeBuilderContext(),
|
||||||
|
comparer,
|
||||||
|
itemDescriptors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DecisionTreeNode<TItem> GenerateNode(
|
||||||
|
TreeBuilderContext context,
|
||||||
|
DecisionCriterionValueEqualityComparer comparer,
|
||||||
|
IList<ItemDescriptor<TItem>> items)
|
||||||
|
{
|
||||||
|
// The extreme use of generics here is intended to reduce the number of intermediate
|
||||||
|
// allocations of wrapper classes. Performance testing found that building these trees allocates
|
||||||
|
// significant memory that we can avoid and that it has a real impact on startup.
|
||||||
|
var criteria = new Dictionary<string, Criterion>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// Matches are items that have no remaining criteria - at this point in the tree
|
||||||
|
// they are considered accepted.
|
||||||
|
var matches = new List<TItem>();
|
||||||
|
|
||||||
|
// For each item in the working set, we want to map it to it's possible criteria-branch
|
||||||
|
// pairings, then reduce that tree to the minimal set.
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
var unsatisfiedCriteria = 0;
|
||||||
|
|
||||||
|
foreach (var kvp in item.Criteria)
|
||||||
|
{
|
||||||
|
// context.CurrentCriteria is the logical 'stack' of criteria that we've already processed
|
||||||
|
// on this branch of the tree.
|
||||||
|
if (context.CurrentCriteria.Contains(kvp.Key))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsatisfiedCriteria++;
|
||||||
|
|
||||||
|
Criterion criterion;
|
||||||
|
if (!criteria.TryGetValue(kvp.Key, out criterion))
|
||||||
|
{
|
||||||
|
criterion = new Criterion(comparer);
|
||||||
|
criteria.Add(kvp.Key, criterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ItemDescriptor<TItem>> branch;
|
||||||
|
if (!criterion.TryGetValue(kvp.Value, out branch))
|
||||||
|
{
|
||||||
|
branch = new List<ItemDescriptor<TItem>>();
|
||||||
|
criterion.Add(kvp.Value, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
branch.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all of the criteria on item are satisfied by the 'stack' then this item is a match.
|
||||||
|
if (unsatisfiedCriteria == 0)
|
||||||
|
{
|
||||||
|
matches.Add(item.Item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate criteria in order of branchiness to determine which one to explore next. If a criterion
|
||||||
|
// has no 'new' matches under it then we can just eliminate that part of the tree.
|
||||||
|
var reducedCriteria = new List<DecisionCriterion<TItem>>();
|
||||||
|
foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count))
|
||||||
|
{
|
||||||
|
var reducedBranches = new Dictionary<object, DecisionTreeNode<TItem>>(comparer.InnerComparer);
|
||||||
|
DecisionTreeNode<TItem> fallback = null;
|
||||||
|
|
||||||
|
foreach (var branch in criterion.Value)
|
||||||
|
{
|
||||||
|
var reducedItems = new List<ItemDescriptor<TItem>>();
|
||||||
|
foreach (var item in branch.Value)
|
||||||
|
{
|
||||||
|
if (context.MatchedItems.Add(item))
|
||||||
|
{
|
||||||
|
reducedItems.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducedItems.Count > 0)
|
||||||
|
{
|
||||||
|
var childContext = new TreeBuilderContext(context);
|
||||||
|
childContext.CurrentCriteria.Add(criterion.Key);
|
||||||
|
|
||||||
|
var newBranch = GenerateNode(childContext, comparer, branch.Value);
|
||||||
|
if (branch.Key.IsCatchAll)
|
||||||
|
{
|
||||||
|
fallback = newBranch;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reducedBranches.Add(branch.Key.Value, newBranch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reducedBranches.Count > 0 || fallback != null)
|
||||||
|
{
|
||||||
|
var newCriterion = new DecisionCriterion<TItem>()
|
||||||
|
{
|
||||||
|
Key = criterion.Key,
|
||||||
|
Branches = reducedBranches,
|
||||||
|
Fallback = fallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
reducedCriteria.Add(newCriterion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DecisionTreeNode<TItem>()
|
||||||
|
{
|
||||||
|
Criteria = reducedCriteria.ToList(),
|
||||||
|
Matches = matches,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TreeBuilderContext
|
||||||
|
{
|
||||||
|
public TreeBuilderContext()
|
||||||
|
{
|
||||||
|
CurrentCriteria = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
MatchedItems = new HashSet<ItemDescriptor<TItem>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TreeBuilderContext(TreeBuilderContext other)
|
||||||
|
{
|
||||||
|
CurrentCriteria = new HashSet<string>(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase);
|
||||||
|
MatchedItems = new HashSet<ItemDescriptor<TItem>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<string> CurrentCriteria { get; private set; }
|
||||||
|
|
||||||
|
public HashSet<ItemDescriptor<TItem>> MatchedItems { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subclass just to give a logical name to a mess of generics
|
||||||
|
private class Criterion : Dictionary<DecisionCriterionValue, List<ItemDescriptor<TItem>>>
|
||||||
|
{
|
||||||
|
public Criterion(DecisionCriterionValueEqualityComparer comparer)
|
||||||
|
: base(comparer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public class DecisionTreeNode<TItem>
|
||||||
|
{
|
||||||
|
public List<TItem> Matches { get; set; }
|
||||||
|
|
||||||
|
public List<DecisionCriterion<TItem>> Criteria { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public interface IClassifier<TItem>
|
||||||
|
{
|
||||||
|
IDictionary<string, DecisionCriterionValue> GetCriteria(TItem item);
|
||||||
|
|
||||||
|
IEqualityComparer<object> ValueComparer { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public class ItemDescriptor<TItem>
|
||||||
|
{
|
||||||
|
public IDictionary<string, DecisionCriterionValue> Criteria { get; set; }
|
||||||
|
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
public TItem Item { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,22 +25,22 @@ namespace Microsoft.AspNet.Mvc.Logging
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of actions that matched all their route constraints, if any.
|
/// The list of actions that matched all their route constraints, if any.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ActionDescriptor> ActionsMatchingRouteConstraints { get; set; }
|
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteConstraints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of actions that matched all their route and method constraints, if any.
|
/// The list of actions that matched all their route and method constraints, if any.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ActionDescriptor> ActionsMatchingRouteAndMethodConstraints { get; set; }
|
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodConstraints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of actions that matched all their route, method, and dynamic constraints, if any.
|
/// The list of actions that matched all their route, method, and dynamic constraints, if any.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; }
|
public IReadOnlyList<ActionDescriptor> ActionsMatchingRouteAndMethodAndDynamicConstraints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The actions that matched with at least one constraint.
|
/// The actions that matched with at least one constraint.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ActionDescriptor> ActionsMatchingWithConstraints { get; set; }
|
public IReadOnlyList<ActionDescriptor> ActionsMatchingWithConstraints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The selected action.
|
/// The selected action.
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@
|
||||||
<Compile Include="ActionResults\HttpNotFoundResult.cs" />
|
<Compile Include="ActionResults\HttpNotFoundResult.cs" />
|
||||||
<Compile Include="Formatters\TextPlainFormatter.cs" />
|
<Compile Include="Formatters\TextPlainFormatter.cs" />
|
||||||
<Compile Include="HttpMethodAttribute.cs" />
|
<Compile Include="HttpMethodAttribute.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionCriterionValue.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionCriterionValueEqualityComparer.cs" />
|
||||||
<Compile Include="Logging\AttributeRouteRouteAsyncValues.cs" />
|
<Compile Include="Logging\AttributeRouteRouteAsyncValues.cs" />
|
||||||
<Compile Include="Logging\LoggerExtensions.cs" />
|
<Compile Include="Logging\LoggerExtensions.cs" />
|
||||||
<Compile Include="Logging\MvcRouteHandlerRouteAsyncValues.cs" />
|
<Compile Include="Logging\MvcRouteHandlerRouteAsyncValues.cs" />
|
||||||
|
|
@ -62,6 +64,18 @@
|
||||||
<Compile Include="OptionDescriptors\ViewEngineDescriptor.cs" />
|
<Compile Include="OptionDescriptors\ViewEngineDescriptor.cs" />
|
||||||
<Compile Include="OptionDescriptors\ViewEngineDescriptorExtensions.cs" />
|
<Compile Include="OptionDescriptors\ViewEngineDescriptorExtensions.cs" />
|
||||||
<Compile Include="ParameterBinding\ModelBindingHelper.cs" />
|
<Compile Include="ParameterBinding\ModelBindingHelper.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionTreeBuilder.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionCriterion.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionTreeNode.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\IClassifier.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\ItemDescriptor.cs" />
|
||||||
|
<Compile Include="ReflectedActionDescriptor.cs" />
|
||||||
|
<Compile Include="ReflectedActionDescriptorProvider.cs" />
|
||||||
|
<Compile Include="ReflectedActionInvoker.cs" />
|
||||||
|
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
|
||||||
|
<Compile Include="ReflectedModelBuilder\ReflectedActionModel.cs" />
|
||||||
|
<Compile Include="ReflectedModelBuilder\ReflectedControllerModel.cs" />
|
||||||
|
<Compile Include="ReflectedModelBuilder\ReflectedApplicationModel.cs" />
|
||||||
<Compile Include="ActionInvokerFactory.cs" />
|
<Compile Include="ActionInvokerFactory.cs" />
|
||||||
<Compile Include="ActionInvokerProviderContext.cs" />
|
<Compile Include="ActionInvokerProviderContext.cs" />
|
||||||
<Compile Include="ActionMethodSelectorAttribute.cs" />
|
<Compile Include="ActionMethodSelectorAttribute.cs" />
|
||||||
|
|
@ -249,13 +263,18 @@
|
||||||
<Compile Include="RouteConstraintAttribute.cs" />
|
<Compile Include="RouteConstraintAttribute.cs" />
|
||||||
<Compile Include="RouteDataActionConstraint.cs" />
|
<Compile Include="RouteDataActionConstraint.cs" />
|
||||||
<Compile Include="RouteKeyHandling.cs" />
|
<Compile Include="RouteKeyHandling.cs" />
|
||||||
|
<Compile Include="Routing\ActionSelectorDecisionTree.cs" />
|
||||||
|
<Compile Include="Routing\IActionSelectorDecisionTreeProvider.cs" />
|
||||||
|
<Compile Include="Routing\ActionSelectorDecisionTreeProvider.cs" />
|
||||||
<Compile Include="Routing\AttributeRoute.cs" />
|
<Compile Include="Routing\AttributeRoute.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteInfo.cs" />
|
<Compile Include="Routing\AttributeRouteInfo.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteLinkGenerationEntry.cs" />
|
<Compile Include="Routing\AttributeRouteLinkGenerationEntry.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteMatchingEntry.cs" />
|
<Compile Include="Routing\AttributeRouteMatchingEntry.cs" />
|
||||||
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
||||||
<Compile Include="Routing\AttributeRouting.cs" />
|
<Compile Include="Routing\AttributeRouting.cs" />
|
||||||
|
<Compile Include="Routing\IActionSelectionDecisionTree.cs" />
|
||||||
<Compile Include="Routing\IRouteTemplateProvider.cs" />
|
<Compile Include="Routing\IRouteTemplateProvider.cs" />
|
||||||
|
<Compile Include="Routing\RouteValueEqualityComparer.cs" />
|
||||||
<Compile Include="TemplateInfo.cs" />
|
<Compile Include="TemplateInfo.cs" />
|
||||||
<Compile Include="UrlHelper.cs" />
|
<Compile Include="UrlHelper.cs" />
|
||||||
<Compile Include="UrlHelperExtensions.cs" />
|
<Compile Include="UrlHelperExtensions.cs" />
|
||||||
|
|
@ -287,4 +306,4 @@
|
||||||
<Compile Include="ViewDataDictionaryOfT.cs" />
|
<Compile Include="ViewDataDictionaryOfT.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -43,7 +43,6 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
switch (keyHandling)
|
switch (keyHandling)
|
||||||
{
|
{
|
||||||
case RouteKeyHandling.AcceptAlways:
|
|
||||||
case RouteKeyHandling.CatchAll:
|
case RouteKeyHandling.CatchAll:
|
||||||
case RouteKeyHandling.DenyKey:
|
case RouteKeyHandling.DenyKey:
|
||||||
case RouteKeyHandling.RequireKey:
|
case RouteKeyHandling.RequireKey:
|
||||||
|
|
@ -81,9 +80,6 @@ namespace Microsoft.AspNet.Mvc
|
||||||
object value;
|
object value;
|
||||||
switch (KeyHandling)
|
switch (KeyHandling)
|
||||||
{
|
{
|
||||||
case RouteKeyHandling.AcceptAlways:
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case RouteKeyHandling.CatchAll:
|
case RouteKeyHandling.CatchAll:
|
||||||
return routeValues.ContainsKey(RouteKey);
|
return routeValues.ContainsKey(RouteKey);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,5 @@ namespace Microsoft.AspNet.Mvc
|
||||||
/// Requires that the key will be in the route values, but ignore the content.
|
/// Requires that the key will be in the route values, but ignore the content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CatchAll,
|
CatchAll,
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Always accept.
|
|
||||||
/// </summary>
|
|
||||||
AcceptAlways,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
// 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.ComponentModel;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using Microsoft.AspNet.Mvc.Internal.DecisionTree;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ActionSelectionDecisionTree : IActionSelectionDecisionTree
|
||||||
|
{
|
||||||
|
private readonly DecisionTreeNode<ActionDescriptor> _root;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="ActionSelectionDecisionTree"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actions">The <see cref="ActionDescriptorsCollection"/>.</param>
|
||||||
|
public ActionSelectionDecisionTree(ActionDescriptorsCollection actions)
|
||||||
|
{
|
||||||
|
Version = actions.Version;
|
||||||
|
|
||||||
|
_root = DecisionTreeBuilder<ActionDescriptor>.GenerateTree(
|
||||||
|
actions.Items,
|
||||||
|
new ActionDescriptorClassifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Version { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<ActionDescriptor> Select(IDictionary<string, object> routeValues)
|
||||||
|
{
|
||||||
|
var results = new List<ActionDescriptor>();
|
||||||
|
Walk(results, routeValues, _root);
|
||||||
|
|
||||||
|
// If we have a match that isn't using catch-all, then it's considered better than matches with catch all
|
||||||
|
// so filter those out.
|
||||||
|
var hasNonCatchAll = false;
|
||||||
|
|
||||||
|
// The common case for MVC has no catch-alls, so avoid allocating.
|
||||||
|
List<ActionDescriptor> filtered = null;
|
||||||
|
|
||||||
|
foreach (var action in results)
|
||||||
|
{
|
||||||
|
var actionHasCatchAll = false;
|
||||||
|
if (action.RouteConstraints != null)
|
||||||
|
{
|
||||||
|
foreach (var constraint in action.RouteConstraints)
|
||||||
|
{
|
||||||
|
if (constraint.KeyHandling == RouteKeyHandling.CatchAll)
|
||||||
|
{
|
||||||
|
actionHasCatchAll = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonCatchAll && actionHasCatchAll)
|
||||||
|
{
|
||||||
|
// Do nothing - we've already found a better match.
|
||||||
|
}
|
||||||
|
else if (actionHasCatchAll)
|
||||||
|
{
|
||||||
|
if (filtered == null)
|
||||||
|
{
|
||||||
|
filtered = new List<ActionDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.Add(action);
|
||||||
|
}
|
||||||
|
else if (hasNonCatchAll)
|
||||||
|
{
|
||||||
|
Contract.Assert(filtered != null);
|
||||||
|
filtered.Add(action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is the first non-catch-all we've found.
|
||||||
|
hasNonCatchAll = true;
|
||||||
|
|
||||||
|
if (filtered == null)
|
||||||
|
{
|
||||||
|
filtered = new List<ActionDescriptor>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filtered.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.Add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered ?? results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Walk(
|
||||||
|
List<ActionDescriptor> results,
|
||||||
|
IDictionary<string, object> routeValues,
|
||||||
|
DecisionTreeNode<ActionDescriptor> node)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < node.Matches.Count; i++)
|
||||||
|
{
|
||||||
|
results.Add(node.Matches[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < node.Criteria.Count; i++)
|
||||||
|
{
|
||||||
|
var criterion = node.Criteria[i];
|
||||||
|
var key = criterion.Key;
|
||||||
|
|
||||||
|
object value;
|
||||||
|
var hasValue = routeValues.TryGetValue(key, out value);
|
||||||
|
|
||||||
|
DecisionTreeNode<ActionDescriptor> branch;
|
||||||
|
if (criterion.Branches.TryGetValue(value ?? string.Empty, out branch))
|
||||||
|
{
|
||||||
|
Walk(results, routeValues, branch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a fallback node we always need to process it when we have a value. We'll prioritize
|
||||||
|
// non-fallback matches later in the process.
|
||||||
|
if (hasValue && criterion.Fallback != null)
|
||||||
|
{
|
||||||
|
Walk(results, routeValues, criterion.Fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ActionDescriptorClassifier : IClassifier<ActionDescriptor>
|
||||||
|
{
|
||||||
|
public ActionDescriptorClassifier()
|
||||||
|
{
|
||||||
|
ValueComparer = new RouteValueEqualityComparer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEqualityComparer<object> ValueComparer { get; private set; }
|
||||||
|
|
||||||
|
public IDictionary<string, DecisionCriterionValue> GetCriteria(ActionDescriptor item)
|
||||||
|
{
|
||||||
|
var results = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (item.RouteConstraints != null)
|
||||||
|
{
|
||||||
|
foreach (var constraint in item.RouteConstraints)
|
||||||
|
{
|
||||||
|
DecisionCriterionValue value;
|
||||||
|
if (constraint.KeyHandling == RouteKeyHandling.CatchAll)
|
||||||
|
{
|
||||||
|
value = new DecisionCriterionValue(value: null, isCatchAll: true);
|
||||||
|
}
|
||||||
|
else if (constraint.KeyHandling == RouteKeyHandling.DenyKey)
|
||||||
|
{
|
||||||
|
// null and string.Empty are equivalent for route values, so just treat nulls as
|
||||||
|
// string.Empty.
|
||||||
|
value = new DecisionCriterionValue(value: string.Empty, isCatchAll: false);
|
||||||
|
}
|
||||||
|
else if (constraint.KeyHandling == RouteKeyHandling.RequireKey)
|
||||||
|
{
|
||||||
|
value = new DecisionCriterionValue(value: constraint.RouteValue, isCatchAll: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We'd already have failed before getting here. The RouteDataActionConstraint constructor
|
||||||
|
// would throw.
|
||||||
|
#if NET45
|
||||||
|
throw new InvalidEnumArgumentException(
|
||||||
|
"item",
|
||||||
|
(int)constraint.KeyHandling,
|
||||||
|
typeof(RouteKeyHandling));
|
||||||
|
#else
|
||||||
|
throw new ArgumentOutOfRangeException("item");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for Javier's cool bug.
|
||||||
|
if (!results.ContainsKey(constraint.RouteKey))
|
||||||
|
{
|
||||||
|
results.Add(constraint.RouteKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
// 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.AspNet.Mvc.Core;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ActionSelectorDecisionTreeProvider : IActionSelectorDecisionTreeProvider
|
||||||
|
{
|
||||||
|
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
|
||||||
|
private ActionSelectionDecisionTree _decisionTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="ActionSelectorDecisionTreeProvider"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="actionDescriptorsCollectionProvider">
|
||||||
|
/// The <see cref="IActionDescriptorsCollectionProvider"/>.
|
||||||
|
/// </param>
|
||||||
|
public ActionSelectorDecisionTreeProvider(
|
||||||
|
IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider)
|
||||||
|
{
|
||||||
|
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IActionSelectionDecisionTree DecisionTree
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var descriptors = _actionDescriptorsCollectionProvider.ActionDescriptors;
|
||||||
|
if (descriptors == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
Resources.FormatPropertyOfTypeCannotBeNull(
|
||||||
|
"ActionDescriptors",
|
||||||
|
_actionDescriptorsCollectionProvider.GetType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_decisionTree == null || descriptors.Version != _decisionTree.Version)
|
||||||
|
{
|
||||||
|
_decisionTree = new ActionSelectionDecisionTree(descriptors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _decisionTree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A data structure that retrieves a list of <see cref="ActionDescriptor"/> matches based on the values
|
||||||
|
/// supplied for the current request by <see cref="Microsoft.AspNet.Routing.RouteData.Values"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IActionSelectionDecisionTree
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the version. The same as the value of <see cref="ActionDescriptorsCollection.Version"/>.
|
||||||
|
/// </summary>
|
||||||
|
int Version { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a set of <see cref="ActionDescriptor"/> based on the route values supplied by
|
||||||
|
/// <paramref name="routeValues"/>/
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeValues">The route values for the current request.</param>
|
||||||
|
/// <returns>A set of <see cref="ActionDescriptor"/> matching the route values.</returns>
|
||||||
|
IReadOnlyList<ActionDescriptor> Select(IDictionary<string, object> routeValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores an <see cref="ActionSelectionDecisionTree"/> for the current value of
|
||||||
|
/// <see cref="IActionDescriptorsCollectionProvider.ActionDescriptors"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IActionSelectorDecisionTreeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IActionDescriptorsCollectionProvider"/>.
|
||||||
|
/// </summary>
|
||||||
|
IActionSelectionDecisionTree DecisionTree
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// 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.Globalization;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IEqualityComparer{object}"/> implementation that compares objects as-if
|
||||||
|
/// they were route value strings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Values that are are not strings are converted to strings using
|
||||||
|
/// <c>Convert.ToString(x, CultureInfo.InvariantCulture)</c>. <c>null</c> values are converted
|
||||||
|
/// to the empty string.
|
||||||
|
///
|
||||||
|
/// strings are compared using <see cref="StringComparison.OrdinalIgnoreCase"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public class RouteValueEqualityComparer : IEqualityComparer<object>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public new bool Equals(object x, object y)
|
||||||
|
{
|
||||||
|
var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture);
|
||||||
|
var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int GetHashCode(object obj)
|
||||||
|
{
|
||||||
|
var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture);
|
||||||
|
if (string.IsNullOrEmpty(stringObj))
|
||||||
|
{
|
||||||
|
return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNet.Mvc.OptionDescriptors;
|
||||||
using Microsoft.AspNet.Mvc.Razor;
|
using Microsoft.AspNet.Mvc.Razor;
|
||||||
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNet.Mvc.Rendering;
|
using Microsoft.AspNet.Mvc.Rendering;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Security;
|
using Microsoft.AspNet.Security;
|
||||||
using Microsoft.Framework.ConfigurationModel;
|
using Microsoft.Framework.ConfigurationModel;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
@ -32,7 +33,10 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
yield return describe.Transient<IControllerFactory, DefaultControllerFactory>();
|
yield return describe.Transient<IControllerFactory, DefaultControllerFactory>();
|
||||||
yield return describe.Singleton<IControllerActivator, DefaultControllerActivator>();
|
yield return describe.Singleton<IControllerActivator, DefaultControllerActivator>();
|
||||||
|
|
||||||
|
yield return describe.Singleton<IActionSelectorDecisionTreeProvider, ActionSelectorDecisionTreeProvider>();
|
||||||
yield return describe.Scoped<IActionSelector, DefaultActionSelector>();
|
yield return describe.Scoped<IActionSelector, DefaultActionSelector>();
|
||||||
|
|
||||||
yield return describe.Transient<IActionInvokerFactory, ActionInvokerFactory>();
|
yield return describe.Transient<IActionInvokerFactory, ActionInvokerFactory>();
|
||||||
yield return describe.Transient<IControllerAssemblyProvider, DefaultControllerAssemblyProvider>();
|
yield return describe.Transient<IControllerAssemblyProvider, DefaultControllerAssemblyProvider>();
|
||||||
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
|
yield return describe.Transient<IActionDiscoveryConventions, DefaultActionDiscoveryConventions>();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
using Microsoft.Framework.DependencyInjection.NestedProviders;
|
using Microsoft.Framework.DependencyInjection.NestedProviders;
|
||||||
|
|
@ -195,12 +196,15 @@ namespace Microsoft.AspNet.Mvc
|
||||||
descriptorProvider);
|
descriptorProvider);
|
||||||
|
|
||||||
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
||||||
|
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
|
||||||
|
|
||||||
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
||||||
|
|
||||||
var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider,
|
var defaultActionSelector = new DefaultActionSelector(
|
||||||
bindingProvider.Object,
|
actionCollectionDescriptorProvider,
|
||||||
NullLoggerFactory.Instance);
|
decisionTreeProvider,
|
||||||
|
bindingProvider.Object,
|
||||||
|
NullLoggerFactory.Instance);
|
||||||
|
|
||||||
return await defaultActionSelector.SelectAsync(context);
|
return await defaultActionSelector.SelectAsync(context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
using Microsoft.Framework.DependencyInjection.NestedProviders;
|
using Microsoft.Framework.DependencyInjection.NestedProviders;
|
||||||
|
|
@ -174,12 +175,15 @@ namespace Microsoft.AspNet.Mvc
|
||||||
descriptorProvider);
|
descriptorProvider);
|
||||||
|
|
||||||
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
var actionCollectionDescriptorProvider = new DefaultActionDescriptorsCollectionProvider(serviceContainer);
|
||||||
|
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionCollectionDescriptorProvider);
|
||||||
|
|
||||||
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
||||||
|
|
||||||
var defaultActionSelector = new DefaultActionSelector(actionCollectionDescriptorProvider,
|
var defaultActionSelector = new DefaultActionSelector(
|
||||||
bindingProvider.Object,
|
actionCollectionDescriptorProvider,
|
||||||
NullLoggerFactory.Instance);
|
decisionTreeProvider,
|
||||||
|
bindingProvider.Object,
|
||||||
|
NullLoggerFactory.Instance);
|
||||||
|
|
||||||
return await defaultActionSelector.SelectAsync(context);
|
return await defaultActionSelector.SelectAsync(context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Http;
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.AspNet.Mvc.Logging;
|
using Microsoft.AspNet.Mvc.Logging;
|
||||||
using Microsoft.Framework.Logging;
|
using Microsoft.Framework.Logging;
|
||||||
|
|
@ -215,12 +216,14 @@ namespace Microsoft.AspNet.Mvc
|
||||||
actionProvider
|
actionProvider
|
||||||
.Setup(p => p.ActionDescriptors).Returns(new ActionDescriptorsCollection(actions, 0));
|
.Setup(p => p.ActionDescriptors).Returns(new ActionDescriptorsCollection(actions, 0));
|
||||||
|
|
||||||
|
var decisionTreeProvider = new ActionSelectorDecisionTreeProvider(actionProvider.Object);
|
||||||
|
|
||||||
var bindingProvider = new Mock<IActionBindingContextProvider>(MockBehavior.Strict);
|
var bindingProvider = new Mock<IActionBindingContextProvider>(MockBehavior.Strict);
|
||||||
bindingProvider
|
bindingProvider
|
||||||
.Setup(bp => bp.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
|
.Setup(bp => bp.GetActionBindingContextAsync(It.IsAny<ActionContext>()))
|
||||||
.Returns(Task.FromResult<ActionBindingContext>(null));
|
.Returns(Task.FromResult<ActionBindingContext>(null));
|
||||||
|
|
||||||
return new DefaultActionSelector(actionProvider.Object, bindingProvider.Object, loggerFactory);
|
return new DefaultActionSelector(actionProvider.Object, decisionTreeProvider, bindingProvider.Object, loggerFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VirtualPathContext CreateContext(object routeValues)
|
private static VirtualPathContext CreateContext(object routeValues)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,286 @@
|
||||||
|
// 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.AspNet.Mvc.Routing;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Internal.DecisionTree
|
||||||
|
{
|
||||||
|
public class DecisionTreeBuilderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_Empty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Criteria);
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_TrivialMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item = new Item();
|
||||||
|
items.Add(item);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Criteria);
|
||||||
|
Assert.Same(item, Assert.Single(tree.Matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_WithMultipleCriteria()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item = new Item();
|
||||||
|
item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin", isCatchAll: false));
|
||||||
|
item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users", isCatchAll: false));
|
||||||
|
item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser", isCatchAll: false));
|
||||||
|
items.Add(item);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
|
||||||
|
var area = Assert.Single(tree.Criteria);
|
||||||
|
Assert.Equal("area", area.Key);
|
||||||
|
Assert.Null(area.Fallback);
|
||||||
|
|
||||||
|
var admin = Assert.Single(area.Branches);
|
||||||
|
Assert.Equal("Admin", admin.Key);
|
||||||
|
Assert.Empty(admin.Value.Matches);
|
||||||
|
|
||||||
|
var controller = Assert.Single(admin.Value.Criteria);
|
||||||
|
Assert.Equal("controller", controller.Key);
|
||||||
|
Assert.Null(controller.Fallback);
|
||||||
|
|
||||||
|
var users = Assert.Single(controller.Branches);
|
||||||
|
Assert.Equal("Users", users.Key);
|
||||||
|
Assert.Empty(users.Value.Matches);
|
||||||
|
|
||||||
|
var action = Assert.Single(users.Value.Criteria);
|
||||||
|
Assert.Equal("action", action.Key);
|
||||||
|
Assert.Null(action.Fallback);
|
||||||
|
|
||||||
|
var addUser = Assert.Single(action.Branches);
|
||||||
|
Assert.Equal("AddUser", addUser.Key);
|
||||||
|
Assert.Empty(addUser.Value.Criteria);
|
||||||
|
Assert.Same(item, Assert.Single(addUser.Value.Matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_WithMultipleItems()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item1 = new Item();
|
||||||
|
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
||||||
|
items.Add(item1);
|
||||||
|
|
||||||
|
var item2 = new Item();
|
||||||
|
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
|
||||||
|
var action = Assert.Single(tree.Criteria);
|
||||||
|
Assert.Equal("action", action.Key);
|
||||||
|
Assert.Null(action.Fallback);
|
||||||
|
|
||||||
|
var buy = action.Branches["Buy"];
|
||||||
|
Assert.Empty(buy.Matches);
|
||||||
|
|
||||||
|
var controller = Assert.Single(buy.Criteria);
|
||||||
|
Assert.Equal("controller", controller.Key);
|
||||||
|
Assert.Null(controller.Fallback);
|
||||||
|
|
||||||
|
var store = Assert.Single(controller.Branches);
|
||||||
|
Assert.Equal("Store", store.Key);
|
||||||
|
Assert.Empty(store.Value.Criteria);
|
||||||
|
Assert.Same(item1, Assert.Single(store.Value.Matches));
|
||||||
|
|
||||||
|
var checkout = action.Branches["Checkout"];
|
||||||
|
Assert.Empty(checkout.Matches);
|
||||||
|
|
||||||
|
controller = Assert.Single(checkout.Criteria);
|
||||||
|
Assert.Equal("controller", controller.Key);
|
||||||
|
Assert.Null(controller.Fallback);
|
||||||
|
|
||||||
|
store = Assert.Single(controller.Branches);
|
||||||
|
Assert.Equal("Store", store.Key);
|
||||||
|
Assert.Empty(store.Value.Criteria);
|
||||||
|
Assert.Same(item2, Assert.Single(store.Value.Matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_WithInteriorMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item1 = new Item();
|
||||||
|
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
||||||
|
items.Add(item1);
|
||||||
|
|
||||||
|
var item2 = new Item();
|
||||||
|
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item2);
|
||||||
|
|
||||||
|
var item3 = new Item();
|
||||||
|
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
||||||
|
items.Add(item3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
|
||||||
|
var action = Assert.Single(tree.Criteria);
|
||||||
|
Assert.Equal("action", action.Key);
|
||||||
|
Assert.Null(action.Fallback);
|
||||||
|
|
||||||
|
var buy = action.Branches["Buy"];
|
||||||
|
Assert.Same(item3, Assert.Single(buy.Matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_WithCatchAll()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item1 = new Item();
|
||||||
|
item1.Criteria.Add("country", new DecisionCriterionValue(value: "CA", isCatchAll: false));
|
||||||
|
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item1);
|
||||||
|
|
||||||
|
var item2 = new Item();
|
||||||
|
item2.Criteria.Add("country", new DecisionCriterionValue(value: "US", isCatchAll: false));
|
||||||
|
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item2);
|
||||||
|
|
||||||
|
var item3 = new Item();
|
||||||
|
item3.Criteria.Add("country", new DecisionCriterionValue(value: null, isCatchAll: true));
|
||||||
|
item3.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item3.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
|
||||||
|
var country = Assert.Single(tree.Criteria);
|
||||||
|
Assert.Equal("country", country.Key);
|
||||||
|
|
||||||
|
var fallback = country.Fallback;
|
||||||
|
Assert.NotNull(fallback);
|
||||||
|
|
||||||
|
var controller = Assert.Single(fallback.Criteria);
|
||||||
|
Assert.Equal("controller", controller.Key);
|
||||||
|
Assert.Null(controller.Fallback);
|
||||||
|
|
||||||
|
var store = Assert.Single(controller.Branches);
|
||||||
|
Assert.Equal("Store", store.Key);
|
||||||
|
Assert.Empty(store.Value.Matches);
|
||||||
|
|
||||||
|
var action = Assert.Single(store.Value.Criteria);
|
||||||
|
Assert.Equal("action", action.Key);
|
||||||
|
Assert.Null(action.Fallback);
|
||||||
|
|
||||||
|
var checkout = Assert.Single(action.Branches);
|
||||||
|
Assert.Equal("Checkout", checkout.Key);
|
||||||
|
Assert.Empty(checkout.Value.Criteria);
|
||||||
|
Assert.Same(item3, Assert.Single(checkout.Value.Matches));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildTree_WithDivergentCriteria()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var items = new List<Item>();
|
||||||
|
|
||||||
|
var item1 = new Item();
|
||||||
|
item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy", isCatchAll: false));
|
||||||
|
items.Add(item1);
|
||||||
|
|
||||||
|
var item2 = new Item();
|
||||||
|
item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store", isCatchAll: false));
|
||||||
|
item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout", isCatchAll: false));
|
||||||
|
items.Add(item2);
|
||||||
|
|
||||||
|
var item3 = new Item();
|
||||||
|
item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh", isCatchAll: false));
|
||||||
|
items.Add(item3);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tree = DecisionTreeBuilder<Item>.GenerateTree(items, new ItemClassifier());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(tree.Matches);
|
||||||
|
|
||||||
|
var action = tree.Criteria[0];
|
||||||
|
Assert.Equal("action", action.Key);
|
||||||
|
|
||||||
|
var stub = tree.Criteria[1];
|
||||||
|
Assert.Equal("stub", stub.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Item
|
||||||
|
{
|
||||||
|
public Item()
|
||||||
|
{
|
||||||
|
Criteria = new Dictionary<string, DecisionCriterionValue>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, DecisionCriterionValue> Criteria { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ItemClassifier : IClassifier<Item>
|
||||||
|
{
|
||||||
|
public IEqualityComparer<object> ValueComparer
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new RouteValueEqualityComparer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, DecisionCriterionValue> GetCriteria(Item item)
|
||||||
|
{
|
||||||
|
return item.Criteria;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
<Compile Include="Formatters\TextPlainFormatterTests.cs" />
|
<Compile Include="Formatters\TextPlainFormatterTests.cs" />
|
||||||
<Compile Include="DefaultViewComponentActivatorTests.cs" />
|
<Compile Include="DefaultViewComponentActivatorTests.cs" />
|
||||||
<Compile Include="HttpMethodProviderAttributesTests.cs" />
|
<Compile Include="HttpMethodProviderAttributesTests.cs" />
|
||||||
|
<Compile Include="Internal\DecisionTree\DecisionTreeBuilderTest.cs" />
|
||||||
<Compile Include="Logging\BeginScopeContext.cs" />
|
<Compile Include="Logging\BeginScopeContext.cs" />
|
||||||
<Compile Include="Logging\TestLoggerFactory.cs" />
|
<Compile Include="Logging\TestLoggerFactory.cs" />
|
||||||
<Compile Include="Logging\WriteCoreContext.cs" />
|
<Compile Include="Logging\WriteCoreContext.cs" />
|
||||||
|
|
@ -119,4 +120,4 @@
|
||||||
<Compile Include="ViewResultTest.cs" />
|
<Compile Include="ViewResultTest.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Controllers\HomeController.cs" />
|
<Compile Include="Controllers\HomeController.cs" />
|
||||||
<Compile Include="Models\DummyClass.cs" />
|
|
||||||
<Compile Include="Startup.cs" />
|
<Compile Include="Startup.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue