在我们常规的调试或者测试的时候,喜欢把一些测试信息打印在控制台或者记录在文件中,对于.netframework项目来说,我们输出控制台的日志信息习惯的用Console.WriteLine来输出查看,不过对于.netcore的项目来说,这种输出看不到任何信息了,不过即使这样,我们建议还是把一些测试的日志记录在文件中,供查看对比。本篇随笔介绍使用Serilog 来记录日志信息,包括应用于.netframework项目和.netcore项目中都适合。

1、在Winform项目中整合Serilog 记录日志

在.NET使用日志框架第一时间会想到NLog或是Log4Net,Serilog 是这几年快速崛起的Log框架之一,Serilog是以Structured logging 为基础进行设计,通过logging API 可以轻松的记录应用程序中对象属性,方便快速进行logging 内容进行查询与分析,并将其纪录内容通过json (可指定) 的方式输出。

不管对于.netframwork或者.netcore的Winform项目来说,使用Serilog 来记录日志信息的操作都很类似,它都支持的。

官网地址:
https://serilog.net/
,或者参考它的Github地址:
https://github.com/serilog/serilog

我们只需要引入对应的几个Serilog 相关DLL即可,如下所示。

然后在Main函数中添加对应的处理代码,如下所示。

    static classProgram
{
/// <summary> ///应用程序的主入口点。/// </summary> [STAThread]static voidMain()
{
Log.Logger
= newLoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
"logs/log.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();

在类代码或者界面代码中,直接通过方法输出日志信息即可。

   //使用Serilog记录日志信息
   Serilog.Log.Information(json);

它和其他Log组件,如Log4net一样,它的使用区分类别来输出不同类型的日志,支持Verbose、Debug、Information、Error、Warning、Fatal 几个接口的输出。

Serilog.Log.Verbose("verbose"); //用于跟踪信息和调试细节,不会输出显示出来//以下为常用记录日志方法
Serilog.Log.Information("info");
Serilog.Log.Debug(
"debug");
Serilog.Log.Warning(
"warning");
Serilog.Log.Error(
"err");
Serilog.Log.Fatal(
"fatal"); 

输出日志信息如下所示。

Serilog 如果想个性化设置日志输出到什么地方,需要引用一些serilog的接收器。Serilog的输出对象称之为Sink

Github上提供了大量的Serilog 第三方的可用
sinks
,这里简单的列举几个常用的:

2、在.NetCore项目的Web API中整合Serilog

在.netcore的Web API项目中配置和上面的有一些差异,引入相关的Serilog组件类库,如下所示。

其中需要在IHostBuilder或者IWebHostBuilder中注入Serilog接口,如下初始化代码所示。

在Web API的控制器中使用代码如下所示

    /// <summary>
    ///用于接收长语音信息回调的处理/// </summary>
[ApiController]
[Route(
"[controller]")]public classLongVoiceController : ControllerBase
{
private readonly ILogger<LongVoiceController>_logger;public LongVoiceController(ILogger<LongVoiceController>logger)
{
_logger
=logger;
}
/// <summary> ///测试控制器,路由:get /longvoice/// </summary> /// <returns></returns> [HttpGet]public List<string>TestList()
{
return new List<string>()
{
"测试列表接口","检查控制器正常"};
}
/// <summary> ///回调写入结果, 路由:post /longvoice/// </summary> /// <param name="input"></param> /// <returns></returns> [HttpPost]public async Task<CommonResultDto>CallBack(LongVoiceResultDto input)
{
CommonResultDto result
= null;var json =JsonConvert.SerializeObject(input, Formatting.Indented);
_logger.LogInformation(json);

result
= new CommonResultDto(0, "操作成功");return awaitTask.FromResult(result);
}
}

我们注意到直接使用web API 的 IIogger<T>接口调用写日志即可。

_logger.LogInformation(json);

这样我们可以看到相关的日志输出

在ABP VNext框架中,即使在它提供的所有案例中,都没有涉及到Winform程序的案例介绍,不过微服务解决方案中提供了一个控制台的程序供了解其IDS4的调用和处理,由于我开发过很多Winform项目,以前基于ABP框架基础上开发的《
ABP快速开发框架
》中就包含了Winform客户端,因此我对于ABP VNext在Winform上的使用也比较关心,花了不少时间来研究框架的相关的授权和窗体构建处理上,因此整理了该随笔内容,主要用于介绍ABP VNext框架中Winform终端的开发和客户端授权信息的处理。

1、ABP VNext框架中Winform终端的开发

不管对于那种终端项目,需要应用ABP VNext模块的,都需要创建一个模块类,继承于AbpModule,然后引入相关的依赖模块,并配置Servcie信息,如下是Winform项目中的Module类,如下所示。

namespaceWinform.TestApp
{
[DependsOn(
typeof(MicroBookStoreHttpApiClientModule),typeof(AbpHttpClientIdentityModelModule)
)]
public classWinformApiClientModule : AbpModule
{
public override voidConfigureServices(ServiceConfigurationContext context)
{
}
}
}

ABP VNext模块的初始化,根据依赖关系进行相关的初始化,我们在创建Winform项目(基于.net Core开发)的时候,需要在Main函数中创建一个应用接口,如下所示。

 //使用 AbpApplicationFactory 创建一个应用
 var app = AbpApplicationFactory.Create<WinformApiClientModule>();//初始化应用
 app.Initialize();

这个app接口对象非常重要,需要用它创建一些接口服务,如下所示。

var service = app.ServiceProvider.GetService<IService1>();

不过由于这个app对象需要在整个应用程序的生命周期中都可能会用到,用来构建一些用到的接口对象等,那么我们就需要创建一个静态类对象用来存储相关的应用接口信息,需要用到它的时候就可以直接使用了,否则丢掉了就没法构建接口使用了。

首先我们创建一个用于存储全局信息类GlobalControl,如下所示。

    /// <summary>
    ///应用程序全局对象/// </summary>
    public classGlobalControl
{
public MainForm? MainDialog { get; set; } = null;public IAbpApplicationWithInternalServiceProvider? app { get; set; }/// <summary> ///创建指定类型窗口实例/// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T CreateForm<T>() whereT :Form
{
if (app == null) return null;else{var form = app.ServiceProvider.GetService<T>();returnform;
}
}
/// <summary> ///创建服务类的接口实例/// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public T GetService<T>() where T : class{if (app == null) return null;else{var service = app.ServiceProvider.GetService<T>();returnservice;
}
}

这样我们在Main方法中创建的时候,构建一个静态的类对象,用于存储我们所需要的信息,这样上面提到的应用接口对象,就可以存储起来,

    public static classPortal
{
/// <summary> ///应用程序的全局静态对象/// </summary> public static GlobalControl gc = new GlobalControl();/// <summary> ///The main entry point for the application./// </summary> [STAThread]static voidMain()
{
Log.Logger
= newLoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
"logs/myapp.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
//使用 AbpApplicationFactory 创建一个应用 var app = AbpApplicationFactory.Create<WinformApiClientModule>();//初始化应用 app.Initialize();
gc.app = app;
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(
false);var form = app.ServiceProvider.GetService<MainForm>();
gc.MainDialog
=form;
Application.Run(gc.MainDialog);
}
}

上面标注红色的部分就是把这个重要的app存放起来,便于后期的使用。

而我们注意到,我们创建窗体的时候,不是使用

var form = new MainForm();

的方式构建,而是使用接口构建的方式。

 var form = app.ServiceProvider.GetService<MainForm>();

和我们前面提到的方式构建接口是一样的。

var service = app.ServiceProvider.GetService<IService1>();

这个是为什么呢?因为我们需要通过构造函数注入接口方式,在窗体中引用相关的接口服务。

由于没有默认构造函数,因此不能再通过new的方式构建了,需要使用ABP VNext的常规接口解析的方式获得对应的窗体对象了。

注意:这里窗体需要继承自
ITransientDependency
接口,这样才可以通过接口的方式构建,否则是不行的。

如果我们在主窗体或者其他界面事件中调用其他窗口,也是类似,如下调用所示。

        private void button2_Click(objectsender, EventArgs e)
{
var form2 = Portal.gc.CreateForm<SecondForm>();
form2.ShowDialog();
}

这个地方就是用到了静态对象GlobalControl里面的方法构建,因为里面在程序启动的时候,已经存储了app应用接口对象了,可以用它来构建相关的接口或者窗体对象。

当然,这里的SecondForm也是不能使用New的方式构建窗体对象,也需要使用服务构建的标准方式来处理,毕竟它的默认构造函数用于接口的注入处理了。

程序看起来效果如下所示,可以正常打开窗体了。

2、Winform客户端授权信息的处理

在ABP VNext微服务的解决方案中,有一个控制台调用服务接口的测试项目,如下所示。

它主要就是介绍如何配置IdentityServer4(也叫IDS4)的授权规则来获得动态客户端的接口调用服务的。

它的配置是通过appsettings.json中配置好IdentityServer4终端的节点信息,用来在客户端调用类中进行相关的授权处理(获得令牌)的,因为我们调用服务接口需要令牌信息,而这些都是封装在内部里面的。

appsettings.json的配置信息如下所示,这个IDS4认证是采用client_credentials方式认证的。

而在构建ABP VNext项目模板的时候,也提供了一个类似控制台的测试项目,如下所示。

这个里面的appsettings.json是使用用户名密码的方式进行认证的,授权方式是密码方式。

看到这些信息,你可能注意到了用户名密码都在里面。

我在想,如果每次让用户使用Winform程序的时候,来修改一下这个appsettings.json,那肯定是不友好的,如果把IDS4信息动态构建,传入接口使用,是不是就可以不用配置文件了呢?

通过分析ABP VNExt框架的类库,你可以看到IDS的授权认证处理是在IdentityModelAuthenticationService 接口实现类里面,它通过下面接口获得通信令牌信息。

public async Task<string> GetAccessTokenAsync(IdentityClientConfiguration configuration)

我们传入对应的IDS4的配置对象即可获得接口的令牌信息。

我们通过IIdentityModelAuthenticationService 接口获得令牌信息,缓存起来可以,但是每次调用的时候,如何设定HttpClient的令牌头部信息呢,通过分析 IdentityModelAuthenticationService 类的代码知道,如果我们在appsetting.json配置了IDS4的标准配置,它就可以根据配置信息获得令牌信息的缓存,并设置到调用的HttpClient里面,如果我们采用刚才说的动态配置对象的传入获得token,没有IDS4配置文件信息它是没法提取出令牌缓存信息的。

        public async Task<bool> TryAuthenticateAsync(HttpClient client, string identityClientName = null)
{
var accessToken = awaitGetAccessTokenOrNullAsync(identityClientName);if (accessToken == null)
{
return false;
}

SetAccessToken(client, accessToken);
return true;
}

那有没有其他方式可以动态设定令牌信息或者类似的操作呢?

有!我们注意到,IRemoteServiceHttpClientAuthenticator 接口就是用来解决终端授权处理的接口,它的接口定义如下所示。

namespaceVolo.Abp.Http.Client.Authentication
{
public interfaceIRemoteServiceHttpClientAuthenticator
{
Task Authenticate(RemoteServiceHttpClientAuthenticateContext context);
}
}

我们参考项目Volo.Abp.Http.Client.IdentityModel.Web的思路

这个项目使用了自定义的接口实现类HttpContextIdentityModelRemoteServiceHttpClientAuthenticator,替换默认的IdentityModelRemoteServiceHttpClientAuthenticator类,我们来看看它的具体实现

namespaceVolo.Abp.Http.Client.IdentityModel.Web
{
[Dependency(ReplaceServices
= true)]public classHttpContextIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator
{
public IHttpContextAccessor HttpContextAccessor { get; set; }publicHttpContextIdentityModelRemoteServiceHttpClientAuthenticator(
IIdentityModelAuthenticationService identityModelAuthenticationService)
:
base(identityModelAuthenticationService)
{
}
public override asyncTask Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
if (context.RemoteService.GetUseCurrentAccessToken() != false)
{
var accessToken = awaitGetAccessTokenFromHttpContextOrNullAsync();if (accessToken != null)
{
context.Request.SetBearerToken(accessToken);
return;
}
}
await base.Authenticate(context);
}
protected virtual async Task<string>GetAccessTokenFromHttpContextOrNullAsync()
{
var httpContext = HttpContextAccessor?.HttpContext;if (httpContext == null)
{
return null;
}
return await httpContext.GetTokenAsync("access_token");
}
}
}

