MvcSiteMapProvider v4 also works with URLs by setting the Url property rather than using {controller}/{action}/{id}. This is exactly the scenario I am using it for (database driven URLs/custom RoutBase derived routes) and it works great. However, you should implement reverse URL lookup in your route as well or your URL resolution won't work.
public class ProductRoute
: RouteBase, IRouteWithArea
{
private readonly string area;
private readonly IApplicationContext appContext;
private readonly IRouteUrlProductListFactory routeUrlProductListFactory;
private readonly IRouteUtilities routeUtilities;
public ProductRoute(
string area,
IApplicationContext appContext,
IRouteUrlProductListFactory routeUrlProductListFactory,
IRouteUtilities routeUtilities
)
{
if (appContext == null) { throw new ArgumentNullException("appContext"); }
if (routeUrlProductListFactory == null) { throw new ArgumentNullException("routeUrlProductListFactory"); }
if (routeUtilities == null) { throw new ArgumentNullException("routeUtilities"); }
this.area = area;
this.appContext = appContext;
this.routeUrlProductListFactory = routeUrlProductListFactory;
this.routeUtilities = routeUtilities;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
var tenant = this.appContext.CurrentTenant;
if (tenant.TenantType.ToString().Equals(this.area, StringComparison.OrdinalIgnoreCase))
{
var localeId = this.appContext.CurrentLocaleId;
// Get all of the pages
var path = httpContext.Request.Path;
var pathLength = path.Length;
var page = this.routeUrlProductListFactory
.GetRouteUrlProductList(tenant.Id)
.Where(x => x.UrlPath.Length.Equals(pathLength))
.Where(x => x.UrlPath.Equals(path))
.FirstOrDefault();
if (page != null)
{
result = this.routeUtilities.CreateRouteData(this);
this.routeUtilities.AddQueryStringParametersToRouteData(result, httpContext);
result.Values["controller"] = "Product";
result.Values["action"] = "Details";
result.Values["localeId"] = localeId;
result.DataTokens["area"] = this.area;
// TODO: May need a compound key here (ProductXTenantLocaleID and
// CategoryId) to allow product to be hosted on pages that are not
// below categories.
result.Values["id"] = page.CategoryXProductId;
}
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData result = null;
if (requestContext.RouteData.IsAreaMatch(this.area))
{
var tenant = this.appContext.CurrentTenant;
// Get all of the pages
var pages = this.routeUrlProductListFactory.GetRouteUrlProductList(tenant.Id);
IRouteUrlProductInfo page = null;
if (this.TryFindMatch(pages, values, out page))
{
if (!string.IsNullOrEmpty(page.VirtualPath))
{
result = this.routeUtilities.CreateVirtualPathData(this, page.VirtualPath);
result.DataTokens["area"] = tenant.TenantType.ToString();
}
}
}
return result;
}
private bool TryFindMatch(IEnumerable<IRouteUrlProductInfo> pages, RouteValueDictionary values, out IRouteUrlProductInfo page)
{
page = null;
Guid categoryXProductId = Guid.Empty;
var localeId = (int?)values["localeId"];
if (localeId == null)
{
return false;
}
if (!Guid.TryParse(Convert.ToString(values["id"]), out categoryXProductId))
{
return false;
}
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (action == "Details" && controller == "Product")
{
page = pages
.Where(x => x.CategoryXProductId.Equals(categoryXProductId))
.Where(x => x.LocaleId.Equals(localeId))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;
}
#region IRouteWithArea Members
public string Area
{
get { return this.area; }
}
#endregion
}
public class RouteUtilities
: IRouteUtilities
{
#region IRouteUtilities Members
public void AddQueryStringParametersToRouteData(RouteData routeData, HttpContextBase httpContext)
{
var queryString = httpContext.Request.QueryString;
if (queryString.Keys.Count > 0)
{
foreach (var key in queryString.AllKeys)
{
routeData.Values[key] = queryString[key];
}
}
}
public RouteData CreateRouteData(RouteBase route)
{
return new RouteData(route, new MvcRouteHandler());
}
public VirtualPathData CreateVirtualPathData(RouteBase route, string virtualPath)
{
return new VirtualPathData(route, virtualPath);
}
#endregion
}
I use caching to load all of the URLs into a data structure (my final application will probably use file caching), so the database is not hit for every URL lookup.
MvcSiteMapProvider is also set up to use multiple paths to a single page by creating multiple nodes to the page (one for each unique URL). You can fix the SEO aspect of using multiple URLs for the same content by implementing the canonical tag using the CanonicalUrl or CanonicalKey properties. See this article for a complete example.
You can also drive MvcSiteMapProvider nodes from a database by implementing IDynamicNodeProvider or ISiteMapNodeProvider.
Do note that the URL matching in MvcSiteMapProvider is case sensitive. It would be best if you ensure your incoming URLs are always lowercase by doing a 301 redirect.