2023年2月

在基于SqlSugar的开发框架中,我们设计了一些系统服务层的基类,在基类中会有很多涉及到相关的数据处理操作的,如果需要跟踪具体是那个用户进行操作的,那么就需要获得当前用户的身份信息,包括在Web API的控制器中也是一样,需要获得对应的用户身份信息,才能进行相关的身份鉴别和处理操作。本篇随笔介绍基于Principal的用户身份信息的存储和读取操作,以及在适用于Winform程序中的内存缓存的处理方式,从而通过在基类接口中注入用户身份信息接口方式,获得当前用户的详细身份信息。

1、用户身份接口的定义和基类接口注入

为了方便获取用户身份的信息,我们定义一个接口 IApiUserSession 如下所示。

   /// <summary>
    ///API接口授权获取的用户身份信息-接口/// </summary>
    public interfaceIApiUserSession
{
/// <summary> ///用户登录来源渠道,0为网站,1为微信,2为安卓APP,3为苹果APP/// </summary> string Channel { get; }/// <summary> ///用户ID/// </summary> int? Id { get; }/// <summary> ///用户名称/// </summary> string Name { get; }/// <summary> ///用户邮箱(可选)/// </summary> string Email { get; }/// <summary> ///用户手机(可选)/// </summary> string Mobile { get; }/// <summary> ///用户全名称(可选)/// </summary> string FullName { get; }/// <summary> ///性别(可选)/// </summary> string Gender { get; }/// <summary> ///所属公司ID(可选)/// </summary> string Company_ID { get; }/// <summary> ///所属公司名称(可选)/// </summary> string CompanyName { get; }/// <summary> ///所属部门ID(可选)/// </summary> string Dept_ID { get; }/// <summary> ///所属部门名称(可选)/// </summary> string DeptName { get; }/// <summary> ///把用户信息设置到缓存中去/// </summary> /// <param name="info">用户登陆信息</param> /// <param name="channel">默认为空,用户登录来源渠道:0为网站,1为微信,2为安卓APP,3为苹果APP</param> void SetInfo(LoginUserInfo info, string channel = null);
}

其中的SetInfo是为了在用户身份登录确认后,便于将用户信息存储起来的一个接口方法。其他属性定义用户相关的信息。

由于这个用户身份信息的接口,我们提供给基类进行使用的,默认我们在基类定义一个接口对象,并通过提供默认的NullApiUserSession实现,便于引用对应的身份属性信息。

NullApiUserSession只是提供一个默认的实现,实际在使用的时候,我们会注入一个具体的接口实现来替代它的。

    /// <summary>
    ///提供一个空白实现类,具体使用IApiUserSession的时候,会使用其他实现类/// </summary>
    public classNullApiUserSession : IApiUserSession
{
/// <summary> ///单件实例/// </summary> public static NullApiUserSession Instance { get; } = newNullApiUserSession();public string Channel => null;public int? Id => null;public string Name => null;
..................

/// <summary> ///设置信息(保留为空)/// </summary> public void SetInfo(LoginUserInfo info, string channel = null)
{
}
}

在之前介绍的SqlSugar框架的时候,我们介绍到数据访问操作的基类定义,如下所示。

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput>: 
IMyCrudService
<TEntity, TKey, TGetListInput> where TEntity : class, IEntity<TKey>, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///数据库上下文信息/// </summary> protectedDbContext dbContext;

/// <summary> ///当前Api用户信息/// </summary> public IApiUserSession CurrentApiUser { get; set; }publicMyCrudService()
{
dbContext
= newDbContext();

CurrentApiUser
= NullApiUserSession.Instance;//空实现 }

在最底层的操作基类中,我们就已经注入了用户身份信息,这样我们不管操作任何函数处理,都可以通过该用户身份信息接口CurrentApiUser获得对应的用户属性信息了。

在具体的业务服务层中,我们继承该基类,并提供构造函数注入方式,让基类获得对应的 IApiUserSession接口的具体实例。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
/// <summary> ///构造函数/// </summary> /// <param name="currentApiUser">当前用户接口</param> publicCustomerService(IApiUserSession currentApiUser)
{
this.CurrentApiUser=currentApiUser;
}

........

}

如果有其他服务接口需要引入,那么我们继续增加其他接口注入即可。

    /// <summary>
    ///角色信息 应用层服务接口实现/// </summary>
    public class RoleService : MyCrudService<RoleInfo,int, RolePagedDto>, IRoleService
{
privateIOuService _ouService;privateIUserService _userService;/// <summary> ///默认构造函数/// </summary> /// <param name="currentApiUser">当前用户接口</param> /// <param name="ouService">机构服务接口</param> /// <param name="userService">用户服务接口</param> publicRoleService(IApiUserSession currentApiUser, IOuService ouService, IUserService userService)
{
this.CurrentApiUser = currentApiUser;this._ouService =ouService;this._userService =userService;
}

由于该接口是通过构造函数注入的,因此在系统运行前,我们需要往IOC容器中注册对应的接口实现类(由于
IApiUserSession
提供了多个接口实现,我们这里不自动加入它的对应接口,而通过手工加入)。

在Winform或者控制台程序,启动程序的时候,手工加入对应的接口到IOC容器中即可。

/// <summary>
///应用程序的主入口点。/// </summary>
[STAThread]static voidMain()
{
//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//services.AddSingleton<IDictDataService, DictDataService>();//调用自定义的服务注册 ServiceInjection.ConfigureRepository(services);//添加IApiUserSession实现类//services.AddSingleton<IApiUserSession, ApiUserCache>();//缓存实现方式 services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式

如果是Web API或者asp.net core项目中加入,也是类似的处理方式。

var builder =WebApplication.CreateBuilder(args);//配置依赖注入访问数据库
ServiceInjection.ConfigureRepository(builder.Services);//添加IApiUserSession实现类
builder.Services.AddSingleton<IApiUserSession, ApiUserPrincipal>();

前面介绍了,IApiUserSession的一个空白实现,是默认的接口实现,我们具体会使用基于Principal或者缓存方式实现记录用户身份的信息实现,如下是它们的类关系。

在上面的代码中,我们注入一个 ApiUserPrincipal 的用户身份接口实现。

2、基于Principal的用户身份信息的存储和读取操作

ApiUserPrincipal 的用户身份接口实现是可以实现Web及Winform的用户身份信息的存储的。

首先我们先定义一些存储声明信息的键,便于统一处理。

    /// <summary>
    ///定义一些常用的ClaimType存储键/// </summary>
    public classApiUserClaimTypes
{
public const string Id =JwtClaimTypes.Id;public const string Name =JwtClaimTypes.Name;public const string NickName =JwtClaimTypes.NickName;public const string Email =JwtClaimTypes.Email;public const string PhoneNumber =JwtClaimTypes.PhoneNumber;public const string Gender =JwtClaimTypes.Gender;public const string FullName = "FullName";public const string Company_ID = "Company_ID";public const string CompanyName = "CompanyName";public const string Dept_ID = "Dept_ID";public const string DeptName = "DeptName";public const string Role =ClaimTypes.Role;
}

ApiUserPrincipal 用户身份接口实现的定义如下代码所示。

    /// <summary>
    ///基于ClaimsPrincipal实现的用户信息接口。/// </summary>
[Serializable]public classApiUserPrincipal : IApiUserSession
{
/// <summary> ///IHttpContextAccessor对象/// </summary> private readonlyIHttpContextAccessor_httpContextAccessor;/// <summary> ///如果IHttpContextAccessor.HttpContext?.User非空获取HttpContext的ClaimsPrincipal,否则获取线程的CurrentPrincipal/// </summary> protected ClaimsPrincipal Principal => _httpContextAccessor?.HttpContext?.User ?? (Thread.CurrentPrincipal asClaimsPrincipal);/// <summary> ///默认构造函数/// </summary> /// <param name="httpContextAccessor"></param> publicApiUserPrincipal(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor
=httpContextAccessor;
}
/// <summary> ///默认构造函数/// </summary> public ApiUserPrincipal() { }

基于Web  API的时候,用户身份信息是基于
IHttpContextAccessor
注入的接口获得 httpContextAccessor?.HttpContext?.User 的 ClaimsPrincipal 属性操作的。

我们获取用户身份的属性的时候,直接通过这个属性判断获取即可。

        /// <summary>
        ///用户ID/// </summary>
        public int? Id => this.Principal?.FindFirst(ApiUserClaimTypes.Id)?.Value.ToInt32();/// <summary>
        ///用户名称/// </summary>
        public string Name => this.Principal?.FindFirst(ApiUserClaimTypes.Name)?.Value;

而上面同时也提供了一个基于Windows的线程Principal 属性(Thread.CurrentPrincipal )的声明操作,操作模型和Web 的一样的,因此Web和WinForm的操作是一样的。

在用户登录接口处理的时候,我们需要统一设置一下用户对应的声明信息,存储起来供查询使用。

        /// <summary>
        ///主要用于Winform写入Principal的ClaimsIdentity/// </summary>
        public void SetInfo(LoginUserInfo info, string channel = null)
{
//new WindowsPrincipal(WindowsIdentity.GetCurrent()); var claimIdentity = new ClaimsIdentity("login");
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Id, info.ID ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Name, info.UserName ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Email, info.Email ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.PhoneNumber, info.MobilePhone ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Gender, info.Gender ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.FullName, info.FullName ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Company_ID, info.CompanyId ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.CompanyName, info.CompanyName ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.Dept_ID, info.DeptId ?? ""));
claimIdentity.AddClaim(
new Claim(ApiUserClaimTypes.DeptName, info.DeptName ?? ""));//此处不可以使用下面注释代码//this.Principal?.AddIdentity(claimIdentity);//Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失//因此统一采用 AppDomain.CurrentDomain.SetThreadPrincipal中设置,确保进程中所有线程都会复制到信息 IPrincipal principal = new GenericPrincipal(claimIdentity, null);
AppDomain.CurrentDomain.SetThreadPrincipal(principal);
}

