Please, consider the following routes:
routes.MapRoute(
"route1",
"{controller}/{month}-{year}/{action}/{user}"
);
routes.MapRoute(
"route2",
"{controller}/{month}-{year}/{action}"
);
And the following tests:
TEST 1
[TestMethod]
public void Test1()
{
RouteCollection routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
RequestContext context = new RequestContext(CreateHttpContext(),
new RouteData());
DateTime now = DateTime.Now;
string result;
context.RouteData.Values.Add("controller", "Home");
context.RouteData.Values.Add("action", "Index");
context.RouteData.Values.Add("user", "user1");
result = UrlHelper.GenerateUrl(null, "Index", null,
new RouteValueDictionary(
new
{
month = now.Month,
year = now.Year
}),
routes, context, true);
//OK, result == /Home/10-2012/Index/user1
Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year),
result);
}
TEST 2
[TestMethod]
public void Test2()
{
RouteCollection routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
RequestContext context = new RequestContext(CreateHttpContext(),
new RouteData());
DateTime now = DateTime.Now;
string result;
context.RouteData.Values.Add("controller", "Home");
context.RouteData.Values.Add("action", "Index");
context.RouteData.Values.Add("user", "user1");
context.RouteData.Values.Add("month", now.Month + 1);
context.RouteData.Values.Add("year", now.Year);
result = UrlHelper.GenerateUrl(null, "Index", null,
new RouteValueDictionary(
new
{
month = now.Month,
year = now.Year
}),
routes, context, true);
//Error because result == /Home/10-2012/Index
Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year),
result);
}
This test emulates a situation when I already have route values in request context and try to generate an outgoing url with UrlHelper.
The problem is that (presented in test 2), if I have values for all the segments from the expected route (here route1
) and try to replace some of them through routeValues
parameter, the wanted route is omitted and the next suitable route is used.
So test 1 works well, as the request context already has values for 3 of 5 segments of route 1, and values for the missing two segments (namely, year
and month
) are passed through the routeValues
parameter.
Test 2 has values for all 5 segments in the request context. And I want to replace some of them (namely, month and year) with other values throught routeValues
. But route 1 appears to be not suitable and route 2 is used.
Why? What is incorrect with my routes?
Am I expected to clear request context manually in such circumstances?
EDIT
[TestMethod]
public void Test3()
{
RouteCollection routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
RequestContext context = new RequestContext(CreateHttpContext(),
new RouteData());
DateTime now = DateTime.Now;
string result;
context.RouteData.Values.Add("controller", "Home");
context.RouteData.Values.Add("action", "Index");
context.RouteData.Values.Add("month", now.Month.ToString());
context.RouteData.Values.Add("year", now.Year.ToString());
result = UrlHelper.GenerateUrl(null, "Index", null,
new RouteValueDictionary(
new
{
month = now.Month + 1,
year = now.Year + 1
}),
routes, context, true);
Assert.AreEqual(string.Format("/Home/{0}-{1}/Index", now.Month + 1, now.Year + 1),
result);
}
This test makes things more confused. Here I'm testing route2. And it works! I have values for all 4 segments in the request context, pass other values through routeValues
, and the generated outgoing url is OK.
So, the problem is with route1. What am I missing?
EDIT
From Sanderson S. Freeman A. - Pro ASP.NET MVC 3 Framework Third Edition:
The routing system processes the routes in the order that they were added to the RouteCollection object passed to the RegisterRoutes method. Each route is inspected to see if it is a match, which requires three conditions to be met:
- A value must be available for every segment variable defined in the URL pattern. To find values for each segment variable, the routing system looks first at the values we have provided (using the properties of anonymous type), then the variable values for the current request, and finally at the default values defined in the route.
- None of the values we provided for the segment variables may disagree with the default-only variables defined in the route. I don't have default values in these routes
- The values for all of the segment variables must satisfy the route constraints. I don't have constraints in these routes
So, according to the first rule I've specified values in an anonymous type, I don't have default values. The variable values for the current request - I suppose this to be the values from the request context.
What is wrong with these reasonings for route2, while they work well for route1?
EDIT
Actually everything started not from unit tests, but from a real mvc application. There I used UrlHelper.Action Method (String, Object) to generate outgoing urls. Since this method is utilized in a layout view (the parent one for the majority of all views), I've taken it into my extension helper method (to exclude extra logic from views), This extension method extracts the action name from the request context, passed to it as an argument. I know I could extract all the current route values through the request context and replace those year and month (or could I create an anonymous route value collection, containing all the values from the context), but I thought it was superfluous, as mvc automatically took into account the values contained in the request context. So, I extracted only action name, as there were no UrlHelper.Action overload without action name (or I'd have liked even to "not specify" action name too), and added the new month and year through anonymous route value object.
This is an extension method:
public static MvcHtmlString GetPeriodLink(this HtmlHelper html,
RequestContext context,
DateTime date)
{
UrlHelper urlHelper = new UrlHelper(context);
return MvcHtmlString.Create(
urlHelper.Action(
(string)context.RouteData.Values["action"],
new { year = date.Year, month = date.Month }));
}
As I described in the tests above, it worked for shorter routes (when request context contained only controller, year and month, and action), but failed for a longer one (when request context contained controller, year and month, action, and user).
I've posted a workaround I use to make the routing work the way I need.
While indeed I would love to find out, why on earth do I have to make any workaround in such scenario and what is the key difference between these two routes that prevents route1
from working the way route2
does.
EDIT
另外的话。 至于在请求上下文的值类型的string
,我决定尝试将它们设置成背景为字符串,以确保没有任何类型的混乱(INT VS字符串)。 我不明白,是什么在这方面有所改变,但部分路线的正确启动产生。 但并不是所有的...这使得尚未意义不大。 我在实际的应用中,不改变测试这一点,因为测试必须int
在上下文中,而不是字符串。
好吧,我发现在其下ROUTE1使用条件-当的值,它是只用month
和year
的背景下,等于在匿名对象给出的那些。 如果它们之间的区别(在测试中,这是真正的既为int
和string
), 路径2被使用。 但为什么?
这证实了我在我的实际应用:
- 我有
string
S IN的范围内,但所提供int
通过匿名对象S,它在某种程度上混淆了MVC和它不能使用route1
。 - 我改变
int
S IN的匿名对象到string
s,而其中的URLmonth
和year
在上下文中是等于匿名对象的那些,开始正确生成; 而所有其他人没有。
所以,我看到一个规则:匿名对象的属性应该是类型的string
在请求上下文匹配路由值的类型。
但这个规则似乎不是强制性的 ,如Test3的 ,我改变了类型(你可以看到,现在上图),它仍然可以正常工作。 MVC管理,以正确地投类型本身。
最后,我找到了解释这一切的行为。 请参阅我的回答如下。