Linux(宝塔)部署.Net Core完整记录 - 果冻栋吖 - 博客园

mikel阅读(812)

来源: Linux(宝塔)部署.Net Core完整记录 – 果冻栋吖 – 博客园

前言#

最近在V站上看到一个外卖推广的小程序,意思大概是类似淘宝联盟那种,别人走自己的链接后,自己可以抽取大概4%-6%的提成。觉得还蛮有意思的,一开始开源的是静态页面写死的,所以我这边用.Net Core写了个简单的后台。

左边是无后台的,右边红色框是后台配置的。当然功能是很简单的,主要是记录发布到Ubuntu18.4的时候遇到的问题与解决办法。

· · ·

安装宝塔#

宝塔Linux面板是提升运维效率的服务器管理软件,支持一键LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA等100多项服务器管理功能。

这里节省时间直接使用宝塔面板了,这个真的是太方便了,哈哈。安装也非常简单。

因为我使用的是Ubuntu,安装脚本

wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh

其他版本请参考官方文档:https://www.bt.cn/download/linux.html

安装完成后会显示登录地址、用户名、密码信息。登录后浏览器将弹出推荐安装套件,为方便直接一键安装LNMP。

安装.NetCore SDK 3.1#

微软官方文档:https://docs.microsoft.com/zh-cn/dotnet/core/install/linux-ubuntu

因为我使用的18.04,所以找到对应文档。

使用 APT 进行安装可通过几个命令来完成。 安装 .NET 之前,请运行以下命令,将 Microsoft 包签名密钥添加到受信任密钥列表,并添加包存储库。

打开终端并运行以下命令:

Copy
wget https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb

安装 SDK#

.NET SDK 使你可以通过 .NET 开发应用。 如果安装 .NET SDK,则无需安装相应的运行时。 若要安装 .NET SDK,请运行以下命令:

Copy
sudo apt-get update; \
  sudo apt-get install -y apt-transport-https && \
  sudo apt-get update && \
  sudo apt-get install -y dotnet-sdk-3.1

安装运行时#

Copy
sudo apt-get update; \
  sudo apt-get install -y apt-transport-https && \
  sudo apt-get update && \
  sudo apt-get install -y aspnetcore-runtime-3.1

作为 ASP.NET Core 运行时的一种替代方法,你可以安装不包含 ASP.NET Core 支持的 .NET 运行时:将上一命令中的 aspnetcore-runtime-5.0 替换为 dotnet-runtime-5.0

Copy
sudo apt-get install -y dotnet-runtime-5.0

其实上述就是照搬微软的官方文档,官方文档还是写的很清楚的。

发布.NetCore项目#

我一开始目标运行时选择的Linux-64,但是出现了这样的错误`错误 NU1605: 检测到包降级: XXXXXXXXXXXXX 从 4.3.0 降级到 XXXXXXXXXXXXX。直接从项目引用包以选择不同版本。

image-20201207114647335

通过查看微软官方文档:https://docs.microsoft.com/zh-cn/nuget/reference/errors-and-warnings/nu1605

问题当在 .NET Core 3.0 或更高版本的项目中同时引用时,与 .NET Core 1.0 和1.1 随附的某些包组合不兼容。 问题包通常以或开头 System. Microsoft. ,并具有4.0.0 和4.3.1 之间的版本号。 在这种情况下,降级消息将具有从运行时开始的包。 依赖关系链。

解决方案若要解决此问题,请添加以下 PackageReference:

Copy
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.0.0" PrivateAssets="all" />

就是添加引用,但实际上你要保证所有项目的包引用版本是一致的。

另一种方法

发布的时候目标运行时直接选择可移植吧~

宝塔面板发布.Net Core项目,并启动项目#

在文件 wwwroot新建项目文件夹,将本地发布文件打包拷贝至服务器解压。

在服务器上终端命令进入部署文件所在目录,然后使用dotnet命令启动服务:

Copy
dotnet XXXXXX.Admin.dll --urls "http://localhost:5000"

image-20201207115503272

Nginx设置反代访问#

现在我们还不能直接访问到我们新部署项目,需要使用Nginx设置反向代理,将特定的端口代理到http://localhost:5000,这一步可以通过宝塔面板来完成,步骤如下:

  • 在宝塔面板上新建一个网站,设置为静态网站即可,并绑定好域名。
  • 在刚才新建的网站中设置反向代理,目标URL填写http://localhost:5000即可,发送域名localhost

image-20201207115748843

image-20201207115850382

浏览器正式可访问项目,此处可能需要重启一下。

使用 Supervisor 守护进程#

现在还有个问题,就是当我们关闭xShell等SSH工具的时候服务进程也会停止运行,我们可使用 Supervisor 守护进程运行。

  • 在宝塔面板上安装Supervisor
  • 添加守护进程(用户建议选择www,不要使用root)

image-20201207120537225

问题#

问题1#

错误 NU1605: 检测到包降级: XXXXXXXXXXXXX 从 4.3.0 降级到 XXXXXXXXXXXXX。直接从项目引用包以选择不同版本

这个问题一开始我按照官方文档修改了,实际还是不可以。所以我选择了可移植发布的。而我在写这篇文章的时候又可以了。

问题2#

验证码我使用了System.Drawing,不过在Linux下的话,这个是无法显示的。

解决办法

System.Drawing.Common 组件提供对GDI+图形功能的访问。它是依赖于GDI+的,那么在Linux上它如何使用GDI+,因为Linux上是没有GDI+的。Mono 团队使用C语言实现了GDI+接口,提供对非Windows系统的GDI+接口访问能力(个人认为是模拟GDI+,与系统图像接口对接),这个就是 libgdiplus。进而可以推测 System.Drawing.Common 这个组件实现时,对于非Windows系统肯定依赖了 ligdiplus 这个组件。如果我们当前系统不存在这个组件,那么自然会报错,找不到它,安装它即可解决。

Ubuntu一键命令

Copy
sudo curl https://raw.githubusercontent.com/stulzq/awesome-dotnetcore-image/master/install/ubuntu.sh|sh

参考:https://www.cnblogs.com/stulzq/p/10172550.html

问题3#

指定端口启动

修改Program.cs

增加代码

Copy
.ConfigureAppConfiguration(builder =>
                {
                    //dotnet test.dll --urls "http://*:5000;https://*:5001"
                    builder.AddCommandLine(args);//设置添加命令行
                })

完整代码

Copy
 public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                //将默认ServiceProviderFactory指定为AutofacServiceProviderFactory https://autofaccn.readthedocs.io/en/latest/integration/aspnetcore.html#asp-net-core-3-0-and-generic-hosting
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureAppConfiguration(builder =>
                {
                    //dotnet test.dll --urls "http://*:5200;https://*:5100"
                    builder.AddCommandLine(args);//设置添加命令行
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });

问题4#

验证码生成代码

验证码生成代码应该是蛮多的,我把我的分享下

Copy
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;

namespace XXX.Util
{
   public static class ValidateCodeHelper
   {
       /// <summary>
       /// 验证码的最大长度
       /// </summary>
       public static int MaxLength => 10;

       /// <summary>
       /// 验证码的最小长度
       /// </summary>
       public static int MinLength => 1;

       /// <summary>
       /// 生成验证码
       /// </summary>
       /// <param name="length">指定验证码的长度</param>
       /// <returns></returns>
       public static string CreateValidateCode(int length)
       {
           int[] randMembers = new int[length];
           int[] validateNums = new int[length];
           string validateNumberStr = "";
           //生成起始序列值
           int seekSeek = unchecked((int)DateTime.Now.Ticks);
           Random seekRand = new Random(seekSeek);
           int beginSeek = (int)seekRand.Next(0, Int32.MaxValue - length * 10000);
           int[] seeks = new int[length];
           for (int i = 0; i < length; i++)
           {
               beginSeek += 10000;
               seeks[i] = beginSeek;
           }
           //生成随机数字
           for (int i = 0; i < length; i++)
           {
               Random rand = new Random(seeks[i]);
               int pownum = 1 * (int)Math.Pow(10, length);
               randMembers[i] = rand.Next(pownum, Int32.MaxValue);
           }
           //抽取随机数字
           for (int i = 0; i < length; i++)
           {
               string numStr = randMembers[i].ToString();
               int numLength = numStr.Length;
               Random rand = new Random();
               int numPosition = rand.Next(0, numLength - 1);
               validateNums[i] = Int32.Parse(numStr.Substring(numPosition, 1));
           }
           //生成验证码
           for (int i = 0; i < length; i++)
           {
               validateNumberStr += validateNums[i].ToString();
           }
           return validateNumberStr;
       }
       /// <summary>
       /// 得到验证码图片的长度
       /// </summary>
       /// <param name="validateNumLength">验证码的长度</param>
       /// <returns></returns>
       public static int GetImageWidth(int validateNumLength)
       {
           return (int)(validateNumLength * 12.0);
       }
       /// <summary>
       /// 得到验证码的高度
       /// </summary>
       /// <returns></returns>
       public static double GetImageHeight()
       {
           return 22.5;
       }


       //C# MVC 升级版
       /// <summary>
       /// 创建验证码的图片
       /// </summary> 
       /// <param name="validateCode">验证码</param>
       public static byte[] CreateValidateGraphic(string validateCode)
       {
           Bitmap image = new Bitmap((int)Math.Ceiling(validateCode.Length * 12.0), 22);
           Graphics g = Graphics.FromImage(image);
           try
           {
               //生成随机生成器
               Random random = new Random();
               //清空图片背景色
               g.Clear(Color.White);
               //画图片的干扰线
               for (int i = 0; i < 25; i++)
               {
                   int x1 = random.Next(image.Width);
                   int x2 = random.Next(image.Width);
                   int y1 = random.Next(image.Height);
                   int y2 = random.Next(image.Height);
                   g.DrawLine(new Pen(Color.Silver), x1, y1, x2, y2);
               }
               Font font = new Font("Arial", 12, (FontStyle.Bold | FontStyle.Italic));
               LinearGradientBrush brush = new LinearGradientBrush(new Rectangle(0, 0, image.Width, image.Height),
                Color.Blue, Color.DarkRed, 1.2f, true);
               g.DrawString(validateCode, font, brush, 3, 2);
               //画图片的前景干扰点
               for (int i = 0; i < 100; i++)
               {
                   int x = random.Next(image.Width);
                   int y = random.Next(image.Height);
                   image.SetPixel(x, y, Color.FromArgb(random.Next()));
               }
               //画图片的边框线
               g.DrawRectangle(new Pen(Color.Silver), 0, 0, image.Width - 1, image.Height - 1);
               //保存图片数据
               MemoryStream stream = new MemoryStream();
               image.Save(stream, ImageFormat.Jpeg);
               //输出图片流
               return stream.ToArray();
           }
           finally
           {
               g.Dispose();
               image.Dispose();
           }
       }
   }
}

总结#

其实就是一篇流水账,记录了发布的过程和遇到的问题及解决办法。之前服务器一直是使用的WinServer,因为熟悉。勇于尝试并去解决问题,慢慢进步~

大学里也学过Linux,受不了。但是真的去使用了,去探索了,嗯,真香~

一个架构师的缓存修炼之路 - IT人的职场进阶 - 博客园

mikel阅读(638)

来源: 一个架构师的缓存修炼之路 – IT人的职场进阶 – 博客园

一位七牛的资深架构师曾经说过这样一句话:

Nginx+业务逻辑层+数据库+缓存层+消息队列,这种模型几乎能适配绝大部分的业务场景。

这么多年过去了,这句话或深或浅地影响了我的技术选择,以至于后来我花了很多时间去重点学习缓存相关的技术。

我在10年前开始使用缓存,从本地缓存、到分布式缓存、再到多级缓存,踩过很多坑。下面我结合自己使用缓存的历程,谈谈我对缓存的认识。

 

01 本地缓存

1. 页面级缓存

我使用缓存的时间很早,2010年左右使用过 OSCache,当时主要用在 JSP 页面中用于实现页面级缓存。伪代码类似这样:

<cache:cache key="foobar" scope="session">   
      some jsp content   
</cache:cache>`

中间的那段 JSP 代码将会以 key=”foobar” 缓存在 session 中,这样其他页面就能共享这段缓存内容。 在使用 JSP 这种远古技术的场景下,通过引入 OSCache 之后 ,页面的加载速度确实提升很快。

但随着前后端分离以及分布式缓存的兴起,服务端的页面级缓存已经很少使用了。但是在前端领域,页面级缓存仍然很流行。

 

2. 对象缓存

2011年左右,开源中国的红薯哥写了很多篇关于缓存的文章。他提到:开源中国每天百万的动态请求,只用 1 台 4 Core 8G 的服务器就扛住了,得益于缓存框架 Ehcache。

这让我非常神往,一个简单的框架竟能将单机性能做到如此这般,让我欲欲跃试。于是,我参考红薯哥的示例代码,在公司的余额提现服务上第一次使用了 Ehcache。

逻辑也很简单,就是将成功或者失败状态的订单缓存起来,这样下次查询的时候,不用再查询支付宝服务了。伪代码类似这样:

添加缓存之后,优化的效果很明显 , 任务耗时从原来的40分钟减少到了5~10分钟。

上面这个示例就是典型的「对象缓存」,它是本地缓存最常见的应用场景。相比页面缓存,它的粒度更细、更灵活,常用来缓存很少变化的数据,比如:全局配置、状态已完结的订单等,用于提升整体的查询速度。

 

3. 刷新策略

2018年,我和我的小伙伴自研了配置中心,为了让客户端以最快的速度读取配置, 本地缓存使用了 Guava,整体架构如下图所示:

那本地缓存是如何更新的呢?有两种机制:

  • 客户端启动定时任务,从配置中心拉取数据。
  • 当配置中心有数据变化时,主动推送给客户端。这里我并没有使用websocket,而是使用了 RocketMQ Remoting 通讯框架。

后来我阅读了 Soul 网关的源码,它的本地缓存更新机制如下图所示,共支持 3 种策略:

▍ zookeeper watch机制

soul-admin 在启动的时候,会将数据全量写入 zookeeper,后续数据发生变更时,会增量更新 zookeeper 的节点。与此同时,soul-web 会监听配置信息的节点,一旦有信息变更时,会更新本地缓存。

▍ websocket 机制

websocket 和 zookeeper 机制有点类似,当网关与 admin 首次建立好 websocket 连接时,admin 会推送一次全量数据,后续如果配置数据发生变更,则将增量数据通过 websocket 主动推送给 soul-web。

▍ http 长轮询机制

http请求到达服务端后,并不是马上响应,而是利用 Servlet 3.0 的异步机制响应数据。当配置发生变化时,服务端会挨个移除队列中的长轮询请求,告知是哪个 Group 的数据发生了变更,网关收到响应后,再次请求该 Group 的配置数据。