在上面中,我特别声明“
Thread.CurrentPrincipal设置会导致在异步线程中设置的结果丢失
” ,这是我在反复测试中发现,不能在异步方法中设置Thread.CurrentPrincipal的属性,否则属性会丢失,因此主线程的Thread.CurrentPrincipal 会赋值替换掉异步线程中的Thread.CurrentPrincipal属性。

而.net 提供了一个程序域的方式设置CurrentPrincipal的方法,可以或者各个线程中统一的信息。

AppDomain.CurrentDomain.SetThreadPrincipal(principal);

基于WInform的程序,我们在登录界面中处理用户登录操作

但用户确认登录的时候,测试用户的账号密码,成功则在本地设置用户的身份信息。

        /// <summary>
        ///统一设置登陆用户相关的信息/// </summary>
        /// <param name="info">当前用户信息</param>
        public asyncTask SetLoginInfo(LoginResult loginResult)
{
var info = loginResult.UserInfo; //用户信息//获取用户的角色集合 var roles = await BLLFactory<IRoleService>.Instance.GetRolesByUser(info.Id);//判断用户是否超级管理员||公司管理员 var isAdmin = roles.Any(r => r.Name == RoleInfo.SuperAdminName || r.Name ==RoleInfo.CompanyAdminName);//初始化权限用户信息 Portal.gc.UserInfo = info; //登陆用户 Portal.gc.RoleList = roles;//用户的角色集合 Portal.gc.IsUserAdmin = isAdmin;//是否超级管理员或公司管理员 Portal.gc.LoginUserInfo = this.ConvertToLoginUser(info); //转换为窗体可以缓存的对象//设置身份信息到共享对象中(Principal或者Cache) BLLFactory<IApiUserSession>.Instance.SetInfo(Portal.gc.LoginUserInfo);awaitTask.CompletedTask;
}

通过SetInfo,我们把当前用户的信息设置到了域的Principal中,进程内的所有线程共享这份用户信息数据。

跟踪接口的调用,我们可以查看到对应的用户身份信息了。

可以看到,这个接口已经注入到了服务类中,并且获得了相应的用户身份信息了。

同样在Web API的登录处理的时候,会生成相关的JWT token的信息的。

           var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip);if (loginResult != null && loginResult.UserInfo != null)
{
var userInfo =loginResult.UserInfo;

authResult.AccessToken
= GenerateToken(userInfo); //令牌 authResult.Expires = expiredDays * 24 * 3600; //失效秒数 authResult.Succes = true;//成功//设置缓存用户信息//SetUserCache(userInfo); }else{
authResult.Error
= loginResult?.ErrorMessage;
}

其中生成的JWT token的逻辑如下所示。

        /// <summary>
        ///生成JWT用户令牌/// </summary>
        /// <returns></returns>
        private stringGenerateToken(UserInfo userInfo)
{
var claims = new List<Claim>{newClaim(ApiUserClaimTypes.Id, userInfo.Id.ToString()),newClaim(ApiUserClaimTypes.Email, userInfo.Email),newClaim(ApiUserClaimTypes.Name, userInfo.Name),newClaim(ApiUserClaimTypes.NickName, userInfo.Nickname),newClaim(ApiUserClaimTypes.PhoneNumber, userInfo.MobilePhone),newClaim(ApiUserClaimTypes.Gender, userInfo.Gender),newClaim(ApiUserClaimTypes.FullName, userInfo.FullName),newClaim(ApiUserClaimTypes.Company_ID, userInfo.Company_ID),newClaim(ApiUserClaimTypes.CompanyName, userInfo.CompanyName),newClaim(ApiUserClaimTypes.Dept_ID, userInfo.Dept_ID),newClaim(ApiUserClaimTypes.DeptName, userInfo.DeptName),
};
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]));var jwt = newJwtSecurityToken
(
issuer: _configuration[
"Jwt:Issuer"],
audience: _configuration[
"Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddDays(expiredDays),
//有效时间 signingCredentials: newSigningCredentials(securityKey, SecurityAlgorithms.HmacSha256)
);
var token = newJwtSecurityTokenHandler().WriteToken(jwt);returntoken;
}

说生成的一系列字符串,我们可以通过解码工具,可以解析出来对应的信息的。

在登录授权的这个时候,控制器会把相关的Claim信息写入到token中的,我们在客户端发起对控制器方法的调用的时候,这些身份信息会转换成对象信息。

我们调试控制器的方法入口,如可以通过Fiddler的测试接口的调用情况。

可以看到CurrentApiUser的信息就是我们发起用户身份信息,如下图所示。

在监视窗口中查看IApiUserSession对象,可以查看到对应的信息。

3、基于内存缓存的用户身份接口实现处理方式

在前面介绍的IApiUserSession的接口实现的时候,我们也提供了另外一个基于MemoryCache的缓存实现方式,和基于Principal凭证信息处理不同,我们这个是基于MemoryCache的存储方式。

它的实现方法也是类似的,我们这里也一并介绍一下。

    /// <summary>
    ///基于MemeoryCache实现的用户信息接口/// </summary>
    public classApiUserCache : IApiUserSession
{
/// <summary> ///内存缓存对象/// </summary> private static readonly ObjectCache Cache =MemoryCache.Default;/// <summary> ///默认构造函数/// </summary> publicApiUserCache()
{
}
/// <summary> ///把用户信息设置到缓存中去/// </summary> /// <param name="info">用户登陆信息</param> public void SetInfo(LoginUserInfo info, string channel = null)
{
SetItem(ApiUserClaimTypes.Id, info.ID);
SetItem(ApiUserClaimTypes.Name, info.UserName);
SetItem(ApiUserClaimTypes.Email, info.Email);
SetItem(ApiUserClaimTypes.PhoneNumber, info.MobilePhone);
SetItem(ApiUserClaimTypes.Gender, info.Gender);
SetItem(ApiUserClaimTypes.FullName, info.FullName);
SetItem(ApiUserClaimTypes.Company_ID, info.CompanyId);
SetItem(ApiUserClaimTypes.CompanyName, info.CompanyName);
SetItem(ApiUserClaimTypes.Dept_ID, info.DeptId);
SetItem(ApiUserClaimTypes.DeptName, info.DeptName);
}
/// <summary> ///设置某个属性对象/// </summary> /// <param name="key"></param> /// <param name="value"></param> private void SetItem(string key, objectvalue)
{
if (!string.IsNullOrEmpty(key))
{
Cache.Set(key, value
?? "", DateTimeOffset.MaxValue, null);
}
}
/// <summary> ///用户ID/// </summary> public int? Id => (Cache.Get(ApiUserClaimTypes.Id) as string)?.ToInt32();/// <summary> ///用户名称/// </summary> public string Name => Cache.Get(ApiUserClaimTypes.Name) as string;/// <summary> ///用户邮箱(可选)/// </summary> public string Email => Cache.Get(ApiUserClaimTypes.Email) as string;

..............
}

