layui form表单 input输入框获取焦点后 阻止Enter回车自动提交 - &执念 - 博客园

mikel阅读(2554)

来源: layui form表单 input输入框获取焦点后 阻止Enter回车自动提交 – &执念 – 博客园

最简单的解决办法,不影响其他操作,给提交按钮增加 type=”button” 属性 完美解决

<button type="button" class="layui-btn" lay-submit lay-filter="*">立即提交</button>

layui表格编辑状态下回车进入下一单元格

$(document).keyup(function (event) {
if (event.keyCode == “13”) {
if ($($(“.layui-table-edit”).parent().next()).length > 0) {
$($(“.layui-table-edit”).parent().next()).click();
} else {
if ($(“.layui-table-edit”).parent().parent().next().find(“td[data-field=’OldMeterNo’]”).length > 0) {
$($(“.layui-table-edit”).parent().parent().next().find(“td[data-field=’OldMeterNo’]”)).click();
}
}
}
});

 

SQLServer之创建索引视图_小子pk了的博客-CSDN博客_索引视图

mikel阅读(870)

来源: SQLServer之创建索引视图_小子pk了的博客-CSDN博客_索引视图

索引视图创建注意事项

对视图创建的第一个索引必须是唯一聚集索引。 创建唯一聚集索引后,可以创建更多非聚集索引。 为视图创建唯一聚集索引可以提高查询性能,因为视图在数据库中的存储方式与具有聚集索引的表的存储方式相同。 查询优化器可使用索引视图加快执行查询的速度。 要使优化器考虑将该视图作为替换,并不需要在查询中引用该视图。

索引视图中列的 large_value_types_out_of_row 选项的设置继承的是基表中相应列的设置。 此值是使用 sp_tableoption设置的。从表达式组成的列的默认设置为 0。 这意味着大值类型存储在行内。

可以对已分区表创建索引视图,并可以由其自行分区。

若要防止 数据库引擎 使用索引视图,请在查询中包含 OPTION (EXPAND VIEWS) 提示。 此外,任何所列选项设置不正确均会阻止优化器使用视图上的索引。 有关 OPTION (EXPAND VIEWS) 提示的详细信息,请参阅 SELECT (Transact-SQL)。

若删除视图,该视图的所有索引也将被删除。 若删除聚集索引,视图的所有非聚集索引和自动创建的统计信息也将被删除。 视图中用户创建的统计信息受到维护。 非聚集索引可以分别删除。 删除视图的聚集索引将删除存储的结果集,并且优化器将重新像处理标准视图那样处理视图。

可以禁用表和视图的索引。 禁用表的聚集索引时,与该表关联的视图的索引也将被禁用。

索引视图创建要求

创建索引视图需要执行下列步骤并且这些步骤对于成功实现索引视图而言非常重要:

  1. 验证是否视图中将引用的所有现有表的 SET 选项都正确。
  2. 在创建任意表和视图之前,验证会话的 SET 选项设置是否正确。
  3. 验证视图定义是否为确定性的。
  4. 使用 WITH SCHEMABINDING 选项创建视图。
  5. 为视图创建唯一的聚集索引。

索引视图所需的 SET 选项

如果执行查询时启用不同的 SET 选项,则在 数据库引擎 中对同一表达式求值会产生不同结果。 例如,将 SET 选项 CONCAT_NULL_YIELDS_NULL 设置为 ON 后,表达式 ‘ abc ‘ + NULL 会返回值 NULL。 但将 CONCAT_NULL_YIEDS_NULL 设置为 OFF 后,同一表达式会生成 ‘ abc ‘。

为了确保能够正确维护视图并返回一致结果,索引视图需要多个 SET 选项具有固定值。 下表中的 SET 选项必须设置中显示的值为RequiredValue列出现以下情况时:

  • 创建视图和视图上的后续索引。
  • 在创建表时,在视图中引用的基表。
  • 对构成该索引视图的任何表执行了任何插入、更新或删除操作。 此要求包括大容量复制、复制和分布式查询等操作。
  • 查询优化器使用该索引视图生成查询计划。
    SET 选项 必需的值 默认服务器值 ,则“默认”

    OLE DB 和 ODBC 值

    ,则“默认”

    DB-Library 值

    ANSI_NULLS ON ON ON OFF
    ANSI_PADDING ON ON ON OFF
    ANSI_WARNINGS* ON ON ON OFF
    ARITHABORT ON ON OFF OFF
    CONCAT_NULL_YIELDS_NULL ON ON ON OFF
    NUMERIC_ROUNDABORT OFF OFF OFF OFF
    QUOTED_IDENTIFIER ON ON ON OFF

    *将 ANSI_WARNINGS 设置为 ON 隐式将 ARITHABORT 设置为 ON。

    如果使用的是 OLE DB 或 ODBC 服务器连接,则唯一必须要修改的值是 ARITHABORT 设置。 必须使用 sp_configure 在服务器级别或使用 SET 命令从应用程序中正确设置所有 DB-Library 值。极力建议在服务器的任一数据库中创建计算列的第一个索引视图或索引后,尽早在服务器范围内将 ARITHABORT 用户选项设置为 ON。