不知道大家发现了没?

  • pull 模式必不可少
  • 增量推送大同小异

长轮询是一个有意思的话题 , 这种模式在 RocketMQ 的消费者模型也同样被使用,接近准实时,并且可以减少服务端的压力。

 

02 分布式缓存

关于分布式缓存, memcached 和 Redis 应该是最常用的技术选型。相信程序员朋友都非常熟悉了,我这里分享两个案例。

1. 合理控制对象大小及读取策略

2013年,我服务一家彩票公司,我们的比分直播模块也用到了分布式缓存。当时,遇到了一个 Young GC 频繁的线上问题,通过 jstat 工具排查后,发现新生代每隔两秒就被占满了。

进一步定位分析,原来是某些 key 缓存的 value 太大了,平均在 300K左右,最大的达到了500K。这样在高并发下,就很容易 导致 GC 频繁。

找到了根本原因后,具体怎么改呢? 我当时也没有清晰的思路。 于是,我去同行的网站上研究他们是怎么实现相同功能的,包括: 360彩票,澳客网。我发现了两点:

1、数据格式非常精简,只返回给前端必要的数据,部分数据通过数组的方式返回

2、使用 websocket,进入页面后推送全量数据,数据发生变化推送增量数据

再回到我的问题上,最终是用什么方案解决的呢?当时,我们的比分直播模块缓存格式是 JSON 数组,每个数组元素包含 20 多个键值对, 下面的 JSON 示例我仅仅列了其中 4 个属性。

[{
     "playId":"2399",
     "guestTeamName":"小牛",
     "hostTeamName":"湖人",
     "europe":"123"
 }]

这种数据结构,一般情况下没有什么问题。但是当字段数多达 20 多个,而且每天的比赛场次非常多时,在高并发的请求下其实很容易引发问题。

基于工期以及风险考虑,最终我们采用了比较保守的优化方案:

1)修改新生代大小,从原来的 2G 修改成 4G

2)将缓存数据的格式由 JSON 改成数组,如下所示:

[["2399","小牛","湖人","123"]]

修改完成之后, 缓存的大小从平均 300k 左右降为 80k 左右,YGC 频率下降很明显,同时页面响应也变快了很多。

但过了一会,cpu load 会在瞬间波动得比较高。可见,虽然我们减少了缓存大小,但是读取大对象依然对系统资源是极大的损耗,导致 Full GC 的频率也不低。

3)为了彻底解决这个问题,我们使用了更精细化的缓存读取策略。

我们把缓存拆成两个部分,第一部分是全量数据,第二部分是增量数据(数据量很小)。页面第一次请求拉取全量数据,当比分有变化的时候,通过 websocket 推送增量数据。

第 3 步完成后,页面的访问速度极快,服务器的资源使用也很少,优化的效果非常优异。

经过这次优化,我理解到: 缓存虽然可以提升整体速度,但是在高并发场景下,缓存对象大小依然是需要关注的点,稍不留神就会产生事故。另外我们也需要合理地控制读取策略,最大程度减少 GC 的频率 , 从而提升整体性能。

 

2. 分页列表查询

列表如何缓存是我非常渴望和大家分享的技能点。这个知识点也是我 2012 年从开源中国上学到的,下面我以「查询博客列表」的场景为例。

我们先说第 1 种方案:对分页内容进行整体缓存。这种方案会 按照页码和每页大小组合成一个缓存key,缓存值就是博客信息列表。 假如某一个博客内容发生修改, 我们要重新加载缓存,或者删除整页的缓存。

这种方案,缓存的颗粒度比较大,如果博客更新较为频繁,则缓存很容易失效。下面我介绍下第 2 种方案:仅对博客进行缓存。流程大致如下:

1)先从数据库查询当前页的博客id列表,SQL类似:

select id from blogs limit 0,10 

2)批量从缓存中获取博客id列表对应的缓存数据 ,并记录没有命中的博客id,若没有命中的id列表大于0,再次从数据库中查询一次,并放入缓存,SQL类似:

select id from blogs where id in (noHitId1, noHitId2)

3)将没有缓存的博客对象存入缓存中

4)返回博客对象列表

理论上,要是缓存都预热的情况下,一次简单的数据库查询,一次缓存批量获取,即可返回所有的数据。另外,关于 缓 存批量获取,如何实现?

  • 本地缓存:性能极高,for 循环即可
  • memcached:使用 mget 命令
  • Redis:若缓存对象结构简单,使用 mget 、hmget命令;若结构复杂,可以考虑使用 pipleline,lua脚本模式

第 1 种方案适用于数据极少发生变化的场景,比如排行榜,首页新闻资讯等。

第 2 种方案适用于大部分的分页场景,而且能和其他资源整合在一起。举例:在搜索系统里,我们可以通过筛选条件查询出博客 id 列表,然后通过如上的方式,快速获取博客列表。

 

03 多级缓存

首先要明确为什么要使用多级缓存?

本地缓存速度极快,但是容量有限,而且无法共享内存。分布式缓存容量可扩展,但在高并发场景下,如果所有数据都必须从远程缓存种获取,很容易导致带宽跑满,吞吐量下降。

有句话说得好,缓存离用户越近越高效!

使用多级缓存的好处在于:高并发场景下, 能提升整个系统的吞吐量,减少分布式缓存的压力。

2018年,我服务的一家电商公司需要进行 app 首页接口的性能优化。我花了大概两天的时间完成了整个方案,采取的是两级缓存模式,同时利用了 guava 的惰性加载机制,整体架构如下图所示:

缓存读取流程如下:

1、业务网关刚启动时,本地缓存没有数据,读取 Redis 缓存,如果 Redis 缓存也没数据,则通过 RPC 调用导购服务读取数据,然后再将数据写入本地缓存和 Redis 中;若 Redis 缓存不为空,则将缓存数据写入本地缓存中。

2、由于步骤1已经对本地缓存预热,后续请求直接读取本地缓存,返回给用户端。

3、Guava 配置了 refresh 机制,每隔一段时间会调用自定义 LoadingCache 线程池(5个最大线程,5个核心线程)去导购服务同步数据到本地缓存和 Redis 中。

优化后,性能表现很好,平均耗时在 5ms 左右。最开始我以为出现问题的几率很小,可是有一天晚上,突然发现 app 端首页显示的数据时而相同,时而不同。

也就是说: 虽然 LoadingCache 线程一直在调用接口更新缓存信息,但是各个 服务器本地缓存中的数据并非完成一致。 说明了两个很重要的点:

1、惰性加载仍然可能造成多台机器的数据不一致

2、 LoadingCache 线程池数量配置的不太合理, 导致了线程堆积

最终,我们的解决方案是:

1、惰性加载结合消息机制来更新缓存数据,也就是:当导购服务的配置发生变化时,通知业务网关重新拉取数据,更新缓存。

2、适当调大 LoadigCache 的线程池参数,并在线程池埋点,监控线程池的使用情况,当线程繁忙时能发出告警,然后动态修改线程池参数。

 

写在最后

缓存是非常重要的一个技术手段。如果能从原理到实践,不断深入地去掌握它,这应该是技术人员最享受的事情。

这篇文章属于缓存系列的开篇,更多是把我 10 多年工作中遇到的典型问题娓娓道来,并没有非常深入地去探讨原理性的知识。

我想我更应该和朋友交流的是:如何体系化的学习一门新技术。

  • 选择该技术的经典书籍,理解基础概念
  • 建立该技术的知识脉络
  • 知行合一,在生产环境中实践或者自己造轮子
  • 不断复盘,思考是否有更优的方案

后续我会连载一些缓存相关的内容:包括缓存的高可用机制、codis 的原理等,欢迎大家继续关注。

关于缓存,如果你有自己的心得体会或者想深入了解的内容,欢迎评论区留言。

 

作者简介:985硕士,前亚马逊工程师,现58转转技术总监

ASP.NET Core 3.0 使用gRPC - 晓晨Master - 博客园

mikel阅读(774)

来源: ASP.NET Core 3.0 使用gRPC – 晓晨Master – 博客园

目录

一.简介

gRPC 是一个由Google开源的,跨语言的,高性能的远程过程调用(RPC)框架。 gRPC使客户端和服务端应用程序可以透明地进行通信,并简化了连接系统的构建。它使用HTTP/2作为通信协议,使用 Protocol Buffers 作为序列化协议。

它的主要优点:

  • 现代高性能轻量级 RPC 框架。
  • 约定优先的 API 开发,默认使用 Protocol Buffers 作为描述语言,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型的服务器和客户端。
  • 支持客户端,服务器双向流调用。
  • 通过Protocol Buffers二进制序列化减少网络使用。
  • 使用 HTTP/2 进行传输

这些优点使gRPC非常适合:

  • 高性能轻量级微服务 – gRPC设计为低延迟和高吞吐量通信,非常适合需要高性能的轻量级微服务。
  • 多语言混合开发 – gRPC工具支持所有流行的开发语言,使gRPC成为多语言开发环境的理想选择。
  • 点对点实时通信 – gRPC对双向流调用提供出色的支持。gRPC服务可以实时推送消息而无需轮询。
  • 网络受限环境 – 使用 Protocol Buffers二进制序列化消息,该序列化始终小于等效的JSON消息,对网络带宽需求比JSON小。

不建议使用gRPC的场景:

  • 浏览器可访问的API – 浏览器不完全支持gRPC。虽然gRPC-Web可以提供浏览器支持,但是它有局限性,引入了服务器代理
  • 广播实时通信 – gRPC支持通过流进行实时通信,但不存在向已注册连接广播消息的概念
  • 进程间通信 – 进程必须承载HTTP/2才能接受传入的gRPC调用,对于Windows,进程间通信管道是一种更快速的方法。

摘自微软官方文档

支持的语言如下:

1569301484094

二.gRPC on .NET Core

gRPC 现在可以非常简单的在 .NET Core 和 ASP.NET Core 中使用,在 .NET Core 上的实现的开源地址:https://github.com/grpc/grpc-dotnet ,它目前由微软官方 ASP.NET 项目的人员进行维护,良好的接入 .NET Core 生态。

.NET Core 的 gRPC 功能如下:

  • Grpc.AspNetCore 一个用于在ASP.NET Core承载gRPC服务的框架,将 gRPC和ASP.NET Core 功能集成在一起,如:日志、依赖注入、身份认证和授权。
  • Grpc.Net.Client 基于HttpClient (HttpClient现已支持HTTP/2)的 gRPC客户端
  • Grpc.Net.ClientFactory 与gRPC客户端集成的HttpClientFactory,允许对gRPC客户端进行集中配置,并使用DI注入到应用程序中

三.使用 ASP.NET Core 创建 gRPC 服务

  1. 通过 Visual Studio 2019 (16.3.0)提供的模板,可以快速创建 gRPC 服务。

1569332979179

扒拉一下默认源码包含了什么东东。

① 配置文件 appsettings.json ,多了Kestrel 启用 HTTP/2 的配置,因为 gRPC 是基于 HTTP/2 来通信的

1569333539435

② PB协议文件 greet.proto 用于自动生成服务、客户端和消息(表示传递的数据)的C# Class

1569333899754

③ 服务类 GreeterService ,服务类集成的 Greeter.GreeterBase 来自于根据proto文件自动生成的,生成的类在 obj\Debug\netcoreapp3.0目录下

1569334077321

自动生成的类:

1569334149194

④ Startup.cs类,将 gRPC服务添加到了终结点路由中

1569334239963

⑤ csproj 项目文件,包含了 proto 文件引用

1569334307823

2.运行

第一次运行会提示是否信任证书,点击“是”

1569334375312

1569334392704

这是因为HTTP/2需要HTTPS,尽管HTTP/2协议没有明确规定需要HTTPS,但是为了安全在浏览器实现上都要求了HTTPS,所以现在的HTTP/2和HTTPS基本都是一对。

1569334575324

四. 创建 gRPC 客户端

1.添加一个.NET Core 控制台应用程序

2.通过nuget添加包:Grpc.Net.Client、Google.Protobuf、Grpc.Tools

1569335021283

3.将服务的 proto 文件复制到客户端

1569335104139

4.编辑客户端项目文件,添加关于proto文件的描述

<ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

注意 GrpcServices="Client" 这里是Client和服务是不一样的

5.生成客户端项目可以通过proto文件生成类

6.添加客户端调用代码

static async Task Main(string[] args)
{
    var channel = GrpcChannel.ForAddress("https://localhost:5001");
    var client = new Greeter.GreeterClient(channel);
    var reply = await client.SayHelloAsync(
        new HelloRequest { Name = "晓晨" });
    Console.WriteLine("Greeter 服务返回数据: " + reply.Message);
    Console.ReadKey();
}

7.先启动服务,然后运行客户端

1569335521902

这里可以看到,客户端成功调用了服务,收到了返回的消息。

五.自己动手写一个服务

前面我们使用的 Greeter 服务是由模板自动给我们创建的,现在我们来自己动手写一个服务。

编写一个“撸猫服务”

1.定义 proto 文件 LuCat.proto,并在csproj项目文件中添加描述

syntax = "proto3";

option csharp_namespace = "AspNetCoregRpcService";

import "google/protobuf/empty.proto";
package LuCat; //定义包名

//定义服务
service LuCat{
    //定义吸猫方法
	rpc SuckingCat(google.protobuf.Empty) returns(SuckingCatResult);
}

message SuckingCatResult{
	string message=1;
}


2.实现服务 LuCatService.cs

public class LuCatService:LuCat.LuCatBase
{
    private static readonly List<string> Cats=new List<string>(){"英短银渐层","英短金渐层","美短","蓝猫","狸花猫","橘猫"};
    private static readonly Random Rand=new Random(DateTime.Now.Millisecond);
    public override Task<SuckingCatResult> SuckingCat(Empty request, ServerCallContext context)
    {
        return Task.FromResult(new SuckingCatResult()
        {
            Message = $"您吸了一只{Cats[Rand.Next(0, Cats.Count)]}"
        });
    }
}

3.在 Startup终结点路由中注册

endpoints.MapGrpcService<LuCatService>();

4.添加客户端调用

var catClient = new LuCat.LuCatClient(channel);
var catReply = await catClient.SuckingCatAsync(new Empty());
Console.WriteLine("调用撸猫服务:"+ catReply.Message);

5.运行测试

1569338919789

六.实际使用中的技巧

技巧1

上面章节的操作步骤中,我们需要在服务和客户端之间复制proto,这是一个可以省略掉的步骤。

1.复制 Protos 文件夹到解决方案根目录(sln文件所在目录)