我们通过
MemoryCache.Default
构造一个内存缓存的对象,然后在设置信息的时候,把用户信息按照键值方式设置即可。在Winform中我们可以采用内存缓存的方式存储用户身份信息,而基于Web方式的,则会存在并发多个用户的情况,不能用缓存来处理。

一般情况下,我们采用 ApiUserPrincipal 来处理用户身份信息就很好了。

4、单元测试的用户身份处理

在做单元测试的时候,我们如果需要设置测试接口的用户身份信息,那么就需要在初始化函数里面设置好用户信息,如下所示。

[TestClass]public classUnitTest1
{
private static IServiceProvider Provider = null;/*带有[ClassInitialize()] 特性的方法在执行类中第一个测试之前调用。
带有[TestInitialize()] 特性的方法在执行每个测试前都会被调用,一般用来初始化环境,为单元测试配置一个特定已知的状态。
带有[ClassCleanup()] 特性的方法将在类中所有的测试运行完后执行。
*/ //[TestInitialize]//每个测试前调用 [ClassInitialize] //测试类第一次调用 public static voidSetup(TestContext context)
{
//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//调用自定义的服务注册 ServiceInjection.ConfigureRepository(services);//注入当前Api用户信息处理实现,服务对象可以通过IApiUserSession获得用户信息//services.AddSingleton<IApiUserSession, ApiUserCache>();//缓存实现方式 services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式//IServiceProvider负责提供实例 Provider =services.BuildServiceProvider();//模拟写入登录用户信息 WriteLoginInfo();
}
/// <summary> ///写入用户登陆信息,IApiUserSession接口才可使用获取身份/// </summary> static voidWriteLoginInfo()
{
var mockUserInfo = newLoginUserInfo()
{
ID
= "1",
Email
= "wuhuacong@163.com",
MobilePhone
= "18620292076",
UserName
= "admin",
FullName
= "伍华聪"};//通过使用全局IServiceProvider的接口获得服务接口实例 Provider.GetService<IApiUserSession>().SetInfo(mockUserInfo);
}

上面的方法初始化了测试类的信息,方法调用的时候,我们获得对应的接口实例处理即可,如下测试代码所示。

        /// <summary>
        ///测试查找记录/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod1()
{
var input = newDictTypePagedDto()
{
Name
= "客户"}; var service = Provider.GetService<IDictTypeService>();var count = await service.CountAsync(s => true);
Assert.AreNotEqual(
0, count);var list = awaitservice.GetAllAsync();
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);

list
= awaitservice.GetListAsync(input);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);var ids = list.Items.Select(s => { return s.Id; }).Take(2);
list
= awaitservice.GetAllByIdsAsync(ids);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);var id = list.Items[0].Id;var info = awaitservice.GetAsync(id);
Assert.IsNotNull(info);
Assert.AreEqual(id, info.Id);

info
= await service.GetFirstAsync(s => true);
Assert.IsNotNull(info);
awaitTask.CompletedTask;
}

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在基于SqlSugar的开发框架的服务层中处理文件上传的时候,我们一般有两种处理方式,一种是常规的把文件存储在本地文件系统中,一种是通过FTP方式存储到指定的FTP服务器上。这种处理应该由程序进行配置,决定使用那种方式,那么这里面我们为了弹性化处理, 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传的配置参数信息。

微软引入
选项模式
,它是用于配置框架服务使用的设置. 选项模式由
Microsoft.Extensions.Options
NuGet包实现,除了ASP.NET Core应用,它还适用于任何类型的应用程序,如果需要了解,
微软的文档
详细解释了选项模式。

选项模式的限制之一是你只能解析(注入)
IOptions <MyOptions>
并在依赖注入配置完成(即所有模块的
ConfigureServices
方法完成)后获取选项值。如果你正在开发一个模块,可能需要让开发者能够设置一些选项,并在依赖注入注册阶段使用这些选项. 你可能需要根据选项值配置其他服务或更改依赖注入的注册代码。IOptions<>是单例,因此一旦生成了,除非通过代码的方式更改,它的值是不会更新的。

1、文件上传处理

在前面我们介绍过,文件上传处理的逻辑中有两部分,一个是本地文件处理,一个是FTP文件处理,它们选择那种方式,依赖于配置参数的信息,如下示意图所示。

在本地文件处理过程中,如果是Web API方式调用服务层,那么就在Web API所在的文件系统中,如果是Winform界面直接调用服务层,那么就是在当前系统中处理文件,这种方式可以有效的管理我们的文件信息。

在FTP文件处理过程中,则是根据选项参数的信息,调用FluentFTP类库进行文件的上传操作。

在Winform界面中上传文件的界面如下所示,它对于采用哪种方式是不知道的,具体由配置参数决定。

而所有的附件信息我们存储在数据库里面,文件则存放在对应的文件夹里面,可以统一进行管理查看和管理。

2、选项模式【Options】的处理

首先根据我们的处理方式,我们定义一个对象,用于承载上传参数的信息,如下代码所示。

    /// <summary>
    ///文件上传处理的选项信息/// </summary>
    public classUploadSettingOptions
{
/// <summary> ///可指定的存储物理地址,如C:\\Attachment,如果没有配置项AttachmentBasePath,则默认一个相对目录。/// </summary> public string AttachmentBasePath { get; set; }/// <summary> ///指定附件上传的方式,如ftp为FTP方式,为空则为普通方式/// </summary> public string AttachmentUploadType { get; set; }/// <summary> ///FTP服务地址/// </summary> public string FtpServer { get; set; }/// <summary> ///FTP用户名/// </summary> public string FtpUser { get; set; }/// <summary> ///FTP密码/// </summary> public string FtpPassword { get; set; }/// <summary> ///FTP的基础路径,如可以指定为IIS的路径:http://www.iqidi.com:8000 ,方便下载打开/// </summary> public string FtpBaseUrl { get; set; }
}

然后在项目中添加Microsoft.Extensions.Options引用。

我们定义文件上传服务类的和它的构造函数,以便于选项模式的处理。

    /// <summary>
    ///上传附件信息 应用层服务接口实现/// </summary>
    public class FileUploadService : MyCrudService<FileUploadInfo,string, FileUploadPagedDto>, IFileUploadService
{
//微软引入选项模式,它是用于配置框架服务使用的设置. 选项模式由Microsoft.Extensions.Options NuGet包实现//在你需要获得一个选项值时,将 IOptions<TOption> 服务注入到你的类中,使用它的 .Value 属性得到值. private readonlyUploadSettingOptions _options;/// <summary> ///参数化构造,注入上传处理设置信息/// </summary> /// <param name="options"></param> public FileUploadService(IOptions<UploadSettingOptions>options)
{
_options
=options.Value;
}

我们看到这里提供了一个注入接口的参数信息,这样完成参数的注入加载后,我们在该服务类调用的时候,就可以使用它的选项参数信息了。

例如我们在其中通用的上传处理方法上如下所示。