确定性视图

索引视图的定义必须是确定性的。 如果选择列表中的所有表达式、WHERE 和 GROUP BY 子句都具有确定性,则视图也具有确定性。 在使用特定的输入值集对确定性表达式求值时,它们始终返回相同的结果。 只有确定性函数可以加入确定性表达式。 例如,DATEADD 函数是确定性函数,因为对于其三个参数的任何给定参数值集它总是返回相同的结果。 GETDATE 不是确定性函数,因为总是使用相同的参数调用它,而它在每次执行时返回结果都不同。

要确定视图列是否为确定性列,请使用 COLUMNPROPERTY 函数的 IsDeterministic 属性。 使用 COLUMNPROPERTY 函数的 IsPrecise 属性确定具有架构绑定的视图中的确定性列是否为精确列。 如果为 TRUE,则 COLUMNPROPERTY 返回 1;如果为 FALSE,则返回 0;如果输入无效,则返回 NULL。 这意味着该列不是确定性列,也不是精确列。

即使是确定性表达式,如果其中包含浮点表达式,则准确结果也会取决于处理器体系结构或微代码的版本。 为了确保数据完整性,此类表达式只能作为索引视图的非键列加入。 不包含浮点表达式的确定性表达式称为精确表达式。 只有精确的确定性表达式才能加入键列,并包含在索引视图的 WHERE 或 GROUP BY 子句中。

其他要求

除对 SET 选项和确定性函数的要求外,还必须满足下列要求:

    • 执行 CREATE INDEX 的用户必须是视图所有者。
    • 创建索引时,IGNORE_DUP_KEY 选项必须设置为 OFF(默认设置)。
    • 在视图定义中,表必须由两部分组成的名称(即 schema.tablename**)引用。
    • 必须已使用 WITH SCHEMABINDING 选项创建了在视图中引用的用户定义函数。
    • 视图中引用的任何用户定义函数都必须由两部分组成的名称(即 schema.function**)引用。
    • 用户定义函数的数据访问属性必须为 NO SQL,外部访问属性必须是 NO。
    • 公共语言运行时 (CLR) 功能可以出现在视图的选择列表中,但不能作为聚集索引键定义的一部分。 CLR 函数不能出现在视图的 WHERE 子句中或视图中的 JOIN 运算的 ON 子句中。
    • 在视图定义中使用的 CLR 函数和 CLR 用户定义类型方法必须具有下表所示的属性设置。
      “属性” 注意
      DETERMINISTIC = TRUE 必须显式声明为 Microsoft .NET Framework 方法的属性。
      PRECISE = TRUE 必须显式声明为 .NET Framework 方法的属性。
      DATA ACCESS = NO SQL 通过将 DataAccess 属性设置为 DataAccessKind.None 并将 SystemDataAccess 属性设置为 SystemDataAccessKind.None 来确定。
      EXTERNAL ACCESS = NO 对于 CLR 例程,该属性的默认设置为 NO。
    • 必须使用 WITH SCHEMABINDING 选项创建视图。
    • 视图必须仅引用与视图位于同一数据库中的基表。 视图无法引用其他视图。
    • 视图定义中的 SELECT 语句不能包含下列 Transact-SQL 元素:
      COUNT ROWSET 函数(OPENDATASOURCE、OPENQUERY、OPENROWSET 和 OPENXML) OUTER 联接(LEFT、RIGHT 或 FULL)
      派生表(通过在 FROM 子句中指定 SELECT 语句来定义) 自联接 通过使用 SELECT * 或 SELECT table_name来指定列。*
      DISTINCT STDEV、STDEVP、VAR、VARP 或 AVG 公用表表达式 (CTE)
      float*text, ntext, image, XML,或filestream 子查询 包括排名或聚合开窗函数的 OVER 子句
      全文谓词(CONTAIN、FREETEXT) 引用可为 Null 的表达式的 SUM 函数 ORDER BY
      CLR 用户定义聚合函数 返回页首 CUBE、ROLLUP 或 GROUPING SETS 运算符
      MIN、MAX UNION、EXCEPT 或 INTERSECT 运算符 TABLESAMPLE
      表变量 OUTER APPLY 或 CROSS APPLY PIVOT、UNPIVOT
      稀疏列集 内联或多语句表值函数 OFFSET
      CHECKSUM_AGG

      *索引的视图可以包含float列; 但是,不能在聚集的索引键中包含此类列。

    • 如果存在 GROUP BY,则 VIEW 定义必须包含 COUNT_BIG(*),并且不得包含 HAVING。 这些 GROUP BY 限制仅适用于索引视图定义。 即使一个索引视图不满足这些 GROUP BY 限制,查询也可以在其执行计划中使用该视图。
    • 如果视图定义包含 GROUP BY 子句,则唯一聚集索引的键只能引用 GROUP BY 子句中指定的列。