1569335816218

2.删除客户端和服务项目中的 Protos 文件夹

3.在客户端项目文件csproj中添加关于proto文件的描述

  <ItemGroup>
    <Protobuf Include="..\..\Protos\greet.proto" GrpcServices="Client" Link="Protos\greet.proto" />
  </ItemGroup>

4.在服务项目文件csproj中添加关于proto文件的描述

  <ItemGroup>
    <Protobuf Include="..\..\Protos\greet.proto" GrpcServices="Server" Link="Protos\greet.proto" />
  </ItemGroup>

在实际项目中,请自己计算相对路径

5.这样两个项目都是使用的一个proto文件,只用维护这一个文件即可

1569336339344

技巧2

我们在实际项目中使用,肯定有多个 proto 文件,难道我们每添加一个 proto 文件都要去更新 csproj文件?

我们可以使用MSBuild变量来帮我们完成,我们将 csproj 项目文件中引入proto文件信息进行修改。

服务端:

  <ItemGroup>
    <Protobuf Include="..\..\Protos\*.proto" GrpcServices="Server" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
  </ItemGroup>

客户端:

  <ItemGroup>
    <Protobuf Include="..\..\Protos\*.proto" GrpcServices="Client" Link="Protos\%(RecursiveDir)%(Filename)%(Extension)" />
  </ItemGroup>

示例:

1569339140058

七.总结

gRPC 现目前是一款非常成熟的高性能RPC框架,当前的生态是非常好的,很多公司的产品或者开源项目都有在使用gRPC,有了它,相信可以让我们更容易的构建.NET Core 微服务,可以让 .NET Core 更好的接入 gRPC 生态。不得不说这是 .NET Core 3.0 带来的最令人振奋的特性之一。

参考资料:

如果大家无法访问proto3说明文档,这里提供一个离线网页版(请另存为下载后用Chrome打开)

ASP.Net Core 3.1 使用gRPC入门指南 - 青春似雨后霓虹 - 博客园

mikel阅读(838)

来源: ASP.Net Core 3.1 使用gRPC入门指南 – 青春似雨后霓虹 – 博客园

主要参考文章微软官方文档: https://docs.microsoft.com/zh-cn/aspnet/core/grpc/client?view=aspnetcore-3.1

此外还参考了文章 https://www.cnblogs.com/stulzq/p/11581967.html并写了一个demo: https://files.cnblogs.com/files/hudean/GrpcDemo.zip

一、简介

gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架。

gRPC 的主要优点是:

  • 现代高性能轻量级 RPC 框架。
  • 协定优先 API 开发,默认使用协议缓冲区,允许与语言无关的实现。
  • 可用于多种语言的工具,以生成强类型服务器和客户端。
  • 支持客户端、服务器和双向流式处理调用。
  • 使用 Protobuf 二进制序列化减少对网络的使用。

这些优点使 gRPC 适用于:

  • 效率至关重要的轻量级微服务。
  • 需要多种语言用于开发的 Polyglot 系统。
  • 需要处理流式处理请求或响应的点对点实时服务。

 

二、创建 gRPC 服务

  • 启动 Visual Studio 并选择“创建新项目”。 或者,从 Visual Studio“文件”菜单中选择“新建” > “项目” 。
  • 在“创建新项目”对话框中,选择“gRPC 服务”,然后选择“下一步” :

    Visual Studio 中的“创建新项目”对话框

  • 将项目命名为 GrpcGreeter。 将项目命名为“GrpcGreeter”非常重要,这样在复制和粘贴代码时命名空间就会匹配。
  • 选择“创建”。
  • 在“创建新 gRPC 服务”对话框中:
    • 选择“gRPC 服务”模板。
    • 选择“创建”。

运行服务

  • 按 Ctrl+F5 以在不使用调试程序的情况下运行。

    Visual Studio 会显示以下对话框:

    此项目配置为使用 SSL。

    如果信任 IIS Express SSL 证书,请选择“是” 。

    将显示以下对话框:

    安全警告对话

    如果你同意信任开发证书,请选择“是”。

日志显示该服务正在侦听 https://localhost:5001

控制台显示如下:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development

 备注

gRPC 模板配置为使用传输层安全性 (TLS)。 gRPC 客户端需要使用 HTTPS 调用服务器。

macOS 不支持 ASP.NET Core gRPC 及 TLS。 在 macOS 上成功运行 gRPC 服务需要其他配置。

检查项目文件

GrpcGreeter 项目文件:

  • greet.proto : Protos/greet.proto 文件定义 Greeter gRPC,且用于生成 gRPC 服务器资产。
  • Services 文件夹:包含 Greeter 服务的实现。
  • appSettings.json :包含配置数据,例如 Kestrel 使用的协议。
  • Program.cs:包含 gRPC 服务的入口点。

Startup.cs :包含配置应用行为的代码。

上述准备工作完成,开始写gRPC服务端代码!



example.proto文件内容如下

 example.proto

其中:

syntax = “proto3”;是使用 proto3 语法,protocol buffer 编译器默认使用的是 proto2 。 这必须是文件的非空、非注释的第一行。

对于 C#语言,编译器会为每一个.proto 文件创建一个.cs 文件,为每一个消息类型都创建一个类来操作。

option csharp_namespace = “GrpcGreeter”;是C#代码的命名空间

package example;包的命名空间

service exampler 是服务的名字