        /// <summary>
        ///上传文件(根据配置文件选择合适的上传方式)/// </summary>
        /// <param name="info">文件信息(包含流数据)</param>
        /// <returns></returns>
        public async Task<CommonResult>Upload(FileUploadInfo info)
{
var uploadType = this._options.AttachmentUploadType;if (string.IsNullOrEmpty(uploadType))
{
return await this.UploadByNormal(info);
}
else if (uploadType.Equals("ftp", StringComparison.OrdinalIgnoreCase))
{
return await this.UploadByFTP(info);
}
else{throw new ArgumentException("AttachmentUploadType配置指定了无效的值, 请置空或者填写ftp。");
}
}

我们来通过读取选项参数的信息,来决定采用哪种上传文件的方式。

我们在基于.net framewrok的Winform项目App.config中指定下面的配置信息

  <appSettings>
    <!--保存目录及FTP配置信息-->
    <!--可指定的存储物理地址,如C:\\Attachment,如果没有配置则默认相对目录。-->
    <addkey="AttachmentBasePath"value="" />
    <!--指定附件默认上传的方式, 如为FTP方式:ftp,为空则为普通方式-->
    <addkey="AttachmentUploadType"value="" />
    <!--非空的时候,读取下面的FTP信息-->
    <addkey="FtpServer"value="114.215.106.96" />
    <addkey="FtpUser"value="web2" />
    <addkey="FtpPassword"value="" />
    <!--可以指定为HTTP或者FTP的路径,供下载查看-->
    <addkey="FtpBaseUrl"value="http://www.iqidi.com/ftp" />
  </appSettings>

而在项目启动的时候,我们需要注入选项参数的内容,以达到完成配置文件信息的读取。

        /// <summary>
        ///应用程序的主入口点。/// </summary>
[STAThread]static voidMain()
{
//IServiceCollection负责注册 IServiceCollection services = newServiceCollection();//添加IApiUserSession实现类 services.AddSingleton<IApiUserSession, ApiUserPrincipal>(); //CurrentPrincipal实现方式//构造对应环境下的UploadSettingOptions信息 services.Configure<UploadSettingOptions>(option =>{
SetUploadOptions(option);
});
        /// <summary>
        ///构造对应环境下的UploadSettingOptions信息/// </summary>
        /// <returns></returns>
        private staticUploadSettingOptions SetUploadOptions(UploadSettingOptions option)
{
var config = newAppConfig();
option.AttachmentBasePath
= config.AppConfigGet("AttachmentBasePath");
option.AttachmentUploadType
= config.AppConfigGet("AttachmentUploadType");
option.FtpServer
= config.AppConfigGet("FtpServer");
option.FtpUser
= config.AppConfigGet("FtpUser");
option.FtpPassword
= config.AppConfigGet("FtpPassword");
option.FtpBaseUrl
= config.AppConfigGet("FtpBaseUrl");returnoption;
}

就这样,在.net framework的WInform项目启动的时候,我们就完成了文件上传选项参数的读取加载,这样在 FileUploadService 里面的构造函数,就可以获得对应的选项参数信息对象了。

对于.netcore的程序,我们知道它的配置信息是appSettings.json,如下节点所示。

    "UploadSettingOptions": {"AttachmentBasePath": "", //可指定的存储物理地址,如C:\\Attachment,如果没有配置则默认相对目录。
      "AttachmentUploadType": "", //ftp或空,为空则为普通方式
      "FtpServer": "114.215.106.96", //FTP服务器地址
      "FtpUser": "web2", //FTP账号
      "FtpPassword": "", //FTP密码
      "FtpBaseUrl": "http://www.iqidi.com/ftp"//可以指定为HTTP或者FTP的路径,供下载查看
    }

例如对于我们在Web API的初始化选项对象信息,具体加载的代码如下所示。

builder.Services.Configure<UploadSettingOptions>(builder.Configuration.GetSection("UploadSettingOptions"));

相对.net framework的处理来说,使用扩展函数来简化了不少。

这样,我们在文件上传处理的服务类中就可以顺利获得对应的配置信息了。

    /// <summary>
    ///上传附件信息 应用层服务接口实现/// </summary>
    public class FileUploadService : MyCrudService<FileUploadInfo,string, FileUploadPagedDto>, IFileUploadService
{
//微软引入选项模式,它是用于配置框架服务使用的设置. 选项模式由Microsoft.Extensions.Options NuGet包实现//在你需要获得一个选项值时,将 IOptions<TOption> 服务注入到你的类中,使用它的 .Value 属性得到值. private readonlyUploadSettingOptions _options;/// <summary> ///参数化构造,注入上传处理设置信息/// </summary> /// <param name="options"></param> public FileUploadService(IOptions<UploadSettingOptions>options)
{
_options
=options.Value;
}

我们在使用FTP上传文件的时候,使用了FluentFtp类库实现FTP文件的上传处理的,构建FTP对象的处理代码如下所示。

//使用FluentFTP操作FTP文件
var client = new FtpClient(this._options.FtpServer, this._options.FtpUser, this._options.FtpPassword);//如果配置指定了端口,则使用特定端口
if (!string.IsNullOrEmpty(this._options.FtpServer) && this._options.FtpServer.Contains(":"))
{
string port = this._options.FtpServer.Split(':')[1];if (!string.IsNullOrEmpty(port))
{
client.Port
=port.ToInt32();
}
}

上传FTP文件的代码如下所示。

    //确定日期时间目录(格式:yyyy-MM),不存在则创建
    string savePath = string.Format("/{0}-{1:D2}/{2}", DateTime.Now.Year, DateTime.Now.Month, category);bool isExistDir =client.DirectoryExists(savePath);if (!isExistDir)
{
client.CreateDirectory(savePath);
}
//使用FTP上传文件//避免文件重复,使用GUID命名 var ext =FileUtil.GetExtension(info.FileName);var newFileName = string.Format("{0}{1}", Guid.NewGuid().ToString(), ext);//FileUtil.GetFileName(file); savePath=savePath.UriCombine(newFileName);var uploaded = await client.UploadAsync(info.FileData, savePath, FtpRemoteExists.Overwrite, true);//成功后,写入数据库 if (uploaded ==FtpStatus.Success)
{

通过选项参数的方式,我们可以配置参数的IOC处理话,从而实现了参数的灵活配置和读取操作。

在其他业务处理的服务类中,如果涉及到一些需要配置的信息,我们都可以利用这种模式实现参数内容的配置处理。

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在我们对数据进行重要修改调整的时候,往往需要跟踪记录好用户操作日志。一般来说,如对重要表记录的插入、修改、删除都需要记录下来,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。本篇随笔介绍如何在基于SqlSugar的开发框架中,实现对用户操作日志记录的配置设置,以及根据配置信息自动实现用户操作日志记录。

1、用户操作日志记录的配置处理

前面提到,由于用户操作日志会带来一定的额外消耗,因此我们通过配置的方式来决定记录那些业务数据的重要调整。

首先我们在系统中定义一个用户操作日志记录表和一个操作日志配置信息表,系统根据配置进行记录重要的修改调整信息。

列表展示信息如下所示

有了这些信息记录,我们可以在操作基类函数中,通过判断SqlSugar实体类信息中的是否插入、更新、删除的重要设置,可以决定记录它们那些操作日志信息。

下面列表记录了对一些表的增加、修改、删除、以及一些重要的系统操作日志信息,如“密码重置”、“密码修改”、“用户过期设置”等操作日志。

2、在基类中实现用户操作日志记录处理

上面界面展示了如何通过配置,自动记录用户对某业务的相关重要操作记录的界面。系统之所以能够进行相关的信息记录,是在基类函数中定义了相关的逻辑,根据配置逻辑,把插入对象的详细信息、修改对象的变化比记录、删除对象的详细信息进行写入,以及对一些重要的处理,如重置密码等,进行自定义的信息记录的。

下面我们来看看如何在基类中处理这些操作。

例如,我们在删除记录的时候,有时候接收的是实体类的ID,有时候接收的是实体类,那么对于这些条件,我们相应的进行日志处理,如下代码所示。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TEntity input)
{
await OnOperationLog(input, OperationLogTypeEnum.删除);return awaitEntityDb.DeleteAsync(input);
}
/// <summary> ///删除指定ID的对象/// </summary> /// <param name="id">记录ID</param> /// <returns></returns> public virtual async Task<bool>DeleteAsync(TKey id)
{
await OnOperationLog(id, OperationLogTypeEnum.删除);return awaitEntityDb.DeleteByIdAsync(id);
}

其中我们根据日志的操作,定义一个枚举的对象,如下所示。

