[.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

 

posted @ 2020-10-31 00:49  李能能  阅读(752)  评论(0编辑  收藏  举报