使用SSMS数据库管理工具创建索引视图

1、连接数据库,选择数据库,展开数据库-》右键视图-》选择新建视图。

2、在添加表弹出框-》选择要创建视图的表、视图、函数、或者同义词等-》点击添加-》添加完成后选择关闭。

3、在关系图窗格中-》选择表与表之间关联的数据列-》选择列的其他排序或筛选条件。

4、右键点击空白处-》选择属性。

5、在视图属性窗格-》绑定到架构选择是-》非重复值选择是。

6、点击保存或者ctrl+s-》查看新创建的视图。

7、在对象资源管理器窗口-》展开视图-》选择视图-》右键点击索引-》选择新建索引-》选择聚集索引。

8、在新建索引弹出框-》选择索引数据列-》索引创建步骤可以参考本博主的创建索引博文-》点击确定(创建唯一聚集索引之后才能创建非聚集索引)。

9、在对象资源管理器中查看视图中的索引。

10、刷新视图-》可以创建非聚集索引,步骤同创建聚集索引(此处省略创建非聚集索引)。

11、点击保存或者ctrl+s-》刷新视图-》查看结果。

12、使用视图。

使用T-SQL脚本创建索引视图

语法:

–声明数据库引用
use 数据库;
go

–判断视图是否存在,如果存在则删除
if exists(select * from sys.views where name=视图名称)
drop view 视图名称;
go

–创建视图
create
view

–视图所属架构的名称。
–[schema_name][.]

–视图名称。 视图名称必须符合有关标识符的规则。 可以选择是否指定视图所有者名称。
[dbo][.]视图名称

–视图中的列使用的名称。 仅在下列情况下需要列名:列是从算术表达式、函数或常量派生的;两个或更多的列可能会具有相同的名称(通常是由于联接的原因);视图中的某个列的指定名称不同于其派生来源列的名称。 还可以在 SELECT 语句中分配列名。
–如果未指定 column,则视图列将获得与 SELECT 语句中的列相同的名称。
–column

with

–适用范围: SQL Server 2008 到 SQL Server 2017 和 Azure SQL Database。
–对 sys.syscomments 表中包含 CREATE VIEW 语句文本的项进行加密。 使用 WITH ENCRYPTION 可防止在 SQL Server 复制过程中发布视图。
–encryption,

–将视图绑定到基础表的架构。 如果指定了 SCHEMABINDING,则不能按照将影响视图定义的方式修改基表或表。 必须首先修改或删除视图定义本身,才能删除将要修改的表的依赖关系。
–使用 SCHEMABINDING 时,select_statement 必须包含所引用的表、视图或用户定义函数的两部分名称 (schema.object)。 所有被引用对象都必须在同一个数据库内。
–不能删除参与了使用 SCHEMABINDING 子句创建的视图的视图或表,除非该视图已被删除或更改而不再具有架构绑定。 否则, 数据库引擎将引发错误。
–另外,如果对参与具有架构绑定的视图的表执行 ALTER TABLE 语句,而这些语句又会影响视图定义,则这些语句将会失败。
schemabinding

–指定为引用视图的查询请求浏览模式的元数据时, SQL Server 实例将向 DB-Library、ODBC 和 OLE DB API 返回有关视图的元数据信息,而不返回基表的元数据信息。
–浏览模式元数据是 SQL Server 实例向这些客户端 API 返回的附加元数据。 如果使用此元数据,客户端 API 将可以实现可更新客户端游标。 浏览模式的元数据包含结果集中的列所属的基表的相关信息。
–对于使用 VIEW_METADATA 创建的视图,浏览模式的元数据在描述结果集内视图中的列时,将返回视图名,而不返回基表名。
–当使用 WITH VIEW_METADATA 创建视图时,如果该视图具有 INSTEAD OF INSERT 或 INSTEAD OF UPDATE 触发器,则视图的所有列(timestamp 列除外)都可更新。 有关可更新视图的详细信息,请参阅“备注”。
–view_metadata

