Adding smart link generation
This feature will enforce a contract that link generation has to point to a real action. Read the comments in code for more details and rationale.
This commit is contained in:
parent
3548a46ca9
commit
ec4b3a29c0
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace MvcSample.Web.Areas.Travel.Controllers
|
||||||
|
{
|
||||||
|
[Area("Travel")]
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return Result.Content("This is the Travel/Home/Index action.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,22 +12,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Getting started</h2>
|
<h2>Back to the main Area.</h2>
|
||||||
<p>
|
<p>Takes you out of the area implicitly.</p>
|
||||||
ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
|
<p><a class="btn btn-default" href="@Url.Action("Edit", "Home")">Go to Home/Edit</a></p>
|
||||||
enables a clean separation of concerns and gives you full control over markup
|
|
||||||
for enjoyable, agile development.
|
|
||||||
</p>
|
|
||||||
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more »</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Get more libraries</h2>
|
<h2>Go to another action in the Area.</h2>
|
||||||
<p>NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.</p>
|
<p>Keeps you in the area implicitly.</p>
|
||||||
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301866">Learn more »</a></p>
|
<p><a class="btn btn-default" href="@Url.Action("Index", "Home")">Go to Travel/Home/Index</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Web Hosting</h2>
|
<h2>Back to the main Area (explicit).</h2>
|
||||||
<p>You can easily find a web hosting company that offers the right mix of features and price for your applications.</p>
|
<p>Leaves the area explicitly.</p>
|
||||||
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301867">Learn more »</a></p>
|
<p><a class="btn btn-default" href="@Url.Action("Index", "Home", new { area = string.Empty })">Go to Home/Index</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,11 @@
|
||||||
}</h3>
|
}</h3>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Getting started</h2>
|
<h2>Book a flight</h2>
|
||||||
<p>
|
<p>
|
||||||
ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that
|
Go to our cool travel reservation system.
|
||||||
enables a clean separation of concerns and gives you full control over markup
|
|
||||||
for enjoyable, agile development.
|
|
||||||
</p>
|
</p>
|
||||||
<p><a class="btn btn-default" href="http://go.microsoft.com/fwlink/?LinkId=301865">Learn more »</a></p>
|
<p><a class="btn btn-default" href="@Url.Action("Fly", "Flight", new { area = "Travel" })">Reserve Now</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Get more libraries</h2>
|
<h2>Get more libraries</h2>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>@ViewBag.Title - My ASP.NET Application</title>
|
<title>@ViewBag.Title - My ASP.NET Application</title>
|
||||||
<link rel="stylesheet" href="~/content/bootstrap.min.css" />
|
<link rel="stylesheet" href="~/content/bootstrap.min.css" />
|
||||||
@RenderSection("header")
|
@RenderSection("header", required: false)
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="navbar navbar-inverse navbar-fixed-top">
|
<div class="navbar navbar-inverse navbar-fixed-top">
|
||||||
|
|
@ -37,6 +37,6 @@
|
||||||
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
|
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@RenderSection("footer")
|
@RenderSection("footer", required: false)
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.DependencyInjection;
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -11,7 +14,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
private readonly INestedProviderManager<ActionDescriptorProviderContext> _actionDescriptorProvider;
|
private readonly INestedProviderManager<ActionDescriptorProviderContext> _actionDescriptorProvider;
|
||||||
private readonly IActionBindingContextProvider _bindingProvider;
|
private readonly IActionBindingContextProvider _bindingProvider;
|
||||||
|
|
||||||
public DefaultActionSelector(INestedProviderManager<ActionDescriptorProviderContext> actionDescriptorProvider,
|
public DefaultActionSelector(INestedProviderManager<ActionDescriptorProviderContext> actionDescriptorProvider,
|
||||||
IActionBindingContextProvider bindingProvider)
|
IActionBindingContextProvider bindingProvider)
|
||||||
{
|
{
|
||||||
_actionDescriptorProvider = actionDescriptorProvider;
|
_actionDescriptorProvider = actionDescriptorProvider;
|
||||||
|
|
@ -25,10 +28,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
throw new ArgumentNullException("context");
|
throw new ArgumentNullException("context");
|
||||||
}
|
}
|
||||||
|
|
||||||
var actionDescriptorProviderContext = new ActionDescriptorProviderContext();
|
var allDescriptors = GetActions();
|
||||||
_actionDescriptorProvider.Invoke(actionDescriptorProviderContext);
|
|
||||||
|
|
||||||
var allDescriptors = actionDescriptorProviderContext.Results;
|
|
||||||
|
|
||||||
var matching = allDescriptors.Where(ad => Match(ad, context)).ToList();
|
var matching = allDescriptors.Where(ad => Match(ad, context)).ToList();
|
||||||
if (matching.Count == 0)
|
if (matching.Count == 0)
|
||||||
|
|
@ -70,9 +70,9 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
// Issues #60 & #65 filed to deal with the ugliness of passing null here.
|
// Issues #60 & #65 filed to deal with the ugliness of passing null here.
|
||||||
var actionContext = new ActionContext(
|
var actionContext = new ActionContext(
|
||||||
httpContext: context.HttpContext,
|
httpContext: context.HttpContext,
|
||||||
router: null,
|
router: null,
|
||||||
routeValues: context.RouteValues,
|
routeValues: context.RouteValues,
|
||||||
actionDescriptor: action);
|
actionDescriptor: action);
|
||||||
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(actionContext);
|
var actionBindingContext = await _bindingProvider.GetActionBindingContextAsync(actionContext);
|
||||||
|
|
||||||
|
|
@ -114,6 +114,210 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return fewestOptionalParameters[0].Action;
|
return fewestOptionalParameters[0].Action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method attempts to ensure that the route that's about to generate a link will generate a link
|
||||||
|
// to an existing action. This method is called by a route (through MvcApplication) prior to generating
|
||||||
|
// any link - this gives WebFX a chance to 'veto' the values provided by a route.
|
||||||
|
//
|
||||||
|
// This method does not take httpmethod or dynamic action constraints into account.
|
||||||
|
public virtual bool HasValidAction([NotNull] VirtualPathContext context)
|
||||||
|
{
|
||||||
|
if (context.ProvidedValues == null)
|
||||||
|
{
|
||||||
|
// We need the route's values to be able to double check our work.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions =
|
||||||
|
GetActions().Where(
|
||||||
|
action =>
|
||||||
|
action.RouteConstraints == null ||
|
||||||
|
action.RouteConstraints.All(constraint => constraint.Accept(context.ProvidedValues)));
|
||||||
|
|
||||||
|
return actions.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is called by the default UrlHelper as part of Action link generation. When a link is requested
|
||||||
|
// specifically for an Action, we manipulate the route data to ensure that the right link is generated.
|
||||||
|
// Read further for details.
|
||||||
|
public virtual IEnumerable<ActionDescriptor> GetCandidateActions(VirtualPathContext context)
|
||||||
|
{
|
||||||
|
// This method attemptss to find a unique 'best' candidate set of actions from the provided route
|
||||||
|
// values and ambient route values.
|
||||||
|
//
|
||||||
|
// The purpose of this process is to avoid allowing certain routes to be too greedy. When a route uses
|
||||||
|
// a default value as a filter, it can generate links to actions it will never hit. The actions returned
|
||||||
|
// by this method are used by the link generation code to manipulate the route values so that routes that
|
||||||
|
// are are greedy can't generate a link.
|
||||||
|
//
|
||||||
|
// The best example of this greediness is the canonical 'area' route from MVC.
|
||||||
|
//
|
||||||
|
// Ex: Areas/Admin/{controller}/{action} (defaults { area = "Admin" })
|
||||||
|
//
|
||||||
|
// This route can generate a link even when the 'area' token is not provided.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// We define 'best' based on the combination of Values and AmbientValues. This set can be used to select a
|
||||||
|
// set of actions, anything in this is set is 'reachable'. We determine 'best' by looking for the 'reachable'
|
||||||
|
// actions ordered by the most total constraints matched, then the most constraints matched by ambient values.
|
||||||
|
//
|
||||||
|
// Ex:
|
||||||
|
// Consider the following actions - Home/Index (no area), and Admin/Home/Index (area = Admin).
|
||||||
|
// ambient values = { area = "Admin", controller = "Home", action = "Diagnostics" }
|
||||||
|
// values = { action = "Index" }
|
||||||
|
//
|
||||||
|
// In this case we want to select the Admin/Home/Index action, and algorithm leads us there.
|
||||||
|
//
|
||||||
|
// Admin/Home/Index: Total score 3, Explicit score 2, Implicit score 1, Omission score 0
|
||||||
|
// Home/Index: Total score 3, Explicit score 2, Implicit score 0, Omission score 1
|
||||||
|
//
|
||||||
|
// The description here is based on the concepts we're using to implement areas in WebFx, but apply
|
||||||
|
// to any tokens that might be used in routing (including REST conventions when action == null).
|
||||||
|
//
|
||||||
|
// This method does not take httpmethod or dynamic action constraints into account.
|
||||||
|
|
||||||
|
var actions = GetActions();
|
||||||
|
|
||||||
|
var candidates = new List<ActionDescriptorLinkCandidate>();
|
||||||
|
foreach (var action in actions)
|
||||||
|
{
|
||||||
|
var candidate = new ActionDescriptorLinkCandidate() { Action = action };
|
||||||
|
if (action.RouteConstraints == null)
|
||||||
|
{
|
||||||
|
candidates.Add(candidate);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isActionValid = true;
|
||||||
|
foreach (var constraint in action.RouteConstraints)
|
||||||
|
{
|
||||||
|
if (constraint.Accept(context.Values))
|
||||||
|
{
|
||||||
|
if (context.Values.ContainsKey(constraint.RouteKey))
|
||||||
|
{
|
||||||
|
// Explicit value is acceptable
|
||||||
|
candidate.ExplicitMatches++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No value supplied and that's OK for this action.
|
||||||
|
candidate.OmissionMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context.Values.ContainsKey(constraint.RouteKey))
|
||||||
|
{
|
||||||
|
// There's an explicitly provided value, but the action constraint doesn't match it.
|
||||||
|
isActionValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (constraint.Accept(context.AmbientValues))
|
||||||
|
{
|
||||||
|
// Ambient value is acceptable, used as a fallback
|
||||||
|
candidate.ImplicitMatches++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No possible match
|
||||||
|
isActionValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActionValid)
|
||||||
|
{
|
||||||
|
candidates.Add(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<ActionDescriptor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all of the actions with the maximum number of total constraint matches.
|
||||||
|
var longestMatches =
|
||||||
|
candidates
|
||||||
|
.GroupBy(c => c.TotalMatches)
|
||||||
|
.OrderByDescending(g => g.Key)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
// Finds all of the actions (from the above set) with the maximum number of explicit constraint matches.
|
||||||
|
var bestMatchesByExplicit =
|
||||||
|
longestMatches
|
||||||
|
.GroupBy(c => c.ExplicitMatches)
|
||||||
|
.OrderByDescending(g => g.Key)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
// Finds all of the actions (from the above set) with the maximum number of implicit constraint matches.
|
||||||
|
var bestMatchesByImplicit =
|
||||||
|
bestMatchesByExplicit
|
||||||
|
.GroupBy(c => c.ImplicitMatches)
|
||||||
|
.OrderByDescending(g => g.Key)
|
||||||
|
.First();
|
||||||
|
|
||||||
|
var bestActions = bestMatchesByImplicit.Select(m => m.Action).ToArray();
|
||||||
|
if (bestActions.Length == 1)
|
||||||
|
{
|
||||||
|
return bestActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
var exemplar = FindEquivalenceClass(bestActions);
|
||||||
|
if (exemplar == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(Resources.ActionSelector_GetCandidateActionsIsAmbiguous);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return bestActions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method determines if the set of action descriptor candidates share a common set
|
||||||
|
// of route constraints, and returns an exemplar if there's a single set. This identifies
|
||||||
|
// a type of ambiguity, more data must be specified to ensure the right action can be selected.
|
||||||
|
//
|
||||||
|
// This is a no-op for our default conventions, but becomes important with custom action
|
||||||
|
// descriptor providers.
|
||||||
|
//
|
||||||
|
// Ex: These are not in the same equivalence class.
|
||||||
|
// Action 1: constraint keys - { action, controller, area }
|
||||||
|
// Action 2: constraint keys - { action, module }
|
||||||
|
//
|
||||||
|
private ActionDescriptor FindEquivalenceClass(ActionDescriptor[] candidates)
|
||||||
|
{
|
||||||
|
Contract.Assert(candidates.Length > 1);
|
||||||
|
|
||||||
|
var criteria = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var exemplar = candidates[0];
|
||||||
|
foreach (var constraint in exemplar.RouteConstraints)
|
||||||
|
{
|
||||||
|
criteria.Add(constraint.RouteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i < candidates.Length; i++)
|
||||||
|
{
|
||||||
|
var candidate = candidates[i];
|
||||||
|
foreach (var constraint in exemplar.RouteConstraints)
|
||||||
|
{
|
||||||
|
if (criteria.Add(constraint.RouteKey))
|
||||||
|
{
|
||||||
|
// This is a new criterion - the candidates have multiple criteria sets
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return exemplar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ActionDescriptor> GetActions()
|
||||||
|
{
|
||||||
|
var actionDescriptorProviderContext = new ActionDescriptorProviderContext();
|
||||||
|
_actionDescriptorProvider.Invoke(actionDescriptorProviderContext);
|
||||||
|
|
||||||
|
return actionDescriptorProviderContext.Results;
|
||||||
|
}
|
||||||
|
|
||||||
private class ActionDescriptorCandidate
|
private class ActionDescriptorCandidate
|
||||||
{
|
{
|
||||||
public ActionDescriptor Action { get; set; }
|
public ActionDescriptor Action { get; set; }
|
||||||
|
|
@ -122,5 +326,24 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
public int FoundOptionalParameters { get; set; }
|
public int FoundOptionalParameters { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ActionDescriptorLinkCandidate
|
||||||
|
{
|
||||||
|
public ActionDescriptor Action { get; set; }
|
||||||
|
|
||||||
|
// Matches from explicit route values
|
||||||
|
public int ExplicitMatches { get; set; }
|
||||||
|
|
||||||
|
// Matches from ambient route values
|
||||||
|
public int ImplicitMatches { get; set; }
|
||||||
|
|
||||||
|
// Matches from explicit route values (by omission)
|
||||||
|
public int OmissionMatches { get; set; }
|
||||||
|
|
||||||
|
public int TotalMatches
|
||||||
|
{
|
||||||
|
get { return ExplicitMatches + ImplicitMatches + OmissionMatches; }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System.Threading.Tasks;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -7,5 +10,9 @@ namespace Microsoft.AspNet.Mvc
|
||||||
Task<ActionDescriptor> SelectAsync(RequestContext context);
|
Task<ActionDescriptor> SelectAsync(RequestContext context);
|
||||||
|
|
||||||
bool Match(ActionDescriptor descriptor, RequestContext context);
|
bool Match(ActionDescriptor descriptor, RequestContext context);
|
||||||
|
|
||||||
|
bool HasValidAction(VirtualPathContext context);
|
||||||
|
|
||||||
|
IEnumerable<ActionDescriptor> GetCandidateActions(VirtualPathContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Abstractions;
|
using Microsoft.AspNet.Abstractions;
|
||||||
using Microsoft.AspNet.DependencyInjection;
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
|
|
@ -20,8 +22,12 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
||||||
{
|
{
|
||||||
// For now just allow any values to target this application.
|
// The contract of this method is to check that the values coming in from the route are valid;
|
||||||
context.IsBound = true;
|
// that they match an existing action, setting IsBound = true if the values are OK.
|
||||||
|
var actionSelector = context.Context.RequestServices.GetService<IActionSelector>();
|
||||||
|
context.IsBound = actionSelector.HasValidAction(context);
|
||||||
|
|
||||||
|
// We return null here because we're not responsible for generating the url, the route is.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -267,7 +267,7 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The '{0}' must return a non null '{1}'.
|
/// The '{0}' must return a non-null '{1}'.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string MethodMustReturnNotNullValue
|
internal static string MethodMustReturnNotNullValue
|
||||||
{
|
{
|
||||||
|
|
@ -275,13 +275,29 @@ namespace Microsoft.AspNet.Mvc.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The '{0}' must return a non null '{1}'.
|
/// The '{0}' must return a non-null '{1}'.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static string FormatMethodMustReturnNotNullValue(object p0, object p1)
|
internal static string FormatMethodMustReturnNotNullValue(object p0, object p1)
|
||||||
{
|
{
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("MethodMustReturnNotNullValue"), p0, p1);
|
return string.Format(CultureInfo.CurrentCulture, GetString("MethodMustReturnNotNullValue"), p0, p1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The supplied route values are ambiguous and can select multiple sets of actions.
|
||||||
|
/// </summary>
|
||||||
|
internal static string ActionSelector_GetCandidateActionsIsAmbiguous
|
||||||
|
{
|
||||||
|
get { return GetString("ActionSelector_GetCandidateActionsIsAmbiguous"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The supplied route values are ambiguous and can select multiple sets of actions.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatActionSelector_GetCandidateActionsIsAmbiguous()
|
||||||
|
{
|
||||||
|
return GetString("ActionSelector_GetCandidateActionsIsAmbiguous");
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -168,4 +168,7 @@
|
||||||
<data name="MethodMustReturnNotNullValue" xml:space="preserve">
|
<data name="MethodMustReturnNotNullValue" xml:space="preserve">
|
||||||
<value>The '{0}' must return a non-null '{1}'.</value>
|
<value>The '{0}' must return a non-null '{1}'.</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<data name="ActionSelector_GetCandidateActionsIsAmbiguous" xml:space="preserve">
|
||||||
|
<value>The supplied route values are ambiguous and can select multiple sets of actions.</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Microsoft.AspNet.Mvc.Core;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -70,41 +72,60 @@ namespace Microsoft.AspNet.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Accept(RequestContext context)
|
public bool Accept([NotNull] IDictionary<string, object> routeValues)
|
||||||
{
|
{
|
||||||
if (context == null)
|
object value;
|
||||||
{
|
|
||||||
throw new ArgumentNullException("context");
|
|
||||||
}
|
|
||||||
|
|
||||||
var routeValues = context.RouteValues;
|
|
||||||
|
|
||||||
if (routeValues == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Need route values", "context");
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (KeyHandling)
|
switch (KeyHandling)
|
||||||
{
|
{
|
||||||
case RouteKeyHandling.AcceptAlways:
|
case RouteKeyHandling.AcceptAlways:
|
||||||
return true;
|
return true;
|
||||||
case RouteKeyHandling.DenyKey:
|
|
||||||
return !routeValues.ContainsKey(RouteKey);
|
|
||||||
case RouteKeyHandling.CatchAll:
|
case RouteKeyHandling.CatchAll:
|
||||||
return routeValues.ContainsKey(RouteKey);
|
return routeValues.ContainsKey(RouteKey);
|
||||||
|
|
||||||
|
case RouteKeyHandling.DenyKey:
|
||||||
|
// Routing considers a null or empty string to also be the lack of a value
|
||||||
|
if (!routeValues.TryGetValue(RouteKey, out value) || value == null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringValue = value as string;
|
||||||
|
if (stringValue != null && stringValue.Length == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case RouteKeyHandling.RequireKey:
|
||||||
|
if (routeValues.TryGetValue(RouteKey, out value))
|
||||||
|
{
|
||||||
|
return Comparer.Equals(value, RouteValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
Debug.Fail("Unexpected routeValue");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Accept([NotNull] RequestContext context)
|
||||||
|
{
|
||||||
|
var routeValues = context.RouteValues;
|
||||||
|
if (routeValues == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
|
||||||
|
"RouteValues",
|
||||||
|
typeof(RequestContext)),
|
||||||
|
"context");
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.Assert(KeyHandling == RouteKeyHandling.RequireKey, "Unexpected routeValue");
|
return Accept(routeValues);
|
||||||
|
|
||||||
object value;
|
|
||||||
if (routeValues.TryGetValue(RouteKey, out value))
|
|
||||||
{
|
|
||||||
return Comparer.Equals(value, RouteValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.Contracts;
|
using System.Diagnostics.Contracts;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNet.Abstractions;
|
using Microsoft.AspNet.Abstractions;
|
||||||
using Microsoft.AspNet.DependencyInjection;
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
using Microsoft.AspNet.Mvc.Rendering;
|
using Microsoft.AspNet.Mvc.Rendering;
|
||||||
|
|
@ -14,12 +16,14 @@ namespace Microsoft.AspNet.Mvc
|
||||||
private readonly HttpContext _httpContext;
|
private readonly HttpContext _httpContext;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly IDictionary<string, object> _ambientValues;
|
private readonly IDictionary<string, object> _ambientValues;
|
||||||
|
private readonly IActionSelector _actionSelector;
|
||||||
|
|
||||||
public UrlHelper(IContextAccessor<ActionContext> contextAccessor)
|
public UrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelector actionSelector)
|
||||||
{
|
{
|
||||||
_httpContext = contextAccessor.Value.HttpContext;
|
_httpContext = contextAccessor.Value.HttpContext;
|
||||||
_router = contextAccessor.Value.Router;
|
_router = contextAccessor.Value.Router;
|
||||||
_ambientValues = contextAccessor.Value.RouteValues;
|
_ambientValues = contextAccessor.Value.RouteValues;
|
||||||
|
_actionSelector = actionSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Action(string action, string controller, object values, string protocol, string host, string fragment)
|
public string Action(string action, string controller, object values, string protocol, string host, string fragment)
|
||||||
|
|
@ -44,6 +48,24 @@ namespace Microsoft.AspNet.Mvc
|
||||||
valuesDictionary["controller"] = controller;
|
valuesDictionary["controller"] = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var context = new VirtualPathContext(_httpContext, _ambientValues, valuesDictionary);
|
||||||
|
var actions = _actionSelector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
var actionCandidate = actions.FirstOrDefault();
|
||||||
|
if (actionCandidate == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var constraint in actionCandidate.RouteConstraints)
|
||||||
|
{
|
||||||
|
if (constraint.KeyHandling == RouteKeyHandling.DenyKey &&
|
||||||
|
_ambientValues.ContainsKey(constraint.RouteKey))
|
||||||
|
{
|
||||||
|
valuesDictionary[constraint.RouteKey] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var path = GeneratePathFromRoute(valuesDictionary);
|
var path = GeneratePathFromRoute(valuesDictionary);
|
||||||
if (path == null)
|
if (path == null)
|
||||||
{
|
{
|
||||||
|
|
@ -67,8 +89,11 @@ namespace Microsoft.AspNet.Mvc
|
||||||
private string GeneratePathFromRoute(IDictionary<string, object> values)
|
private string GeneratePathFromRoute(IDictionary<string, object> values)
|
||||||
{
|
{
|
||||||
var context = new VirtualPathContext(_httpContext, _ambientValues, values);
|
var context = new VirtualPathContext(_httpContext, _ambientValues, values);
|
||||||
|
|
||||||
var path = _router.GetVirtualPath(context);
|
var path = _router.GetVirtualPath(context);
|
||||||
|
if (path == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// See Routing Issue#31
|
// See Routing Issue#31
|
||||||
if (path.Length > 0 && path[0] != '/')
|
if (path.Length > 0 && path[0] != '/')
|
||||||
|
|
@ -76,7 +101,15 @@ namespace Microsoft.AspNet.Mvc
|
||||||
path = "/" + path;
|
path = "/" + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _httpContext.Request.PathBase.Add(new PathString(path)).Value;
|
var fullPath = _httpContext.Request.PathBase.Add(new PathString(path)).Value;
|
||||||
|
if (fullPath.Length == 0)
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Content([NotNull] string contentPath)
|
public string Content([NotNull] string contentPath)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
|
{
|
||||||
|
public class DefaultActionSelectorTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_NonArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: null, controller: "Home", action: "Index");
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_AreaExplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: "Admin", controller: "Home", action: "Index");
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(new { area = "Admin", controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_AreaImplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: "Admin", controller: "Home", action: "Index");
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { controller = "Home", action = "Index" },
|
||||||
|
new { area = "Admin", controller = "Home", action = "Diagnostics" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_NonAreaImplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: null, controller: "Home", action: "Edit");
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { controller = "Home", action = "Edit" },
|
||||||
|
new { area = "Admin", controller = "Home", action = "Diagnostics" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_NonAreaExplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: null, controller: "Home", action: "Index");
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { area = (string)null, controller = "Home", action = "Index" },
|
||||||
|
new { area = "Admin", controller = "Home", action = "Diagnostics" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_RestExplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: null, controller: "Product", action: null);
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { controller = "Product", action = (string)null },
|
||||||
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_Match_RestImplicit()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
var expected = GetActions(actions, area: null, controller: "Product", action: null);
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { controller = "Product" },
|
||||||
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCandidateActions_NoMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(
|
||||||
|
new { area = "Admin", controller = "Home", action = "Edit" },
|
||||||
|
new { area = "Admin", controller = "Home", action = "Index" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var candidates = selector.GetCandidateActions(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasValidAction_Match()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(new { });
|
||||||
|
context.ProvidedValues = new RouteValueDictionary(new { controller = "Home", action = "Index"});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isValid = selector.HasValidAction(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasValidAction_NoMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actions = GetActions();
|
||||||
|
|
||||||
|
var selector = CreateSelector(actions);
|
||||||
|
var context = CreateContext(new { });
|
||||||
|
context.ProvidedValues = new RouteValueDictionary(new { controller = "Home", action = "FakeAction" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var isValid = selector.HasValidAction(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(isValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionDescriptor[] GetActions()
|
||||||
|
{
|
||||||
|
return new ActionDescriptor[]
|
||||||
|
{
|
||||||
|
// Like a typical RPC controller
|
||||||
|
CreateAction(area: null, controller: "Home", action: "Index"),
|
||||||
|
CreateAction(area: null, controller: "Home", action: "Edit"),
|
||||||
|
|
||||||
|
// Like a typical REST controller
|
||||||
|
CreateAction(area: null, controller: "Product", action: null),
|
||||||
|
CreateAction(area: null, controller: "Product", action: null),
|
||||||
|
|
||||||
|
// RPC controller in an area with the same name as home
|
||||||
|
CreateAction(area: "Admin", controller: "Home", action: "Index"),
|
||||||
|
CreateAction(area: "Admin", controller: "Home", action: "Diagnostics"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<ActionDescriptor> GetActions(
|
||||||
|
IEnumerable<ActionDescriptor> actions,
|
||||||
|
string area,
|
||||||
|
string controller,
|
||||||
|
string action)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
actions
|
||||||
|
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "area" && c.Comparer.Equals(c.RouteValue, area)))
|
||||||
|
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "controller" && c.Comparer.Equals(c.RouteValue, controller)))
|
||||||
|
.Where(a => a.RouteConstraints.Any(c => c.RouteKey == "action" && c.Comparer.Equals(c.RouteValue, action)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultActionSelector CreateSelector(IEnumerable<ActionDescriptor> actions)
|
||||||
|
{
|
||||||
|
var actionProvider = new Mock<INestedProviderManager<ActionDescriptorProviderContext>>(MockBehavior.Strict);
|
||||||
|
actionProvider
|
||||||
|
.Setup(p => p.Invoke(It.IsAny<ActionDescriptorProviderContext>()))
|
||||||
|
.Callback<ActionDescriptorProviderContext>(c => c.Results.AddRange(actions));
|
||||||
|
|
||||||
|
var bindingProvider = new Mock<IActionBindingContextProvider>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
return new DefaultActionSelector(actionProvider.Object, bindingProvider.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualPathContext CreateContext(object routeValues)
|
||||||
|
{
|
||||||
|
return CreateContext(routeValues, ambientValues: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualPathContext CreateContext(object routeValues, object ambientValues)
|
||||||
|
{
|
||||||
|
var httpContext = new Mock<HttpContext>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
return new VirtualPathContext(
|
||||||
|
httpContext.Object,
|
||||||
|
new RouteValueDictionary(ambientValues),
|
||||||
|
new RouteValueDictionary(routeValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionDescriptor CreateAction(string area, string controller, string action)
|
||||||
|
{
|
||||||
|
var actionDescriptor = new ActionDescriptor()
|
||||||
|
{
|
||||||
|
Name = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action),
|
||||||
|
RouteConstraints = new List<RouteDataActionConstraint>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
actionDescriptor.RouteConstraints.Add(
|
||||||
|
area == null ?
|
||||||
|
new RouteDataActionConstraint("area", RouteKeyHandling.DenyKey) :
|
||||||
|
new RouteDataActionConstraint("area", area));
|
||||||
|
|
||||||
|
actionDescriptor.RouteConstraints.Add(
|
||||||
|
controller == null ?
|
||||||
|
new RouteDataActionConstraint("controller", RouteKeyHandling.DenyKey) :
|
||||||
|
new RouteDataActionConstraint("controller", controller));
|
||||||
|
|
||||||
|
actionDescriptor.RouteConstraints.Add(
|
||||||
|
action == null ?
|
||||||
|
new RouteDataActionConstraint("action", RouteKeyHandling.DenyKey) :
|
||||||
|
new RouteDataActionConstraint("action", action));
|
||||||
|
|
||||||
|
return actionDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateHttpContext(appRoot);
|
var context = CreateHttpContext(appRoot);
|
||||||
var contextAccessor = CreateActionContext(context);
|
var contextAccessor = CreateActionContext(context);
|
||||||
var urlHelper = new UrlHelper(contextAccessor);
|
var urlHelper = CreateUrlHelper(contextAccessor);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var path = urlHelper.Content(contentPath);
|
var path = urlHelper.Content(contentPath);
|
||||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
// Arrange
|
// Arrange
|
||||||
var context = CreateHttpContext(appRoot);
|
var context = CreateHttpContext(appRoot);
|
||||||
var contextAccessor = CreateActionContext(context);
|
var contextAccessor = CreateActionContext(context);
|
||||||
var urlHelper = new UrlHelper(contextAccessor);
|
var urlHelper = CreateUrlHelper(contextAccessor);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var path = urlHelper.Content(contentPath);
|
var path = urlHelper.Content(contentPath);
|
||||||
|
|
@ -75,5 +75,11 @@ namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
.Returns(actionContext);
|
.Returns(actionContext);
|
||||||
return contextAccessor.Object;
|
return contextAccessor.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UrlHelper CreateUrlHelper(IContextAccessor<ActionContext> contextAccessor)
|
||||||
|
{
|
||||||
|
var actionSelector = new Mock<IActionSelector>(MockBehavior.Strict);
|
||||||
|
return new UrlHelper(contextAccessor, actionSelector.Object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"version" : "0.1-alpha-*",
|
"version" : "0.1-alpha-*",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
|
"Microsoft.AspNet.Abstractions": "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
|
"Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Mvc.Core" : "",
|
"Microsoft.AspNet.Mvc.Core" : "",
|
||||||
"Microsoft.AspNet.Mvc" : "",
|
"Microsoft.AspNet.Mvc" : "",
|
||||||
|
"Microsoft.AspNet.Routing": "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Testing": "0.1-alpha-*",
|
"Microsoft.AspNet.Testing": "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
||||||
"Microsoft.AspNet.Mvc.Rendering": "",
|
"Microsoft.AspNet.Mvc.Rendering": "",
|
||||||
|
|
@ -25,4 +26,4 @@
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"net45": { }
|
"net45": { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue