原文:Dynamic controller routing in ASP.NET Core 3.0
做者:Filip W
译文:http://www.javashuo.com/article/p-celdwqng-dn.html
译者:Lamond Luhtml
今天在网上看到了这篇关于ASP.NET Core动态路由的文章,感受蛮有意思的,给你们翻译一下,虽然文中的例子不必定会在平常编码中出现,可是也给咱们提供了必定的思路。git
相对于ASP.NET MVC以及ASP.NET Core MVC中的旧版本路由特性, 在ASP.NET Core 3.0中新增了一个不错的扩展点,即程序获取到路由后,能够将其动态指向一个给定的controller/action.github
这个功能有很是多的使用场景。若是你正在使用从ASP.NET Core 3.0 Preview 7及更高版本,你就能够在ASP.NET Core 3.0中使用它了。web
PS: 官方没有在Release Notes中提到这一点。数据库
下面就让咱们一块儿来看一看ASP.NET Core 3.0中的动态路由。c#
当咱们使用MVC路由的时候,最典型的用法是,咱们使用路由特性(Route Attributes)来定义路由信息。使用这种方法,咱们须要要为每一个路由进行显式的声明。app
public class HomeController : Controller { [Route("")] [Route("Home")] [Route("Home/Index")] public IActionResult Index() { return View(); } }
相对的,你可使用中心化的路由模型,使用这种方式,你就不须要显式的声明每个路由 - 这些路由会自动被全部发现的控制器的自动识别。 然而,这样作的前提是,全部的控制器首先必须存在。async
如下是ASP.NET Core 3.0中使用新语法Endpoint Routing的实现方式。ide
app.UseEndpoints( endpoints => { endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); } );
以上两种方式的共同点是,全部的路由信息都必须在应用程序启动时加载。ui
可是,若是你但愿可以动态定义路由, 并在应用程序运行时添加/删除它们,该怎么办?
下面我给你们列举几个动态定义路由的使用场景。
这个问题的处理过程应该至关的好理解。咱们但愿尽早的拦截路由处理,检查已为其解析的当前路由值,并使用例如数据库中的数据将它们“转换”为一组新的路由值,这些新的路由值指向了一个实际存在的控制器。
在旧版本的ASP.NET Core MVC中, 咱们一般经过自定义IRouter
接口,来解决这个问题。然而在ASP.NET Core 3.0中这种方式已经行不通了,由于路由已经改由上面提到的Endpoint Routing来处理。值得庆幸的是,ASP.NET Core 3.0 Preview 7以及后续版本中,咱们能够经过一个新特性MapDynamicControllRoute
以及一个扩展点DynamicRouteValueTransformer
, 来支持咱们的需求。下面让咱们看一个具体的例子。
想象一下,在你的项目中,有一个OrderController
控制器,而后你但愿它支持多语言翻译路由。
public class OrdersController : Controller { public IActionResult List() { return View(); } }
咱们可能但愿的请求的URL是这样的,例如
那么咱们如今该如何解决这个问题呢?咱们可使用新特性MapDynamicControllerRoute
来替代默认的MVC路由, 并将其指向咱们自定义的DynamicRouteValueTransformer
类, 该类实现了咱们以前提到的路由值转换 。
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Latest); services.AddSingleton<TranslationTransformer>(); services.AddSingleton<TranslationDatabase>(); } public void Configure(IApplicationBuilder app) { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapDynamicControllerRoute<TranslationTransformer>("{language}/{controller}/{action}"); }); } }
这里咱们定义了一个TranslationTransformer
类,它继承了DynamicRouteValueTransformer
类。这个新类将负责将特定语言路由值,转换为能够在咱们应用能够匹配到controller/action的路由值字典,而这些值一般不能直接和咱们应用中的任何controller/action匹配。因此这里简单点说,就是在德语场景下,controller名会从“Bestellungen”转换成"Orders", action名"Liste"转换成"List"。
TranslationTransformer
类被做为泛型类型参数,传入MapDynamicControllerRoute
方法中,它必须在依赖注入容器中注册。这里,咱们还须要注册一个TranslationDatabase
类,可是这个类仅仅为了帮助演示,后面咱们会须要它。
public class TranslationTransformer : DynamicRouteValueTransformer { private readonly TranslationDatabase _translationDatabase; public TranslationTransformer(TranslationDatabase translationDatabase) { _translationDatabase = translationDatabase; } public override async ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext , RouteValueDictionary values) { if (!values.ContainsKey("language") || !values.ContainsKey("controller") || !values.ContainsKey("action")) return values; var language = (string)values["language"]; var controller = await _translationDatabase.Resolve(language, (string)values["controller"]); if (controller == null) return values; values["controller"] = controller; var action = await _translationDatabase.Resolve(language, (string)values["action"]); if (action == null) return values; values["action"] = action; return values; } }
在这个转换器中,咱们须要尝试提取3个路由参数, language
, controller
,action
,而后咱们须要在模拟用的数据库类中,找到其对应的翻译。正如咱们以前提到的,你一般会但愿从数据库中查找对应的内容,由于使用这种方式,咱们能够在应用程序生命周期的任什么时候刻,动态的影响路由。为了说明这一点,咱们将使用TranslationDatabase
类来模拟数据库操做,这里你能够把它想象成一个真正的数据库仓储服务。
public class TranslationDatabase { private static Dictionary<string, Dictionary<string, string>> Translations = new Dictionary<string, Dictionary<string, string>> { { "en", new Dictionary<string, string> { { "orders", "orders" }, { "list", "list" } } }, { "de", new Dictionary<string, string> { { "bestellungen", "orders" }, { "liste", "list" } } }, { "pl", new Dictionary<string, string> { { "zamowienia", "order" }, { "lista", "list" } } }, }; public async Task<string> Resolve(string lang, string value) { var normalizedLang = lang.ToLowerInvariant(); var normalizedValue = value.ToLowerInvariant(); if (Translations.ContainsKey(normalizedLang) && Translations[normalizedLang] .ContainsKey(normalizedValue)) { return Translations[normalizedLang][normalizedValue]; } return null; } }
到目前为止,咱们已经很好的解决了这个问题。这里经过在MVC应用中启用这个设置,咱们就能够向咱们以前定义的3个路由发送请求了。
每一个请求都会命中OrderController
控制器和List
方法。当前你能够将这个方法进一步扩展到其余的控制器。但最重要的是,若是新增一种新语言或者新的路由别名映射到现有语言中的controller/actions,你是不须要作任何代码更改,甚至重启项目的。
请注意,在本文中,咱们只关注路由转换,这里仅仅是为了演示ASP.NET Core 3.0中的动态路由特性。若是你但愿在应用程序中实现本地化,你可能还须要阅读ASP.NET Core 3.0的本地化指南, 由于你能够须要根据语言的路由值设置正确的CurrentCulture
。
最后, 我还想再补充一点,在咱们以前的例子中,咱们在路由模板中显式的使用了{controller}
和{action}
占位符。这并非必须的,在其余场景中,你还可使用"catch-all"
路由通配符,并将其转换为controller/action路由值。
"catch-all"
路由通配符是CMS系统中的典型解决方案,你可使用它来处理不一样的动态“页面”路由。
它看起来可能相似:
endpoints.MapDynamicControllerRoute<PageTransformer>("pages/{**slug}");
而后,你须要将pages
以后的整个URL参数转换为现有可执行控制器的内容 - 一般URL/路由的映射是保存在数据库中的。
但愿你会发现这篇文章颇有用 - 全部的演示源代码均可以在Github上找到。