这里看到,它主要就是从httpContext中获得access_token的头部信息,然后通过SetBearerToken的接口设置到对应的HttpRequest请求中去的,也就是先获得令牌,然后设置请求对象的令牌,从而完成了授权令牌的信息处理。

我们如果是Winform或者控制台,那么调用请求类是HttpClient,我们可以模仿项目 Volo.Abp.Http.Client.IdentityModel.Web 这个方式创建一个项目,然后通过依赖方式来替换默认授权处理接口的实现;也可以通过在本地项目中创建一个IdentityModelRemoteServiceHttpClientAuthenticator的子类来替换默认的,如下所示。

namespaceWinform.TestApp
{
public classMyIdentityModelRemoteServiceHttpClientAuthenticator : IdentityModelRemoteServiceHttpClientAuthenticator
{

在ABP VNext框架类IdentityModelAuthenticationService中获得令牌的时候,就会设置获得的令牌到分布式缓存中,它的键是IdentityClientConfiguration对象的键值生成的,如下代码逻辑所示。

那么我们只需要在自定义的 MyIdentityModelRemoteServiceHttpClientAuthenticator 类中根据键获得缓存就可以设置令牌信息了。

通过上面的处理,我们就可以动态根据账号密码获得令牌,并根据配置信息的键从缓存中获得令牌,设置到对应的对象上去,完成了令牌的信息设置,这样ABP VNext动态客户端的代理接口类,就可以正常调用获得数据了。

数据记录展示如下。

这样,整个测试的例子就完成了多个Winform窗体的生成和调用展示,并通过令牌的处理,完成了客户端的IDS4授权,可以正常调用动态客户端的接口类,完美解决了相关的技术点了。

在ABP VNext框架中,HttpApi项目是我们作为Restful格式的控制器对象的封装项目,但往往很多案例都是简单的继承基类控制器AbpControllerBase,而需要在每个控制器里面重写很多类似的Create/Update/Delete/Get/GetList等常规Restful接口的调用,千篇一律的重复,本篇随笔介绍如何对这些内容通过基类的方式实现,子类无需重复代码,并且强类型所有的接口实现。

1、Restful接口的CRUD实现

在我们使用HttpApi项目进一步封装ABP VNext框架的Application项目中的应用服务,作为Restful格式的控制器对象,往往都需要实现基本的Create/Update/Delete/Get/GetList等常规Restful接口的实现调用,官方很多案例也都是把这部分代码进行重复在重复,如下所示。

例如对于客户对象Customer的HttpApi项目控制器的代码如下:

    /// <summary>
    ///客户信息控制器/// </summary>
    //[Area("crm")]
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController : AbpControllerBase, ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;publicCustomerController(ICustomerAppService customerAppService)
{
_customerAppService
=customerAppService;
}
/// <summary> ///创建对象/// </summary> [HttpPost]public Task<CustomerDto>CreateAsync(CreateCustomerDto input)
{
return_customerAppService.CreateAsync(input);
}
/// <summary> ///删除对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpDelete]
[Route(
"{id}")]public Task DeleteAsync(stringid)
{
return_customerAppService.DeleteAsync(id);
}
/// <summary> ///根据ID获取指定对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet]
[Route(
"{id}")]public Task<CustomerDto> GetAsync(stringid)
{
return_customerAppService.GetAsync(id);
}
/// <summary> ///分页获取列表记录/// </summary> /// <param name="input"></param> /// <returns></returns> [HttpGet]public Task<PagedResultDto<CustomerDto>>GetListAsync(CustomerPagedDto input)
{
return_customerAppService.GetListAsync(input);
}
/// <summary> ///更新对象/// </summary> [HttpPut]
[Route(
"{id}")]public Task<CustomerDto> UpdateAsync(stringid, CustomerDto input)
{
return_customerAppService.UpdateAsync(id, input);
}
/// <summary> ///获取字段列别名/// </summary> /// <returns></returns> [HttpGet]
[Route(
"columnalias")]public Task<Dictionary<string, string>>GetColumnNameAlias()
{
return_customerAppService.GetColumnNameAlias();
}
}

对于其他业务对象,这部分基本上千篇一律的重复一次,就是为了简单的封装一下CRUD的常规接口。

那么我们是否可以考虑通过基类的方式来抽取这部分代码,放到基类里面去实现,以后只需要继承该基类就完事了呢?

考虑到这些Restful的API接口实现,很多都是特定的业务对象,如上面的CustomerDto、CustomerPagedDto 等,那这些就需要通过泛型的方式指定类型给基类了。

而业务接口 ICustomerAppService 也是一个特定的业务接口,也需要传递给基类处理,这样才能进行调用的。

2、HttpApi 基类控制器的实现

我们注意到上面项目的ICustomerService的接口定义如下:

    /// <summary>
    ///客户信息,应用层服务接口定义/// </summary>
    public interfaceICustomerAppService : 
ICrudAppService
<CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>{/////<summary> ///// 获取指定条件的数量/////</summary> /////<param name="input">查找条件</param> /////<returns></returns> //Task<int> CountAsync(CustomerPagedDto input); /// <summary> ///获取字段中文别名(用于界面显示)的字典集合/// </summary> /// <returns></returns> Task<Dictionary<string, string>>GetColumnNameAlias();

}

它是继承自ICrudAppService接口(Abp的基类接口)并传递几个相关的实体类参数作为基类的接口强类型构建的。

那么我们的HttpApi 基类控制器也可以采用这种方式来传递对应的类型,作为基类接口的处理需要。

我们定义一个控制器基类MyAbpControllerBase,让它继承自常规的AbpControllerBase接口,并实现ICrudAppService接口,如下所示。

    /// <summary>
    ///自定义ABP控制器基类,用于实现通用的CRUD等方法/// </summary>
    public abstract class MyAbpControllerBase<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>: AbpControllerBase, 
IMyCrudAppService
<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput> where TEntityDto : IEntityDto<TKey> whereTGetListInput : IPagedAndSortedResultRequestwhere TCreateInput : IEntityDto<TKey> where TUpdateInput : IEntityDto<TKey>{protected IMyCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>_service;public MyAbpControllerBase(IMyCrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>service)
{
_service
=service;
}

这样我们就定义好这个基类,并且通过让它传递相关的业务对象和对象外键类型,强类型相关的接口处理,并让它实现了相关的构造函数。

那么对应的接口实现,我们只需要调用 _service 的处理即可。

        /// <summary>
        ///创建对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[HttpPost]public Task<TEntityDto>CreateAsync(TCreateInput input)
{
return_service.CreateAsync(input);
}
/// <summary> ///删除对象/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpDelete]
[Route(
"{id}")]publicTask DeleteAsync(TKey id)
{
return_service.DeleteAsync(id);
}
/// <summary> ///获取指定id的记录/// </summary> /// <param name="id"></param> /// <returns></returns> [HttpGet]
[Route(
"{id}")]public Task<TEntityDto>GetAsync(TKey id)
{
return_service.GetAsync(id);
}
/// <summary> ///获取条件的列表/// </summary> [HttpGet]public Task<PagedResultDto<TEntityDto>>GetListAsync(TGetListInput input)
{
return_service.GetListAsync(input);
}
/// <summary> ///更新对象/// </summary> [HttpPut]
[Route(
"{id}")]public Task<TEntityDto>UpdateAsync(TKey id, TUpdateInput input)
{
return_service.UpdateAsync(id, input);
}

我们还可以自己增加一些特殊的接口和基类的实现,这样我们对于常规的接口就不需要添加重复的实现代码了,只需要继承基类就可以了。

子类继承基类的代码如下所示。

    /// <summary>
    ///客户信息控制器/// </summary>
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController :
MyAbpControllerBase
<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;public CustomerController(ICustomerAppService customerAppService) : base(customerAppService)
{
_customerAppService
=customerAppService;
}
}

这样这个CustomerController默认就具有所有相关的常规接口了,不需要千篇一律的重写那些繁杂的代码,清爽了很多。

而如果我们需要额外增加一些接口的处理,那么在其接口定义增加,并实现即可,如下代码所示。

    /// <summary>
    ///客户信息,应用层服务接口定义/// </summary>
    public interfaceICustomerAppService : 
IMyCrudAppService
<CustomerDto, string, CustomerPagedDto, CreateCustomerDto, CustomerDto>{/// <summary> ///增加的额外测试接口/// </summary> /// <returns></returns> Task<bool>TestExtra();
}

HttpApi项目的实现代码如下所示。

    /// <summary>
    ///客户信息控制器/// </summary>
[RemoteService]
[ControllerName(
"Customer")]
[Route(
"api/customer")]public classCustomerController :
MyAbpControllerBase
<CustomerDto, string, CustomerPagedDto,CreateCustomerDto, CustomerDto>,
ICustomerAppService
{
private readonlyICustomerAppService _customerAppService;public CustomerController(ICustomerAppService customerAppService) : base(customerAppService)
{
_customerAppService
=customerAppService;
}
/// <summary> ///测试额外的接口调用/// </summary> /// <returns></returns> [HttpGet]
[Route(
"test-extra")]public async Task<bool>TestExtra()
{
return await_customerAppService.TestExtra();
}
}

启动Swagger的查看接口界面,我们可以看到,Customer控制器所发布的接口信息,如下所示。

一切都是那么的美好,以后再也不用重复书写或看到那些重复的,没有技术含量的代码了。

我们在做某件事情的时候,一般需要详细了解它的特点,以及内在的逻辑关系,一旦我们详细了解了整个事物后,就可以通过一些辅助手段来提高我们的做事情的效率了。本篇随笔介绍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>();
}
}

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