rpc UnaryCall (ExampleRequest) returns (ExampleResponse); 意思是rpc调用方法 UnaryCall 方法参数是ExampleRequest类型 返回值是ExampleResponse 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
message ExampleRequest {
    int32 id = 1;
    string name = 2;
}<br>
指定字段类型
在上面的例子中,所有字段都是标量类型:一个整型(id),一个string类型(name)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
分配标识号
我们可以看到在上面定义的消息中,给每个字段都定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 <br>[1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。类似地,你不能使用之前保留的任何标识符。
指定字段规则
消息的字段可以是一下情况之一:
singular(默认):一个格式良好的消息可以包含该段可以出现 0 或 1 次(不能大于 1 次)。
repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。
默认情况下,标量数值类型的repeated字段使用packed的编码方式。

 

 

在GrpcGreeter.csproj文件添加:

<ItemGroup>
<Protobuf Include=”Protos\example.proto” GrpcServices=”Server” />
</ItemGroup>

点击保存

在Services文件夹下添加ExampleService类,代码如下:

 ExampleService

在Startup类里Configure中加入一个这个 endpoints.MapGrpcService<ExampleService>();

 Startup

就此 gRPC服务端代码完成了

 

 

 

 

在 .NET 控制台应用中创建 gRPC 客户端

  • 打开 Visual Studio 的第二个实例并选择“创建新项目”。
  • 在“创建新项目”对话框中,选择“控制台应用(.NET Core)”,然后选择“下一步” 。
  • 在“项目名称”文本框中,输入“GrpcGreeterClient”,然后选择“创建” 。

添加所需的包

gRPC 客户端项目需要以下包:

  • Grpc.Net.Client,其中包含 .NET Core 客户端。
  • Google.Protobuf 包含适用于 C# 的 Protobuf 消息。
  • Grpc.Tools 包含适用于 Protobuf 文件的 C# 工具支持。 运行时不需要工具包,因此依赖项标记为 PrivateAssets="All"
  •  

通过包管理器控制台 (PMC) 或管理 NuGet 包来安装包。

用于安装包的 PMC 选项

  • 从 Visual Studio 中,依次选择“工具” > “NuGet 包管理器” > “包管理器控制台”
  • 从“包管理器控制台”窗口中,运行 cd GrpcGreeterClient 以将目录更改为包含 GrpcGreeterClient.csproj 文件的文件夹。

 

  运行以下命令:
PowerShell

1
2
3
Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

 

 

 

管理 NuGet 包选项以安装包

  • 右键单击“解决方案资源管理器” > “管理 NuGet 包”中的项目 。
  • 选择“浏览”选项卡。
  • 在搜索框中输入 Grpc.Net.Client。
  • 从“浏览”选项卡中选择“Grpc.Net.Client”包,然后选择“安装” 。
  • 为 Google.Protobuf 和 Grpc.Tools 重复这些步骤。

添加 greet.proto

  • 在 gRPC 客户端项目中创建 Protos 文件夹。
  • 从 gRPC Greeter 服务将 Protos\greet.proto 文件复制到 gRPC 客户端项目。
  • 将 greet.proto 文件中的命名空间更新为项目的命名空间:
    option csharp_namespace = "GrpcGreeterClient";
    
  • 编辑 GrpcGreeterClient.csproj 项目文件:

    右键单击项目,并选择“编辑项目文件”。


添加具有引用 greet.proto 文件的 <Protobuf> 元素的项组:

XML

1
2
3
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
</ItemGroup>

 

创建 Greeter 客户端

构建客户端项目,以在 GrpcGreeter 命名空间中创建类型。 GrpcGreeter 类型是由生成进程自动生成的。

使用以下代码更新 gRPC 客户端的 Program.cs 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;
namespace GrpcGreeterClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // The port number(5001) must match the port of the gRPC server.
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client =  new Greeter.GreeterClient(channel);
            var reply = await client.SayHelloAsync(
                              new HelloRequest { Name = "GreeterClient" });
            Console.WriteLine("Greeting: " + reply.Message);
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

添加内容如下:

1
2
3
4
5
6
7
8
9
10
<ItemGroup>
    <Protobuf Include="Protos\example.proto" GrpcServices="Server" />
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Protos\example.proto" GrpcServices="Client" />
  </ItemGroup>

 

在gRPC客户端写调用服务端代码,代码如下:

复制代码
using Grpc.Core;
using Grpc.Net.Client;
using GrpcGreeter;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace GrpcGreeterClient
{
    class Program
    {
        //static void Main(string[] args)
        //{
        //    Console.WriteLine("Hello World!");
        //}
        //static async Task Main(string[] args)
        //{
        //    // The port number(5001) must match the port of the gRPC server.
        //    using var channel = GrpcChannel.ForAddress("https://localhost:5001");
        //    var client = new Greeter.GreeterClient(channel);
        //    var reply = await client.SayHelloAsync(
        //                      new HelloRequest { Name = "GreeterClient" });
        //    Console.WriteLine("Greeting: " + reply.Message);
        //    Console.WriteLine("Press any key to exit...");
        //    Console.ReadKey();
        //}


        static async Task Main(string[] args)
        {
            // The port number(5001) must match the port of the gRPC server.
            using var channel = GrpcChannel.ForAddress("https://localhost:5001");
            var client = new exampler.examplerClient(channel);

            #region 一元调用

            //var reply = await client.UnaryCallAsync(new ExampleRequest { Id = 1, Name = "hda" });
            //Console.WriteLine("Greeting: " + reply.Msg);

            #endregion 一元调用

            #region  服务器流式处理调用

            //using var call = client.StreamingFromServer(new ExampleRequest { Id = 1, Name = "hda" });

            //while (await call.ResponseStream.MoveNext(CancellationToken.None))
            //{
            //    Console.WriteLine("Greeting: " + call.ResponseStream.Current.Msg);

            //}
            //如果使用 C# 8 或更高版本,则可使用 await foreach 语法来读取消息。 IAsyncStreamReader<T>.ReadAllAsync() 扩展方法读取响应数据流中的所有消息:
            //await foreach (var response in call.ResponseStream.ReadAllAsync())
            //{
            //    Console.WriteLine("Greeting: " + response.Msg);
            //    // "Greeting: Hello World" is written multiple times
            //}

            #endregion  服务器流式处理调用

            #region  客户端流式处理调用
            //using var call = client.StreamingFromClient();
            //for (int i = 0; i < 5; i++)
            //{
            //    await call.RequestStream.WriteAsync(new ExampleRequest { Id = i, Name = "hda" + i });
            //}
            //await call.RequestStream.CompleteAsync();
            //var response = await call;
            //Console.WriteLine($"Count: {response.Msg}");
            #endregion 客户端流式处理调用

            #region  双向流式处理调用

            //通过调用 EchoClient.Echo 启动新的双向流式调用。
            //使用 ResponseStream.ReadAllAsync() 创建用于从服务中读取消息的后台任务。
            //使用 RequestStream.WriteAsync 将消息发送到服务器。
            //使用 RequestStream.CompleteAsync() 通知服务器它已发送消息。
            //等待直到后台任务已读取所有传入消息。
            //双向流式处理调用期间,客户端和服务可在任何时间互相发送消息。 与双向调用交互的最佳客户端逻辑因服务逻辑而异。
            using var call = client.StreamingBothWays();
            Console.WriteLine("Starting background task to receive messages");
            var readTask = Task.Run(async () =>
            {
                await foreach (var response in call.ResponseStream.ReadAllAsync())
                {
                    Console.WriteLine(response.Msg);
                    // Echo messages sent to the service
                }
            });
            Console.WriteLine("Starting to send messages");
            Console.WriteLine("Type a message to echo then press enter.");
            while (true)
            {
                var result = Console.ReadLine();
                if (string.IsNullOrEmpty(result))
                {
                    break;
                }

                await call.RequestStream.WriteAsync(new ExampleRequest { Id=1,Name= result });
            }

            Console.WriteLine("Disconnecting");
            await call.RequestStream.CompleteAsync();
            await readTask;
            #endregion 双向流式处理调用



            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}
复制代码

 

代码链接地址: https://files.cnblogs.com/files/hudean/GrpcGreeter.zip

C# 高性能对象映射(表达式树实现) - coder拾遗 - 博客园

mikel阅读(669)

来源: C# 高性能对象映射(表达式树实现) – coder拾遗 – 博客园

前言

上篇简单实现了对象映射,针对数组,集合,嵌套类并没有给出实现,这一篇继续完善细节。

 

开源对象映射类库映射分析

 1.AutoMapper

实现原理:主要通过表达式树Api 实现对象映射

优点: .net功能最全的对象映射类库。

缺点:当出现复杂类型和嵌套类型时性能直线下降,甚至不如序列化快

 2.TinyMapper

实现原理:主要通过Emit 实现对象映射

优点:速度非常快。在处理复杂类型和嵌套类型性能也很好

缺点:相对AutoMapper功能上少一些,Emit的实现方案,在代码阅读和调试上相对比较麻烦,而表达式树直接观察 DebugView中生成的代码结构便可知道问题所在

 

 3. 本文的对象映射库

针对AutoMapper 处理复杂类型和嵌套类型时性能非常差的情况,自己实现一个表达式树版的高性能方案

 

此篇记录下实现对象映射库的过程

构造测试类

复制代码
 1     public class TestA
 2     {
 3         public int Id { get; set; }
 4         public string Name { get; set; }
 5 
 6         public TestC TestClass { get; set; }
 7 
 8         public IEnumerable<TestC> TestLists { get; set; }
 9     }
10 
11     public class TestB
12     {
13         public int Id { get; set; }
14         public string Name { get; set; }
15 
16         public TestD TestClass { get; set; }
17 
18         public TestD[] TestLists { get; set; }
19     }
20 
21     public class TestC
22     {
23         public int Id { get; set; }
24         public string Name { get; set; }
25 
26         public TestC SelfClass { get; set; }
27     }
28 
29     public class TestD
30     {
31         public int Id { get; set; }
32         public string Name { get; set; }
33 
34         public TestD SelfClass { get; set; }
35     }
复制代码

 

 1.初步实现

利用表达式树给属性赋值 利用 Expresstion.New构造 var b=new B{};

复制代码
 1       private static Func<TSource, TTarget> GetMap<TSource, TTarget>()
 2         {
 3             var sourceType = typeof(TSource);
 4             var targetType = typeof(TTarget);
 5 
 6             //构造 p=>
 7             var parameterExpression = Expression.Parameter(sourceType, "p");
 8 
 9             //构造 p=>new TTarget{ Id=p.Id,Name=p.Name };
10             var memberBindingList = new List<MemberBinding>();
11             foreach (var sourceItem in sourceType.GetProperties())
12             {
13                 var targetItem = targetType.GetProperty(sourceItem.Name);
14                 if (targetItem == null || sourceItem.PropertyType != targetItem.PropertyType)
15                     continue;
16 
17                 var property = Expression.Property(parameterExpression, sourceItem);
18                 var memberBinding = Expression.Bind(targetItem, property);
19                 memberBindingList.Add(memberBinding);
20             }
21             var memberInitExpression = Expression.MemberInit(Expression.New(targetType), memberBindingList);
22 
23             var lambda = Expression.Lambda<Func<TSource, TTarget>>(memberInitExpression, parameterExpression );
24 
25             Console.WriteLine(lambda);
26             return lambda.Compile();
27         }
复制代码

 

调用如下

复制代码
14 
15     class Program
16     {
17         static void Main(string[] args)
18         {
19             var testA = new TestA { Id = 1, Name = "张三" };
20             var func = Map<TestA, TestB>();
21             TestB testB = func(testA);
22             Console.WriteLine($"testB.Id={testB.Id},testB.Name={testB.Name}");
23             Console.ReadLine();
24         }
25     }
复制代码

输出结果

总结:此方法需要调用前需要手动编译下,然后再调用委托没有缓存委托,相对麻烦。

2.缓存实现

利用静态泛型类缓存泛型委托

复制代码
 1     public class DataMapper<TSource, TTarget>
 2     {
 3         private static Func<TSource, TTarget> MapFunc { get; set; }
 4 
 5         public static TTarget Map(TSource source)
 6         {
 7             if (MapFunc == null)
 8                 MapFunc = GetMap();//方法在上边
 9             return MapFunc(source);
10         }11    }
复制代码

调用方法

复制代码
1         static void Main(string[] args)
2         {
3             var testA = new TestA { Id = 1, Name = "张三" };
4             TestB testB = DataMapper<TestA, TestB>.Map(testA);//委托不存在时自动生成,存在时调用静态缓存
5 
6             Console.WriteLine($"testB.Id={testB.Id},testB.Name={testB.Name}");
7             Console.ReadLine();
8         }
复制代码

输出结果

 总结:引入静态泛型类能解决泛型委托缓存提高性能,但是有两个问题  1.当传入参数为null时 则会抛出空引用异常 2.出现复杂类型上述方法便不能满足了

 

3.解决参数为空值和复杂类型的问题

首先先用常规代码实现下带有复杂类型赋值的情况

复制代码
 1 public TestB GetTestB(TestA testA)
 2         {
 3             TestB testB;
 4             if (testA != null)
 5             {
 6                 testB = new TestB();
 7                 testB.Id = testA.Id;
 8                 testB.Name = testA.Name;
 9                 if (testA.TestClass != null)
10                 {
11                     testB.TestClass = new TestD();
12                     testB.TestClass.Id = testA.TestClass.Id;
13                     testB.TestClass.Name = testA.TestClass.Name;
14                 }
15             }
16             else
17             {
18                 testB = null;
19             }
20             return testB;
21         }
复制代码

将上面的代码翻译成表达式树

复制代码
 1         private static Func<TSource, TTarget> GetMap()
 2         {
 3             var sourceType = typeof(TSource);
 4             var targetType = typeof(TTarget);
 5 
 6             //Func委托传入变量
 7             var parameter = Expression.Parameter(sourceType);
 8 
 9             //声明一个返回值变量
10             var variable = Expression.Variable(targetType);
11             //创建一个if条件表达式
12             var test = Expression.NotEqual(parameter, Expression.Constant(null, sourceType));// p==null;
13             var ifTrue = Expression.Block(GetExpression(parameter, variable, sourceType, targetType));
14             var IfThen = Expression.IfThen(test, ifTrue);
15 
16             //构造代码块 
17             var block = Expression.Block(new[] { variable }, parameter, IfThen, variable);
18 
19             var lambda = Expression.Lambda<Func<TSource, TTarget>>(block, parameter);
20             return lambda.Compile();
21         }
22 
23         private static List<Expression> GetExpression(Expression parameter, Expression variable, Type sourceType, Type targetType)
24         {
25             //创建一个表达式集合
26             var expressions = new List<Expression>();
27 
28             expressions.Add(Expression.Assign(variable, Expression.MemberInit(Expression.New(targetType))));
29 
30             foreach (var targetItem in targetType.GetProperties().Where(x => x.PropertyType.IsPublic && x.CanWrite))
31             {
32                 var sourceItem = sourceType.GetProperty(targetItem.Name);
33 
34                 //判断实体的读写权限
35                 if (sourceItem == null || !sourceItem.CanRead || sourceItem.PropertyType.IsNotPublic)
36                     continue;
37 
38                 var sourceProperty = Expression.Property(parameter, sourceItem);
39                 var targetProperty = Expression.Property(variable, targetItem);
40 
41                 //判断都是class 且类型不相同时
42                 if (targetItem.PropertyType.IsClass && sourceItem.PropertyType.IsClass && targetItem.PropertyType != sourceItem.PropertyType)
43                 {
44                     if (targetItem.PropertyType != targetType)//不处理嵌套循环的情况
45                     {
46                         //由于类型是class 所以默认值是null
47                         var testItem = Expression.NotEqual(sourceProperty, Expression.Constant(null, sourceItem.PropertyType));
48 
49                         var itemExpressions = GetExpression(sourceProperty, targetProperty, sourceItem.PropertyType, targetItem.PropertyType);
50                         var ifTrueItem = Expression.Block(itemExpressions);
51 
52                         var IfThenItem = Expression.IfThen(testItem, ifTrueItem);
53                         expressions.Add(IfThenItem);
54 
55                         continue;
56                     }
57                 }
58 
59                 //目标值类型时 且两者类型不一致时跳过
60                 if (targetItem.PropertyType != sourceItem.PropertyType)
61                     continue;
62 
63                 expressions.Add(Expression.Assign(targetProperty, sourceProperty));
64             }
65 
66             return expressions;
67         }
复制代码

总结:此方案,运用 Expression.IfThen(testItem, ifTrueItem) 判断空值问题,通过递归调用 GetExpression()方法,处理复杂类型。 但是。。。针对嵌套类仍然不能解决。因为表达式树是在实际调用方法之前就生成的,在没有实际的

           参数值传入之前,生成的表达式是不知道有多少层级的。有个比较low的方案是,预先设定嵌套层级为10层,然后生成一个有10层 if(P!=null) 的判断。如果传入的参数层级超过10层了呢,就得手动调整生成的树,此方案也否决。

           最后得出的结论只能在表达式中动态调用方法。

4.最终版本

通过动态调用方法解决嵌套类,代码如下

复制代码
  using static System.Linq.Expressions.Expression;
    public static class Mapper<TSource, TTarget> where TSource : class where TTarget : class
    {
public readonly static Func<TSource, TTarget> MapFunc = GetMapFunc();

public readonly static Action<TSource, TTarget> MapAction = GetMapAction();

/// <summary>
/// 将对象TSource转换为TTarget
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
public static TTarget Map(TSource source) => MapFunc(source);

public static List<TTarget> MapList(IEnumerable<TSource> sources)=> sources.Select(MapFunc).ToList();



/// <summary>
/// 将对象TSource的值赋给给TTarget
/// </summary>
/// <param name="source"></param>
/// <param name="target"></param>
public static void Map(TSource source, TTarget target) => MapAction(source, target);

private static Func<TSource, TTarget> GetMapFunc()
        {
            var sourceType = typeof(TSource);
            var targetType = typeof(TTarget);
            //Func委托传入变量
            var parameter = Parameter(sourceType, "p");

            var memberBindings = new List<MemberBinding>();
            var targetTypes = targetType.GetProperties().Where(x => x.PropertyType.IsPublic && x.CanWrite);
            foreach (var targetItem in targetTypes)
            {
                var sourceItem = sourceType.GetProperty(targetItem.Name);

                //判断实体的读写权限
                if (sourceItem == null || !sourceItem.CanRead || sourceItem.PropertyType.IsNotPublic)
                    continue;

                //标注NotMapped特性的属性忽略转换
                if (sourceItem.GetCustomAttribute<NotMappedAttribute>() != null)
                    continue;

                var sourceProperty = Property(parameter, sourceItem);

                //当非值类型且类型不相同时
                if (!sourceItem.PropertyType.IsValueType && sourceItem.PropertyType != targetItem.PropertyType)
                {
                    //判断都是(非泛型)class
                    if (sourceItem.PropertyType.IsClass && targetItem.PropertyType.IsClass &&
                        !sourceItem.PropertyType.IsGenericType && !targetItem.PropertyType.IsGenericType)
                    {
                        var expression = GetClassExpression(sourceProperty, sourceItem.PropertyType, targetItem.PropertyType);
                        memberBindings.Add(Bind(targetItem, expression));
                    }

                    //集合数组类型的转换
                    if (typeof(IEnumerable).IsAssignableFrom(sourceItem.PropertyType) && typeof(IEnumerable).IsAssignableFrom(targetItem.PropertyType))
                    {
                        var expression = GetListExpression(sourceProperty, sourceItem.PropertyType, targetItem.PropertyType);
                        memberBindings.Add(Bind(targetItem, expression));
                    }

                    continue;
                }

                if (targetItem.PropertyType != sourceItem.PropertyType)
                    continue;

                memberBindings.Add(Bind(targetItem, sourceProperty));
            }

            //创建一个if条件表达式
            var test = NotEqual(parameter, Constant(null, sourceType));// p==null;
            var ifTrue = MemberInit(New(targetType), memberBindings);
            var condition = Condition(test, ifTrue, Constant(null, targetType));

            var lambda = Lambda<Func<TSource, TTarget>>(condition, parameter);
            return lambda.Compile();
        }

        /// <summary>
        /// 类型是clas时赋值
        /// </summary>
        /// <param name="sourceProperty"></param>
        /// <param name="targetProperty"></param>
        /// <param name="sourceType"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        private static Expression GetClassExpression(Expression sourceProperty, Type sourceType, Type targetType)
        {
            //条件p.Item!=null    
            var testItem = NotEqual(sourceProperty, Constant(null, sourceType));

            //构造回调 Mapper<TSource, TTarget>.Map()
            var mapperType = typeof(Mapper<,>).MakeGenericType(sourceType, targetType);
            var iftrue = Call(mapperType.GetMethod(nameof(Map), new[] { sourceType }), sourceProperty);

            var conditionItem = Condition(testItem, iftrue, Constant(null, targetType));

            return conditionItem;
        }

        /// <summary>
        /// 类型为集合时赋值
        /// </summary>
        /// <param name="sourceProperty"></param>
        /// <param name="targetProperty"></param>
        /// <param name="sourceType"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        private static Expression GetListExpression(Expression sourceProperty, Type sourceType, Type targetType)
        {
            //条件p.Item!=null    
            var testItem = NotEqual(sourceProperty, Constant(null, sourceType));

            //构造回调 Mapper<TSource, TTarget>.MapList()
            var sourceArg = sourceType.IsArray ? sourceType.GetElementType() : sourceType.GetGenericArguments()[0];
            var targetArg = targetType.IsArray ? targetType.GetElementType() : targetType.GetGenericArguments()[0];
            var mapperType = typeof(Mapper<,>).MakeGenericType(sourceArg, targetArg);

            var mapperExecMap = Call(mapperType.GetMethod(nameof(MapList), new[] { sourceType }), sourceProperty);

            Expression iftrue;
            if (targetType == mapperExecMap.Type)
            {
                iftrue = mapperExecMap;
            }
            else if (targetType.IsArray)//数组类型调用ToArray()方法
            {
                iftrue = Call(mapperExecMap, mapperExecMap.Type.GetMethod("ToArray"));
            }
            else if (typeof(IDictionary).IsAssignableFrom(targetType))
            {
                iftrue = Constant(null, targetType);//字典类型不转换
            }
            else
            {
                iftrue = Convert(mapperExecMap, targetType);
            }

            var conditionItem = Condition(testItem, iftrue, Constant(null, targetType));

            return conditionItem;
        }

        private static Action<TSource, TTarget> GetMapAction()
        {
            var sourceType = typeof(TSource);
            var targetType = typeof(TTarget);
            //Func委托传入变量
            var sourceParameter = Parameter(sourceType, "p");

            var targetParameter = Parameter(targetType, "t");

            //创建一个表达式集合
            var expressions = new List<Expression>();

            var targetTypes = targetType.GetProperties().Where(x => x.PropertyType.IsPublic && x.CanWrite);
            foreach (var targetItem in targetTypes)
            {
                var sourceItem = sourceType.GetProperty(targetItem.Name);

                //判断实体的读写权限
                if (sourceItem == null || !sourceItem.CanRead || sourceItem.PropertyType.IsNotPublic)
                    continue;

                //标注NotMapped特性的属性忽略转换
                if (sourceItem.GetCustomAttribute<NotMappedAttribute>() != null)
                    continue;

                var sourceProperty = Property(sourceParameter, sourceItem);
                var targetProperty = Property(targetParameter, targetItem);

                //当非值类型且类型不相同时
                if (!sourceItem.PropertyType.IsValueType && sourceItem.PropertyType != targetItem.PropertyType)
                {
                    //判断都是(非泛型)class
                    if (sourceItem.PropertyType.IsClass && targetItem.PropertyType.IsClass &&
                        !sourceItem.PropertyType.IsGenericType && !targetItem.PropertyType.IsGenericType)
                    {
                        var expression = GetClassExpression(sourceProperty, sourceItem.PropertyType, targetItem.PropertyType);
                        expressions.Add(Assign(targetProperty, expression));
                    }

                    //集合数组类型的转换
                    if (typeof(IEnumerable).IsAssignableFrom(sourceItem.PropertyType) && typeof(IEnumerable).IsAssignableFrom(targetItem.PropertyType))
                    {
                        var expression = GetListExpression(sourceProperty, sourceItem.PropertyType, targetItem.PropertyType);
                        expressions.Add(Assign(targetProperty, expression));
                    }

                    continue;
                }

                if (targetItem.PropertyType != sourceItem.PropertyType)
                    continue;


                expressions.Add(Assign(targetProperty, sourceProperty));
            }

            //当Target!=null判断source是否为空
            var testSource = NotEqual(sourceParameter, Constant(null, sourceType));
            var ifTrueSource = Block(expressions);
            var conditionSource = IfThen(testSource, ifTrueSource);

            //判断target是否为空
            var testTarget = NotEqual(targetParameter, Constant(null, targetType));
            var conditionTarget = IfThen(testTarget, conditionSource);

            var lambda = Lambda<Action<TSource, TTarget>>(conditionTarget, sourceParameter, targetParameter);
            return lambda.Compile();
        }
    }
复制代码

 

 

 

 

输出的 表达式

格式化后

复制代码
 1 p => IIF((p != null), 
 2      new TestB() 
 3      {
 4        Id = p.Id, 
 5        Name = p.Name, 
 6        TestClass = IIF(
 7                    (p.TestClass != null),
 8                     Map(p.TestClass),
 9                     null
10                     ),
11        TestLists = IIF(
12                      (p.TestLists != null),
13                       MapList(p.TestLists).ToArray(),
14                       null
15                      )
16        },
17        null)
复制代码

说明 Map(p.TestClass)   MapList(p.TestLists).ToArray(),  完整的信息为 Mapper<TestC,TestD>.Map()   Mapper<TestC,TestD>.MapList()

   总结:解决嵌套类的核心代码

101             //构造回调 Mapper<TSource, TTarget>.Map()
102             var mapperType = typeof(DataMapper<,>).MakeGenericType(sourceType, targetType);
103             var mapperExecMap = Expression.Call(mapperType.GetMethod(nameof(Map), new[] { sourceType }), sourceProperty);

   利用Expression.Call  根据参数类型动态生成 对象映射的表达式

 

性能测试

写了这么多最终目的还是为了解决性能问题,下面将对比下性能

  1.测试类

复制代码
  1     public static class MapperTest
  2     {
  3         //执行次数
  4         public static int Count = 100000;
  5 
  6         //简单类型
  7         public static void Nomal()
  8         {
  9             Console.WriteLine($"******************简单类型:{Count / 10000}万次执行时间*****************");
 10             var model = new TestA
 11             {
 12                 Id =1,
 13                 Name = "张三",
 14             };
 15 
 16             //计时
 17             var sw = Stopwatch.StartNew();
 18             for (int i = 0; i < Count; i++)
 19             {
 20                 if (model != null)
 21                 {
 22                     var b = new TestB
 23                     {
 24                         Id = model.Id,
 25                         Name = model.Name,
 26                     };
 27                 }
 28             }
 29             sw.Stop();
 30             Console.WriteLine($"原生的时间:{sw.ElapsedMilliseconds}ms");
 31 
 32             Exec(model);
 33         }
 34 
 35         //复杂类型
 36         public static void Complex()
 37         {
 38             Console.WriteLine($"********************复杂类型:{Count / 10000}万次执行时间*********************");
 39             var model = new TestA
 40             {
 41                 Id = 1,
 42                 Name = "张三",
 43                 TestClass = new TestC
 44                 {
 45                     Id = 2,
 46                     Name = "lisi",
 47                 },
 48             };
 49 
 50             //计时
 51             var sw = Stopwatch.StartNew();
 52             for (int i = 0; i < Count; i++)
 53             {
 54 
 55                 if (model != null)
 56                 {
 57                     var b = new TestB
 58                     {
 59                         Id = model.Id,
 60                         Name = model.Name,
 61                     };
 62                     if (model.TestClass != null)
 63                     {
 64                         b.TestClass = new TestD
 65                         {
 66                             Id = i,
 67                             Name = "lisi",
 68                         };
 69                     }
 70                 }
 71             }
 72             sw.Stop();
 73             Console.WriteLine($"原生的时间:{sw.ElapsedMilliseconds}ms");
 74             Exec(model);
 75         }
 76 
 77         //嵌套类型
 78         public static void Nest()
 79         {
 80             Console.WriteLine($"*****************嵌套类型:{Count / 10000}万次执行时间*************************");
 81             var model = new TestA
 82             {
 83                 Id = 1,
 84                 Name = "张三",
 85                 TestClass = new TestC
 86                 {
 87                     Id = 1,
 88                     Name = "lisi",
 89                     SelfClass = new TestC
 90                     {
 91                         Id = 2,
 92                         Name = "lisi",
 93                         SelfClass = new TestC
 94                         {
 95                             Id = 3,
 96                             Name = "lisi",
 97                             SelfClass = new TestC
 98                             {
 99                                 Id = 4,
100                                 Name = "lisi",
101                             },
102                         },
103                     },
104                 },
105             };
106             //计时
107             var item = model;
108             var sw = Stopwatch.StartNew();
109             for (int i = 0; i < Count; i++)
110             {
111                 //这里每一步需要做非空判断的,书写太麻烦省去了
112                 if (model != null)
113                 {
114                     var b = new TestB
115                     {
116                         Id = model.Id,
117                         Name = model.Name,
118                         TestClass = new TestD
119                         {
120                             Id = model.TestClass.Id,
121                             Name = model.TestClass.Name,
122                             SelfClass = new TestD
123                             {
124                                 Id = model.TestClass.SelfClass.Id,
125                                 Name = model.TestClass.SelfClass.Name,
126                                 SelfClass = new TestD
127                                 {
128                                     Id = model.TestClass.SelfClass.SelfClass.Id,
129                                     Name = model.TestClass.SelfClass.SelfClass.Name,
130                                     SelfClass = new TestD
131                                     {
132                                         Id = model.TestClass.SelfClass.SelfClass.SelfClass.Id,
133                                         Name = model.TestClass.SelfClass.SelfClass.SelfClass.Name,
134                                     },
135                                 },
136                             },
137                         },
138                     };
139                 }
140             }
141             sw.Stop();
142             Console.WriteLine($"原生的时间:{sw.ElapsedMilliseconds}ms");
143 
144             Exec(model);
145         }
146 
147         //集合
148         public static void List()
149         {
150             Console.WriteLine($"********************集合类型:{Count/10000}万次执行时间***************************");
151 
152             var model = new TestA
153             {
154                 Id = 1,
155                 Name = "张三",
156                 TestLists = new List<TestC> {
157                             new TestC{
158                              Id = 1,
159                             Name =  "张三",
160                            },
161                             new TestC{
162                             Id = -1,
163                             Name =  "张三",
164                            },
165                         }
166             };
167 
168 
169             //计时
170             var sw = Stopwatch.StartNew();
171             for (int i = 0; i < Count; i++)
172             {
173                 var item = model;
174                 if (item != null)
175                 {
176                     var b = new TestB
177                     {
178                         Id = item.Id,
179                         Name = item.Name,
180                         TestLists = new List<TestD> {
181                             new TestD{
182                                    Id = item.Id,
183                             Name = item.Name,
184                            },
185                             new TestD{
186                             Id = -item.Id,
187                             Name = item.Name,
188                            },
189                         }.ToArray()
190                     };
191                 }
192             }
193             sw.Stop();
194             Console.WriteLine($"原生的时间:{sw.ElapsedMilliseconds}ms");
195 
196             Exec(model);
197         }
198 
199         public static void Exec(TestA model)
200         {
201             //表达式
202             Mapper<TestA, TestB>.Map(model);
203             var sw = Stopwatch.StartNew();
204             for (int i = 0; i < Count; i++)
205             {
206                 var b = Mapper<TestA, TestB>.Map(model);
207             }
208             sw.Stop();
209             Console.WriteLine($"表达式的时间:{sw.ElapsedMilliseconds}ms");
210 
211             //AutoMapper
212             sw.Restart();
213             for (int i = 0; i < Count; i++)
214             {
215                 var b = AutoMapper.Mapper.Map<TestA, TestB>(model);
216             }
217             sw.Stop();
218             Console.WriteLine($"AutoMapper时间:{sw.ElapsedMilliseconds}ms");
219 
220             //TinyMapper
221             sw.Restart();
222             for (int i = 0; i < Count; i++)
223             {
224                 var b = TinyMapper.Map<TestA, TestB>(model);
225             }
226             sw.Stop();
227             Console.WriteLine($"TinyMapper时间:{sw.ElapsedMilliseconds}ms");
228         }
229     }
230 
231
复制代码

2.调用测试

复制代码
 1         static void Main(string[] args)
 2         {
 3             AutoMapper.Mapper.Initialize(cfg => cfg.CreateMap<TestA, TestB>());
 4             TinyMapper.Bind<TestA, TestB>();
 5             Mapper<TestA, TestB>.Map(new TestA());
 6 
 7 
 8             MapperTest.Count = 10000;
 9             MapperTest.Nomal();
10             MapperTest.Complex();
11             MapperTest.Nest();
12             MapperTest.List();
13 
14             MapperTest.Count = 100000;
15             MapperTest.Nomal();
16             MapperTest.Complex();
17             MapperTest.Nest();
18             MapperTest.List();
19 
20             MapperTest.Count = 1000000;
21             MapperTest.Nomal();
22             MapperTest.Complex();
23             MapperTest.Nest();
24             MapperTest.List();
25 
26             MapperTest.Count = 10000000;
27             MapperTest.Nomal();
28             MapperTest.Complex();
29             MapperTest.Nest();
30             MapperTest.List();
31 
32 
33             Console.WriteLine($"------------结束--------------------");
34             Console.ReadLine();
35         }
复制代码

3.结果

1万次

 

10万次

100万次

 

1000万次

 

上图结果AutoMapper 在非简单类型的转换上比其他方案有50倍以上的差距,几乎就跟反射的结果一样。

 

作者:costyuan

GitHub地址:https://github.com/bieyuan/.net-core-DTO

地址:http://www.cnblogs.com/castyuan/p/9324088.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果文中有什么错误,欢迎指出,谢谢!

关于对象映射(Dto->model) 思路的一些想法 - EpicOfGilgamesh - 博客园

mikel阅读(767)

来源: 关于对象映射(Dto->model) 思路的一些想法 – EpicOfGilgamesh – 博客园

最近粗浅的学习了下AutoMapper 这个做对象映射的第三方工具,觉得非常方便使用,所以简单的总结了一下我能想到的简单的对象映射的方式。

占时先不考虑源对象成员到目标对象成员的指定映射(即成员名不一致),先准备好两个类Students-StudentsDto;Teachers-TeachersDto

复制代码
 1     public class Students
 2     {
 3         public int No { get; set; }
 4         public string Name { get; set; }
 5         public bool Gender { get; set; }
 6         public string Class { get; set; }
 7 
 8         public string _remark;
 9     }
10 
11     public class StudentsDto
12     {
13         public int No { get; set; }
14         public string Name { get; set; }
15         public bool Gender { get; set; }
16         public string Class { get; set; }
17 
18         public string _remark;
19     }
20 
21     public class Teachers
22     {
23         public int No { get; set; }
24 
25         public string Course { get; set; }
26 
27         public string Name { get; set; }
28     }
29 
30     public class TeachersDto
31     {
32         public int No { get; set; }
33 
34         public string Course { get; set; }
35 
36         public string Name { get; set; }
37     }
复制代码

我们先使用普通的对象装载方式:

复制代码
 1 StudentsDto studentsDto = new StudentsDto { No = 1, Name = "Epic", Gender = true, Class = "家里蹲一班", _remark = "逗比" };
 2 TeachersDto teachersDto = new TeachersDto { No = 2, Name = "Eleven", Course = ".net" };
 3 Students students = new Students
 4                     {
 5                         No = studentsDto.No,
 6                         Name = studentsDto.Name,
 7                         Gender = studentsDto.Gender,
 8                         Class = studentsDto.Class,
 9                         _remark = studentsDto._remark
10                     };
11  Teachers teachers = new Teachers
12                     {
13                         No = teachersDto.No,
14                         Name = teachersDto.Name,
15                         Course = teachersDto.Course
16                     };
复制代码

总结:其思路无非就是先new一个对象实例,然后将该实例的成员一一赋值。

1.通过反射的方式来实现对象映射,是我们最容易想到的方式,思路也很简单

 

复制代码
 1     public static TModel Trans<TModel, TModelDto>(TModelDto dto)
 2             where TModel : class
 3             where TModelDto : class
 4         {
 5             TModel model = Activator.CreateInstance(typeof(TModel)) as TModel;
 6             //获取TModel的属性集合
 7             PropertyInfo[] modlePropertys = typeof(TModel).GetProperties();
 8             //获取TModelDto的属性集合
 9             Type type = dto.GetType();
10             PropertyInfo[] propertys = type.GetProperties();
11             foreach (var property in propertys)
12             {
13                 foreach (var mproperty in modlePropertys)
14                 {
15                     //如果属性名称一致,则将该属性值赋值到TModel实例中
16                     //这里可以用Attribute来实现成员的自定义映射
17                     if (property.Name.Equals(mproperty.Name))
18                     {
19                         mproperty.SetValue(model, property.GetValue(dto));
20                         break;
21                     }
22                 }
23             }
24 
25             //获取TModel的字段集合
26             FieldInfo[] modelfieldInfos = typeof(TModel).GetFields();
27             //获取TModelDto的字段集合
28             FieldInfo[] fieldInfos = type.GetFields();
29             foreach (var field in fieldInfos)
30             {
31                 foreach (var mfield in modelfieldInfos)
32                 {
33                     //如果字段名称一致,则将该字段值赋值到TModel实例中
34                     if (field.Name.Equals(mfield.Name))
35                     {
36                         mfield.SetValue(model, field.GetValue(dto));
37                         break;
38                     }
39                 }
40             }
41             return model;
42         }
复制代码

总结:通过反射来创建对象实例,然后将实例成语的值通过反射的方式获取并赋值。

2.通过序列号的方式,对象类型可以转换成json字符串,然后再由json字符串转换成所需的对象不就可以了么

复制代码
1 public static TModel Trans<TModel, TModelDto>(TModelDto dto)
2             where TModel : class
3             where TModelDto : class
4         {
5             return JsonConvert.DeserializeObject<TModel>(JsonConvert.SerializeObject(dto));
6         }
复制代码

总结:通过序列号然后反序列化,这样使用感觉并不是Newtonsoft.Json的初衷,不知道性能到底如何呢?

3.使用Expression表达式的方式来解决,将所需实例对象new、赋值的过程先写入表达式,然后生成lambda表达式,最后编译该表达式生成委托,invoke即可

复制代码
 1    public static class ExpressionAndSeesionMethod
 2 
 3     {
 4         public static Dictionary<string, object> _dictionary = new Dictionary<string, object>();
 5 
 6         public static TModel Trans<TModel, TModelDto>(TModelDto dto)
 7         {
 8             Type modelType = typeof(TModel);
 9             Type modelDtoType = typeof(TModelDto);
10 
11             //如果_dictionary中不存在该key,则存进去
12             string key = $"{modelDtoType.Name}-->{modelType.Name}";
13             if (!_dictionary.ContainsKey(key))
14             {
15                 //创建一个lambda参数x,定义的对象为TModelDto
16                 ParameterExpression parameterExpression = Expression.Parameter(modelDtoType, "x");
17                 //开始生成lambda表达式
18                 List<MemberBinding> list = new List<MemberBinding>();
19                 foreach (var item in modelType.GetProperties())
20                 {
21                     //为x参数表达式生成一个属性值
22                     MemberExpression property = Expression.Property(parameterExpression, modelDtoType.GetProperty(item.Name));
23                     //将该属性初始化 eg:No=x.No
24                     MemberBinding memberBinding = Expression.Bind(item, property);
25                     list.Add(memberBinding);
26                 }
27 
28                 foreach (var item in typeof(TModel).GetFields())
29                 {
30                     //为x参数表达式生成一个字段值
31                     MemberExpression field = Expression.Field(parameterExpression, modelDtoType.GetField(item.Name));
32                     //将该字段初始化
33                     MemberBinding memberBinding = Expression.Bind(item, field);
34                     list.Add(memberBinding);
35                 }
36                 //调用构造函数,初始化一个TModel eg: new{No=x.No...}
37                 MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(modelType), list);
38                 //创建lambda表达式  eg: x=>new{ No=x.No...}
39                 Expression<Func<TModelDto, TModel>> lambda = Expression.Lambda<Func<TModelDto, TModel>>(memberInitExpression, parameterExpression);
40                 //将lambda表达式生成委托
41                 Func<TModelDto, TModel> func = lambda.Compile();
42                 _dictionary[key] = func;
43             }
44             return ((Func<TModelDto, TModel>)_dictionary[key]).Invoke(dto);
45         }
46     }
复制代码

总结:使用表达式树的方式,可以将生成的委托保存起来,这里使用dictionary字典,在需要使用的时候调用即可(一次生成委托,后续可多次使用);既然是多个委托,那是不是可以使用的泛型委托Func<TIn,TResult>呢?

使用泛型缓存:

复制代码
 1     /// <summary>
 2     ///泛型委托基于泛型类之上
 3     ///泛型静态类在确定参数类型的时候会调用其静态函数
 4     ///在执行委托时,泛型委托会内置查找相应的委托来执行
 5     /// </summary>
 6     public static class ExpressionAndFuncMethod<TModel, TModelDto>
 7        where TModel : class
 8        where TModelDto : class
 9     {
10         static ExpressionAndFuncMethod()
11         {
12             ExpressionMapper();
13         }
14 
15         public static Func<TModelDto, TModel> _func = null;
16 
17         public static void ExpressionMapper()
18         {
19             Type modelType = typeof(TModel);
20             Type modelDtoType = typeof(TModelDto);
21 
22             //创建一个lambda参数x,定义的对象为TModelDto
23             ParameterExpression parameterExpression = Expression.Parameter(modelDtoType, "x");
24             //开始生成lambda表达式
25             List<MemberBinding> list = new List<MemberBinding>();
26             foreach (var item in modelType.GetProperties())
27             {
28                 //为x参数表达式生成一个属性值
29                 MemberExpression property = Expression.Property(parameterExpression, modelDtoType.GetProperty(item.Name));
30                 //将该属性初始化 eg:No=x.No
31                 MemberBinding memberBinding = Expression.Bind(item, property);
32                 list.Add(memberBinding);
33             }
34 
35             foreach (var item in typeof(TModel).GetFields())
36             {
37                 //为x参数表达式生成一个字段值
38                 MemberExpression field = Expression.Field(parameterExpression, modelDtoType.GetField(item.Name));
39                 //将该字段初始化
40                 MemberBinding memberBinding = Expression.Bind(item, field);
41                 list.Add(memberBinding);
42             }
43             //调用构造函数,初始化一个TModel eg: new{No=x.No...}
44             MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(modelType), list);
45             //创建lambda表达式  eg: x=>new{ No=x.No...}
46             Expression<Func<TModelDto, TModel>> lambda = Expression.Lambda<Func<TModelDto, TModel>>(memberInitExpression, parameterExpression);
47             //将lambda表达式生成委托
48             _func = lambda.Compile();
49         }
50 
51         public static TModel Trans(TModelDto dto)
52         {
53             if (_func != null)
54                 return _func(dto);
55             return default(TModel);
56         }
57     }
复制代码

总结:使用泛型委托时,即用到了其泛型缓存,在调用指定参数的委托时,能快速查找并调用(这个性能应该是高于使用字典的查找)

4.使用AutoMapper

复制代码
 1     public static class AutoMapperMethod
 2     {
 3         /// <summary>
 4         /// AutoMapper 必须先创建映射
 5         /// </summary>
 6         /// <param name="dictionary"></param>
 7         public static void Init(Dictionary<Type, Type> dictionary)
 8         {
 9             AutoMapper.Mapper.Initialize(x =>
10             {
11                 foreach (var item in dictionary)
12                 {
13                     x.CreateMap(item.Key, item.Value);
14                 }
15             });
16         }
17 
18 
19         public static TModel Trans<TModel, TModelDto>(TModelDto dto)
20                            where TModel : class
21        where TModelDto : class
22         {
23             return AutoMapper.Mapper.Map<TModelDto, TModel>(dto);
24         }
25     }
复制代码

总结:AutoMapper先创建再调用的原则,非常适合core项目的 注册-调用 思想,在Configure中进行注册,然后使用时Map即可,AutoMap使用emit代码开发(不太明白),性能很好,是现在最流行的映射工具。

最后,来看一下以上几种方式的性能对比吧,测试条件是将StudentsDto实例转换成Students实例,将TeachersDto实例转换成Teachers实例,各转换50万次,耗时如下图:

从上往下依次是:普通类型装载、反射、序列化、表达式缓存、表达式泛型缓存、AutoMapper

由此可见,反射和序列化是比较慢的,表达式和AutoMapper的表现差不多,一般项目中,类型映射的次数也不会很大,使用AutoMapper就已经非常够用了。

本人不才,还希望园内技术牛人多多指正。

公布一个 150 行左右的 ORM - Lenic - 博客园

mikel阅读(589)

来源: 公布一个 150 行左右的 ORM – Lenic – 博客园

今天,一个因为 ORM 的性能问题引发了一场血案,唉。。。

 

突然想起来几年前我写的一个小东西,放上来大家评论一下,有兴趣的可以测试一下性能,呵呵。

 

原理很简单,利用 Lambda 表达式树生成一个 Delegate ,然后缓存起来。不多说了,下面上代码:

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Lenic.Extensions;

namespace Lenic.Data.Extensions
{
    /// <summary>
    /// IDataReader 扩展方法集合
    /// </summary>
    [DebuggerStepThrough]
    public static class DataReaderExtensions
    {
        #region Private Methods
        private static readonly Dictionary<Type, Delegate> cache = new Dictionary<Type, Delegate>();
        private static readonly object cacheLocker = new object();
        #endregion

        #region Business Methods
        /// <summary>
        /// 返回指定字段的值, 并执行 ConvertTo 函数转换。
        /// </summary>
        /// <typeparam name="T">返回值类型。</typeparam>
        /// <param name="reader">一个实现了 IDataReader 接口的实例对象。</param>
        /// <param name="name">要查找的字段的名称。</param>
        /// <returns>转换完毕的 T 类型的结果。</returns>
        public static T Field<T>(this IDataReader reader, string name)
        {
            return reader[name].ConvertTo<T>(default(T), false);
        }

        /// <summary>
        /// 返回指定字段的值, 并执行 ConvertTo 函数转换。
        /// </summary>
        /// <typeparam name="T">返回值类型。</typeparam>
        /// <param name="reader">一个实现了 IDataReader 接口的实例对象。</param>
        /// <param name="index">要查找的字段的索引。</param>
        /// <returns>转换完毕的 T 类型的结果。</returns>
        public static T Field<T>(this IDataReader reader, int index)
        {
            return reader[index].ConvertTo<T>(default(T), false);
        }

        /// <summary>
        /// 解析当前 IDataReader 类型的实例对象并提取一个 T 类型的列表。
        /// </summary>
        /// <typeparam name="T">待解析的元素类型, 该类型必须包含一个默认的构造函数。</typeparam>
        /// <param name="reader">一个实现了 IDataReader 接口的实例对象。</param>
        /// <returns>一个 T 类型的列表。</returns>
        public static List<T> ToList<T>(this IDataReader reader) where T : class, new()
        {
            return Fill<T>(reader, DynamicCreateEntity<T>()).ToList();
        }

        /// <summary>
        /// 解析当前 IDataReader 类型的实例对象并提取一个 T 类型的列表。
        /// </summary>
        /// <typeparam name="T">待解析的元素类型, 该类型必须包含一个默认的构造函数。</typeparam>
        /// <param name="reader">一个实现了 IDataReader 接口的实例对象。</param>
        /// <param name="predicate">映射委托。</param>
        /// <returns>一个 T 类型的列表。</returns>
        public static List<T> ToList<T>(this IDataReader reader, Func<IDataReader, T> predicate)
            where T : class, new()
        {
            return Fill<T>(reader, predicate).ToList();
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// 创建一个 构造函数 委托。
        /// </summary>
        /// <typeparam name="T">构造目标类型。</typeparam>
        /// <returns>构造完毕的 Func 委托。</returns>
        private static Func<IDataReader, T> DynamicCreateEntity<T>() where T : class, new()
        {
            var type = typeof(T);
            if (cache.ContainsKey(type))
                return (Func<IDataReader, T>)cache[type];

            lock (cacheLocker)
            {
                if (cache.ContainsKey(type))
                    return (Func<IDataReader, T>)cache[type];

                var result = DynamicCreateEntityLogic<T>();
                cache.Add(type, result);
                return result;
            }
        }

        /// <summary>
        /// 创建一个 构造函数 委托(逻辑实现)。
        /// </summary>
        /// <typeparam name="T">构造目标类型。</typeparam>
        /// <returns>构造完毕的 Func 委托。</returns>
        private static Func<IDataReader, T> DynamicCreateEntityLogic<T>() where T : class, new()
        {
            // Compiles a delegate of the form (IDataReader r) => new T { Prop1 = r.Field<Prop1Type>("Prop1"), ... }
            ParameterExpression r = Expression.Parameter(typeof(IDataReader), "r");

            // Get Properties of the property can read and write
            var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.CanRead && p.CanWrite)
                .ToArray();

            // Create property bindings for all writable properties
            List<MemberBinding> bindings = new List<MemberBinding>(props.Length);

            // Get the binding method
            var method = typeof(DataReaderExtensions).GetMethods()
                .First(p => p.Name == "Field" &&
                            p.GetParameters().Length == 2 &&
                            p.GetParameters()[1].ParameterType == typeof(string));

            foreach (PropertyInfo property in (typeof(T).GetProperties()))
            {
                // Create expression representing r.Field<property.PropertyType>(property.Name)
                MethodCallExpression propertyValue = Expression.Call(
                    method.MakeGenericMethod(property.PropertyType),
                    r, Expression.Constant(property.Name));

                // Assign the property value to property through a member binding
                MemberBinding binding = Expression.Bind(property, propertyValue);
                bindings.Add(binding);
            }
            // Create the initializer, which instantiates an instance of T and sets property values

            // using the member bindings we just created
            Expression initializer = Expression.MemberInit(Expression.New(typeof(T)), bindings);

            // Create the lambda expression, which represents the complete delegate (r => initializer)
            Expression<Func<IDataReader, T>> lambda = Expression.Lambda<Func<IDataReader, T>>(initializer, r);

            return lambda.Compile();
        }

        /// <summary>
        /// 从一个 IDataReader 的实例对象中提取一个 T 类型的列表。
        /// </summary>
        /// <typeparam name="T">结果列表中的元素类型, 该类型必须包含一个默认的构造函数。</typeparam>
        /// <param name="reader">一个实现了 IDataReader 接口的实例对象。</param>
        /// <returns>一个 T 类型的列表。</returns>
        private static IEnumerable<T> Fill<T>(IDataReader reader, Func<IDataReader, T> predicate) where T : class, new()
        {
            while (reader.Read())
                yield return predicate(reader);
        }
        #endregion
    }
}

 

上面用到了一个全能转换方法,代码如下:

#region Type Convert Extensions
private static string typeIConvertibleFullName = typeof(IConvertible).FullName;

/// <summary>
/// 将当前实例对象类型转换为 T 类型.
/// </summary>
/// <typeparam name="T">目标类型.</typeparam>
/// <param name="obj">当前实例.</param>
/// <returns>转换完成的 T 类型的一个实例对象.</returns>
public static T ConvertTo<T>(this object obj)
{
    return ConvertTo(obj, default(T));
}

/// <summary>
/// 将当前实例对象类型转换为 T 类型.
/// </summary>
/// <typeparam name="T">目标类型.</typeparam>
/// <param name="obj">当前实例.</param>
/// <param name="defaultValue">转换失败时的返回值.</param>
/// <returns>转换完成的 T 类型的一个实例对象.</returns>
public static T ConvertTo<T>(this object obj, T defaultValue)
{
    if (obj != null)
    {
        if (obj is T)
            return (T)obj;

        var sourceType = obj.GetType();
        var targetType = typeof(T);

        if (targetType.IsEnum)
            return (T)Enum.Parse(targetType, obj.ToString(), true);

        if (sourceType.GetInterface(typeIConvertibleFullName) != null &&
            targetType.GetInterface(typeIConvertibleFullName) != null)
            return (T)Convert.ChangeType(obj, targetType);

        var converter = TypeDescriptor.GetConverter(obj);
        if (converter != null && converter.CanConvertTo(targetType))
            return (T)converter.ConvertTo(obj, targetType);

        converter = TypeDescriptor.GetConverter(targetType);
        if (converter != null && converter.CanConvertFrom(sourceType))
            return (T)converter.ConvertFrom(obj);

        throw new ApplicationException("convert error.");
    }
    throw new ArgumentNullException("obj");
}

/// <summary>
/// 将当前实例对象类型转换为 T 类型.
/// </summary>
/// <typeparam name="T">目标类型.</typeparam>
/// <param name="obj">当前实例.</param>
/// <param name="defaultValue">转换失败时的返回值.</param>
/// <param name="ignoreException">如果设置为 <c>true</c> 表示忽略异常信息, 直接返回缺省值.</param>
/// <returns>转换完成的 T 类型的一个实例对象.</returns>
public static T ConvertTo<T>(this object obj, T defaultValue, bool ignoreException)
{
    if (ignoreException)
    {
        try
        {
            return obj.ConvertTo<T>(defaultValue);
        }
        catch
        {
            return defaultValue;
        }
    }
    return obj.ConvertTo<T>(defaultValue);
}
#endregion

 

再次欢迎大家品鉴。

对象克隆(C# 快速高效率复制对象另一种方式 表达式树转) - 三小 - 博客园

mikel阅读(633)

来源: 对象克隆(C# 快速高效率复制对象另一种方式 表达式树转) – 三小 – 博客园

1、需求

在代码中经常会遇到需要把对象复制一遍,或者把属性名相同的值复制一遍。

比如:

复制代码
复制代码
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; } 
        public int Age { get; set; } 
    }

    public class StudentSecond
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; } 
    }
