We've embarked on the mission of doing a Mvc3 implementation of our framework sites, currently with our existing WebForms implementation.
This task has allowed us to integrate IoC and DI with Structuremap for flexibility.
To give you some background, we have the following project structure:
App.Core <- core class library
App.Mvc <- Mvc class library
App.Mvc.Web <- Mvc3 APP
App.WebForms <- Webforms class library
App.WebForms.Web <- Webforms app or site
We are using Mvc's routing on both the Mvc and the WebForms implementations, we used the same method for route publishing as in the Orchard Project by using a IRouteProvider, where N number of implementation of IRouteProvider can be created to add routes for a publisher to register and order by their priorities.
This is working fine, we are able to register and use the routes with the UrlHelper in MVC or Page.GetRouteUrl in WebForms.
The problem is that we now have a requirement for the App.Core to also be able to resolve these routes (not all of them, but some of the most common ones) and these can change according to the site being implemented.
For example, product detail default route may be "/{category}/{product_name}/{product_id}", but we want the ability to override this and for a certain site have "/{brand}/{product_name}/{product_id}" instead.
This means that in the Core we can't simply just use RouteTable.Routes.GetVirtualPath with a fixed set of parameters, because these might change from site to site.
We created a IRouteResolver interface with methods for the most common routes, which has a default implementation that is registered with SM in each of the class libraries (Mvc or Webforms), but could also be overriden on each site.
The interface looks like:
public interface IRouteResolver
{
string GetRouteUrl(object routeParameters);
string GetRouteUrl(RouteValueDictionary routeParameters);
string GetRouteUrl(string routeName, object routeParameters);
string GetRouteUrl(string routeName, RouteValueDictionary routeParameters);
string GetUrlFor(Product product);
string GetUrlFor(Category category);
string GetUrlFor(Brand brand);
}
The default Mvc implementation of the interface looks like:
public class MvcRouteResolver : IRouteResolver
{
UrlHelper _urlHelper;
ICategoryModelBroker _categoryModelBroker;
IBrandModelBroker _brandModelBroker;
IProductModelBroker _productModelBroker;
public MvcRouteResolver(UrlHelper urlHelper)
{
_urlHelper = urlHelper;
_categoryModelBroker = ObjectFactory.GetInstance<ICategoryModelBroker>();
_brandModelBroker = ObjectFactory.GetInstance<IBrandModelBroker>();
_productModelBroker = ObjectFactory.GetInstance<IProductModelBroker>();
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
return _urlHelper.RouteUrl(routeName, routeParameters);
}
public string GetUrlFor(Product product)
{
string category = string.Empty;
if (product.Categories.Count > 0)
category = product.Categories[0].Breadcrumb.Replace("@@", "-");
else if (product.Brands.Any())
category = product.Brands.FirstOrDefault().Name;
else
category = "detail";
return GetRouteUrl(new { controller="Product", action="Detail", productId = product.Id, brandName = _productModelBroker.GetSlug(product), productName = _productModelBroker.GetSluggedName(product) });
}
public string GetUrlFor(Category category)
{
return GetRouteUrl(new { controller = "Product", action = "ListByCategory", id = category.Id, name = _categoryModelBroker.GetSlug(category) });
}
public string GetUrlFor(Brand brand)
{
return GetRouteUrl(new { controller = "Product", action = "ListByBrand", id = brand.Id, name = _brandModelBroker.GetSlug(brand) });
}
}
The default WebForms implementation looks like:
public class WebRouteResolver : IRouteResolver
{
Control _control;
HttpContext _context;
public WebRouteResolver()
:this(HttpContext.Current)
{
}
public WebRouteResolver(HttpContext context)
{
_context = context;
}
public WebRouteResolver(Control control)
{
_control = control;
}
public WebRouteResolver(Page page)
{
_control = page as Control;
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
VirtualPathData virtualPath = null;
if(_control.IsNotNull())
virtualPath = RouteTable.Routes.GetVirtualPath(_control.Page.Request.RequestContext, routeName, routeParameters);
else
virtualPath = RouteTable.Routes.GetVirtualPath(_context.Request.RequestContext, routeName, routeParameters);
if (virtualPath != null)
{
return virtualPath.VirtualPath;
}
return null;
}
private string ResolveUrl(string originalUrl)
{
if(_control.IsNotNull())
return _control.ResolveUrl(originalUrl);
// *** Absolute path - just return
if (originalUrl.IndexOf("://") != -1)
return originalUrl;
// *** Fix up image path for ~ root app dir directory
if (originalUrl.StartsWith("~"))
{
string newUrl = "";
if (_context != null)
newUrl = _context.Request.ApplicationPath +
originalUrl.Substring(1).Replace("//", "/");
else
// *** Not context: assume current directory is the base directory
throw new ArgumentException("Invalid URL: Relative URL not allowed.");
// *** Just to be sure fix up any double slashes
return newUrl;
}
return originalUrl;
}
public string GetUrlFor(Product product)
{
string category = string.Empty;
if (product.Categories.Count > 0)
category = product.Categories[0].Breadcrumb.Replace("@@", "-");
else if (product.Brands.Any())
category = product.Brands.FirstOrDefault().Name;
else
category = "detail";
if (Config.RoutingEnabled)
{
return GetRouteUrl(new { @category = CommonHelper.ToFriendlyUrl(category), name = CommonHelper.ToFriendlyUrl(product.Name), id = product.Id });
}
return ResolveUrl(Config.GetStoreSetting("productDetailUrl")) + "?id={0}&name={1}&category={2}".Fill(product.Id, CommonHelper.ToFriendlyUrl(product.Name), CommonHelper.ToFriendlyUrl(category));
}
public string GetUrlFor(Category category)
{
string breadcrumb = category.Breadcrumb.Replace("@@", "-");
if (Config.RoutingEnabled)
return GetRouteUrl(new { @category = CommonHelper.ToFriendlyUrl(breadcrumb), category_id = category.Id});
return ResolveUrl(Config.GetStoreSetting("productListingUrl") + "?category_id={0}&category={1}".Fill(category.Id, CommonHelper.ToFriendlyUrl(category.Name)));
}
public string GetUrlFor(Brand brand)
{
if (Config.RoutingEnabled)
return GetRouteUrl(new { @brand = CommonHelper.ToFriendlyUrl(brand.Name), brand_id = brand.Id });
return ResolveUrl(Config.GetStoreSetting("productListingUrl") + "?brand_id={0}&brand={1}".Fill(brand.Id, CommonHelper.ToFriendlyUrl(brand.Name)));
}
}
Problem now is that because of the constructors arguments (UrlHelper on the Mvc and Page or Control on the Webforms) we are forced to use concrete types instead of using SM to grab the instance of the IRouteResolver plugin.
For example I have the following extensions to make the resolver available on the Page or Control
public static IRouteResolver RouteResolver(this Control control)
{
return new WebRouteResolver(control);
}
public static IRouteResolver RouteResolver(this Page page)
{
return new WebRouteResolver(page);
}
This covers the default behaviour for Web or Mvc but not the case when we want to specifically override the resolver on a per site basis.
Questions are, is it safe to add these constructor arguments as plugins in SM?
Is there another approach/pattern you can recommend for this feature request?
Any ideas/suggestions will be greatly appreciated.
Many thanks, P.
I've taken another approach, the above did not work on the Core. I now rely only on the RouteCollection and the HttpContext to resolve the routes:
public abstract class BaseRouteResolver : IRouteResolver
{
protected HttpContext _context;
protected RouteCollection _routeCollection;
public BaseRouteResolver()
:this(RouteTable.Routes, HttpContext.Current)
{
}
public BaseRouteResolver(RouteCollection routeCollection, HttpContext context)
{
_routeCollection = routeCollection;
_context = context;
}
public string GetRouteUrl(object routeParameters)
{
return GetRouteUrl(new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(System.Web.Routing.RouteValueDictionary routeParameters)
{
return GetRouteUrl(null, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, object routeParameters)
{
return GetRouteUrl(routeName, new RouteValueDictionary(routeParameters));
}
public string GetRouteUrl(string routeName, System.Web.Routing.RouteValueDictionary routeParameters)
{
VirtualPathData virtualPath = _routeCollection.GetVirtualPath(_context.Request.RequestContext, routeName, routeParameters);
if (virtualPath != null)
return virtualPath.VirtualPath;
return null;
}
public abstract string GetUrlFor(Product product);
public abstract string GetUrlFor(Category category);
public abstract string GetUrlFor(Brand brand);
}