问题描述
我有一个产品类别表来表示分层类别结构,这是数据库中典型的Parent-Child
关系表.
I have a product category table to represent a hierarchical category structure, a typical Parent-Child
relationship table in the database.
以Guitar Center的数据为例进行填充:
Fill it with Guitar Center's data as an example:
如果将它们渲染到具有<ul>
和<li>
的页面:
If you render them to a page with <ul>
and <li>
:
蓝色文本是我要生成的URL.对于任何给定类别,链接均由其子弹和其父子弹组成.
Texts in blue are the URLs I would like to generate. For any given category, the link consists of its slug and its parents' slugs.
请注意,我列出的示例只有两个父子级别.从理论上讲,通过自我指称结构,任何孩子都可以有无限的父母.
- 如何设置路由模板来实现这一目标?
- 如果设置了路由模板来支持该模板,那么如何检索叶子类别?例如,我想从URL
categories/guitars/acoustic-guitars
中检索acoustic-guitars
作为叶子类别,并能够获取该acoustic-guitars
类别下的所有产品.注意:我不希望在URL上进行手动解析.理想情况下,如果潜在顾客类别是通过模型绑定来绑定的,那将是最好的.
- How to set up routing template to achieve that?
- If the routing template is set up to support that, how to retrieve the leaf category? For example, from the URL
categories/guitars/acoustic-guitars
, I would like to retrieveacoustic-guitars
as the leaf category, and able to get all products under thatacoustic-guitars
category. Note: I don't want manual parsing on the URL. Ideally it would be the best if the lead category is binded through model binding.
推荐答案
不能.但是您可以降低到一个较低的级别,并创建一个数据驱动的 IRouter
实现,用于CMS样式的路由管理.
You can't. But you can drop to a lower level and make a data-driven IRouter
implementation for CMS-style route management.
这里是跟踪和缓存主键到URL的1-1映射的示例.这是一个通用类,我已经测试了它是否可以用作主键int或Guid.
Here is an example that tracks and caches a 1-1 mapping of primary key to URL. It is a generic class and I have tested that it works whether the primary key is int or Guid.
有一个必须插入的可插入部分,ICachedRouteDataProvider
可以在其中实现对数据库的查询.您还需要提供控制器和操作,因此此路由足够通用,可以通过使用多个实例将多个数据库查询映射到多个操作方法.
There is a pluggable piece that must be injected, ICachedRouteDataProvider
where the query for the database can be implemented. You also need to supply the controller and action, so this route is generic enough to map multiple database queries to multiple action methods by using more than one instance.
public class CachedRoute<TPrimaryKey> : Microsoft.AspNetCore.Routing.IRouter
{
private readonly string _controller;
private readonly string _action;
private readonly ICachedRouteDataProvider<TPrimaryKey> _dataProvider;
private readonly IMemoryCache _cache;
private readonly IRouter _target;
private readonly string _cacheKey;
private object _lock = new object();
public CachedRoute(
string controller,
string action,
ICachedRouteDataProvider<TPrimaryKey> dataProvider,
IMemoryCache cache,
IRouter target)
{
if (string.IsNullOrWhiteSpace(controller))
throw new ArgumentNullException("controller");
if (string.IsNullOrWhiteSpace(action))
throw new ArgumentNullException("action");
if (dataProvider == null)
throw new ArgumentNullException("dataProvider");
if (cache == null)
throw new ArgumentNullException("cache");
if (target == null)
throw new ArgumentNullException("target");
_controller = controller;
_action = action;
_dataProvider = dataProvider;
_cache = cache;
_target = target;
// Set Defaults
CacheTimeoutInSeconds = 900;
_cacheKey = "__" + this.GetType().Name + "_GetPageList_" + _controller + "_" + _action;
}
public int CacheTimeoutInSeconds { get; set; }
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page id that matches.
//If this returns false, that means the URI did not match
if (!GetPageList(context.HttpContext).TryGetValue(requestPath, out TPrimaryKey id))
{
return;
}
//Invoke MVC controller/action
var routeData = context.RouteData;
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = _controller;
routeData.Values["action"] = _action;
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = id;
await _target.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
if (TryFindMatch(GetPageList(context.HttpContext), context.Values, out string virtualPath))
{
result = new VirtualPathData(this, virtualPath);
}
return result;
}
private bool TryFindMatch(IDictionary<string, TPrimaryKey> pages, IDictionary<string, object> values, out string virtualPath)
{
virtualPath = string.Empty;
TPrimaryKey id;
if (!values.TryGetValue("id", out object idObj))
{
return false;
}
id = SafeConvert<TPrimaryKey>(idObj);
values.TryGetValue("controller", out object controller);
values.TryGetValue("action", out object action);
// The logic here should be the inverse of the logic in
// RouteAsync(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action.Equals(_action) && controller.Equals(_controller))
{
// The 'OrDefault' case returns the default value of the type you're
// iterating over. For value types, it will be a new instance of that type.
// Since KeyValuePair<TKey, TValue> is a value type (i.e. a struct),
// the 'OrDefault' case will not result in a null-reference exception.
// Since TKey here is string, the .Key of that new instance will be null.
virtualPath = pages.FirstOrDefault(x => x.Value.Equals(id)).Key;
if (!string.IsNullOrEmpty(virtualPath))
{
return true;
}
}
return false;
}
private IDictionary<string, TPrimaryKey> GetPageList(HttpContext context)
{
if (!_cache.TryGetValue(_cacheKey, out IDictionary<string, TPrimaryKey> pages))
{
// Only allow one thread to poplate the data
lock (_lock)
{
if (!_cache.TryGetValue(_cacheKey, out pages))
{
pages = _dataProvider.GetPageToIdMap(context.RequestServices);
_cache.Set(_cacheKey, pages,
new MemoryCacheEntryOptions()
{
Priority = CacheItemPriority.NeverRemove,
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds)
});
}
}
}
return pages;
}
private static T SafeConvert<T>(object obj)
{
if (typeof(T).Equals(typeof(Guid)))
{
if (obj.GetType() == typeof(string))
{
return (T)(object)new Guid(obj.ToString());
}
return (T)(object)Guid.Empty;
}
return (T)Convert.ChangeType(obj, typeof(T));
}
}
CategoryCachedRouteDataProvider
在这里,我们从数据库中查找类别,然后将这些标签递归地连接到URL中.
CategoryCachedRouteDataProvider
Here we lookup the categories from the database and recursively join the slugs into a URL.
public interface ICachedRouteDataProvider<TPrimaryKey>
{
IDictionary<string, TPrimaryKey> GetPageToIdMap(IServiceProvider serviceProvider);
}
public class CategoryCachedRouteDataProvider : ICachedRouteDataProvider<int>
{
// NOTE: I wasn't able to figure out how to constructor inject ApplicationDbContext
// because there doesn't seem to be a way to access the scoped services there,
// so we are using a service locator here. If someone could let me know how
// that is done in Startup.Configure() of .NET Core 2.0, please leave a comment.
public IDictionary<string, int> GetPageToIdMap(IServiceProvider serviceProvider)
{
using (var dbContext = serviceProvider.GetRequiredService<ApplicationDbContext>())
{
// Query the categories so we can build all of the URLs client side
var categories = dbContext.Categories.ToList();
var scratch = new StringBuilder();
return (from category in categories
select new KeyValuePair<string, int>(
GetUrl(category, categories, scratch),
category.CategoryId)
).ToDictionary(pair => pair.Key, pair => pair.Value);
}
}
private string GetUrl(Category category, IEnumerable<Category> categories, StringBuilder result)
{
result.Clear().Append(category.Slug);
while ((category = categories.FirstOrDefault(c => c.CategoryId == category.ParentCategoryId)) != null)
{
result.Insert(0, string.Concat(category.Slug, "/"));
}
return result.ToString();
}
}
CategoryController
除了在这一点上我们根本不需要处理URL或代码之外,控制器中没有什么特别的事情.我们只接受映射到记录主键的id
参数,然后您就可以从那里知道要做什么...
CategoryController
There is nothing special going on in the controller, except for the fact that we don't need to deal with a URL or a slug at this point at all. We simply accept the id
parameter that maps to the primary key of the record and then you know what to do from there...
public class CategoryController : Controller
{
public IActionResult Index(int id)
{
// Lookup category based on id...
return View();
}
}
用法
我们在Startup.cs
中对其进行如下配置:
Usage
We configure this in Startup.cs
as follows:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMvc();
services.AddSingleton<CategoryCachedRouteDataProvider>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.Routes.Add(
new CachedRoute<int>(
controller: "Category",
action: "Index",
dataProvider: app.ApplicationServices.GetRequiredService<CategoryCachedRouteDataProvider>(),
cache: app.ApplicationServices.GetRequiredService<IMemoryCache>(),
target: routes.DefaultHandler)
{
CacheTimeoutInSeconds = 900
});
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
请注意,可以重复使用CachedRoute<TPrimaryKey>
为其他表创建其他路由.因此,如果您愿意,还可以通过在Category表上使用联接并使用类似的方法来构建产品URL,例如guitars/acoustic-guitars/some-fancy-acoustic-guitar
.
Note that the CachedRoute<TPrimaryKey>
can be reused to create additional routes for other tables. So, if you wanted to, you could also make your product URLs like guitars/acoustic-guitars/some-fancy-acoustic-guitar
by using a join on the Category table and using a similar method to build the URL.
可以使用标签帮助程序或其他任何基于UrlHelper
的方法.例如:
The URLs can be generated and added to the UI using Tag Helpers or any of the other UrlHelper
based methods. For example:
<a asp-area="" asp-controller="Category" asp-action="Index" asp-route-id="12">Effects</a>
生成为
<a href="/amps-and-effects/effects">Effects</a>
您当然可以使用模型的主键来生成链接的URL和文本-使用具有主键和名称的模型,这是自动且直接的.
You can of course then use the primary key of the model to generate the URL and the text for the link - it is all automatic and straightforward using models that have the primary key and name.
您唯一需要做的就是为链接显示创建层次结构.但这超出了路由的范围.
The only thing extra you would need to do is to create the hierarchy for the link display. But that is outside of the scope of routing.
请注意,路由中根本没有层次结构的概念-它只是在每个请求上从上到下匹配的路由列表.
Note that there is no concept of hierarchy at all in routing - it is simply a list of routes that is matched from top to bottom on each request.
目前尚不清楚为什么需要叶子类别",因为这与入站或出站路由无关,也不需要查找数据库数据.同样,主键是根据路由生成整个URL所需要的全部,并且它应该是查看所有产品所需的全部.但是,如果您确实需要访问它,则可以在控制器中查找它.
It is unclear why you would need a "leaf category" as this has nothing to do with incoming or outgoing routes, nor is it required to lookup the database data. Again, the primary key is all you need to generate the entire URL based on routing and it should be all that is required to view all of the products. But if you really need access to it you can look it up in your controller.
根据您的特定要求,您可能需要更改缓存策略.
You may need to change the caching strategy depending on your specific requirements.
- 您可能希望将LRU缓存策略与RAM中固定的最大链接数一起使用
- 您可能希望跟踪URL被点击的频率,并将访问频率最高的URL移至列表顶部
- 您可能希望在路由和更新操作方法之间共享高速缓存,因此,在数据库中成功更新URL时,也会同时在高速缓存中为实时" URL更新它们.
- 您可能希望分别缓存每个URL并一次查找一个URL,而不是一次缓存整个列表.
这篇关于MVC路由模板表示无限的自引用层次结构类别结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!