复制代码
复制代码

Student s = new Student() { Age = 20, Id = 1, Name = “Emrys” };

我们需要给新的Student赋值

Student ss = new Student { Age = s.Age, Id = s.Id, Name = s.Name };

再或者给另一个类StudentSecond的属性赋值,两个类属性的名称和类型一致。

StudentSecond ss = new StudentSecond { Age = s.Age, Id = s.Id, Name = s.Name };

 

2、解决办法

当然最原始的办法就是把需要赋值的属性全部手动手写。这样的效率是最高的。但是这样代码的重复率太高,而且代码看起来也不美观,更重要的是浪费时间,如果一个类有几十个属性,那一个一个属性赋值岂不是浪费精力,像这样重复的劳动工作更应该是需要优化的。

2.1、反射

反射应该是很多人用过的方法,就是封装一个类,反射获取属性和设置属性的值。

复制代码
复制代码
 private static TOut TransReflection<TIn, TOut>(TIn tIn)
        {
            TOut tOut = Activator.CreateInstance<TOut>();
            var tInType = tIn.GetType();
            foreach (var itemOut in tOut.GetType().GetProperties())
            {
                var itemIn = tInType.GetProperty(itemOut.Name); ;
                if (itemIn != null)
                {
                    itemOut.SetValue(tOut, itemIn.GetValue(tIn));
                }
            }
            return tOut;
        }
