[.Net Core] 基于.netcore middleware 机制, 实现模块化开发框架
一. 背景:
在开发过程中,我们有时候想要独立出一块业务模块,进行模块化开发。但是微软自带的dotnet core mvc 框架秉承了传统 dotnet framework mvc 框架,再UI 层面层面,难以进行业务区分,这就回导致整个项目想当庞大。在这种背景下,我开始思考,能不能使用模块化开发方案进,从而降低单个项目的业务复杂度。
二. 搜集:
在以往的开发过程中,我所了解到的模块化开发框架有 微软的 Orchard CMS 框架,最近这款框架也支持了.net core 版本。以及业界鼎鼎大名的Abp .net 框架,这款框架现在也支持了.net core 版本。
Orchard CMS
优势:Orchard CMS 功能完善,支持热插拔
劣势:学习成本较高,难以将单独的模块式开发功能拿到自己的项目中来。
Abp .Net
优势:功能完善,原生支持模块化开发
劣势:引入模块化开发需要引入成套的Apb .Net 框架,难以与现有项目进行结合。
通过搜集现有的框架,发现已有的框架引太过庞大,难以直接应用到现有项目中。在这个基础上,我们开始思考能不能通过dotnet core 现有的middleware功能来实现以个模块化开发框架。
三. 思路
要自定义Request Middleware 首先要处理的就是路由问题。我们的方法是,单独维护一个Module 内部的路由表,然后在Module 定义时将路由表保存起来,然后当请求到达时去查询路由表,然后执行对应的方法。
注:值得注意的是,我们这里的路由表支持参数路由。
1. 路由表
路由表的设计需要注意以下几点:
a) 路由的添加
b) 路由表通过依赖注入设定为单例模式
c) 路由的解析,需要区别 get post , ... 等不同类型,需要支持参数表达式 /path/{id} 这样的方式
在这里的路由表设计,以及定义路由表的方式参考了nodejs express 的写法,在定Module方法的时候同时将路由添加到路由表:
a) 路由的添加:
首先我们要解决的是路由添加的问题,我参考了nodejs express的定义方法通过以下代码完成路由的添加:
public class InstanceBDModule : OryxWebModule { public InstanceBDModule() { Get("/user/info", async ctx => { }); } }
注:Get 在这里为快捷方法,Get方法的含义是 通过get 请求 /user/info 这个路由并执行,在内部通过处理将路由值处理为 /user/info_get 并保存在路由表,同时还要将Get请求的处理方法委托进行保存。通过这种方式我们可以处理get,post ,websocket 等类型的请求。
1 public void Request(string route, string method, Func<OryxWebContext, Task> func) 2 { 3 RouteTable.Add(route + "_" + method, new RouteTableItem 4 { 5 Func = func, 6 Path = route, 7 Method = method 8 }); 9 }
b) 路由表类的定义:
1 public class RouteTable:Dictionary<string, RouteTableItem> 2 { 3 4 }
c) 路由的解析:
1 public static IApplicationBuilder UseOryxWeb(this IApplicationBuilder applicationBuilder) 2 { 3 4 //通过applicationBuilder.Use 完成中间件处理 5 return applicationBuilder.Use(async (ctx, next) => 6 { 7 var routeTable = ctx.RequestServices.GetService<RouteTable>(); 8 9 var serverRouteTable = ctx;10 //匹配路由表中的值,支持参数匹配 11 var targetRoute = matchRoute(ctx, routeTable); 12 //如果为websocket 请求,则单独处理websocket 13 if (targetRoute.Method == "ws" && ctx.WebSockets.IsWebSocketRequest) 14 { 15 //非关键代码,在此注释 16 } 17 18 if (targetRoute != null) 19 { 20 var routeTableItem = targetRoute; 21 var func = routeTableItem.Func; 22 if (func != null && ctx.Request.Method.ToLower() == routeTableItem.Method) 23 { 24 //包装HttpContext , 然后交由路由表中保存的委托方法处理 25 var webContext = new OryxWebContext(ctx); 26 webContext.RouteValue(targetRoute.RouteValue); 27 using (webContext.HttpContext.RequestServices.CreateScope()) 28 { 29 webContext.Body = await webContext.HttpContext.Request.Body.GetString(); 30 if (webContext.Body.IsTrue()) 31 { 32 webContext.JsonObj = JObject.Parse(webContext.Body); 33 } 34 await func(webContext); 35 } 36 } 37 else 38 { 39 await next(); 40 } 41 } 42 else 43 { 44 await next(); 45 } 46 }); 47 }
至此,我们完成了路由的请求、解析并处理的过程。处理过程中我们用到了MatchRoute 方法来解析路由模板中的参数,这个方法采用了 Microsoft.AspNetCore.Routing.Template.TemplateParser.Parse 的方法来解析路由模板,通过TemplateMatcher 来获取模板路由中的值,对此方法的使用我会在另一片文章中单独介绍。具体的使用方法,大家可以查看完整的源代码来学习。
2. 处理器
完成了路由表的设计,还要有配套的处理器设计(类似mvc中的action 的设计)。
Request请求:在以往的开发过程中,我很羡慕其他阵营中框架帮忙处理好请求内容,我们直接使用即可。但是dotnet core mvc在处理的时候经常会遇到一些莫名其妙的问题,主要是由于前段content-type配置与后端 不匹配导致了发送过来的body没有数据,调试起来很头疼。所以在设计处理器框架这部分,我直接将请求的数据进行预处理,将常见的一些数据 例如json, querystring 进行处理,然后将数据通过内置的body方法传递给处理器。
Response相应:dotnetcore mvc 内置了Razor engine ,他本身很不错,但是cshtml 是预编译的,而我想使用的是动态处理的模板引擎,这样可以实时修改模板内容,实时更改页面。尝试了很多方式,razor engine 单独使用并不理想,所以在此框架内部我选择使用了scriban 模板引擎(dotnet liquid 也是一款非常不错的模板引擎,并且由于使用广泛,还可以兼容nodejs php使用同一种模板语法的模板引擎 )
在这里我们最终将Request和Response包装成OryxWebContext,代码如下:
Request 包含: get post websocket 处理
Reponse包含: Ajax WriteString RenderTemplate Send(websocket)方法
1 public class OryxWebContext 2 { 3 public HttpContext HttpContext { get; } 4 5 public WebSocket WebSocket { get; set; } 6 7 public Dictionary<string, string> ParamDictionary { get; } 8 9 public string Body { get; set; } 10 11 public dynamic JsonObj { get; set; } 12 13 public T Json<T>() 14 { 15 var setting = new JsonSerializerSettings(); 16 return JsonConvert.DeserializeObject<T>(Body, setting); 17 } 18 19 public async Task Send(string content) 20 { 21 var buffer = new byte[1024 * 4]; 22 var arrByte = new ArraySegment<byte>(Encoding.UTF8.GetBytes(content)); 23 await WebSocket.SendAsync(arrByte, WebSocketMessageType.Text, true, CancellationToken.None); 24 } 25 26 public Stream Stream { get; set; } 27 28 public OryxWebContext(HttpContext httpContext) 29 { 30 HttpContext = httpContext; 31 ParamDictionary = new Dictionary<string, string>(); 32 HttpContext.Request.Query.ToList().ForEach(item => 33 { 34 ParamDictionary.Add(item.Key, item.Value); 35 }); 36 } 37 38 public async Task Write(string content) 39 { 40 await HttpContext.Response.WriteAsync(content); 41 } 42 public async Task Write(Stream stream) 43 { 44 HttpContext.Response.ContentType = "application/octet-stream"; 45 await stream.CopyToAsync(HttpContext.Response.Body); 46 } 47 48 public async Task Ajax(object jsonObj) 49 { 50 HttpContext.Response.ContentType = "application/json"; 51 var jsonSetting = new JsonSerializerSettings(); 52 jsonSetting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; 53 var jsonStr = JsonConvert.SerializeObject(jsonObj, jsonSetting); 54 await HttpContext.Response.WriteAsync(jsonStr); 55 } 56 57 public async Task Render(string tempPath, object dataObj) 58 { 59 var tmpStr = ReadTemplateStr(tempPath); 60 61 TemplateContext ctx = new TemplateContext(); 62 ctx.TemplateLoader = new OryxTemplateLoader(@"\OryxWeb\Shared"); 63 64 var scriptObject = new ScriptObject(); 65 scriptObject.Import(dataObj); 66 ctx.PushGlobal(scriptObject); 67 68 var template = Template.Parse(tmpStr); 69 var result = await template.RenderAsync(ctx); 70 HttpContext.Response.ContentType = "text/html"; 71 await HttpContext.Response.WriteAsync(result); 72 } 73 74 public async Task Render(string tempPath) 75 { 76 var tmpStr = ReadTemplateStr(tempPath); 77 TemplateContext ctx = new TemplateContext(); 78 ctx.TemplateLoader = new OryxTemplateLoader(@"OryxWeb\Shared"); 79 80 var baseDir = AppContext.BaseDirectory; 81 var dir = baseDir + Path.GetDirectoryName(tempPath); 82 83 //var ll = ctx.TemplateLoader.GetPath(ctx, callerSpan, "index.html"); 84 85 var template = Template.Parse(tmpStr, dir, 86 lexerOptions: new LexerOptions() 87 { 88 EnableIncludeImplicitString = true 89 , 90 Mode = ScriptMode.Default 91 }); 92 93 var result = await template.RenderAsync(ctx); 94 //var eva = Template.Evaluate(tmpStr, ctx); 95 96 HttpContext.Response.ContentType = "text/html"; 97 await HttpContext.Response.WriteAsync(result); 98 } 99 100 public async Task RenderWithLayout(string tmepPath, string LayoutPath) 101 { 102 TemplateContext ctxsub = new TemplateContext(); 103 //获得子页面 104 var subTemplate = GetTemplate(tmepPath, ctxsub); 105 var tempResult = subTemplate.RenderAsync(); 106 107 //将父页面作为参数传入Layout 108 TemplateContext ctxLayout = new TemplateContext(); 109 var scriptObject = new ScriptObject(); 110 scriptObject.Import(new { renderbody = tempResult }); 111 ctxLayout.PushGlobal(scriptObject); 112 var layoutTemplate = GetTemplate(LayoutPath, ctxLayout); 113 var result = await layoutTemplate.RenderAsync(ctxLayout); 114 115 HttpContext.Response.ContentType = "text/html"; 116 await HttpContext.Response.WriteAsync(result); 117 } 118 119 public Template GetTemplate(string tempPath, TemplateContext ctx) 120 { 121 var tmpStr = ReadTemplateStr(tempPath); 122 ctx.TemplateLoader = new OryxTemplateLoader(@"OryxWeb\Shared"); 123 124 var callerSpan = new SourceSpan(); 125 var baseDir = AppContext.BaseDirectory; 126 var dir = baseDir + Path.GetDirectoryName(tempPath); 127 128 callerSpan.FileName = dir + "\\index.html"; 129 130 //var ll = ctx.TemplateLoader.GetPath(ctx, callerSpan, "index.html"); 131 132 var template = Template.Parse(tmpStr, dir, 133 lexerOptions: new LexerOptions() 134 { 135 EnableIncludeImplicitString = true 136 , 137 Mode = ScriptMode.Default 138 }); 139 return template; 140 } 141 142 private string ReadTemplateStr(string path) 143 { 144 var absolutePath = MapPath(path); 145 return File.ReadAllText(absolutePath); 146 } 147 148 private string MapPath(string path) 149 { 150 return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path.TrimStart('/').Replace("/", "\\")); 151 } 152 153 public void RouteValue(RouteValueDictionary routeValue) 154 { 155 if (routeValue != null) 156 { 157 routeValue.ToList().ForEach(item => 158 { 159 ParamDictionary.Add(item.Key, item.Value.ToString()); 160 }); 161 } 162 } 163 164 public string this[string key] 165 { 166 get 167 { 168 if (!ParamDictionary.ContainsKey(key)) 169 { 170 return string.Empty; 171 } 172 return ParamDictionary[key]; 173 } 174 } 175 176 public T Service<T>() 177 { 178 return HttpContext.RequestServices.GetService<T>(); 179 } 180 }
3. Startup
dotnet core startup 类主要有两个入口,一个用来管理类依赖注入的ConfigureServices ,一个用来配置请求Middleware 的Configure 方法。我们主要通过自定义处理 Request Middleare 来完成我们的请求处理,同时通过依赖注入将宿主的类实例引入到我们的模块中使用,Startup.cs :
1 public class Startup 2 { 3 public Startup(IConfiguration configuration) 4 { 5 Configuration = configuration; 6 } 7 8 public IConfiguration Configuration { get; } 9 10 // This method gets called by the runtime. Use this method to add services to the container. 11 public void ConfigureServices(IServiceCollection services) 12 { 13 services.AddOryxWeb<SNSModule>(); 14 } 15 16 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) 18 { 19 app.UseOryxWeb(); 20 } 21 }
完整的代码地址:https://github.com/OryxLib/Oryx.FastAdmin/tree/master/Libs/Oryx.Web.Core