2023年2月

我们在做某件事情的时候,一般需要详细了解它的特点,以及内在的逻辑关系,一旦我们详细了解了整个事物后,就可以通过一些辅助手段来提高我们的做事情的效率了。本篇随笔介绍ABP VNext框架各分层项目的规则,以及结合代码生成工具Database2Sharp来实现项目类代码,项目文件等内容的快速生成。

ABP VNext框架在官方下载项目的时候,会生成一个标准的空白项目框架,本代码工具不是替代这个项目代码生成,而是基于这个基础上进行基于数据表的增量式开发模块的需求(毕竟官方没有针对数据表的项目代码生成),最终所有的子模块可以集成在主模块上,形成一个完整的系统。

1、ABP VNext框架的项目关系

目前框架代码生成包括:应用服务层:Application.Contracts和Application项目,领域层:Domain.Shared和Domain项目,基础设施层:EntityFrameworkCore项目,HTTP 层:HttpApi和HttpApi.Client项目。生成代码集成相关的基类代码,简化项目文件的类代码。

应用服务层:

Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO)。
Application,包含应用服务实现,依赖于 Domain 包和 Application.Contracts 包。

领域层:
Domain.Shared,包含常量,枚举和其他类型.
Domain 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象,依赖于 Domain.Shared 包.

基础设施层:

EntityFrameworkCore,包含EF的ORM处理,使用仓储模式,实现数据的存储功能。

HTTP 层
HttpApi项目, 为模块开发REST风格的HTTP API。
HttpApi.Client项目,它将应用服务接口实现远程端点的客户端调用,提供的动态代理HTTP C#客户端的功能。

各个层的依赖关系如下图所示。

2、ABP VNext框架各层的项目代码

我在上篇随笔《
在ABP VNext框架中对HttpApi模块的控制器进行基类封装
》中介绍了为了简化子类一些繁复代码的重复出现,使用自定义基类方式,封装了一些常用的函数,通过泛型参数的方式,可以完美的实现强类型接口的各种处理。

对于ABP VNext个项目的内容,我们继续推演到它的项目组织上来。为了简便,我们以简单的客户表T_Customer表来介绍框架项目的分层和关系。

对于这个额外添加的表,首先我们来看看应用服务层的Application.Contracts项目文件,如下所示。

其中映射DTO放在DTO目录中,而应用服务的接口定义则放在Interface目录中,使用目录的好处是利于查看和管理,特别是在业务表比较多的情况下。

DTO类的定义如下所示。

其中用到了基类对象EntityDto、CreationAuditedEntityDto、AuditEntityDto、FullAuditedEntityDto几个基类DTO对象,具体采用哪个基类DTO,依赖于我们表的包含哪些系统字段。如只包含CreationTime、CreatorId那么就采用CreationAuditedEntityDto,其他的依次类推。

领域层的实体类关系和前面DTO关系类似,如下所示。

这样我们利用代码生成工具生成代码的时候,就需要判断表的系统字段有哪些来使用不同的系统DTO基类了。

而应用服务层的接口定义文件如下所示,它使用了我们前面随笔介绍过的自定义基类或接口。

通过传入泛型类型,我们可以构建强类型化的接口定义。

应用服务层的Application项目包含DTO映射文件和应用服务层接口实现类,如下所示。

其中映射DTO、Domain Entity(领域实体)关系的Automapper文件放在MapProfile文件夹中,而接口实现类文件则放在Service目录中,也是方便管理。

映射类文件,主要定义DTO和Domain Entity(领域实体)关系,如下所示。

这样文件单独定义,在模块中会统一加载整个程序集的映射文件,比较方便。

    public classTestProjectApplicationModule : AbpModule
{
public override voidConfigureServices(ServiceConfigurationContext context)
{
Configure
<AbpAutoMapperOptions>(options =>{
options.AddMaps
<TestProjectApplicationModule>();
});
}
}

应用服务层的接口实现如下定义所示。

    /// <summary>
    ///Customer,应用层服务接口实现/// </summary>
    public classCustomerAppService : 
MyCrudAppService
<Customer, CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>,
ICustomerAppService

通过继承相关的自定义基类,可以统一封装一些常见的接口实现,传入对应的泛型类型,可以构建强类型的接口实现。

另外实现类还需要包含一些方法的重载,以重写某些规则,如排序、查询处理、以及一些通用的信息转义等,详细的应用服务层接口实现代码如下所示。

    /// <summary>
    ///Customer,应用层服务接口实现/// </summary>
    public classCustomerAppService : 
