• [.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(@"OryxWebShared");
     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(@"OryxWebShared");
     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(@"OryxWebShared");
    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

  • 相关阅读:
    Django:同一个app支持多个数据库
    Django部署以及整合celery
    软件破解码等
    操作日志的设计小结by大熊
    用户-权限-组织-三员分立
    mysql调优
    笔试面试中常见的位运算用法
    Ubuntu系统下搭建Java平台
    All about Oracle Character Set
    各位技术大牛们的逆袭集锦!屌丝们都看过来!
  • 原文地址:https://www.cnblogs.com/blackcatpolice/p/13903299.html
Copyright © 2020-2023  润新知