复制代码
复制代码

 

调用:StudentSecond ss= TransReflection<Student, StudentSecond>(s);

调用一百万次耗时:2464毫秒

 

2.2、序列化

序列化的方式有很多种,有二进制、xml、json等等,今天我们就用Newtonsoft的json进行测试。

调用:StudentSecond ss= JsonConvert.DeserializeObject<StudentSecond>(JsonConvert.SerializeObject(s));

调用一百万次耗时:2984毫秒

从这可以看出序列化和反射效率差别不大。

 

3、表达式树

3.1、简介

关于表达式树不了解的可以百度。

也就是说复制对象也可以用表达式树的方式。

        Expression<Func<Student, StudentSecond>> ss = (x) => new StudentSecond { Age = x.Age, Id = x.Id, Name = x.Name };
        var f = ss.Compile();
        StudentSecond studentSecond = f(s);

这样的方式我们可以达到同样的效果。

有人说这样的写法和最原始的复制没有什么区别,代码反而变多了呢,这个只是第一步。

 

3.2、分析代码

我们用ILSpy反编译下这段表达式代码如下:

复制代码
复制代码
   ParameterExpression parameterExpression;
    Expression<Func<Student, StudentSecond>> ss = Expression.Lambda<Func<Student, StudentSecond>>(Expression.MemberInit(Expression.New(typeof(StudentSecond)), new MemberBinding[]
    {
        Expression.Bind(methodof(StudentSecond.set_Age(int)), Expression.Property(parameterExpression, methodof(Student.get_Age()))),
        Expression.Bind(methodof(StudentSecond.set_Id(int)), Expression.Property(parameterExpression, methodof(Student.get_Id()))),
        Expression.Bind(methodof(StudentSecond.set_Name(string)), Expression.Property(parameterExpression, methodof(Student.get_Name())))
    }), new ParameterExpression[]
    {
        parameterExpression
    });
    Func<Student, StudentSecond> f = ss.Compile();
    StudentSecond studentSecond = f(s);