–指定视图要执行的操作。
as
select_statement
go

–创建索引详情请参考索引博客
if not exists (select * from sys.indexes where name=索引名称)
–设置索引
create
unique
clustered
index
索引名称
on
dbo.视图名
(列名 [ asc | desc],列名  [ asc | desc],……);
go

示例:本示例演示在视图上创建一个唯一聚集索引。

–声明数据库引用
use testss;
go

–判断视图是否存在,如果存在则删除
if exists(select * from sys.views where name=’indexview1′)
drop view indexview1;
go

–创建视图
create
view

–视图所属架构的名称。
–[schema_name][.]

–视图名称。 视图名称必须符合有关标识符的规则。 可以选择是否指定视图所有者名称。
dbo.indexview1

–视图中的列使用的名称。 仅在下列情况下需要列名:列是从算术表达式、函数或常量派生的;两个或更多的列可能会具有相同的名称(通常是由于联接的原因);视图中的某个列的指定名称不同于其派生来源列的名称。 还可以在 SELECT 语句中分配列名。
–如果未指定 column,则视图列将获得与 SELECT 语句中的列相同的名称。
–column

with

–适用范围: SQL Server 2008 到 SQL Server 2017 和 Azure SQL Database。
–对 sys.syscomments 表中包含 CREATE VIEW 语句文本的项进行加密。 使用 WITH ENCRYPTION 可防止在 SQL Server 复制过程中发布视图。
–encryption,

–将视图绑定到基础表的架构。 如果指定了 SCHEMABINDING,则不能按照将影响视图定义的方式修改基表或表。 必须首先修改或删除视图定义本身,才能删除将要修改的表的依赖关系。
–使用 SCHEMABINDING 时,select_statement 必须包含所引用的表、视图或用户定义函数的两部分名称 (schema.object)。 所有被引用对象都必须在同一个数据库内。
–不能删除参与了使用 SCHEMABINDING 子句创建的视图的视图或表,除非该视图已被删除或更改而不再具有架构绑定。 否则, 数据库引擎将引发错误。
–另外,如果对参与具有架构绑定的视图的表执行 ALTER TABLE 语句,而这些语句又会影响视图定义,则这些语句将会失败。
schemabinding

–指定为引用视图的查询请求浏览模式的元数据时, SQL Server 实例将向 DB-Library、ODBC 和 OLE DB API 返回有关视图的元数据信息,而不返回基表的元数据信息。
–浏览模式元数据是 SQL Server 实例向这些客户端 API 返回的附加元数据。 如果使用此元数据,客户端 API 将可以实现可更新客户端游标。 浏览模式的元数据包含结果集中的列所属的基表的相关信息。
–对于使用 VIEW_METADATA 创建的视图,浏览模式的元数据在描述结果集内视图中的列时,将返回视图名,而不返回基表名。
–当使用 WITH VIEW_METADATA 创建视图时,如果该视图具有 INSTEAD OF INSERT 或 INSTEAD OF UPDATE 触发器,则视图的所有列(timestamp 列除外)都可更新。 有关可更新视图的详细信息,请参阅“备注”。
–view_metadata

–指定视图要执行的操作。
as
select a.id,a.age,a.height,a.name,b.id as classid from dbo.test1 as a
inner join dbo.test3 as b on a.classid=b.id
–要求对该视图执行的所有数据修改语句都必须符合 select_statement 中所设置的条件。 通过视图修改行时,WITH CHECK OPTION 可确保提交修改后,仍可通过视图看到数据。
–with check option;
go

if not exists (select * from sys.indexes where name=’umiqueindexview1′)
–设置索引
create
unique
clustered
index
umiqueindexview1
on
dbo.indexview1
(name asc);
go

示例结果:因为数据量太小,查询时间和效果不是很明显。

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

mikel阅读(1026)

来源: 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阅读(793)

来源: 一个架构师的缓存修炼之路 – 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阅读(923)

来源: 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阅读(1024)

来源: 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阅读(843)

来源: 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阅读(1006)

来源: 关于对象映射(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阅读(716)

来源: 公布一个 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阅读(804)

来源: 对象克隆(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、总结

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

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