RoutePublisher Route Name

Topics: Customizing Orchard, Writing modules
Aug 10, 2011 at 8:25 AM

I'm writing a module that includes custom named route routing.  This works fine until another tenant is added that also uses the same module. From what I can see in the "Publish" method of Orchard.Mvc.Routes.RoutePublisher, routes are per tenant but they are only added to the ASP.Net MVC route collection by name so when multiple tenants use the same module that includes custom routing (specifically with named routes), the following error occurs.

A route named 'BLAH' is already in the route collection. Route names must be unique.Parameter name: name

Note: the cropArray which is supposed to remove pre-existing routes does not work here because the shell name is different for each tenant.  

Also, even without named routes, if for example a module includes fifty routes, and the site has say one hundred tenants all of which use the module, there would be five thousand routes in the routing table when only fifty were really ever required.

I'm not sure the best way around this or whether I've just missed something that enables a module to either share routing across tenants or properly isolate routes to tenants even when using name routes.  Any ideas?

Aug 10, 2011 at 11:02 AM

I'm currently working around this with a bit of a hack but it's good enough for my purposes currently (inside "Publish" of Orchard.Mvc.Routes.RoutePublisher @line 50):

// Also remove any named routes already in the collection, Mvc routes cannot exist under the same name
// HACK: this is a bit of a hack really as any routes that have the same name and genuinely conflict (such as those defined by another module) will silently be ignored.
List<RouteBase> cropByRouteName = new List<RouteBase>();
foreach (var route in routesArray)
{	
    var rt = _routeCollection[route.Name];

    if (rt != null && rt is ShellRoute) { cropByRouteName.Add(rt); }
}
foreach (var crop in cropByRouteName)
{
    _routeCollection.Remove(crop);
}
 

Coordinator
Aug 10, 2011 at 8:01 PM

I'm curious to understand the scenario here.

Aug 11, 2011 at 7:52 AM

The place where I specifically need the named routing is in part of my module that produces a sitemap.xml document (this is based on another sitemap.xml module - http://kosfiz.ru/2011/04/14/orchard-cms-programming-sitemap-module/).  I wanted to be able to loop through the routes in the routing table and for certain routes loop over the possible values for these routes.  For example:

Given a route of

products/{series}/{model}

I would want to loop through all the possible productseries, then all the productseries/productmodel combinations.

I have other routes defined in my module's Route file that do not have a route name and I'm only interested in providing a sitemap for specific routes so idenfication by name seemed the most obvious way to do it.

I considered ignoring the routes set in the routes table and just using a string that was the same format as the urls set in the routes table but the sitemap would break if the routes ever changed so I would like to keep it based on the routing table.

Effectively my modifactions to the sitemap module from kosfiz are as follows:

//...... sitemap code....

public override void ExecuteResult(ControllerContext context)
{
	//...... sitemap code....
	
	Routes sellerRoutes = new Routes();

	IEnumerable<Orchard.Mvc.Routes.RouteDescriptor> rts = sellerRoutes.GetRoutes();

	foreach (Orchard.Mvc.Routes.RouteDescriptor rd in rts)
	{
		switch (rd.Name)
		{
			case "SearchSeries":
				foreach (Series series in seriesList)
				{
					var absUrl = GetUrl(context, new { series = series.Name }, rd.Name);
					xml.Append(GetXmlSitemapUrlEntry(HttpUtility.HtmlEncode(absUrl)));
				}
				break;
			case "SearchSeriesModel":
				foreach (Model model in models)
				{
					var absUrl2 = GetUrl(context, new { series = series.Name, model = model.Name }, rd.Name);
					xml.Append(GetXmlSitemapUrlEntry(HttpUtility.HtmlEncode(absUrl2)));
				}
				break;
			default:
				break;
		}
	}
}

private string GetXmlSitemapUrlEntry(string absUrl)
{
	return string.Format(
			@"<url>
				<loc>{0}</loc>
				<priority>0.7</priority>
				<changefreq>weekly</changefreq>
			</url>", absUrl);
}

private string GetUrl(ControllerContext controllerContext, object routeValues, string routeName)
{
	var values = new RouteValueDictionary(routeValues);
	var context = new RequestContext(controllerContext.HttpContext, controllerContext.RouteData);
	var url = RouteTable.Routes.GetVirtualPath(context, routeName, values).VirtualPath;
	
	return new Uri(context.HttpContext.Request.Url, url).AbsoluteUri;
}

//...... sitemap code....

 

And my custom module routes are as follows:

/////////////////////////////////////
// CORRESPONDING ROUTES FROM MODULE
new RouteDescriptor() {
	Priority = 5,
	Name = "SearchSeriesModel",
	Route = new Route(
		"products/{series}-{model}", new RouteValueDictionary {
		{ "area", "Seller" },
		{ "controller", "Stock" },
		{ "action", "ListStock" }
	},
	new RouteValueDictionary{
		{"series", @"\w*"},
		{"model", @"\w*"}
	},
	new RouteValueDictionary { { "area", "Seller"} },
	new MvcRouteHandler())
},
new RouteDescriptor() {
	Priority = 5,
	Name = "SearchSeries",
	Route = new Route(
		"products/{series}", new RouteValueDictionary {
		{ "area", "Seller" },
		{ "controller", "Stock" },
		{ "action", "ListStock" }
	},
	new RouteValueDictionary{
		{"series", @"\w*"},
		{"location", @"\w*"}
	},
	new RouteValueDictionary { { "area", "Seller"} },
	new MvcRouteHandler())
}

Hope that goes some way to explain my method if not my madness...

Aug 11, 2011 at 10:30 AM

FYI, I've reverted my change as I was having problems later on with the routes not being available to all tenants but not sure.  I'm no longer basing my sitemap on the route table.

May 23, 2012 at 3:21 PM

I have exactly the same problem. I find it very strange that when the tenant is not working i'll automatically routed to the default website. So when i go to /admin i see the default website admin but with the url of the tenant. Very confusing!