我正在创建一个ASP.net MVC / Entity Framework购物车,以便更熟悉该技术。我想要的功能之一是基于关闭独特的slugs而不是将实体ID嵌入到URL中的URL。一些例子:
slugs在所有内容类型中都是唯一的,但是例如,test-tshirt可以出现在多个类别中:
我创建了一个自定义路由,它接受路径中的最后一个slug并使用它来查找当前页面。
public class SlugRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
string path = HttpContext.Current.Request.Path.TrimStart('/').TrimEnd('/');
if (string.IsNullOrEmpty(path))
path = "home";
string[] slugs = path.Split('/');
string slug = slugs[slugs.Length - 1];
CatalogPage page = Token.Instance.DB.Pages.SingleOrDefault(p => p.UrlSlug == slug);
if (page != null)
{
// Cache current page in context
HttpContext.Current.Items["CurrentPage"] = page;
// Set up route data
RouteData data = new RouteData(this, new MvcRouteHandler());
data.Values["action"] = "Index";
data.Values["id"] = page.Id;
data.DataTokens.Add("namespaces", new string[] { "MyProject.Presentation.Controllers" });
// Set controller value if specified in db, or set based on entity type
if (!string.IsNullOrEmpty(page.Controller))
data.Values["controller"] = page.Controller;
else if (page.GetUnproxiedType() == typeof(CategoryPage))
data.Values["controller"] = "Category";
else if (page.GetUnproxiedType() == typeof(ProductPage))
data.Values["controller"] = "Product";
else
data.Values["controller"] = "Content";
return data;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
这很好用,我在实现自定义逻辑和显示模板方面有很大的灵活性(内容类型也有“View”属性,所以我可以动态设置控制器中的视图。)
然而,在实现面包屑方面,我偶然发现了一点。快速而肮脏的方法是使用URL中的路径,对路径中的每个slug进行查询,并忽略页面是否实际上是该类别的子节点。另一个解决方案是使用类似MvcSiteMapProvider的东西并在后端添加内容时构建XML树...我不确定这个特定的实现是如何工作的,因为它似乎非常关注标准{controller} / {action} / {id}路由模式。
您使用过或看过哪些其他类型的实现?
答案 0 :(得分:1)
MvcSiteMapProvider v4也可以通过设置Url属性而不是使用{controller} / {action} / {id}来处理URL。这正是我使用它(数据库驱动的URL /自定义RoutBase派生路由)的场景,它运行良好。但是,您也应该在路由中实现反向URL查找,否则您的URL解析将无效。
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
}
我使用缓存将所有URL加载到数据结构中(我的最终应用程序可能会使用文件缓存),因此每次URL查找都不会触发数据库。
MvcSiteMapProvider也设置为通过为页面创建多个节点(每个唯一URL一个节点)来使用multiple paths to a single page。通过使用CanonicalUrl或CanonicalKey属性实现规范标记,您可以修复为同一内容使用多个URL的SEO方面。有关完整示例,请参阅this article。
您还可以通过实施IDynamicNodeProvider或ISiteMapNodeProvider从数据库中驱动MvcSiteMapProvider节点。
请注意,MvcSiteMapProvider中的URL匹配区分大小写。最好通过执行301重定向确保传入的URL总是小写。