复制代码
复制代码

那么也就是说我们只要用反射循环所有的属性然后Expression.Bind所有的属性。最后调用Compile()(s)就可以获取正确的StudentSecond。

看到这有的人又要问了,如果用反射的话那岂不是效率很低,和直接用反射或者用序列化没什么区别吗?

当然这个可以解决的,就是我们的表达式树可以缓存。只是第一次用的时候需要反射,以后再用就不需要反射了。

 

3.3、复制对象通用代码

为了通用性所以其中的Student和StudentSecond分别泛型替换。

复制代码
复制代码
        private static Dictionary<string, object> _Dic = new Dictionary<string, object>();

        private static TOut TransExp<TIn, TOut>(TIn tIn)
        {
            string key = string.Format("trans_exp_{0}_{1}", typeof(TIn).FullName, typeof(TOut).FullName);
            if (!_Dic.ContainsKey(key))
            {
                ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
                List<MemberBinding> memberBindingList = new List<MemberBinding>();

                foreach (var item in typeof(TOut).GetProperties())
                {  
  
            if (!item.CanWrite)
              continue;
                    MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
                    MemberBinding memberBinding = Expression.Bind(item, property);
                    memberBindingList.Add(memberBinding);
                }

                MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
                Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[] { parameterExpression });
                Func<TIn, TOut> func = lambda.Compile();

                _Dic[key] = func;
            }
            return ((Func<TIn, TOut>)_Dic[key])(tIn);
        }
复制代码
复制代码

调用:StudentSecond ss= TransExp<Student, StudentSecond>(s);

调用一百万次耗时:564毫秒

 

3.4、利用泛型的特性再次优化代码

不用字典存储缓存,因为泛型就可以很容易解决这个问题。

 

复制代码
复制代码
 public static class TransExpV2<TIn, TOut>
    {

        private static readonly Func<TIn, TOut> cache = GetFunc();
        private static Func<TIn, TOut> GetFunc()
        {
            ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
            List<MemberBinding> memberBindingList = new List<MemberBinding>();

            foreach (var item in typeof(TOut).GetProperties())
            {
         if (!item.CanWrite)
              continue;
                MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
                MemberBinding memberBinding = Expression.Bind(item, property);
                memberBindingList.Add(memberBinding);
            }

            MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
            Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[] { parameterExpression });

            return lambda.Compile();
        }

        public static TOut Trans(TIn tIn)
        {
            return cache(tIn);
        }

    }
复制代码
复制代码

 

调用:StudentSecond ss= TransExpV2<Student, StudentSecond>.Trans(s);

调用一百万次耗时:107毫秒

耗时远远的小于使用automapper的338毫秒。

 

4、总结

从以上的测试和分析可以很容易得出,用表达式树是可以达到效率书写方式二者兼备的方法之一,总之比传统的序列化和反射更加优秀。

最后望对各位有所帮助,本文原创,欢迎拍砖和推荐

干货来袭-整套完整安全的API接口解决方案 - hubro - 博客园

mikel阅读(517)

来源: 干货来袭-整套完整安全的API接口解决方案 – hubro – 博客园

在各种手机APP泛滥的现在,背后都有同样泛滥的API接口在支撑,其中鱼龙混杂,直接裸奔的WEB API大量存在,安全性令人堪优

在以前WEB API概念没有很普及的时候,都采用自已定义的接口和结构,对于公开访问的接口,专业点的都会做下安全验证,数据签名之类

反而现在,谁都可以用WEB API估接口,安全性早忘一边了,特别是外包小公司的APP项目,80%都有安全漏洞(面试了大半年APP开发得出的结论)

特在过年之前,整理了下在用的解决方案,本方案解决了

  • 数据安全问题
  • 标准消息结构
  • 接口测试程序
  • 接口文档体现

正文

数据结构

对于一个接口,返回的内容除了要返回业务数据外,还得返回处理状态,并且这个状态是在每个接口都得有

所以数据格式都会定义为:

数据头(描述数据信息)

———————————–

数据体(具体数据)

本文定义结构为

复制代码
/// <summary>
    /// 处理结果
    /// </summary>
    public class DealResult
    {
        /// <summary>
        /// 处理结果
        /// </summary>
        public bool Result
        {
            get;
            set;
        }
        /// <summary>
        /// 消息
        /// </summary>
        public string Message
        {
            get;
            set;
        }
        /// <summary>
        /// 关联数据
        /// </summary>
        public object Data
        {
            get;
            set;
        }
    }
复制代码

所有接口都返回此对象,会描述本次请求的状态,和对应的数据,服务端则根据实际情况,返回处理结果和对应的数据

 

数据安全

开方式接口安全性就不用多说了,解决方法为加密,或数据签名验证,本文方案为进行数据签名

同返回的数据一样,提交到服务器的数据格式也统一约定,定义一个数据头基类

复制代码
    /// <summary>
    /// 参数基类
    /// </summary>
    [Serializable]
    public class ParameBase
    {
        string time = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
        /// <summary>
        /// 时间 格式 yyyy-MM-dd hh:mm:ss
        /// </summary>
        public string Time
        {
            get
            {
                return time;
            }
            set
            {
                time = value;
            }
        }
        /// <summary>
        /// 来源网站 = 1, IOS = 2,Android = 3, 微信 = 4
        /// </summary>
        public int SourceFrom
        {
            get;
            set;
        }
        /// <summary>
        /// 签名
        /// </summary>
        public string Token
        {
            get;
            set;
        }
       
    }
复制代码

 

一个登录对象表示为

复制代码
    /// <summary>
    /// 登录
    /// </summary>
    public class Login : ParameBase
    {
        /// <summary>
        /// 用户名
        /// </summary>
        public string Name
        {
            get;
            set;
        }
        /// <summary>
        /// 密码
        /// </summary>
        public string Password
        {
            get;
            set;
        }
    }
复制代码

数据签名表示为(KEY稍后讲到)

Token=MD5(属性值1+值2….+KEY)

按此对象表示为 MD5(Name+PassWord+Source+Time+KEY)

如果是GET参数怎么办,一样,按参数名计算,同时传递的参数要附带上Source,Time,Token

 

密钥机制

有的喜欢把密钥放在客户端,或固定密钥,显然都有安全问题,解决方法是动态获取

这就意味着在设计接口时,有一个接口是首先要调用的,让服务器返回密钥,于是就有了登录的概念

过程表示为

登录>返回用户信息和密钥=>存储用户信息和密钥=>使用密钥调用其它接口

这样只有登录者和服务器才知道自已的密钥了

综上所述,数据结构表示为

客户端提交结构为 ParameBase(附带签名信息)

服务端返回结构为 DealResult

 

登录机制

同网页请求一样,怎么知道多次调用是同一个人呢,这里采用了COOKIE的形式,登录后服务端返回一个COOKIE,客户端再请求时带上这个COOKIE

服务端需要存储这个COOKIE标识,所有的验证处理都会基于此标识来判断用户

 

有了上面基础,进入项目阶段

WEB API项目

其实用什么项目类型都行,只是WEB API方便了对象结构序列化和传参

默认WEB API路由RESUFUL形式,没有控制器方法,只能按METHOD来定义,很不方便,改成控制器的形式,这样就能用方法名来访问了

更改路由配置为

1
2
3
4
5
config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",//加上路由ACTION参数
                defaults: new { id = RouteParameter.Optional }
            );

在此文,数据分为请求和返回,以登录返回用户信息为例,登录为请求,用户信息为返回,示例对象结构为

用户对象

复制代码
/// <summary>
    /// 登录返回用户
    /// </summary>
    public class User
    {
        /// <summary>
        /// 用户编号
        /// </summary>
        public int Id
        {
            get;
            set;
        }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name
        {
            get;
            set;
        }
        /// <summary>
        /// 本次登录的KEY
        /// </summary>
        public string Key
        {
            get;
            set;
        }
        /// <summary>
        /// 本资登录的凭证
        /// </summary>
        public string Voucher
        {
            get;
            set;
        }

       
    }
复制代码

 

请求方式

这里只采用了GET,POST两种方式,根据实际情况定义,控制器方法一定需要都标明,不然会出现路由BUG

定义登录方法

复制代码
/// <summary>
        /// 登录
        /// </summary>
        /// <param name="parame"></param>
        /// <returns>User</returns>
        [HttpPost]
        [AnonymousSign]
        public DealResult Login([FromBody] Login parame)
        {
            if (parame.Password != "123")
            {
                return DealResult(false, "密码不正确");
            }
            string key2 = System.Guid.NewGuid().ToString();
            string voucher = System.Guid.NewGuid().ToString();
            var user = new User() { Name = parame.Name, Id = 1, Key = key2, Voucher = voucher };
            var timeDiff = (DateTime.Now - Convert.ToDateTime(parame.Time)).TotalSeconds;//保存客户端和服务端时间差
            LoginStatusContext.SetLoginStatus(voucher, user.Id, key2, timeDiff);
            CoreHelper.CookieHelper.AddCookies("user", voucher);//存入COOKIE
            return DealResult(true, "", user);
        }
复制代码

这里可以看到,创建了两个GUID,一个为用户凭证,一个为用户密钥,放入用户信息返回,同时调用LoginStatusContext.SetLoginStatus保存登录信息

同时使用了AnonymousSign标注,此方法使用默认签名Setting.DefaultKey

定义获取用信息方法

复制代码
        /// <summary>
        /// 基本信息
        /// </summary>
        /// <param name="name">参数name</param>
        /// <returns>User</returns>
        [HttpGet]
        public DealResult GetBasicInfo(string name)
        {
            var user = new User() { Name = name, Id = CurrentUserId };
            return DealResult(true, string.Empty, user);
        }
复制代码

 

示例控制器完整定义