MyCrudAppService
<Customer, CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonly IRepository<Customer, string> _repository;//业务对象仓储对象 public CustomerAppService(IRepository<Customer, string> repository) : base(repository)
{
_repository
=repository; }/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override async Task<IQueryable<Customer>>CreateFilteredQueryAsync(CustomerPagedDto input)
{
var query = await base.CreateFilteredQueryAsync(input);
query
=query
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t=>t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals//区间查询 .WhereIf(input.AgeStart.HasValue, s => s.Age >=input.AgeStart.Value)
.WhereIf(input.AgeEnd.HasValue, s
=> s.Age <=input.AgeEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value)
;
returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<Customer> ApplySorting(IQueryable<Customer>query, CustomerPagedDto input)
{
//按创建时间倒序排序 return base.ApplySorting(query, input).OrderByDescending(s => s.CreationTime);//时间降序//先按第一个字段排序,然后再按第二字段排序//return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s => s.Seq); }/// <summary> ///获取字段中文别名(用于界面显示)的字典集合/// </summary> /// <returns></returns> public override Task<Dictionary<string, string>>GetColumnNameAlias()
{
Dictionary
<string, string> dict = new Dictionary<string, string>();#region 添加别名解析 //系统部分字段 dict.Add("Id", "编号");
dict.Add(
"UserName", "用户名");
dict.Add(
"Creator", "创建人");
dict.Add(
"CreatorUserName", "创建人");
dict.Add(
"CreationTime", "创建时间");//其他字段 dict.Add("Name", "姓名");
dict.Add(
"Age", "");#endregion returnTask.FromResult(dict);
}
/// <summary> ///对记录进行转义/// </summary> /// <param name="item">dto数据对象</param> /// <returns></returns> protected override voidConvertDto(CustomerDto item)
{
//如需要转义,则进行重写 #region 参考代码 //用户名称转义//if (item.Creator.HasValue)//{// //需在CustomerDto中增加CreatorUserName属性//var user = _userRepository.FirstOrDefault(item.Creator.Value);//if (user != null)//{//item.CreatorUserName = user.UserName;//}//}//if (item.UserId.HasValue)//{//item.UserName = _userRepository.Get(item.UserId.Value).UserName;//}//IP地址转义//if (!string.IsNullOrEmpty(item.ClientIpAddress))//{//item.ClientIpAddress = item.ClientIpAddress.Replace("::1", "127.0.0.1");//} #endregion}/// <summary> ///用于测试的额外接口/// </summary> public Task<bool>TestExtra()
{
return Task.FromResult(true);
}
}

这些与T_Customer 表相关的信息,如表信息,字段信息等相关的内容,可以通过代码生成工具元数据进行统一处理即可。

领域层的内容,包含Domain、Domain.Share两个项目,内容和Applicaiton.Contracts项目类似,主要定义一些实体相关的内容,这部分也是根据表和表的字段进行按规则生成。而其中一些类则根据命名控件和项目名称构建即可。

而Domain项目中的Customer领域实体定义代码如下所示。

而领域实体和聚合根的基类关系如下所示。

具体使用***
Entity
(如FullAuditedEntity基类)还是使用聚合根***
AggregateRoot
(如FullAuditedAggregateRoot)作为领域实体的基类,生成的时候,我们需要判断表的字段关系即可,如果表包含ExtraProperties和ConcurrencyStamp,则使用聚合根相关的基类。

我们的T_Customer包含聚合根基类所需要的字段,代码生成的时候,则基类应该使用
FullAuditedAggregateRoot<T>
基类。

对于EntityFrameworkCore项目文件,它主要就是生成对应表的DbSet然后用于操作即可。

其中DbContext文件如下所示

namespaceWHC.TestProject.EntityFrameworkCore
{
[ConnectionStringName(
"Default")]public class TestProjectDbContext : AbpDbContext<TestProjectDbContext>{/// <summary> ///T_Customer,数据表对象/// </summary> public virtual DbSet<Customer> Customers { get; set; }public TestProjectDbContext(DbContextOptions<TestProjectDbContext>options)
:
base(options)
{
}
protected override voidOnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureTestProject();
}
}
}

按照规则生成即可,其他的可以不管了。

我们注意一下EntityFrameworkCoreModule中的内容处理,如果不是采用聚合根作为领域实体的基类,而是采用**Entity标准实体(如FullAuditedEntity基类)作为基类,那么需要在该文件中默认设置为true处理,因为ABP VNext框架默认只是加入聚合根的领域实体处理。

namespaceWHC.TestProject.EntityFrameworkCore
{
[DependsOn(
typeof(TestProjectDomainModule),typeof(AbpEntityFrameworkCoreModule)
)]
public classTestProjectEntityFrameworkCoreModule : AbpModule
{
public override voidConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext
<TestProjectDbContext>(options =>{//options.AddDefaultRepositories();//默认情况下,这将为每个聚合根实体(从派生的类AggregateRoot)创建一个存储库。//如果您也想为其他实体创建存储库,请设置includeAllEntities为true://参考https://docs.abp.io/en/abp/latest/Entity-Framework-Core#add-default-repositories options.AddDefaultRepositories(includeAllEntities: true);
});
}
}
}

上面的处理,即使是采用**Entity标准实体(如FullAuditedEntity基类)作为基类,也没问题了,可以顺利反射Repository对象出来了。

而对于Http项目分层,包含的HttpApi项目和HttpApi.Client,前者是重从应用服务层的服务接口,使用知道自定义的API规则,虽然默认可以使用应用服务层ApplicationService的自动API发布,不过为了更强的控制规则,建议重写(也是官方的做法)。两个项目文件如下所示。

其中HttpApi项目控制器,也是采用了此前介绍过的自定义基类,可以减少重复代码的处理。

namespaceWHC.TestProject.Controllers
{
/// <summary> ///Customer,控制器对象/// </summary> //[RemoteService]//[Area("crm")] [ControllerName("Customer")]
[Route(
"api/Customer")]public classCustomerController :
MyAbpControllerBase
<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonlyICustomerAppService _appService;public CustomerController(ICustomerAppService appService) : base(appService)
{
_appService
=appService;
}

}
}

其中MyAbpControllerBase控制器基类,封装了很多常见的CRUD方法(Create/Update/Delete/GetList/Get),以及一些BatchDelete、Count、GetColumnNameAlias等基础方法。

对于ABP VNext各个项目的项目文件的生成,我们这里顺便说说,其实这个文件很简单,没有太多的内容,包含命名空间,项目名称,以及一些常见的引用而已,它本身也是一个XML文件,填入相关信息生成文件即可。

而对于解决方案,它就是包含不同的项目文件,以及各个项目文件有一个独立的GUID,因此我们动态构建对应的GUID值,然后绑定在模板上即可。

代码工具中,后端提供数据绑定到前端模板即可实现内容的动态化了。

3、使用代码生成工具Database2Sharp生成ABP VNext框架项目

上面介绍了ABP VNext框架的各个项目层的代码生成,以及一些代码生成处理的规则,那么实际的代码生成工具生成是如何的呢?

代码生成工具下载地址:
http://www.iqidi.com/database2sharp.htm

首先单击左侧节点展开ABP VNext项目的数据库,让数据库的元数据读取出来,便于后面的代码生成。

然后从右键菜单中选择【代码生成】【ABP VNext框架代码生成】或者工具栏中选择快速入口,一样的效果。

在弹出的对话框中选择相关的数据表,用于生成框架代码即可,注意修改合适的主命名空间,可以是TestProject或者WHC.Project等类似的名称。

最后下一步生成确认即可生成相关的解决方案代码。

生成后所有项目关系已经完善,可以直接打开解决方案查看到整个项目情况如下所示。

这样生成的解决方案,可以编译为一单独的模块,需要的时候,直接在主项目中引用并添加依赖即可。

例如我们在一个ABP VNext的标准项目MicroBookStore.Web中引入刚才代码生成工具生成的模块,那么在MicroBookStoreWebModule.cs 中添加依赖即可,如下所示。

由于我们是采用DLL的引用方式,那么在项目添加对应的引用关系才可以使用。

同时在EFCore项目中添加项目的TestProject项目的EF依赖关系如下所示。

这样跑动起来项目,就可以有Swagger的接口可以查看并统一调用了。

以上就是对于ABP VNext框架项目的分析和项目关系的介绍,并重要介绍利用代码生成工具来辅助增量式模块化开发的操作处理,这样我们在开发ABP VNext项目的时候,更加方便高效了。

前面介绍了一些ABP VNext架构上的内容,随着内容的细化,我们会发现ABP VNext框架中的Entity Framework处理表之间的引用关系还是比较麻烦的,一不小心就容易出错了,本篇随笔介绍在ABP VNext框架中处理和用户相关的多对多的关系处理。

我们这里需要在一个基础模块中创建一个岗位管理,岗位需要包含一些用户,和用户是多对多的关系,因此需要创建一个中间表来放置他们的关系,如下所示的数据库设计。

这个是典型的多对多关系的处理,我们来看看如何在在ABP VNext框架中处理这个关系。

1、扩展系统用户信息

为了模块间不产生依赖,例如用户表,迁移dbcontext中使用了IdentityUser,而运行的dbcontext使用了appuser进行了对其的映射,
https://github.com/abpframework/abp/issues/1998

