TransactionScope事务处理方法介绍及.NET Core中的注意事项 SQL Server数据库漏洞评估了解一下 预热ASP.NET MVC 的VIEW [AUTOMAPPER]反射自动注册AUTOMAPPER PROFILE
TransactionScope事务处理方法介绍及.NET Core中的注意事项
作者:依乐祝
今天在写CzarCms的UnitOfWork的使用使用到了这个TransactionScope事务,因此对它进行了相关资料的查阅并记录如下,希望对大伙在.NET Core中使用有所帮助。
写在前面
您是否曾尝试使用C#代码来实现事务?通常,我们在SQL中一次执行多个Insert / Update语句的话可能就会使用到事务。事务遵循ACID(原子性,一致性,隔离性,持久性)规则,这样所有的语句要么全部执行成功要么全部被取消并执行回滚操作。 而我们今天要讲的TransactionScope则可以允许我们在应用程序级别实现这个过程。在某些情况下,您可能需要在同一个数据库甚至多个数据库(分布式事务)中执行不同的操作,或者由于某些其他约束,它无法在数据库级别来完成,或者应用程序的开发人员对数据库的接触较少,那么这时候TransactionScope将会让你游刃有余。
什么是TransactionScope呢?
TransactionScope作为System.Transactions的一部分被引入到.NET 2.0。同时SqlClient for .NET Core 从 2.1 及以上版本开始提供对System.Transactions的支持 。 它是一个类,它提供了一种简单的方法,可以将一组操作作为事务的一部分来进行处理,而不必担心场景背后的复杂性。如果某个操作在执行的过程中失败的话,则整个事务将失败并执行回滚操作,从而撤消已完成的所有操作。所有这些都将由框架处理,从而确保数据的一致性。
如何使用TransactionScope呢?
要使用它,您需要添加System.Transactions的引用,如果你使用的是.net core的话。这个引用被包含在netcoreapp2.2\System.Transactions.Local.dll
中, 该引用是框架库的一部分(通常默认情况下不会自动添加)。添加后,在我们想要使用它的地方添加名称空间 System.Transactions即可。代码如下所示:
try
{
using (TransactionScope scope = new TransactionScope())
{
// Do Operation 1
// Do Operation 2
//...
// 如果所有的操作都执行成功,则Complete()会被调用来提交事务
// 如果发生异常,则不会调用它并回滚事务
scope.Complete();
}
}
catch (ThreadAbortException ex)
{
// 处理异常
}
在上面的代码中我们可以看到我们在创建TransactionScope实例时使用了using
语句块及Disposable块,它确保了当dispose离开块并结束事务范围时调用dispose来进行资源的释放。
在一个Transaction范围中,我们可以做多个连接甚至链接到不同数据库的操作的,如下所示:
using (TransactionScope scope = new TransactionScope())
{
using (con = new SqlConnection(conString1))
{
con.Open();
// 执行操作 1
// 执行操作 2
//...
}
using (con = new SqlConnection(conString2))
{
con.Open();
// 执行操作 1
// 执行操作 2
//...
}
scope.Complete();
}
下面我们使用两个不同的数据库连接字符串来连接不同的数据库。当然我们也可以根据我们的业务要求使用尽可能多数据库。我们也可以再事务中嵌套事务。如下代码所示:
public void DoMultipleTransaction()
{
try
{
using (TransactionScope scope = new TransactionScope())
{
using (con = new SqlConnection(conString1))
{
con.Open();
// 执行操作1
}
OtherTransaction();
scope.Complete();
}
}
catch (ThreadAbortException ex)
{
// 处理异常
}
}
private void OtherTransaction()
{
using (TransactionScope scope = new TransactionScope())
{
using (con = new SqlConnection(conString2))
{
con.Open();
// 执行操作
}
scope.Complete();
}
}
这里最顶层的事务范围称为根范围。另外这里需要注意的是即使通过调用scope.Complete()完成内部事务(上面的OtherTransaction ),如果由于各种原因无法调用rootscope complete,那么整个事务也将被回滚包括内部的事务。
*注意:执行分布式trsanctions时,您可能会收到以下异常之一*
- 服务器上的MSDTC不可用
- 已禁用分布式事务管理器(MSDTC)的网络访问。
这两个错误都是由于同样的原因,第一个是在数据库和应用程序是同一个服务器时发生的,而在另一个则是服务跟数据库分别部署在两台服务器上。对于同一台服务器,请转到run-> cmd-> services.msc。运行名为Distributed Transaction Coordinator的服务并自动启动启动类型,以便在系统重新启动时再次启动它。对于2,你可能需要参照这个链接的内容进行相应的设置
TransactionScope 类提供了多个重载构造函数,它们接受 TransactionScopeOption 类型的枚举,而该枚举定义事务范围行为。
TransactionScope对象有以下三个选项:
- Required:联接环境事务,或者在环境事务不存在的情况下创建新的环境事务。
- RequiresNew:成为新的根范围,也就是说,启动一个新事务并使该事务成为其自己范围中的新环境事务。
- Suppress:根本不参与事务。 因此没有环境事务。
如果用 Required] 实例化范围并且存在环境事务,则该范围会联接该事务。 相反,如果不存在环境事务,该范围就会创建新的事务并成为根范围。 这是默认值。 在使用 Required时,无论范围是根范围还是仅联接环境事务,该范围中的代码都不需要有不同的行为。 该代码在这两种情况下的行为应相同。
如果用 RequiresNew 实例化范围,则它始终为根范围。 它会启动一个新事务,并且其事务成为该范围中的新环境事务。
如果用 Suppress 实例化范围,则无论是否存在环境事务,范围都从不参与事务。 始终使用此值实例化的作用域具有null作为其环境事务。
下面来让我们看一组实例代码:
using (TransactionScope scope = new TransactionScope())
{
// 联接环境事务,或者在环境事务不存在的情况下创建新的环境事务。
using (TransactionScope scope1 = new TransactionScope(TransactionScopeOption.Required))
{
// Do Operation
scope1.Complete();
}
//成为新的根范围,也就是说,启动一个新事务并使该事务成为其自己范围中的新环境事务。
using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.RequiresNew))
{
// Do Operation
scope2.Complete();
}
//根本不参与事务。 因此没有环境事务。
using (TransactionScope scope3 = new TransactionScope(TransactionScopeOption.Suppress))
{
// Do Operation
scope3.Complete();
}
scope.Complet
在这里,我们使用不同的TransactionScopeOptions在父事务下创建了三个事务。默认情况下,范围是required ,这里父事务就是采用的这个默认参数进行创建的。它是一个创建新事务的根范围,并将其标记为环境事务。scope1也是使用required创建的,因为我们已经有了一个环境事务(范围),所以它加入到父事务中。scope2是使用RequiresNew选项创建的,这意味着它是一个独立于环境事务处理的新事务。scope3是用suppress创建的选项,这意味着它不参与任何环境事务。无论环境事务是否成功执行,它都会被执行。父(全局)范围完成后,将提交所有环境事务。
注意点
-
EF Core 依赖数据库提供程序以实现对 System.Transactions 的支持。 虽然支持在 .NET Framework 的 ADO.NET 提供程序之间十分常见,但最近才将 API 添加到 .NET Core,因此支持并未得到广泛应用。 如果提供程序未实现对 System.Transactions 的支持,则可能会完全忽略对这些 API 的调用。 SqlClient for .NET Core 从 2.1 及以上版本开始支持 System.Transactions。如果尝试在低版本中 如.NET Core 2.0中尝试使用该功能将引发异常。
-
自版本 2.1 起,.NET Core 中的 System.Transactions 实现将不包括对分布式事务的支持,因此不能使用
TransactionScope
或CommittableTransaction
来跨多个资源管理器协调事务。主要是不依赖windows中的mstsc功能。 -
异步方法使用时需要注意:
在下面的例子中,我们在
TransactionScope
内部使用await
。using(var scope = new TransactionScope()) { var groups = await Context.ProductGroups.ToListAsync()。ConfigureAwait(false); }
看起来没有问题,但它会抛出一个
System.InvalidOperationException:``A TransactionScope must be disposed on the same thread that it was created.
原因是默认情况下
TransactionScope
不会从一个线程切换到另一个线程。为了解决这个问题,我们必须使用TransactionScopeAsyncFlowOption.Enabled
:using(var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { var groups = await Context.ProductGroups.ToListAsync()。ConfigureAwait(false); }
现在应该可以了吧?这取决于下面的情况。
如果我们使用和不使用
TransactionScopeAsyncFlowOption这个
选项的时候都使用了相同的数据库连接,并且第一次执行的时候没有使用这个选项,那么我们会得到另一个异常:System.InvalidOperationException:``Connection currently has transaction enlisted. Finish current transaction and retry.
换句话说,由于第一个访问的原因,第二个会话将会失败。如下代码所示:
try { using (var scope = new TransactionScope()) { // We know this one - System.InvalidOperationException: // TransactionScope必须放在与创建它相同的线程上。 var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false); } } catch (Exception e) { // error handling } using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // Implemented correctly but throws anyways // System.InvalidOperationException: // 当前连接已经被记录。完成当前事务并重试。 var groups = await Context.ProductGroups.ToListAsync().ConfigureAwait(false); }
想象一下,如果第一个调用是在第三方库或您正在使用的框架中完成的,二您不了解其中的代码 - 如果您之前没有看到此错误,那么你讲无从下手来解决这个问题。
总结
本文带着大家熟悉了一遍TransactionScope并对其使用进行了介绍!同时介绍了在.NET Core中使用TransactionScope的一些注意事项!希望对大家有所帮助。另附上.NET Core实战项目交流群:637326624
SQL Server数据库漏洞评估了解一下
SQL Server Management Studio 17.4或更高版本的SSMS中提供了SQL Server漏洞侦测(VA)功能,此功能允许SQL Server扫描您的数据库以查找潜在的安全漏洞,并且可以针对SQL Server 2012或更高版本运行。如果您还没有使用SSMS上的较新版本,请不要担心,您可以在此处 进行下载。
写在前面
当我们对数据进行任何类型的扫描时总是让我很担心,因为进行数据库扫描时的性能影响可能真的会毁了你的一天。幸运的是,VA是轻量级的,并且可以在不影响性能的情况下运行,同时可以深入了解并指出我们可以在哪里改进SQL Server的安全性。该过程被设计成使用知识库规则来满足数据隐私标准和遵从性,这些规则寻找与Microsoft最佳实践的偏差从而给出。
实战演练
-
要运行漏洞侦测,只需选择我们需要扫描的数据库,然后右键单击并选择“任务”。在这里,您将看到漏洞评估选项接着选择扫描漏洞。如果您之前运行过一个VA,则可以通过选择“打开现有扫描”来访问它。
-
它会弹出一个窗口,然后我们按下图所示选择好结果保存的位置。单击“确定”后,该过程将运行。
-
在这里,点击上面的确定按钮后就会立即执行,执行完成后将弹出结果窗口。这里你可以看到我们的CzarCms的检查结果中有6个失败的检查结果,52个已通过的检查结果。它列出了每个检查的明细并给出了对应的评定的风险等级。
-
在错误列表中随便单击一个的失败检查结果,我们将看到更多详细信息以及对应的修复步骤,并提供进行修复的脚本(想想是不是有点小激动呢)。赶紧打开看看吧。
-
这里我们简单选择第二个吧,
VA1143 - 'dbo' user should not be used for normal service operation
什么你看不懂?我擦,不会百度翻译啊?““dbo”用户不应该用于正常的服务操作”,啥意思呢?“dbo”或数据库所有者是一个用户帐户,它隐含了执行数据库中所有活动的权限。sysadmin固定服务器角色的成员自动映射到dbo。此规则检查dbo不是唯一允许访问此数据库的帐户。请注意,在新创建的干净数据库中,在创建其他角色之前,此规则将失败。总结一句话就是你得为你的数据库创建一个单独的用户来提高安全性。如图所示: -
您可以在下面看到,它向我们描述了没有遵循的最佳实践规则,并提供了一个查询,我们可以运行该查询来查看结果。我真的很喜欢这个特性,并且它是一个方便的脚本,用于以后评估其他服务器的健康状况时使用。它甚至给了我们一个小的复制按钮,以复制出脚本和选项打开它在查询窗口。
-
只指出错误而不给出解决方法的行为是可耻的,所以伟大的微软给出了我们来补救的步骤以及脚本。这里我们进一步向下滚动,您将看到建议的补救步骤和脚本。如果没有提供脚本,它将为您提供一个链接,通过这个链接可以找到有关如何解决问题的正确文档。在我看来,VA做了很好的解释解决问题所需的东西。请记住,虽然这是由Microsoft创建的,但我还是建议你在生产环境部署之前来运行这个漏洞检查并进行相关的补救。
-
这里需要注意一下,如下图所示你可以设置结果基线 。基线允许您对结果报告中的错误结果进行接收,这样在下次漏洞扫描的时候这个错误的结果就不会出现在错误列表里面了。
-
通过将结果标记为BASELINE,您告诉VA,这个错误在您的环境中是可接受的,尽管它可能不符合最佳实践或监管标准。将来与基线匹配的任何内容都标记为在后续扫描中传递,并将记录按自定义基线传递的原因。这个基线匹配的结果会在后期的漏洞扫描进行传递,如下所示:我讲两个结果设置为了基线
-
当我再次扫描时,我们将会看到这一点。如下所示,扫描报告现在显示我只有1个失败(我没有修复的问题),附加信息列显示原因的基线。
总结
SQL Server漏洞评估是评估数据隐私、安全性和遵从性标准的一个非常好的第三方工具,并且非常容易使用。纸上得来终觉浅,还不赶紧尝试一下,看看数据库存在哪些可以提升的地方吧。有兴趣的朋友可以加入我们的DotNetCore实战项目交流群637326624进行交流。
预热ASP.NET MVC 的VIEW
ASP.NET MVC 的View 预设是Load on Demand(按需加载),也就是说View 第一次要Render 的时候才会去载入跟编译,这个就会造成一个现象,即使Web 应用程式已经完成启动,在浏览页面的时候也是会感觉到一点延迟,尤其Web 应用程式部署在Azure App Service 上更为明显,既然这样,那我们就在Web 应用程式启动时候,把所有View 载入跟编译,然后Render 一次就行了,我们来看看怎么做?
要Render 一个页面有时候还会牵扯到一些外部资源(Database、File...等),这个就不在讨论范围,我们只单纯关注View 的部分,我会以ASP.NET MVC 的范本专案来当做我的范例,使用的Azure App Service 方案是B1。
新增预热用的Action
这个预热用的Action 可以随意看要新增在哪个Controller 底下,不一定要是与预热的View 有相关的Controller,为了看出有无预热的差异,在这边我新增一个WarmUpController 放置预热的程式码,接下来我们就去找出我们Web 应用程式底下所有的View,然后透过ViewEngine 把它们逐一Render 一次,就完成预热的程序。
View 的编译并不是一个档案编译成一个组件,而是一个目录编译成一个组件,举例来说,~/Views/Home/ 底下有三个.cshtml 档案:Contact.cshtml、Index.cshtml、About.cshtml,它们三个会被编译在同一个组件。
应用程式初始化(Application Initialization)
有关于应用程式初始化,就参考微软官方的说明,我们在这边就直接动手在Web.Config加入以下设定,让Web应用程式在启动时对/WarmUp发出请求。
我们来比较一下有无预热的结果,原先在Web 应用程式完成启动后,首页下载完成的时间为1.94s,在预热之后减少到78ms,可以看出View 的初次渲染真的很花时间。
但是这个会拉长Web 应用程式启动的时间,不过我们可以透过升级Azure App Service 方案至少到S 等级,然后部署我们的Web 应用程式到预备位置,预热完成之后再自动交换到Production 环境来避开此问题。
[AUTOMAPPER]反射自动注册AUTOMAPPER PROFILE
AutoMapper 帮我我们方便管理物件跟物件之间属性值格式转换
模型转换
UserInfoModel 当作我们从DB捞取出来模型资料
1
2
3
4
5
6
|
public class UserInfoModel { public int RowId { get ; set ; } public string Name { get ; set ; } public int Age { get ; set ; } } |
UserInfoViewModel 是呈现在UI或其他地方的模型
其中 Detail
栏位由 UserInfoModel
的Name
和Age
属性组成的
1
2
3
4
|
public class UserInfoViewModel { public string Detail { get ; set ; } } |
这时我们就会引用AutoMapper 帮我们统一管理转换模型上的问题
建立一个PROFILE
设置UserInfoModel
对于UserInfoViewModel
之前的栏位转换
1
2
3
4
5
6
7
8
9
|
public class UserInfoProfile : Profile { public UserInfoProfile() { CreateMap<UserInfoModel, UserInfoViewModel>() .ForMember(t => t.Detail, s => s.MapFrom(_ => $ "DetailInfo:{_.Name} {_.Age}" )); } } |
而我们在注册时会呼叫AddProfile
方法
1
|
Mapper.Initialize(x => x.AddProfile<UserInfoProfile>()); |
但每次新加Profile这边都需要设置新的Profile,我们就会想有没有方法可以让他自动注册?
反射自动注册AUTOMAPPER PROFILE
此程式我使用我的 ExtenionTool
1
2
3
4
5
6
7
|
var profiles = Assembly.GetExecutingAssembly() .GetInstancesByAssembly<Profile>(); foreach ( var profile in profiles) { Mapper.Initialize(x => x.AddProfile(profile)); } |
上面程式码很简单清晰,呼叫 取得目前组件所有的 物件实体并且加到中,我们将上面程式码在初始化执行一次GetInstancesByAssembly()
Profile
Profile
1
2
3
4
5
6
7
|
public static IEnumerable<TResult> GetInstancesByAssembly<TResult>( this Assembly ass) { return ass.GetTypes() .Where(x => typeof (TResult).IsAssignableFrom(x) && x.IsNormalClass()) .Select(x => Activator.CreateInstance(x)) .Cast<TResult>(); } |
核心程式使用Linq 动态取得你所需的类型并使用反射创建
之后我们就可以不用在手动把Profile
加至AutoMapper
容器中了