    /// <summary>
    ///操作日志的枚举类型/// </summary>
    public enumOperationLogTypeEnum
{
增加,
删除,
修改
}

对于删除记录的Id,我们需要把它转换为对应的实体类,然后进行记录的。

        /// <summary>
        ///统一处理实体类的日志记录/// </summary>
        /// <param name="id">实体对象Id</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override asyncTask OnOperationLog(TKey id, OperationLogTypeEnum logType)
{
var enableLog = awaitCheckOperationLogEnable(logType);if(enableLog)
{
var input = await this.EntityDb.GetByIdAsync(id);if (input != null)
{
string note =JsonConvert.SerializeObject(input, Formatting.Indented);awaitAddOperationLog(logType.ToString(), note);
}
}
await Task.CompletedTask;//结束处理 }

其中
CheckOperationLogEnable
就是用来判断是否存在指定操作类型的配置信息的,如果存在,那么就记录操作日志。

我们是根据实体类的全名进行判断,如果存在指定的操作设置,就返回True,如下所示。(刚好基类中可以判断泛型约束TEntity的全名)

        /// <summary>
        ///判断指定的类型(增加、删除、修改)是否配置启用/// </summary>
        /// <param name="logType">指定的类型(增加、删除、修改)</param>
        /// <returns></returns>
        protected async Task<bool>CheckOperationLogEnable(OperationLogTypeEnum logType)
{
var result = false;string tableName = typeof(TEntity).FullName;//表名称或者实体类全名 var settingInfo = await this._logService.GetOperationLogSetting(tableName);if (settingInfo != null)
{
if (logType ==OperationLogTypeEnum.修改)
{
result
= settingInfo.UpdateLog > 0;
}
else if (logType ==OperationLogTypeEnum.增加)
{
result
= settingInfo.InsertLog > 0;
}
else if (logType ==OperationLogTypeEnum.删除)
{
result
= settingInfo.DeleteLog > 0;
}
}
returnresult;
}

对于插入记录,我们也可以同时进行判断并处理日志信息。

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>InsertAsync(TEntity input)
{
SetIdForGuids(input);
//如果Id为空,设置有序的GUID值 await OnOperationLog(input, OperationLogTypeEnum.增加);//判断并记录日志 return awaitEntityDb.InsertAsync(input);
}

对于更新原有记录,它也只需要接收更新前的对象,然后进行判断处理即可。

        /// <summary>
        ///更新对象/// </summary>
        /// <param name="input">实体对象</param>
        /// <returns></returns>
        public virtual async Task<bool>UpdateAsync(TEntity input)
{
SetIdForGuids(input);
//如果Id为空,设置有序的GUID值 await OnOperationLog(input, OperationLogTypeEnum.修改);//判断并记录日志 return awaitEntityDb.UpdateAsync(input);
}

比较两者,我们需要提供一个操作日志方法重载用于记录信息即可。

由于修改的信息,我们需要对比两个不同记录之间的差异信息,这样我们才能友好的判断那些信息变化了。也就是更新前后两个实体对象之间的属性差异信息,需要获取出来。

        /// <summary>
        ///统一处理实体类的日志记录/// </summary>
        /// <param name="input">实体对象</param>
        /// <param name="logType">记录类型</param>
        /// <returns></returns>
        protected override asyncTask OnOperationLog(TEntity input, OperationLogTypeEnum logType)
{
var enableLog = awaitCheckOperationLogEnable(logType);if (enableLog && input != null)
{
if (logType ==OperationLogTypeEnum.修改)
{
var oldInput = await this.EntityDb.GetByIdAsync(input.Id);//对于更新记录,需要判断更新前后两个对象的差异信息 var changeNote = oldInput.GetChangedNote(input); //计算差异的部分 if (!string.IsNullOrEmpty(changeNote))
{
awaitAddOperationLog(logType.ToString(), changeNote);
}
}
else{//对于插入、删除的操作,只需要记录对象的信息 var note =JsonConvert.SerializeObject(input, Formatting.Indented);awaitAddOperationLog(logType.ToString(), note);
}
}
await Task.CompletedTask;//结束处理 }

而对于差异信息,我能定义一个扩展函数来处理他们的差异信息,如下所示。

    /// <summary>
    ///对象属性的处理操作/// </summary>
    public static classObjectExtensions
{
/// <summary> ///对比两个属性的差异信息/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="val1">对象实例1</param> /// <param name="val2">对象实例2</param> /// <returns></returns> public static List<Variance> DetailedCompare<T>(thisT val1, T val2)
{
var propertyInfo =val1.GetType().GetProperties();return propertyInfo.Select(f => newVariance
{
Property
=f.Name,
ValueA
= (f.GetValue(val1, null)?.ToString()) ?? "", //确保不为null ValueB = (f.GetValue(val2, null)?.ToString()) ?? ""})
.Where(v
=> !v.ValueA.Equals(v.ValueB)) //调用内置的Equals判断 .ToList();
}
/// <summary> ///把两个对象的差异信息转换为JSON格式/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="val1">对象实例1</param> /// <param name="val2">对象实例2</param> /// <returns></returns> public static string GetChangedNote<T>(thisT oldVal, T newVal)
{
var specialList = new List<string> { "edittime", "createtime", "lastupdated"};var list = DetailedCompare<T>(oldVal, newVal);var newList = list.Select(s => new { Property = s.Property, OldValue = s.ValueA, NewValue =s.ValueB })
.Where(s
=> !specialList.Contains(s.Property.ToLower())).ToList();//排除某些属性 string note = null;if (newList?.Count > 0)
{
//增加一个ID属性记录显示 var id = EntityHelper.GetEntityId(oldVal)?.ToString();
newList.Add(
new { Property = "Id", OldValue = id, NewValue =id });

note
=JsonConvert.SerializeObject(newList, Formatting.Indented);
}
returnnote;
}
public classVariance
{
public string Property { get; set; }public string ValueA { get; set; }public string ValueB { get; set; }
}
}

这样我们通过LINQ把两个对象的差异信息生成,就可以用来记录变更操作的信息了,最终可以获得类似下面界面提示的差异信息。

也就是获得类似字符串的差异信息。

[
{
"Property": "PID","OldValue": "-1","NewValue": "0"},
{
"Property": "OfficePhone","OldValue": "","NewValue": "18620292076"},
{
"Property": "WorkAddr","OldValue": "广州市白云区同和路**小区**号","NewValue": "广州市白云区同和路330号君立公寓B栋1803房"},
{
"Property": "Id","OldValue": "1","NewValue": "1"}
]

最后的属性Id,是我们强行加到变化列表中的,因为不记录Id的话,不清楚那个记录变更了。

这样我们就实现了增删改的重要操作的记录,并且由于是基类实现,我们只需要在系统中配置决定哪些业务类需要记录即可自动实现重要日志的记录。

另外,我们在类别中还发现了其他一些不同类别的重要操作日志,如重置密码、修改密码、用户过期设置等,这些操作我们提供接口给这些处理调用即可。

        /// <summary>
        ///设置用户的过期与否/// </summary>
        /// <param name="userId">用户ID</param>
        /// <param name="expired">是否禁用,true为禁用,否则为启用</param>
        public async Task<bool> SetExpire(int userId, boolexpired)
{
bool result = false;var info = await this.GetAsync(userId);if (info != null)
{
info.IsExpire
=expired;
result
= await this.UpdateAsync(info);if(result)
{
//记录用户修改密码日志 string note = string.Format("{0} {1}了用户【{2}】的账号", this.CurrentApiUser.FullName, expired ? "禁用" : "启用", info.Name);await base.AddOperationLog("用户过期设置", note);
}
}
returnresult;
}

其中
AddOperationLog
就是我们调用基类插入指定类型和日志信息的记录的,通过自定义类型和自定义日志信息,可以让我们弹性化的处理一些重要日志记录。

系列文章:

系列文章:


基于SqlSugar的开发框架的循序渐进介绍(1)--框架基础类的设计和使用


基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理


基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发


基于SqlSugar的开发框架循序渐进介绍(4)-- 在数据访问基类中对GUID主键进行自动赋值处理


基于SqlSugar的开发框架循序渐进介绍(5)-- 在服务层使用接口注入方式实现IOC控制反转


基于SqlSugar的开发框架循序渐进介绍(6)-- 在基类接口中注入用户身份信息接口


基于SqlSugar的开发框架循序渐进介绍(7)-- 在文件上传模块中采用选项模式【Options】处理常规上传和FTP文件上传


基于SqlSugar的开发框架循序渐进介绍(8)-- 在基类函数封装实现用户操作日志记录


基于SqlSugar的开发框架循序渐进介绍(9)-- 结合Winform控件实现字段的权限控制


基于SqlSugar的开发框架循序渐进介绍(10)-- 利用axios组件的封装,实现对后端API数据的访问和基类的统一封装处理


基于SqlSugar的开发框架循序渐进介绍(11)-- 使用TypeScript和Vue3的Setup语法糖编写页面和组件的总结


基于SqlSugar的开发框架循序渐进介绍(12)-- 拆分页面模块内容为组件,实现分而治之的处理


基于SqlSugar的开发框架循序渐进介绍(13)-- 基于ElementPlus的上传组件进行封装,便于项目使用


基于SqlSugar的开发框架循序渐进介绍(14)-- 基于Vue3+TypeScript的全局对象的注入和使用


基于SqlSugar的开发框架循序渐进介绍(15)-- 整合代码生成工具进行前端界面的生成


基于SqlSugar的开发框架循序渐进介绍(16)-- 工作流模块的功能介绍


基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理


基于SqlSugar的开发框架循序渐进介绍(18)-- 基于代码生成工具Database2Sharp,快速生成Vue3+TypeScript的前端界面和Winform端界面

在Winform开发中有时候我们为了不影响主UI线程的处理,以前我们使用后台线程BackgroundWorker来处理一些任务操作,不过随着异步处理提供的便利性,我们可以使用Async-Awati异步任务处理替换原来的后台线程BackgroundWorker处理方式,更加的简洁明了。

在一些耗时的操作过程中,在长时间运行时可能会导致用户界面 (UI) 处于停止响应状态,因此使用使用Async-Awati异步任务处理或者后台线程BackgroundWorker来处理一些任务操作很有必要。

在使用BackgroundWorker的过程中,我们可以定义自己的状态参数信息,从而实现线程状态的实时跟踪以及进度和信息提示,方便我们及时通知UI进行更新。

现在使用Async-Awati异步任务处理,一样可以在处理过程中通知UI更新进度和提示信息。

1、回顾BackgroundWorker后台线程的处理代码

我们先来了解一下BackgroundWorker后台线程的操作代码,对比下再介绍使用Async-Awati异步任务处理和通知操作。

一般的使用代码是需要初始化后台线程对象的,如下代码所示。

    public partial classMainFrame : BaseForm
{
/// <summary> ///增加一个变量来记录线程状态/// </summary> private bool IsThreadRunning = false;private BackgroundWorker worker = newBackgroundWorker();publicMainFrame()
{
InitializeComponent();

Portal.gc.InitData();

worker.WorkerSupportsCancellation
= true; //支持取消 worker.WorkerReportsProgress = true; //支持报告进度 worker.DoWork += worker_DoWork; //处理过程 worker.RunWorkerCompleted += worker_RunWorkerCompleted; //完成操作 worker.ProgressChanged += worker_ProgressChanged; //报告进度 }

例如进度条的通知,主要就是计算总任务的数量,并用于显示当前的任务进度信息,实例代码如下所示

        /// <summary>
        ///进度条的通知/// </summary>
        void worker_ProgressChanged(objectsender, ProgressChangedEventArgs e)
{
this.barProgress.EditValue =e.ProgressPercentage;
CollectStateInfo stateInfo
= e.UserState asCollectStateInfo;if (stateInfo != null)
{
var message = string.Format("正在采集 {0} 的 {1} , 项目名称为:{2}", stateInfo.TotalRecords, stateInfo.CompletedRecord + 1, stateInfo.CurrentItemName);this.lblTips.Text =message;this.barTips.Caption =message;//记录运行位置 JobParameterHelper.SaveData(newCurrentJobParameter(stateInfo));
}
}

后台进程处理的关键事件就是处理过程的代码实现,它处理任务的时候,把当前的状态通过事件方式通知UI显示。

        private void backgroundWorker1_DoWork(objectsender, DoWorkEventArgs e)
{
BackgroundWorker worker
= sender asBackgroundWorker;
Random r
= newRandom();int numCount = 0;while (worker.CancellationPending == false)
{
int num = r.Next(0, 10000);if (num % 5 == 0)
{
numCount
++;
worker.ReportProgress(
0, num);
Thread.Sleep(
1000);
}
}
e.Result
=numCount;
}

触发任务开始的时候,我们调用代码如下所示。

    if (!worker.IsBusy)
{
worker.RunWorkerAsync(stateInfo);
}

任务完成后,通知更新界面即可。

void worker_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
//还原按钮状态 InitCollectState();
IsThreadRunning
= false;string message = "采集操作完成";
MessageDxUtil.ShowTips(message);
}

2、使用Async-Awati异步任务处理代替BackgroundWorker

为了测试使用Asyn-Await异步处理,我创建一个简单的Demo程序,用于测试其效果。

窗体里面放置一个按钮,触发按钮执行任务操作,并逐步提示进度条信息,完成后提示任务完成。

为了在异步处理中提示进度信息,我们引入了Progress 线程通知对象。

定义一个线程通知的Progress对象,如下所示。这里的int也可以换为自定义的对象类,以方便承载更多的信息。

  var reporter = new Progress<int>(progressChanged);

其中progressChanged 是我们定义的一个通知UI显示进度的处理函数,如下所示。

        /// <summary>
        ///报告进度/// </summary>
        /// <param name="percentage">当前进度</param>
        void progressChanged(intpercentage)
{
this.progressBar1.EditValue =percentage;this.progressPanel.Caption = percentage == 100 ? "任务已完成": "任务正在处理";this.progressPanel.Description = String.Format("完成【{0}%】", percentage);
}

接着我们定义一个处理任务的WorkStart方法,接收一个Progress对象,如下代码所示。

      var reporter = new Progress<int>(progressChanged);var result = await this.WorkStart(reporter);

为了简单样式异步调用,我们这里只是延迟了一下处理任务,实际处理的话,调用异步方法即可。

        /// <summary>
        ///执行任务/// </summary>
        private async Task<CommonResult> WorkStart(IProgress<int>progress)
{
var result = newCommonResult();for(int i = 0; i < 100; i++)
{
await Task.Delay(100);
progress.Report(i
+ 1);
}
result.Success
= true;returnresult;
}

我们可以看到,任务每次执行到一个节点,就会调用对象方法Report进行通知处理。

而任务完成后,我们简单的通知处理即可。整段代码如下所示。

    /// <summary>
    ///Async Await异步线程处理/// </summary>
    public partial classFrmAsyncAwaitDemo : DevExpress.XtraEditors.XtraForm
{
publicFrmAsyncAwaitDemo()
{
InitializeComponent();
this.progressBar1.Visible = false;this.progressPanel.Visible = false;
}
private async void btnStart_Click(objectsender, EventArgs e)
{
this.btnStart.Enabled = false;this.progressBar1.Visible = true;this.progressPanel.Visible = true;var reporter = new Progress<int>(progressChanged);var result = await this.WorkStart(reporter);this.WorkCompleted(result);
}
/// <summary> ///任务完成/// </summary> /// <param name="result">返回结果CommonResult</param> voidWorkCompleted(CommonResult result)
{
if(result.Success)
{
//操作成功的处理 }var alert = newAlertControl();
alert.FormLocation
=AlertFormLocation.TopRight;
alert.AutoFormDelay
= 2000;
alert.Show(
this, "任务提示", result.Success ? "任务处理完成,操作成功": result.ErrorMessage);this.progressBar1.Visible = false;this.progressPanel.Visible = false;this.btnStart.Enabled = true;
}
/// <summary> ///报告进度/// </summary> /// <param name="percentage">当前进度</param> void progressChanged(intpercentage)
{
this.progressBar1.EditValue =percentage;this.progressPanel.Caption = percentage == 100 ? "任务已完成": "任务正在处理";this.progressPanel.Description = String.Format("完成【{0}%】", percentage);
}
/// <summary> ///执行任务/// </summary> private async Task<CommonResult> WorkStart(IProgress<int>progress)
{
var result = newCommonResult();for(int i = 0; i < 100; i++)
{
await Task.Delay(100);
progress.Report(i
+ 1);
}
result.Success
= true;returnresult;
}
}

在我们实际的案例中,文件上传处理就使用了这种方式来通知UI线程,任务处理的代码如下所示。

因此使用Async-Awati异步任务处理代替BackgroundWorker,代码更加简便,而且使用
IProgress
接口类来处理通知,也是非常方便的。

在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser、DynamicExpresso、Z.Expressions,它们各自功能有所不同,不过基本上都能满足要求。它们都可以根据相关的参数进行字符串表达式的求值,本篇随笔介绍它们三者的使用代码,以及总结其中的一些经验。

数学表达式求值应该是最常见的,一般我们在应用程序中如果需要计算,是需要对参数进行类型转换,然后在后台进行相应计算的。但是如果是计算一些符合的式子或者公式,特别是参数不一定的情况下,这个就比较麻烦。利用第三方组件,对表达式进行快速求值,可以满足我们很多实际项目上的需求,而且处理起来也很方便。

这几个第三方组件,它们的GitHub或官网地址:

https://github.com/nreco/lambdaparser

https://github.com/dynamicexpresso/DynamicExpresso

https://eval-expression.net/eval-execute

不过Z.Expressions是收费的,前两者都是免费的。

我使用字符串表达式进行求值的场景,主要就是想对一个SQL条件的表达式,转换为普通的字符串表达式,然后根据对象的参数值,进行求值处理,这几个表达式求值组件都支持这样的操作,为了更好演示它们的使用效果及代码,我们专门创建了一个案例代码进行测试验证,确认满足我的实际需求。

1、Z.Expressions.Eval 表达式解析

Z.Expression.Eval是一个免费开源的(后续收费了),可扩展的,超轻量级的公式化语言解析执行工具包,可以在运行时解析C#表达式的开源免费组件。Z.Expressions从2.0开始支持了NetCore,但是收费的。参考地址:
https://riptutorial.com/eval-expression/learn/100000/getting-started
或者
https://eval-expression.net/eval-execute

在运行时解析C#表达式,例如一些工资或者成本核算系统,就需要在后台动态配置计算表达式,从而进行计算求值。

下面对几个不同的案例代码进行介绍及输出结果验证

匿名类型处理

//匿名类型
string expression = "a*2 + b*3 - 3";int result = Eval.Execute<int>(expression, new { a = 10, b = 5});
Console.WriteLine(
"{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32

指定参数

//指定参数
expression = "{0}*2 + {1}*3 - 3";
result
= Eval.Execute<int>(expression, 10, 5);
Console.WriteLine(
"{0} = {1}", expression, result);//{0}*2 + {1}*3 - 3 = 32

类对象

//类对象
expression = "a*2 + b*3 - 3";dynamic expandoObject = newExpandoObject();
expandoObject.a
= 10;
expandoObject.b
= 5;

result
= Eval.Execute<int>(expression, expandoObject);
Console.WriteLine(
"{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32

字典对象

//字典对象
expression = "a*2 + b*3 - 3";var values = new Dictionary<string, object>()
{
{
"a", 10},
{
"b", 5}
};

result
= Eval.Execute<int>(expression, values);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32

委托类型

//委托类型1
expression = "{0}*2 + {1}*3";var compiled = Eval.Compile<Func<int, int, int>>(expression);
result
= compiled(10, 15);
Console.WriteLine(
"{0} = {1}", expression, result);//{0}*2 + {1}*3 = 65//委托类型2 expression = "a*2 + b*3";
compiled
= Eval.Compile<Func<int, int, int>>(expression, "a", "b");
result
= compiled(10, 15);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 = 65

字符串扩展支持

//字符串扩展支持-匿名类型
expression = "a*2 + b*3 - 3";
result
= expression.Execute<int>(new { a = 10, b = 5});
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32//字符串扩展支持-字典类型 expression = "a*2 + b*3 - 3";
values
= new Dictionary<string, object>()
{
{
"a", 10},
{
"b", 5}
};
result
= expression.Execute<int>(values);
Console.WriteLine(
"{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32

可以看出,该组件提供了非常丰富的表达式运算求值处理方式。

2、NReco.LambdaParser 表达式解析

我看中这个组件的处理,主要是因为它能够传入参数是字典类型,这样我可以非常方便的传入各种类型的参数,并且这个组件比较接近SQL语法,可以设置利用常规的=代替表达式的==,这样对于SQL语句来说是方便的。

它的案例代码如下所示。

/// <summary>
///NReco.LambdaParser 表达式解析/// </summary>
private void btnLamdaParser_Click(objectsender, EventArgs e)
{
var lambdaParser = newNReco.Linq.LambdaParser();var dict = new Dictionary<string, object>();
dict[
"pi"] = 3.14M;
dict[
"one"] =1M;
dict[
"two"] =2M;
dict[
"test"] = "test";
Console.WriteLine(lambdaParser.Eval(
"pi>one && 0<one ? (1+8)/3+1*two : 0", dict)); //--> 5 Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); //--> TEST Console.WriteLine(lambdaParser.Eval("pi>one && 0<one", dict)); //--> True Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); //--> TEST }

同样它支持的算术符号操作有:+, -, *, /, %,以及常规的逻辑判断:==, !=, >, <, >=, <=,如果需要它允许把=作为==比较,那么设置属性 AllowSingleEqualSign  = true 即可,如下代码。

    var lambdaParser = newLambdaParser();
lambdaParser.AllowSingleEqualSign
= true;//可以使用 = 作为逻辑判断,如Title ="Leader",而不用Title =="Leader" var evalResult = lambdaParser.Eval(repalce, dict);

该组件没有过多提供例子,不过它的例子提供的关键点,基本上都能实现我们实际的表达式求值处理要求了。

3、DynamicExpresso 表达式解析

相对于LambdaParser的简洁、Z.Expressions收费处理,Dynamic Expresso 可以说是提供了一个非常强大的、免费开源的处理类库,它提供非常多的表达式求值的实现方式。

简单的字符串表达式求值如下代码

var interpreter = newInterpreter();var result = interpreter.Eval("8 / 2 + 2");

但是一般我们需要传入一定的参数进行表达式求值的。

var target = newInterpreter();double result = target.Eval<double>("Math.Pow(x, y) + 5",new Parameter("x", typeof(double), 10),new Parameter("y", typeof(double), 2));

或者

var interpreter = newInterpreter();var parameters = new[] {new Parameter("x", 23),new Parameter("y", 7)
};
Assert.AreEqual(
30, interpreter.Eval("x + y", parameters));

或者赋值指定的参数

var target = new Interpreter().SetVariable("myVar", 23);
Assert.AreEqual(
23, target.Eval("myVar"));

对于字典类型的处理,是我喜欢的方式,它的案例代码如下所示。

var interpreter = newInterpreter();var dict = new Dictionary<string, object>();
dict.Add(
"a", 1.0);
dict.Add(
"b", 2);
dict.Add(
"d", 4);
dict.Add(
"e", 5);
dict.Add(
"str", 'f');foreach (var v indict)
{
object value =v.Value;int para = 0;if (int.TryParse(v.Value.ToString(), outpara))
{
value
= (float)para;
}
interpreter.SetVariable(v.Key, value);
}
Console.WriteLine(interpreter.Eval(
"a+b").ToString()); //3 Console.WriteLine(interpreter.Eval("a/b").ToString()); //0.5 Console.WriteLine(interpreter.Eval("a > b").ToString()); //False Console.WriteLine(interpreter.Eval("str == 'f'").ToString()); //True

对于类的属性表达式查询,测试代码如下所示

    var customers = new List<Customer>{new Customer() { Name = "David", Age = 31, Gender = 'M'},new Customer() { Name = "Mary", Age = 29, Gender = 'F'},new Customer() { Name = "Jack", Age = 2, Gender = 'M'},new Customer() { Name = "Marta", Age = 1, Gender = 'F'},new Customer() { Name = "Moses", Age = 120, Gender = 'M'},
};
string whereExpression = "customer.Age > 18 && customer.Gender == 'F'";

Func
<Customer, bool> dynamicWhere = interpreter.ParseAsDelegate<Func<Customer, bool>>(whereExpression, "customer");
Console.WriteLine(customers.Where(dynamicWhere).Count());
//=> 1 var customer_query = (new List<Customer>{new Customer() { Name = "David", Age = 31, Gender = 'M'},new Customer() { Name = "Mary", Age = 29, Gender = 'F'},new Customer() { Name = "Jack", Age = 2, Gender = 'M'},new Customer() { Name = "Marta", Age = 1, Gender = 'F'},new Customer() { Name = "Moses", Age = 120, Gender = 'M'},
}).AsQueryable();
whereExpression
= "customer.Age > 18 && customer.Gender == 'F'";var expression = interpreter.ParseAsExpression<Func<Customer, bool>>(whereExpression, "customer");
Console.WriteLine(customer_query.Where(expression).Count());
//=> 1

4、SQL条件语句的正则表达式和字符串求值处理

前面介绍了几个表达式求值处理的组件,他们基本上都能够满足实际的求值处理,只是提供的功能有所侧重。

我主要希望用它来对特定的表达式进行求布尔值,判断表达式是否满足条件的。

例如对于sql条件语句:(Amount> 500 and Title ='Leader') or Age> 32, 以及一个字典对象的参数集合,我希望能够提取里面的Amount、Title、Leader、Age这样的键,然后给字典赋值,从而判断表达式的值。

由于sql表达式和C#代码的表达式逻辑语法有所差异,我们需要替换and Or 为实际的&& || 字符,因此给定替换的正则表达式:\sand|\sor

而我需要先提取条件语句的键值内容,然后获得指定的键参数,那么也要提供一个正则表达式:\w*[^>=<!'()\s] ,这个正则表达式主要就是提取特定的字符匹配。

提取内容的C#代码逻辑如下所示。

        private void btnRegexExtract_Click(objectsender, EventArgs e)
{
var source = this.txtSource.Text;//先替换部分内容 \sand|\sor source = Regex.Replace(source, this.txtReplaceRegex.Text, "");//替换表达式//增加一行记录主内容 this.txtContent.Text += "替换正则表达式后内容:";this.txtContent.AppendText(Environment.NewLine);this.txtContent.Text +=source;this.txtContent.AppendText(Environment.NewLine);//在匹配内容处理 var regex = new Regex(this.txtRegex.Text);var matches =regex.Matches(source);//遍历获得每个匹配的内容 var fieldList = new List<string>();int i = 0;foreach (Match match inmatches)
{
this.txtContent.AppendText(match.Value);this.txtContent.AppendText(Environment.NewLine);if (i++ % 2 == 0)
{
fieldList.Add(match.Value);
}
}
this.txtContent.AppendText("获得表达式键:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(fieldList.ToJson());this.txtContent.AppendText(Environment.NewLine);var repalce = ReplaceExpress(this.txtSource.Text);this.txtContent.AppendText("替换And=>&& or=>|| '=> \" 操作符后内容:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(repalce);
}
        /// <summary>
        ///替换And=>&& or=>|| '=> \" 操作符后内容/// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        private string ReplaceExpress(stringsource)
{
//操作符替换表达式 var repalce = Regex.Replace(source, @"\sand\s", "&&"); //and => && repalce = Regex.Replace(repalce, @"\sor\s", "||"); //or => || repalce = Regex.Replace(repalce, @"'", "\""); //'=> \" returnrepalce;
}

表达式处理结果如下所示

它的逻辑代码如下。

        private void btnRunExpression_Click(objectsender, EventArgs e)
{
//操作符替换表达式 var repalce = ReplaceExpress(this.txtSource.Text);this.txtContent.Text = "替换And=>&& or=>|| '=> \" 操作符后内容:";this.txtContent.AppendText(Environment.NewLine);this.txtContent.Text +=repalce;this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);//(Amount> 500 and Title ='Leader') or Age> 32 var dict = new Dictionary<string, object>();
dict[
"Amount"] = 600;
dict[
"Title"] = "Leader";
dict[
"Age"] = 40;this.txtContent.AppendText("字典内容");foreach(var key indict.Keys)
{
this.txtContent.AppendText($"{key}:{dict[key]}");
}
this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);//var valComparer = new ValueComparer() { NullComparison = ValueComparer.NullComparisonMode.Sql };//var lambdaParser = new LambdaParser(valComparer); var lambdaParser = newLambdaParser();
lambdaParser.AllowSingleEqualSign
= true;//可以使用=作为判断,如Title ="Leader",而不用Title =="Leader" var express1 = "(Amount> 500 && Title = \"Leader\") or Age>30";var result1 =lambdaParser.Eval(express1, dict);this.txtContent.AppendText("LambdaParser 表达式处理:");this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express1 + "=>" +result1);var express2 = "( Amount> 500 && Title =\"leader\" )"; //字符串比较(''=> "") var result2 =lambdaParser.Eval(express2, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express2 + "=>" +result2);var express3 = "Amount> 500";var result3 =lambdaParser.Eval(express3, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result3);var express4 = "Title = \"Leader\""; //字符串比较(''=> "") var result4 =lambdaParser.Eval(express4, dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result4);this.txtContent.AppendText(Environment.NewLine);
Console.WriteLine(lambdaParser.Eval(
"Title.ToString()", dict)); //--> Leader//DynamicExpresso 表达式解析处理 this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText("DynamicExpresso 表达式解析处理:");var interpreter = newInterpreter();foreach (var v indict)
{
interpreter.SetVariable(v.Key, v.Value);
}
//express3 = "Amount> 500"; var result33 =interpreter.Eval(express3);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result33);//使用''出错,字符串比较需要使用"" try{
express4
= "Title == \"Leader\"";var result44 =interpreter.Eval(express4);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result44);
}
catch(Exception ex)
{
this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + ",解析出错 =>" +ex.Message);
}
//var dict = new Dictionary<string, object>();//dict["Amount"] = 600;//dict["Title"] = "Leader";//dict["Age"] = 40; this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText("Z.Expressions.Eval 表达式解析:");var result333 = express3.Execute<bool>(dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express3 + "=>" +result333);

express4
= "Title == 'Leader'"; //Z.Expressions可以接受 ' 代替 " var result444 = express4.Execute<bool>(dict);this.txtContent.AppendText(Environment.NewLine);this.txtContent.AppendText(express4 + "=>" +result444);
}

这样我们就可以转换SQL条件表达式为实际的C#表达式,并通过赋值参数,实现动态表达式的求值处理。