因此参照实例模块Bloging(
https://github.com/abpframework/abp/tree/dev/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/Users
)中的BlogUser来扩展一下模块的用户对象

   public class AppUser : AggregateRoot<Guid>, IUser, IUpdateUserData
{
public virtual Guid? TenantId { get; protected set; }public virtual string UserName { get; protected set; }public virtual string Email { get; protected set; }public virtual string Name { get; set; }public virtual string Surname { get; set; }public virtual bool EmailConfirmed { get; protected set; }public virtual string PhoneNumber { get; protected set; }public virtual bool PhoneNumberConfirmed { get; protected set; }protectedAppUser()
{

}
publicAppUser(IUserData user)
:
base(user.Id)
{
TenantId
=user.TenantId;
UpdateInternal(user);
}
public virtual boolUpdate(IUserData user)
{
if (Id !=user.Id)
{
throw new ArgumentException($"Given User's Id '{user.Id}' does not match to this User's Id '{Id}'");
}
if (TenantId !=user.TenantId)
{
throw new ArgumentException($"Given User's TenantId '{user.TenantId}' does not match to this User's TenantId '{TenantId}'");
}
if(Equals(user))
{
return false;
}

UpdateInternal(user);
return true;
}
protected virtual boolEquals(IUserData user)
{
return Id == user.Id &&TenantId== user.TenantId &&UserName== user.UserName &&Name== user.Name &&Surname== user.Surname &&Email== user.Email &&EmailConfirmed== user.EmailConfirmed &&PhoneNumber== user.PhoneNumber &&PhoneNumberConfirmed==user.PhoneNumberConfirmed;
}
protected virtual voidUpdateInternal(IUserData user)
{
Email
=user.Email;
Name
=user.Name;
Surname
=user.Surname;
EmailConfirmed
=user.EmailConfirmed;
PhoneNumber
=user.PhoneNumber;
PhoneNumberConfirmed
=user.PhoneNumberConfirmed;
UserName
=user.UserName;
}
}

另外我们还需要参照创建一个AppUserLookupService来快捷获取用户的对象信息。只需要继承自UserLookupService即可,如下代码所示,放在领域层中。

    public class AppUserLookupService : UserLookupService<AppUser, IAppUserRepository>, IAppUserLookupService
{
publicAppUserLookupService(
IAppUserRepository userRepository,
IUnitOfWorkManager unitOfWorkManager)
:
base(
userRepository,
unitOfWorkManager)
{

}
protected overrideAppUser CreateUser(IUserData externalUser)
{
return newAppUser(externalUser);
}
}

这样就可以在需要的时候(一般在AppService应用服务层中注入IAppUserLookupService),可以利用这个接口获取对应的用户信息,来实现相关的用户关联操作。

2、领域对象的关系处理

在常规的岗位领域对象中,增加一个和中间表的关系信息。

这个中间表的领域对象如下所示。

    /// <summary>
    ///岗位用户中间表对象,领域对象/// </summary>
    [Table("TB_JobPostUser")]public classJobPostUser : CreationAuditedEntity, IMultiTenant
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicJobPostUser()
{
}
/// <summary> ///参数化构造函数/// </summary> /// <param name="postId"></param> /// <param name="userId"></param> /// <param name="tenantId"></param> public JobPostUser(string postId, Guid userId, Guid? tenantId = null)
{
PostId
=postId;
UserId
=userId;
TenantId
=tenantId;
}
/// <summary> ///复合键的处理/// </summary> /// <returns></returns> public override object[] GetKeys()
{
return new object[] { PostId, UserId };
}
#region Property Members[Required]public virtual string PostId { get; set; }

[Required]
public virtual Guid UserId { get; set; }/// <summary> ///租户ID/// </summary> public virtual Guid? TenantId { get; protected set; }#endregion}