复制代码
 /// <summary>
    /// 帐号操作
    /// </summary>
    [SignCheckAttribute]
    public class AccountController : BaseController
    {
        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="parame"></param>
        /// <returns>User</returns>
        [HttpPost]
        [AnonymousSign]
        public DealResult Login([FromBody] Login parame)
        {
            if (parame.Password != "123")
            {
                return DealResult(false, "密码不正确");
            }
            string key2 = System.Guid.NewGuid().ToString();
            string voucher = System.Guid.NewGuid().ToString();
            var user = new User() { Name = parame.Name, Id = 1, Key = key2, Voucher = voucher };
            var timeDiff = (DateTime.Now - Convert.ToDateTime(parame.Time)).TotalSeconds;//保存客户端和服务端时间差
            LoginStatusContext.SetLoginStatus(voucher, user.Id, key2, timeDiff);
            CoreHelper.CookieHelper.AddCookies("user", voucher);//存入COOKIE
            return DealResult(true, "", user);
        }


        /// <summary>
        /// 基本信息
        /// </summary>
        /// <param name="name">参数name</param>
        /// <returns>User</returns>
        [HttpGet]
        public DealResult GetBasicInfo(string name)
        {
            var user = new User() { Name = name, Id = CurrentUserId };
            return DealResult(true, string.Empty, user);
        }

        /// <summary>
        /// 测试异常
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public DealResult TestException()
        {
            int a = 0;
            var b = 10 / a;
            return DealResult(true);
        }

    }
复制代码

 

此控制器标注了SignCheckAttribute用以进行签名判断

具体实现可看SignCheckAttribute代码

SignCheckAttribute里实现了有

  • 数据签名判断
  • 签名超时判断
  • 用户登录限制
  • 签名重复使用处理(一个签名只能使用一次)
  • 过期登录用户处理(没有主动退出用户清理)

为了统一处理异常,配置了异常处理

1
GlobalConfiguration.Configuration.Filters.Add(new ExceptionAttribute());

对接口进行测试

大杀器来了,配合此方案放出了对应的测试工具,虽然WEB API有个扩展,但没法对此方案测试

使用此工具能方便按方案要求调用接口,为了方便参数拼接,POST和GET都采用URL参数的形式输入

测试登录/api/account/login

测试获取信息/api/account/GetBasicInfo

测试异常处理/api/account/TestException

在未登录情况下调用获取信息

接口文档

接口结构文档一直是很让人头疼的事,手写更改了又得维护,版本不一样还麻烦,自动生成最好了,同样WEB API 带扩展没法表示此结构详细

大杀器2号来了,按代码注释动态生成接口文档,文档格式与控制器保持一致

Home控制器代码实现

复制代码
    public ActionResult Index(SummaryAnalysis.ExportType exportType = SummaryAnalysis.ExportType.NONE)
        {
            if (exportType != SummaryAnalysis.ExportType.NONE)
            {
                var str = SummaryAnalysis.Load(exportType);
                return File(str, "application/octet-stream", "Model_" + exportType + ".zip");
            }
            else
            {
                if (string.IsNullOrEmpty(outPut))
                {
                    outPut = SummaryAnalysis.Load(exportType);
                }
                ViewBag.OutPut = outPut;
                return View();
            }
        }
    }
复制代码

在见过的开发文档,我觉得这是最好的展现形式了,还有锚点,快速定位到对象结构,并且与源代码保持一致

附WEB API 自带文档生成区别

附上项目源码

http://pan.baidu.com/s/1c2rDacK

项目结构:

———-WPF测试程序

———-接口示例

虽然跟CRL快速开发框架无关,但还是加上CRL的名,好文要顶!

打造独特的ORM开发框架 - hubro - 博客园

mikel阅读(670)

来源: 打造独特的ORM开发框架 – hubro – 博客园

ORM一直是长久不衰的话题,各种重复造轮子的过程一直在进行,轮子都一样是圆的,你的又有什么特点呢?

CRL这个轮子造了好多年,功能也越来越标准完备,在开发过程中,解决了很多问题,先上一张脑图描述CRL的功能

开发框架的意义在于

  • 开发更标准,更统一,不会因为不同人写的代码不一样
  • 开发效率更高,无需重新造轮子,重复无用的代码,同时简化开发流程
  • 运行效率得到控制,程序稳定性得到提高

围绕这几点,抛开常规的增删改查,我们来讲些与众不同的

1.与众不同之一,动态数据源,天生适合分库分表

可动态配置的功能总比静态的灵活,扩展性强

目前看到的框架多数访问对象实例化都类似于

var context = new MsSqlContext(ConnectionString);

在对象初始时,就绑定上了数据库连接串, 这样写没什么问题,但是不好扩展
如:需要动态切换库,表,根据租户信息访问不同的数据库,或不同类型的数据库,或是读写分离,这时,急需处理的技术问题就来了,分库分表的解决方案,读写分离的方案
在数据连接绑定的情况下,这种问题很不好解决
又或者传入多个连接串,在调用时,手动选择调用的库或表,对于这种方式,只能说耦合太严重,得关心配置,又得关心调用,在CRL之前的版本里,有这样实现过,弃用了

然而根据IOC的理念,这种问题也不是不好解决,让数据访问对象抽象化实现就能办到了
数据查询方法不再直接调用数据访问对象,而是调用抽象工厂方法,由抽象工厂方法来实例化访问对象,过程表示为

数据查询方法(组件内) => 抽象工厂(组件内) => 抽象实现(组件外)

基于这样的理念,CRL在设计之初,就使用了的这样的方式,以代码为例

!数据访问实现

以下实现了分库分表和mongoDB切换

以下在程序启动时初始

复制代码
var builder = new CRL.SettingConfigBuilder();
            builder.UseMongoDB();//引用CRL.Mongo 使用MongoDB
                                 //注册自定义定位,按MemberSharding传入数据定义数据源位置
                                 //注册一
            builder.RegisterLocation<Code.Sharding.MemberSharding>((t, a) =>
            {
                var tableName = t.TableName;
                if (a.Name == "hubro")//当名称为hubro,则定位到库testdb2 表MemberSharding1
                {
                    tableName = "MemberSharding1";
                    return new CRL.Sharding.Location("testdb2", tableName);
                }
                //返回定位库和表名
                return new CRL.Sharding.Location("testdb", tableName);
            });
            //注册二
            builder.RegisterDBAccessBuild(dbLocation =>
            {
                if (dbLocation.ManageName == "mongo")
                {
                    var conn = CRL.Core.CustomSetting.GetConfigKey("mongodb");
                    return new CRL.DBAccessBuild(DBType.MongoDB, conn);
                }
                return null;
            });
            //注册三
            builder.RegisterDBAccessBuild(dbLocation =>
            {
                //自定义定位,由注册一传入
                if (dbLocation.ShardingLocation != null)
                {
                    return new CRL.DBAccessBuild(DBType.MSSQL, "Data Source=.;Initial Catalog=" + dbLocation.ShardingLocation.DataBaseName + ";User ID=sa;Password=123");
                }
                return new CRL.DBAccessBuild(DBType.MSSQL, "server=.;database=testDb; uid=sa;pwd=123;");
            });
复制代码

 

!数据访问类,类似于仓储的形式,根据实际业务实现
定位使用示例

复制代码
public class MemberManage : CRL.Sharding.BaseProvider<MemberSharding>
{
}
var instance=new MemberManage();
instance.Add(new MemberSharding(){Name="hubro"});
复制代码

根据定位规则 运行到注册一,此数据将会插入到 库testdb2 表MemberSharding1

常规切换示例

1
2
3
4
5
6
public class MongoDBTestManage : CRL.BaseProvider<MongoDBModel2>
{
    public override string ManageName => "mongo";
}
var instance=new MongoDBTestManage();
instance.Add(new MongoDBModel2(){name="hubro"});

根据数据访问规则,运行到注册二,此数据将会插入mongodb

可以看到,在上面代码中,没有看到任何数据连接串的传入,数据的访问都由初始时动态分配,对于方法调用是不透明的,调用者不用关心数据源的问题

2.与众不同之二,表结构自动维护

在新技术的支持下,程序和数据库的绑定关系越来越模糊,现在可能是用的SQLServer,回头可能改成MySQL了,或者改成mongoDB
依赖数据库开发变成越来越不可取,效率也很低
再后来出现了DBFirst方式,虽解决了部份问题,但也很麻烦,如:

建立数据库模型=>导入数据库=>T4模版生成代码(修修补补)

而使用CRL后,过程一步到位,在别人还在用PM设计表结构索引时,你已经设计好了业务结构,效率杠杠的

编写实体类,实现对象访问=>调试运行,自动创建表结构(关键字,长度,索引)

同时,CRL还提供了手动维护方法,使能够按实体结构重建/检查数据表
也提供了对象结构文档导出,不用提心文档的问题
详细介绍看这里
https://www.cnblogs.com/hubro/p/6038118.html

3.与众不同之三,动态缓存

使用缓存可以大大提高程序的运行效率,使用REDIS或MONGODB之类的又需要额外维护
对于单应用程序,程序集内缓存非常有用
CRL内置了缓存实现和维护
只需按方法调用就行了,缓存创建维护全自动
如:
从数据库查

var item = instance.QueryItem(b => b.Id==1)

从缓存查

var item = instance.QueryItemFromCache(b=>b.Id==1);

也支持按查询自定义缓存

复制代码
var query = Code.ProductDataManage.Instance.GetLambdaQuery();
//缓存会按条件不同缓存不同的数据,条件不固定时,慎用
query.Where(b => b.Id < 700);
int exp = 10;//过期分钟
query.Expire(exp);
var list = query.ToList();
复制代码

 

基于这样的形式,可以将所有查询都走缓存,再也不用担心数据库查询效率了,简值中小项目开发利器
详细介绍看这里
https://www.cnblogs.com/hubro/p/6038540.html

4.与众不同之四,应对复杂查询

因为没有查询分支的概念,处理复杂的查询,一票ORM估计得退场了,虽然合理的结构设计会减少查询复杂度,但谁能保证呢
CRL查询分支过程如下

主查询 => CreateQuery子查询 => 返回匿名对象筛选LambdaQueryResultSelect => 主查询嵌套子查询 => 返回结果

理论上只要符合调用逻辑,可以无限嵌套
示例:

复制代码
var q1 = Code.OrderManage.Instance.GetLambdaQuery();//主查询
            var q2 = q1.CreateQuery<Code.ProductData>();//创建一个子查询
            q2.Where(b => b.Id > 0);
            var view = q2.CreateQuery<Code.Member>().GroupBy(b => b.Name).Where(b => b.Id > 0).Select(b => new { b.Name, aa = b.Id.COUNT() });//GROUP查询
            var view2 = q2.Join(view, (a, b) => a.CategoryName == b.Name).Select((a, b) => new { ss1 = a.UserId, ss2 = b.aa });//关联GROUP
            q1.Join(view2, (a, b) => a.Id == b.ss1).Select((a, b) => new { a.Id, b.ss1 });//再关联
            var result = view2.ToList();
            var sql = q1.ToString();
复制代码

生成SQL打印如下

复制代码
SELECT t1.[Id] AS Id,
t2.[ss1] AS ss1
FROM [OrderProduct] t1 with(nolock)
INNER JOIN
(SELECT t2.[UserId] AS ss1,
t3.[aa] AS ss2
FROM [ProductData] t2 with(nolock)
INNER JOIN
(SELECT t3.[Name] AS Name,
COUNT(t3.Id) AS aa
FROM [Member] t3 with(nolock)
WHERE (t3.[Id]>@par1)
GROUP BY t3.[Name]) t3 ON (t2.[CategoryName]=t3.[Name])
WHERE (t2.[Id]>@par0) ) t2 ON (t1.[Id]=t2.[ss1])
复制代码

 

不管是JOIN后再GROUP,还是GROUP后再GROUP,还是GROUP后再JOIN,通通不是问题
详细介绍看这里
https://www.cnblogs.com/hubro/p/6096544.html

5.与众不同之五,查询抽象,非关系型数据库支持

通过对Lambda表达式的解析,可以实现不同的查询转换,如MongoDB,或ElasticSearch(目前只实现了MongoDB)
有人问,这样有什么用呢?
好处就是,在CRL框架下,一套LambdaQuery走天下,不用写各种差异很大的查询方法了,在动态数据源的支持下,数据拆分游刃有余
如:
之前有个报表存在MSSQL里,发现数据量太大了,查询慢,改由MongoDB,程序不用怎么调整,直接在配置里改为MongoDB即可

以MongoDB为例

CRLLambdaQuery=>CRLExpression=>BsonDocument=>MongoDB

在[数据访问实现]示例中,演示了如何切换到MongoDB
代码实现见项目:CRL.Mongo

6.题外之六,请使用仓储模式

在上文提到,好多框架会直接返回一个数据访问对象,如

var obj1context.Query<TestEntity>(b=>b.Id==1).ToSingle();

然而这样会导致滥用,直接在WEB层用,在Service层随意用,如

var obj2=context.Query<TestEntity2>(b=>b.Id==1).ToSingle();
var obj3=context.Query<TestEntity3>(b=>b.Id==1).ToSingle();

某一天,TestEntity3要换库了,查找一下引用,傻眼了,上百个引用(接手别人的项目,亲身体验过这种痛苦,一个个改)
好在CRL开始就杜绝了这种情况发生,对据的访问必须通过BaseProvider实现,而BaseProvider就是一个仓储的形式

7.题外之七,查询效率

ORM效率无非分两点,实体映射效率和语法解析效率,

对于映射反映在,一次返回多行数据,转换为实体集合

对于语法解析效率,按参数调用多次,返回一行数据,转换为实体

测式程序和SQL为本机,CPU空闲正常,2核6G服务器

一张图表明一切(不同机器实际情况可能有差异)

CRL效率虽不是最高的,但也不是最差的,测试项目见:

https://github.com/hubro-xx/CRL5/tree/master/Test/TestConsole

 

大概列举了以上几项,还有好多特有的东西,轮子好不好,东西南北滚滚试试

CRL开发框架虽然写好长时间,但一直在Debug状态中, 最近又升级了,分离了数据访问层,不同数据库引用不同的数据访问层,数据访问层实现也很简单,只需要写两个文件,如MySql,实现MySqlHelper和MySQLDBAdapter
见:https://github.com/hubro-xx/CRL5/tree/master/CRL.Providers/CRL.MySql
同时,版本也升级到5.1,项目结构发生了改变

源码地址:https://github.com/hubro-xx/CRL5

CRL目前.NET版本为.net 4.5, 有时间了再整理整理netstandard版本

除了ORM,CRL还带 动态API,RPC,WebSocket,api客户端代理实现
https://www.cnblogs.com/hubro/p/11652687.html
微服务注册,发现,调用集成参见:
https://github.com/hubro-xx/CRL5/blob/master/Consul/ConsulTest/Program.cs