作者: Filip W http://twitter.com/filip_woj
译文: https://www.cnblogs.com/lwqlun/p/11461657.html
译者: Lamond Lu
译者注
今天在网上看到了这篇关于 ASP.NET Core 动态路由的文章, 感觉蛮有意思的, 给大家翻译一下, 虽然文中的例子不一定会在日常编码中出现, 但是也给我们提供了一定的思路.
前言
相对于 ASP.NET MVC 以及 ASP.NET Core MVC 中的旧版本路由特性, 在 ASP.NET Core 3.0 中新增了一个不错的扩展点, 即程序获取到路由后, 可以将其动态指向一个给定的 controller/action.
这个功能有非常多的使用场景. 如果你正在使用从 ASP.NET Core 3.0 Preview 7 及更高版本, 你就可以在 ASP.NET Core 3.0 中使用它了.
PS: 官方没有在 Release Notes 中提到这一点.
下面就让我们一起来看一看 ASP.NET Core 3.0 中的动态路由.
背景
当我们使用 MVC 路由的时候, 最典型的用法是, 我们使用路由特性 (Route Attributes) 来定义路由信息. 使用这种方法, 我们需要要为每个路由进行显式的声明.
- public class HomeController : Controller
- {
- [Route("")]
- [Route("Home")]
- [Route("Home/Index")]
- public IActionResult Index()
- {
- return View();
- }
- }
相对的, 你可以使用中心化的路由模型, 使用这种方式, 你就不需要显式的声明每一个路由 - 这些路由会自动被所有发现的控制器的自动识别. 然而, 这样做的前提是, 所有的控制器首先必须存在.
以下是 ASP.NET Core 3.0 中使用新语法 Endpoint Routing 的实现方式.
- App.UseEndpoints(
- endpoints =>
- {
- endpoints.MapControllerRoute("default",
- "{controller=Home}/{action=Index}/{id?}");
- }
- );
以上两种方式的共同点是, 所有的路由信息都必须在应用程序启动时加载.
但是, 如果你希望能够动态定义路由, 并在应用程序运行时添加 / 删除它们, 该怎么办?
下面我给大家列举几个动态定义路由的使用场景.
多语言路由, 以及使用新语言时, 针对那些新语言路由的修改.
在 CMS 类型的系统中, 我们可能会动态添加一些新页面, 这些新页面不需要创建的控制器或者在源码中硬编码路由信息.
多租户应用中, 租户路由可以在运行时动态激活或者取消激活.
这个问题的处理过程应该相当的好理解. 我们希望尽早的拦截路由处理, 检查已为其解析的当前路由值, 并使用例如数据库中的数据将它们 "转换" 为一组新的路由值, 这些新的路由值指向了一个实际存在的控制器.
实例问题 - 多语言翻译路由问题
在旧版本的 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 是这样的, 例如
英语 - /en/orders/list
德语 - /de/bestellungen/liste
波兰语 - /pl/zamowienia/lista
使用动态路由处理多语言翻译路由问题
那么我们现在该如何解决这个问题呢? 我们可以使用新特性 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 个路由发送请求了.
英语 - /en/orders/list
德语 - /de/bestellungen/liste
波兰语 - /pl/zamowienia/lista
每个请求都会命中 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 上找到.
来源: https://www.cnblogs.com/lwqlun/p/11461657.html