这里主要就是注意复合键的处理,其他的都是代码自动生成的(利用代码生成工具
Database2Sharp

然后在EntityFramework项目中处理它们之间的关系,如下代码所示

    public static classFrameworkDbContextModelBuilderExtensions
{
public static voidConfigureFramework(
[NotNull]
thisModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<JobPost>(b =>{
b.ConfigureByConvention();
b.HasMany(x
=> x.Users).WithOne().HasForeignKey(jp =>jp.PostId);
b.ApplyObjectExtensionMappings();
});
builder.Entity
<JobPostUser>(b =>{
b.ConfigureByConvention();

b.HasKey(pu
=> new{ pu.PostId, pu.UserId });
b.HasIndex(pu
=> new{ pu.PostId, pu.UserId }); b.ApplyObjectExtensionMappings();
});
builder.TryConfigureObjectExtensions<FrameworkDbContext>();
}
}

通过JobPost关系中的
HasForeignKey(jp =>
jp.PostId),
建立它们的外键关系,通过JobPostUser关系中
b.HasKey(pu
=> new
{ pu.PostId, pu.UserId });
创建中间表的复合键关系。

默认在获取实体类的时候,关联信息是没有加载的,我们可以通过设置的方式实现预先加载或者懒加载处理,如下是通过设置,可以设置JobPost中加载用户信息。

不过不是所有的实体信息,都是要设置这样,否则有性能问题的。

最后测试的时候,可以看到返回的JobPost领域对象中附带有用户相关的信息,如下截图所示。

这样我们就可以通过该对象获取用户的相关信息,来进行相关的处理。

我们领域对象JobPost里面有Users属性,它是一个中间表的信息,

而我们在Dto层,一般直接面向的是用户信息,那么JobPostDto的信息定义如下所示。

那么我们在映射的时候,需要注意他们类型不一致的问题,需要忽略它的这个属性的映射。

    /// <summary>
    ///JobPost,映射文件///注:一个业务对象拆分为一个映射文件,方便管理。/// </summary>
    public classJobPostMapProfile : Profile  
{
publicJobPostMapProfile()
{
CreateMap
<JobPostDto, JobPost>();
CreateMap
<JobPost, JobPostDto>().Ignore(x => x.Users); //忽略Users,否则类型不对出错 CreateMap<CreateJobPostDto, JobPost>();
}
}

这样就可以顺利转换获得对应的信息。

在早期一直使用Lumisoft.NET组件来进行邮件的处理查找,对于邮件的处理非常方便,之前在随笔《
基于Lumisoft.NET组件的POP3邮件接收和删除操作
》中也介绍过基于POP3和SMPT进行邮件的收发处理,一般邮件服务器对于Pop3都是支持很好的,常规使用测试多个服务器都没问题,所以就没怎么研究IMAP协议的处理,本篇随笔基于原来POP3 的需求扩展了IMAP协议的处理。

1、创建IMAP收件辅助类

为了方便进行收取IMAP邮件的处理,我们创建一个ImapHelper,并传入相关用到的一些参数,用来封装收件的处理操作。

如下辅助类所示,传入服务器域名地址,端口,是否SSL,用户名和密码等信息。

接下来,我们需要连接服务器,并尝试获取授权信息,如果通过,则可以进行下一步获取邮件信息的操作,如下代码所示。

        /// <summary>
        ///收取邮件操作/// </summary>
        public voidReceive()
{
using (var client = newIMAP_Client())
{
//创建日志处理 client.Logger = newLogger();
client.Logger.WriteLog
+= new EventHandler<WriteLogEventArgs>(WriteLog);//响应记录显示//使用账号密码,连接服务器 client.Connect(server, port, useSsl);//登录获取授权操作 client.Login(username, password);//var identity = client.AuthenticatedUserIdentity;//获取各个邮箱目录的概要信息 client.GetFolders(null).ToList().ForEach(f =>{

Console.WriteLine(f.FolderName);
var s =client.FolderStatus(f.FolderName);
s.ToList().ForEach(sIt
=>{
Console.WriteLine(
"总数:{0},未读:{1},最近{2}", sIt.MessagesCount, sIt.MessagesCount, sIt.UnseenCount);
});

});

我们登录获得授权后,测试获取各个目录的概要邮件信息,如总邮件数量,以及未读数量等等。

然后通过选择具体的邮箱目录,并设置返回信息包含的内容格式,以及从服务器返回那些序号的邮件等等,如下代码所示。

    //选择邮箱
    client.SelectFolder("INBOX");//首先确定取第x到第n封邮件,"1:*"表示第1封到最后一封
    var seqSet = IMAP_t_SeqSet.Parse("1:*");var items = newIMAP_t_Fetch_i[]
{
new IMAP_t_Fetch_i_Envelope(), //邮件的标题、正文等信息 new IMAP_t_Fetch_i_Uid(), //返回邮件的UID号,UID号是唯一标识邮件的一个号码 new IMAP_t_Fetch_i_Flags(), //此邮件的标志,应该是已读未读标志 new IMAP_t_Fetch_i_InternalDate(),//貌似是收到的日期 new IMAP_t_Fetch_i_Rfc822() //Rfc822是标准的邮件数据流,可以通过Lumisoft.Net.Mail.Mail_Message对象解析出邮件的所有信息 };

接着我们通过传入条件,并给他一个回调匿名函数处理相关的邮件信息,如下所示。

    //Fetch 第一个参数false时seqSet有效
    client.Fetch(false, seqSet, items, (s, e) =>{//处理邮件的匿名函数内容
    });

接着我们处理邮件信息的转换,吧邮件信息转换为
Mail_Message
对象的信息,这个包含邮件相关的头部信息,正文,以及附件信息等全部内容。

   var email = e.Value asIMAP_r_u_Fetch;if (email.Rfc822 != null)
{
email.Rfc822.Stream.Position
= 0;var mime_message =Mail_Message.ParseFromStream(email.Rfc822.Stream);
email.Rfc822.Stream.Close();

然后我们把邮件的信息进一步转换为我们需要存储在数据库的对象信息,最后写入数据库即可。

    receiveInfo.ReceivedDate = DateTime.Now;//接收本地时间
    receiveInfo.Company_ID = this.companyId;
receiveInfo.User_ID
= this.userId;
receiveInfo.Email
= this.email;//接收Email账号 receiveInfo.MailConfig_ID = this.mailConfig_ID;//接收Email账号的配置记录ID//每封Email会有一个在Pop3服务器范围内唯一的Id,检查这个Id是否存在就可以知道以前有没有接收过这封邮件 receiveInfo.MailUid =email.UID.UID.ToString();try{//可能会出现【LumiSoft.Net.ParseException: Header field 'Date' parsing failed】异常错误。 receiveInfo.SendDate =mime_message.Date;
}
catch(Exception ex)
{
receiveInfo.SendDate
= Convert.ToDateTime("1900-1-1");//错误赋值一个日期 error = string.Format("转换邮件的Date出错:账号{0} 邮件标题:{1}", username, mime_message.Subject);
LogTextHelper.Error(error, ex);
}
//可能出现乱码问题,通过函数进行转换 receiveInfo.Title = mime_message.Subject;//DecodeString(mime_header.Subject); receiveInfo.MailBody=mime_message.BodyText;try{if (!string.IsNullOrEmpty(mime_message.BodyHtmlText))
{
receiveInfo.MailBody
=mime_message.BodyHtmlText;
}
}
catch{//屏蔽编码出现错误的问题,错误在BodyText存在而BodyHtmlText不存在的时候,访问BodyHtmlText会出现 }

写入数据库处理,调用我们通用处理类处理数据信息的存储即可。

    #region 写入邮件信息到数据库
    int mailId = -1;try{
mailId
= BLLFactory<MailReceive>.Instance.Insert2(receiveInfo);
}
catch(Exception ex)
{
error
= string.Format("写入邮件信息到数据库出错:账号{0} 邮件标题:{1}", username, mime_message.Subject);
LogTextHelper.Error(error, ex);
}
if (mailId <= 0) return; //如果邮件没有保存,不要保存附件 #endregion

2、邮件的附件处理

邮件的附件,包含常规的邮件附件,以及嵌入正文的附件图片,因此需要进行不同类型的判断,并一起把附件获取下来存储,这样在显示的时候,才能正常显示相关的附件。

其中
Mail_Message
对象有一个函数,可以获取全部这两类附件的信息到列表中。

public MIME_Entity[] GetAttachments(bool includeInline, bool includeEmbbedMessage)

这样我们来调用这个函数,然后进行附件的提取存储处理即可。

    #region 邮件附件内容
    foreach (var entity in mime_message.GetAttachments(true, true))
{
string fileName = "";#region 判断是普通附件还是嵌入的内容附件 if (entity.ContentDisposition != null &&entity.ContentDisposition.DispositionType==MIME_DispositionTypes.Attachment)
{
Console.WriteLine(
"Attachment:" +entity.ContentDisposition.Param_FileName);
fileName
=entity.ContentDisposition.Param_FileName;
}
else{string cid = entity.ContentID.Substring(1, entity.ContentID.Length - 2);if (entity.ContentType.Param_Name != null &&mime_message.BodyHtmlText.Contains(string.Format("cid:{0}", cid)))
{
Console.WriteLine(
"Embeded image:" +cid);
fileName
=cid;
}
else{
Console.WriteLine(
"Unknown attachment.");
}
}

邮件的附件信息,entity对象需要转换为MIME_b_SinglepartBase进行处理的。

 var byteObj = entity.Body as MIME_b_SinglepartBase;

因此我们可以通过文件方式存储它的字节数据,如下所示。

File.WriteAllBytes(filename, byteObj.Data);

或者调用附件信息进行存储处理(可以是本地存储、或者FTP上传等方式)

如对于测试带有嵌入图片,附件信息的邮件,这样处理能够顺利获取所有的附件信息。

因此可以使用邮件管理模块中的定时收发邮件的处理,实现邮件的接收和发送。

3、163邮箱对于IMAP协议不支持

在测试IMAP协议收取邮件的时候,对于POP3发现大多数邮箱都是支持的。

但虽然163邮箱对POP3的支持不错,对IMAP协议却不支持,都是使用授权码进行登录,也确实登录成功了,但是IMAP协议切换邮箱进行邮件收取的时候,就会提示

提示错误信息。

00023 NO SELECT Unsafe Login. Please contact kefu@188.com for help

如有兴趣,了解Lumisoft.NET组件的相关使用内容,请参考我相关随笔,谢谢。


基于Lumisoft.NET组件和.NET API实现邮件发送功能的对比


基于Lumisoft.NET实现的邮件发送功能


基于Lumisoft.NET组件开发碰到乱码等一些问题的解决


基于Lumisoft.NET组件的SMTP账号登陆检测


邮件代收代发软件操作说明


邮件代收代发功能模块的操作界面设计和阶段性总结

在我的各种开发框架中,数据访问有的基于微软企业库,有的基于EFCore的实体框架,两者各有其应用场景,不过多的去比较。最近在使用SqlSugar的时候,觉得这个数据访问处理的组件确实很灵活,据说性能也是很不错,于是用来整合测试一下,它对多种关系型数据库如SqlServer、Oracle、Mysql、SQLite、PostgreSQL都很容易提供支持,通过特性标注的方式,可以很好的实现数据访问的处理,这点很类似EfCore的处理,同时SqlSugar又提供很灵活的SQL处理操作。因此多花了一些时间,把SqlSugar的数据访问操作进行一定的封装处理,最后使之适应更广泛的应用。在这个处理过程中,我编写一些单元测试用来测试其相关功能,并编写了几个模块的Winform界面进行测试,效果还是很不错,借此总结分享一下。

1、SQLSugar的相关介绍

SqlSugar是一款 老牌 .NET 开源ORM框架,由果糖大数据科技团队维护和更新 ,使用文档可以参考官方文档:
https://www.donet5.com/Home/Doc
, GitHub的地址是:
https://github.com/donet5/SqlSugar

优点: 简单易用、功能齐全、高性能、轻量级,支持数据库:MySql、SqlServer、Sqlite、Oracle 、 postgresql、达梦、人大金仓。

由于它是ORM的框架组件,因此标识性的处理也是常规的操作,一般的SQLSugar对数据实体对象提供和数据库信息的标注处理。

如对于数据库表的标注:

[SugarTable("TB_DictData")]public classDictDataInfo
{
}

以及对字段信息主键的标注

        /// <summary>
        ///编号/// </summary>
        [SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }

或者是自增字段的标注处理

    public classPerson 
{
//数据库字段 [SqlSugar.SugarColumn(IsPrimaryKey =true,IsIdentity=true)]public int Id { get; set; }

而有些字段,和数据库字段是没有对应关系的,可以设置忽略标识,如下所示。

    public classPerson 
{
//数据库字段 [SqlSugar.SugarColumn(IsPrimaryKey =true,IsIdentity =true)]public int Id { get; set; }public int SexId { get; set; }//非数据库字段 [SqlSugar.SugarColumn(IsIgnore =true)]public string SexName { get; set; }

.......

定义了这些实体和数据库关系后,我们操作数据库,可以使用 SqlSugarClient 或者 SqlSugarScope 对数据库进行增、删、查、改等功能,SqlSugarClient 和SqlSugarScope 几乎一样,两者差异之处,是后者使用单例(单件)模式,如果我们的对象也是单件模式,就考虑使用SqlSugarScope 对象操作数据库。

例如我们创建一个SqlSugarClient的对象实例,用它来操作数据库获取信息。

var db = new SqlSugarClient(newConnectionConfig()
{
DbType
=DbType.SqlServer,
ConnectionString
=connectionString,
InitKeyType
=InitKeyType.Attribute,
IsAutoCloseConnection
= true,
AopEvents
= newAopEvents
{
OnLogExecuting
= (sql, p) =>{
Log.Information(sql);
Log.Information(
string.Join(",", p?.Select(it => it.ParameterName + ":" +it.Value)));
}
}
});

那接下来,我们就可以利用db来进行数据的增删改查处理操作了。

     //查询表的所有
     var list = db.Queryable<Student>().ToList();//插入
     db.Insertable(new Student() { SchoolId = 1, Name = "jack"}).ExecuteCommand();//更新
     db.Updateable(new Student() { Id = 1, SchoolId = 2, Name = "jack2"}).ExecuteCommand();//删除
     db.Deleteable<Student>().Where(it => it.Id == 1).ExecuteCommand();

一般来说,我们可能倾向于把操作封装为一个函数处理,如下所示

/// <summary>
///保存数据到数据库/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task<bool>SaveData(LongVoiceResultDto dto)
{
bool result = false;if(dto != null)
{
using(var db =CreateDb())
{
var info = newConsultationInfo();
info.DiscernStatus
=dto.taskId;
info.OperateStatus
= "未识别";if (dto.data != null && dto.data.speechResult != null)
{
if (dto.data.statusCode == 3)
{
info.OperateStatus
= "已识别";
}
var speechResult =dto.data.speechResult;
info.DiscernText
=speechResult.resultText;
}

result
= await db.Insertable(info).ExecuteCommandAsync() > 0;
}
}
returnresult;
}

从上面的代码来看,我们定义好实体信息后,就可以直接用SqlSugarClient的对象实例来处理数据库信息了,过程非常简单高效,特别对于一些简单的单表操作,非常简洁。

2、SQLSugar的基类封装

上面的简单代码,我们可以看到SqlSugarClient的对象实例的快捷操作数据库操作,非常方便。

不过一般来说,对于一个成熟的项目,我们一般是要尽可能的重用一些处理代码,并提供最大程度的简化封装。因此我们在实际使用来开发项目的时候,需要对 SqlSugar数据库的处理进行一定的封装操作,以期最大程度的优化代码。

首先我们定义一个对象用来承载数据库SqlSugarScope(或者SqlSugarClient)实例的信息,用于数据访问的基类上下文方便使用的目的。

    /// <summary>
    ///数据库上下文信息/// </summary>
    public classDbContext
{
/// <summary> ///数据库类型。/// </summary> public DbType DbType { get; set; }/// <summary> ///连接字符串。/// </summary> public string ConnectionString { get; set; }/// <summary> ///数据库类型。/// </summary> public SqlSugarScope Client { get; set; }publicDbContext()
{
//默认采用配置项名//appSettings/DefaultDb 配置项为指定连接字符串的name var dbConfigName = ConfigurationManager.AppSettings["DefaultDb"];
Init(dbConfigName);
}
public DbContext(stringdbConfigName)
{
Init(dbConfigName);
}

我们为了方便配置不同的数据库信息,因此通过定义一个默认的键 DefaultDb 来确定具体使用那个连接字符串。如下是我们的数据库连接字符串。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <!--Sqlserver数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI" />
    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=winframework;Uid=root;Pwd=123456;SslMode=none" />
    <!--sqlite数据库字符串,路径符号|DataDirectory|代表当前运行目录-->
    <addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WinFramework.db;Version=3;" />
    <!--PostgreSQL数据库的连接字符串-->
    <addname="npgsql"providerName="Npgsql"connectionString="Server=localhost;Port=5432;Database=winframework;User Id=postgres;Password=123456" />
    <!--不受驱动影响,32位64位均可使用-->
    <addname="oracle"providerName="OracleManaged"connectionString="Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=orcl)));User ID=win;Password=win" />
    <!--达梦数据库的连接字符串-->
    <addname="Dm"providerName="Dm"connectionString="Server=localhost;User ID=SYSDBA;PWD=SYSDBA;Database=WINFRAMEWORK;" />
  </connectionStrings>
  
  <appSettings>
    <!--指定默认的数据库类型,如果不指定则使用第一个连接字符串-->
    <addkey="DefaultDb"value="sqlserver" />
  </appSettings>
  <startup>
    <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.8" />
  </startup>
</configuration>

其中我们通过连接字符串中的 providerName 的类别来确定具体使用那种数据库类型。

       /// <summary>
        ///根据链接字符串的providerName决定那种数据库类型/// </summary>
        /// <param name="setting"></param>
        /// <returns></returns>
        privateDbType GetSugarDbType(ConnectionStringSettings setting)
{
DbType dbType
= DbType.SqlServer; //默认值 var providerName =setting.ProviderName;if (providerName != null)
{
//数据库providerName:SqlClient MySqlClient SQLite OracleManaged/OracleClient Npgsql if (providerName.EndsWith(".SqlClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.SqlServer;
}
else if (providerName.EndsWith(".MySqlClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.MySql;
}
else if (providerName.EndsWith(".SQLite", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Sqlite;
}
else if (providerName.EndsWith("OracleManaged", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Oracle;
}
else if (providerName.EndsWith(".OracleClient", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Oracle;
}
else if (providerName.EndsWith("Npgsql", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.PostgreSQL;
}
else if (providerName.EndsWith("Dm", StringComparison.OrdinalIgnoreCase))
{
dbType
=DbType.Dm;
}
}
returndbType;
}

这样我们就可以动态设置数据库的配置信息了,我们可以使用配置信息,初始化数据库操作实例的代码逻辑。

数据库上下文对象处理好后,我们就来设计我们的数据库操作基类对象了,基类对象需要基于实体信息来定义一些常规的CRUD接口,并应最大程度的提供一些重写或者设置处理。

    /// <summary>
    ///基于SqlSugar的数据库访问操作的基类对象/// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public class MyCrudService<TEntity, TKey, TGetListInput>
        where TEntity : class, new()whereTGetListInput : IPagedAndSortedResultRequest
{
/// <summary> ///数据库上下文信息/// </summary> protectedDbContext dbContent;/// <summary> ///简化SugarClient 的 ADO对象/// </summary> protectedIAdo Ado
{
get{returndbContent.Client.Ado;
}
}
/// <summary> ///实体对象处理类/// </summary> protected SimpleClient<TEntity>EntityDb
{
get{return dbContent.Client.GetSimpleClient<TEntity>();
}
}
/// <summary> ///数据库配置名称,默认为空。///可在子类指定不同的配置名称,用于访问不同的数据库/// </summary> public string DbConfigName { get; set; }publicMyCrudService()
{
dbContent
= newDbContext();
}

我们看到基类提供一些SqlSugarClient对象的应用,以方便对数据的处理操作。

我们看看获取所有,以及根据Lamda条件表达式获取列表的操作代码,非常方便的。

        /// <summary>
        ///获取所有记录/// </summary>
        public virtual async Task<ListResultDto<TEntity>>GetAllAsync()
{
var list = awaitEntityDb.GetListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}
/// <summary> ///根据条件,获取所有记录/// </summary> public virtual async Task<ListResultDto<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> input, string orderBy = null)
{
var query =EntityDb.AsQueryable().Where(input);
query
= query.OrderByIF(!string.IsNullOrEmpty(orderBy), orderBy);var list = awaitquery.ToListAsync();return new ListResultDto<TEntity>()
{
Items
=list
};
}

由于本身的SqlSugarClient/SqlSugarScope提供了很多接口函数,因此我们的基类只需要在它的基础上进行一些简单的封装即可,如删除处理代码。

        /// <summary>
        ///删除指定ID的对象/// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool>DeleteAsync(TEntity input)
{
return awaitEntityDb.DeleteAsync(input);
}
/// <summary> ///根据指定条件,删除集合/// </summary> /// <param name="input"></param> /// <returns></returns> public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>>input)
{
var result = awaitEntityDb.DeleteAsync(input);returnresult;
}
/// <summary> ///删除指定ID的对象/// </summary> /// <param name="id">记录ID</param> /// <returns></returns> public virtual async Task<bool>DeleteByIdAsync(TKey id)
{
return awaitEntityDb.DeleteByIdAsync(id);
}
/// <summary> ///删除集合/// </summary> /// <param name="input">删除条件集合</param> /// <returns></returns> public async virtual Task<bool> DeleteByIdsAsync(IEnumerable<TKey>input)
{
dynamic ids =input.ToArray();return awaitEntityDb.DeleteByIdsAsync(ids);
}

上面删除,可以根据实体类,Lamda条件表达式,主键或者主键列表等,简单封装一下就可以了。

根据相关的数据操作需要,我们为该基类定义很多常规通用的基类接口,包含很多常规的CRUD等的方法,列出一个列表方便参考即可。

3、SQLSugar数据访问的单元测试

为了对不同数据库类型的不同操作进行检查,看其是否能够正常工作,我们需要编写一些测试的代码用于检查我们基类函数封装的有效性,只有对每一个基类接口进行测试了,才能够放心的使用。

为了编写单元测试,我们需要为几个表编写对应的实体类和相应的服务类(继承自SQLSugar的数据访问基类),我们可以使用
代码生成工具Database2Sharp
来快速生成实体类代码,如下所示。

生成代码直接显示在代码工具上,可以复制下来使用。

后面有空会调整一下代
码生成工具Database2Sharp
,把SQLSugar的ORM实体类和基于CRUD基类的服务类一并生成代码出来就完美了(和其他项目开发一样,快速生成项目代码即可)。

完成了实体类信息的处理后,我们来继承一下基类服务类并重写查询条件处理和列表排序的函数即可,如下代码所示。

   /// <summary>
    ///应用层服务接口实现/// </summary>
    public class DictDataService : MyCrudService<DictDataInfo, string, DictDataPagedDto>{/// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<DictDataInfo>CreateFilteredQueryAsync(DictDataPagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);

query
=query
.WhereIF(
!input.Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIF(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIF(
!string.IsNullOrEmpty(input.Value), t => t.Value ==input.Value)
.WhereIF(
!string.IsNullOrEmpty(input.DictType_ID), t => t.DictType_ID ==input.DictType_ID);returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictDataInfo> ApplySorting(ISugarQueryable<DictDataInfo>query, DictDataPagedDto input)
{
return base.ApplySorting(query, input).OrderBy(s => s.DictType_ID).OrderBy(s =>s.Seq);//先按第一个字段排序,然后再按第二字段排序//return base.ApplySorting(query, input).OrderBy(s=>s.DictData_ID).OrderBy(s => s.Seq); }
}

其中 CreateFilteredQueryAsync 代码是重写构建查询条件处理的逻辑,而ApplySorting函数用于指定列表的排序规则。

有了代码生成工具的辅助,因此我们编写一些单元测试函数用于测试,编写单元测试也是非常方便的事情。

代码的单元测试,编写如下所示。

[TestClass]public classUnitTest1
{
/// <summary> ///测试查找记录/// </summary> /// <returns></returns> [TestMethod]public async Task TestMethod1()

创建单元测试项目,并指定测试类为[Testclass]以及测试方法[TestMethod]即可,测试方法我们根据实际要求编写覆盖所有方法的测试即可。

例如我对于测试返回列表和单体数据的接口,编写单元代码如下所示。

[TestClass]public classUnitTest1
{
/// <summary> ///测试查找记录/// </summary> /// <returns></returns> [TestMethod]public asyncTask TestMethod1()
{
var input = newDictTypePagedDto()
{
Name
= "客户"};//可以使用BLLFactory工厂类处理 var service = BLLFactory<DictTypeService>.Instance;//new DictTypeService(); 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;
}

测试增删改查的接口的单元测试代码如下所示。

        /// <summary>
        ///测试增删改查/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod2()
{
var info = newDictTypeInfo()
{
ID
=Guid.NewGuid().ToString(),
Code
= "test",
Name
= "test",
Remark
= "test",
PID
= "-1",
Seq
= "001"};var service = newDictTypeService();var insert = awaitservice.InsertAsync(info);
Assert.IsTrue(insert);

info.Name
= "test2";var update = awaitservice.UpdateAsync(info);
Assert.IsTrue(update);
var deleted = awaitservice.DeleteByIdAsync(info.ID);
Assert.IsTrue(deleted);
var entity = awaitservice.GetAsync(info.ID);
Assert.IsNull(entity);
}

测试对SQL语句执行过程的单元测试代码如下

       /// <summary>
        ///测试执行语句的处理/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod3()
{
var service = newDictTypeService();var sql = string.Format("Select * from TB_DictType");var table =service.SqlTable(sql);
Assert.IsNotNull(table);
Assert.IsTrue(table.Rows.Count
> 0);var ds =service.SqlDataSet(sql);
Assert.IsNotNull(ds);
Assert.IsTrue(ds.Tables.Count
> 0);

sql
= string.Format("Select Name from TB_DictType");var list =service.SqlValueList(sql);
Assert.IsNotNull(list);
//完全没有执行任何更新、插入,返回-1 var result =service.SqlExecute(sql);
Assert.IsTrue(result
== -1);awaitTask.CompletedTask;
}

测试数据库参数化及多数据库切换处理的单元测试代码如下所示。

       /// <summary>
        ///测试数据库参数化及多数据处理/// </summary>
        /// <returns></returns>
[TestMethod]public asyncTask TestMethod4()
{
var service = newDictTypeService();var sql = string.Format("Select * from TB_DictType Where PID = @pid");var parameters = new List<SugarParameter>() { new SugarParameter("pid", "-1") };//默认SQLServer数据库 var table =service.SqlTable(sql, parameters);
Console.WriteLine(table.Rows.Count);
Assert.IsNotNull(table);
Assert.IsTrue(table.Rows.Count
> 0);//切换到MySQL数据库 service.SetDbConfigName("mysql");var list =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list);
Assert.IsNotNull(list.Items);
Assert.IsTrue(list.Items.Count
> 0);//切换到SQLITE数据库 service.SetDbConfigName("sqlite");var list2 =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list2);
Assert.IsNotNull(list2.Items);
Assert.IsTrue(list2.Items.Count
> 0);//切换到npgsql数据库 service.SetDbConfigName("npgsql");var list3 =service.SqlQuery(sql, parameters);
Assert.IsNotNull(list3);
Assert.IsNotNull(list3.Items);
Assert.IsTrue(list3.Items.Count
> 0);awaitTask.CompletedTask;
}

在开发机器上安装几个不同的关系数据库,用于测试,并准备好数据库文件导入。

在单元测试项目中右键运行测试,如下图所示。

全部测试通过,这几个单元测试覆盖了我们的所有方法的测试了。

以上就是我们基于SqlSugar的ORM处理的封装,并提供了丰富的基类接口和弹性化的实体类泛型约束,因此 子类代码非常简洁,只需要实现条件查询和排序的处理即可,因此代码生成的时候,会更加简便。

通过上面的预演,我们基本上了解了SqlSugar的使用操作,确实非常方便,特别是我们基于代码生成工具的辅助开发之后,会更加省事快捷,使用基于强大的CRUD基类,我们子类的代码更少,更容易维护。

而对于一些多表关联的操作,我们可以在子类额外定义一些处理函数即可。

在上篇随笔《
基于SqlSugar的数据库访问处理的封装,支持多数据库并使之适应于实际业务开发中
》中介绍了SqlSugar的基础用法,以及实现对常规项目中对数据访问的基类封装,并通过编写单元测试覆盖相关的功能测试,虽然最后编写单元测试的代码就是实际调用数据处理的代码,不过没有界面不太直观,本篇随笔继续深入SqlSugar的使用介绍,介绍基于Winform项目界面的整合测试。

1、数据访问层的实现

在上篇随笔,我们介绍了SqlSugar使用起来还是非常简单的,首先定义好和数据表对应的实体类信息,通过特性声明给的方式,声明表名和字段信息(包括主键信息)

如对于数据库表的标注:

[SugarTable("TB_DictData")]
public class DictDataInfo
{
}

以及对字段信息主键的标注

        /// <summary>
        ///编号/// </summary>
        [SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }

或者是自增字段的标注处理

    public classPerson 
{
//数据库字段 [SugarColumn(IsPrimaryKey=true,IsIdentity=true)]public int Id { get; set; }

例如我们对于Winform开发框架中的字典数据库,设计关系如下所示。

我们生成器对应的SQLSugar实体信息如下所示,这些枯燥的工作可以交给配套的代码生成工具Database2sharp来完成。

    /// <summary>
    ///DictTypeInfo/// </summary>
    [SugarTable("TB_DictType")]public classDictTypeInfo
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictTypeInfo()
{
this.ID =System.Guid.NewGuid().ToString();this.LastUpdated =System.DateTime.Now;

}
#region Property Members[SugarColumn(IsPrimaryKey= true)]public virtual string ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典代码/// </summary> public virtual string Code { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///编辑者/// </summary> public virtual string Editor { get; set; }/// <summary> ///编辑时间/// </summary> public virtual DateTime LastUpdated { get; set; }/// <summary> ///分类:0 客房/1 KTV/2 茶室/// </summary> public virtual string PID { get; set; }#endregion}

同时为了方便条件的分页处理,我们定义一个分页的Dto对象,如下所示。

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public classDictTypePagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
{
/// <summary> ///默认构造函数/// </summary> public DictTypePagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skip ///">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictTypePagedDto(int skipCount, intresultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public DictTypePagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
#region Property Members /// <summary> ///不包含的对象的ID,用于在查询的时候排除对应记录/// </summary> public virtual string ExcludeId { get; set; }public virtual string Name { get; set; }public virtual string Code { get; set; }public virtual string Remark { get; set; }public virtual string Seq { get; set; }public virtual string PID { get; set; }/// <summary> ///创建时间-开始/// </summary> public DateTime? CreationTimeStart { get; set; }/// <summary> ///创建时间-结束/// </summary> public DateTime? CreationTimeEnd { get; set; }#endregion}

同理对于字典项目的实体信息,也是类似的定义方式,如下所示。

    /// <summary>
    ///DictDataInfo/// </summary>
    [SugarTable("TB_DictData")]public classDictDataInfo
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicDictDataInfo()
{
this.ID =System.Guid.NewGuid().ToString();this.LastUpdated =System.DateTime.Now;

}
#region Property Members /// <summary> ///编号/// </summary> [SugarColumn(IsPrimaryKey = true)]public virtual string ID { get; set; }/// <summary> ///字典大类/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///字典名称/// </summary> public virtual string Name { get; set; }/// <summary> ///字典值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }/// <summary> ///排序/// </summary> public virtual string Seq { get; set; }/// <summary> ///编辑者/// </summary> public virtual string Editor { get; set; }/// <summary> ///编辑时间/// </summary> public virtual DateTime LastUpdated { get; set; }#endregion}

最终我们定义完成实体信息后,需要集成上篇随笔提到的数据访问基类,并重写一下查询条件处理,排序的规则信息即可,如下代码所示。

    /// <summary>
    ///应用层服务接口实现/// </summary>
    public class DictTypeService : MyCrudService<DictTypeInfo, string, DictTypePagedDto>{/// <summary>
        ///获取字段中文别名(用于界面显示)的字典集合/// </summary>
        /// <returns></returns>
        public override Task<Dictionary<string, string>>GetColumnNameAliasAsync()
{
var dict = new Dictionary<string, string>();#region 添加别名解析dict.Add("ID", "编号");
dict.Add(
"Name", "类型名称");
dict.Add(
"Code", "字典代码");
dict.Add(
"Remark", "备注");
dict.Add(
"Seq", "排序");
dict.Add(
"Editor", "编辑者");
dict.Add(
"LastUpdated", "编辑时间");
dict.Add(
"PID", "父ID");#endregion returnTask.FromResult(dict);
}
/// <summary> ///自定义条件处理/// </summary> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictTypeInfo>CreateFilteredQueryAsync(DictTypePagedDto input)
{
var query = base.CreateFilteredQueryAsync(input);
query
=query
.WhereIF(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.ID != input.ExcludeId) //不包含排除ID .WhereIF(!string.IsNullOrEmpty(input.Code), t => t.Code ==input.Code)
.WhereIF(
!string.IsNullOrEmpty(input.PID), t => t.PID ==input.PID)
.WhereIF(
!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals .WhereIF(!input.Remark.IsNullOrWhiteSpace(), t => t.Remark.Contains(input.Remark)) //如需要精确匹配则用Equals .WhereIF(!input.Seq.IsNullOrWhiteSpace(), t => t.Seq.Contains(input.Seq)) //如需要精确匹配则用Equals//创建日期区间查询 .WhereIF(input.CreationTimeStart.HasValue, s => s.LastUpdated >=input.CreationTimeStart.Value)
.WhereIF(input.CreationTimeEnd.HasValue, s
=> s.LastUpdated <=input.CreationTimeEnd.Value)
;
returnquery;
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override ISugarQueryable<DictTypeInfo> ApplySorting(ISugarQueryable<DictTypeInfo>query, DictTypePagedDto input)
{
return base.ApplySorting(query, input).OrderBy(s =>s.Seq);
}
}

其中
MyCrudService
采用了泛型的定义方式,传入对应的实体类,主键类型,以及排序分页的对象DTO等,方便基类实现强类型的接口处理。

这个子类我们也可以通过代码生成的方式实现批量生成即可。

整合到项目里面,把实体类和数据访问的服务类区分不同的目录放置,便于管理即可。

2、Winform界面的开发和调用数据操作处理

上面我们完成了数据库表的实体类和对应数据访问服务类的处理后,我们接下来的就是设计Winform界面用来处理相关的数据处理。

我这里把我的基于微软企业库访问模式的Winform界面部分拷贝过来调整一下,如下界面所示。

查看和编辑字典大类界面

编辑字典项目

对于数据访问类的调用,我们使用了一个工厂类来创建对应的单例应用,如下获取字典大类列表。

        /// <summary>
        ///绑定树的数据源/// </summary>
        private asyncTask BindTree()
{
var pageDto = newDictTypePagedDto();var result = await BLLFactory<DictTypeService>.Instance.GetListAsync(pageDto);if (result != null)
{
this.tree.DataSource =result.Items;this.tree.ExpandAll();
}
}

而但我们单击某个字典大类的时候,应该列出对应大类下的字典项目,因此获取字典项目的数据操作如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<DictDataInfo>> GetData(stringdictType)
{
//构建分页的条件和查询条件 var pagerDto = new DictDataPagedDto(this.winGridViewPager1.PagerInfo)
{
DictType_ID
=dictType
};
var result = await BLLFactory<DictDataService>.Instance.GetListAsync(pagerDto);//new DictDataService().GetListAsync(pagerDto); returnresult;
}

我们这里使用了分页查询的条件
DictDataPagedDto
,如果是需要获取全部,我们也可以通过调用GetAllAsync()函数来实现,如下导出全部的时候代码如下所示。

        private async void winGridViewPager1_OnStartExport(objectsender, EventArgs e)
{
if (this.winGridViewPager1.IsExportAllPage)
{
var result = await BLLFactory<DictDataService>.Instance.GetAllAsync();this.winGridViewPager1.AllToExport =result.Items;
}
}

这些处理都是基类预先定义好的API,我们通过子类强类型传入即可,非常方便,也简化很多代码。

同样,我们可以通过Get接口获取指定ID的实体信息,如下所示。

            if (!string.IsNullOrEmpty(ID))
{
var info = await BLLFactory<DictDataService>.Instance.GetAsync(ID);if (info != null)
{
this.txtName.Text =info.Name;this.txtNote.Text =info.Remark;this.txtSeq.Text =info.Seq;this.txtValue.Text =info.Value;
}
}

在Winform编辑界面中,我们重写保存更新的代码如下所示。

        public override async Task<bool>SaveUpdated()
{
var info = await BLLFactory<DictDataService>.Instance.GetAsync(ID);if (info != null)
{
SetInfo(info);
try{return await BLLFactory<DictDataService>.Instance.UpdateAsync(info);
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return false;
}

以上是Winform界面中对常规数据处理接口的调用,这些都是通过强类型实体的方式调用基类函数,非常方便快捷,同时以提供了很好的API统一性实现。

最终界面效果和原先Winform开发框架一样功能。