ABP的由来
从零开始创建一个企业应用程序是一件繁琐的事,因为需要重复做很多常见的基础工作。许多公司都在开发自己的应用程序框架来重用于不同的项目,然后在框架的基础上开发一些新的功能。但并不是每个公司都有这样的实力。ABP因此诞生,作者之所以把项目命名为“ASP.NET Boilerplate”,就是希望它能成为开发一般企业WEB应用的新起点,直接把ABP作为项目模板。
一、什么是ABP
ABP是为新的现代Web应用程序使用最佳实践和使用最流行工具的一个起点。可作为一般用途的应用程序的基础框架或项目模板。ABP 提供了一个应用程序开发模型用于最佳实践。它拥有基础类、接口和工具使我们容易建立起可维护的大规模的应用程序。
二、ABP具有什么的功能
- 依赖注入:ABP使用并提供一个强大而且符合约定的DI框架。上述的应用服务,按照约定临时的(每个请求Request创建一个)注册到DI容器,它能简单地注入所有依赖项(如示例中的Irepository)。
- 仓储:ABP能为每个实体创建一个默认的仓储(如示例中的Irepository)。默认仓储包含许多有用的方法,如示例中的FirstOrDefault方法。我们可以根据需要,很容易地扩展默认仓储。仓储抽象了DBMS和ORM以及简化了数据访问逻辑。
- 授权:ABP可以检查许可。如果当前用户没有“updating
task”权限或是未登录,ABP就会阻止他们访问UpdateTask方法。用陈述性的特性来简化授权,当然还有另外的授权方式。 - 验证:ABP自动检查input是否为null。根据标准的数据注释特性和自定义验证规则,检查一个input的所有属性。如果请求没有通过验证,会抛出一个对应的验证异常。
- 审计日志:根据约定和配置,每个请求的用户、浏览器、Ip地址、调用服务、方法、参数、调用时间、执行耗时和其它的一些信息会被自动地保存下来。
- 工作单元:在ABP里,每个应用服务方法都默认地被认定为一个工作单元。在方法开始前,它自动创建一个连接并开启一个事务。如果方法成功完成,接着事务会被提交并释放连接。即使是使用不同的仓储或是方法,它们都可以是原子性(事务性)的,并且当事务提交时实体中所有的修改都自动地被保存。因此,如同示例所示,我们甚至不需要去调用_repository.Update(task)方法。
- 异常处理:我们几乎不用在一个使用ABP的Web应用中写异常处理。所有的异常都自动地被默认处理。当一个异常发生,ABP自动记录它并返回一个对应的结果给客户端。例如,一个AJAX请求,它会返回一个Json对象给客户端,告知发生了一个错误。如示例所示,UserFriendlyException可以向客户端隐藏具体的异常,显示友好信息。它同样可以在客户端理解并处理客户端错误,并向用户显示对应的信息。
- 日志:如你所见,我们可以用定义在基类中的Logger对象写日志。默认使用Log4Net,不过这是可修改和可配置的。
- 本地化:请注意我们在抛出异常时,使用了L方法。因此,它可自动依据用户区域,使用相应的本地化信息。当然,我们需要在某处定义CouldNotFoundTheTaskMessage(更多信息参见“本地化”文档)。
- 自动映射:最后一行代码,我们使用ABP的MapTo扩展方法来映射input属性到实体属性。它使用AutoMapper库来执行映射。因此,我们可以简单地基于命名约定,从一个对象映射到另一个。
- 动态Web API 层:实际上TaskAppService是一个简单的类(甚至是不需要从ApplicationService继承)。我们通常包装一个Web API 控制器为Javascript客户端公开方法,ABP会在运行时自动地完成这件事。因此,我们可以直接在客户端使用应用服务。
- 动态Javascript AJAX 代理:ABP创建Javascript代理方法,以便就本地调用一样,来调用应用服务。
三、如何搭建启动一个ABP项目
-
去官网下载一个ABP项目,并填写你的项目名。https://aspnetboilerplate.com/Templates
-
解压zip文件,用visual studio打开解决方案。并修改Web.Host项目下的appsetting.json文件,配置数据库连接字符串。
-
设置EntityFrameworkCore项目为启动项目,并打开程序包管理控制台
输入命令:Add-Migration,会看到在这个项目下会生成好多个文件
再执行命令:Update-Database
命令执行完后会自动生成一个新的数据库。
把Web.Host设置为启动项目,并启动程序。
- 启动前端Vue项目,用visual studio code打开Vue项目
打开一个新的TERMINAL
输入命令:npm install ,安装项目所依赖的包(第一次安装需要较长的时间,请耐心等候)。
所有依赖包都安装完成后,执行命令:npm run serve,启动项目(账号:admin,密码:123qwe)。
四、如何添加一个简单的功能
- 在Core项目下定义一个APJUser类:
public class APJUser : Entity<int>
{
[Column("Id")]
public override int Id { get; set; }
public string Name { get; set; }
public string Gender { get; set; }
public string Email { get; set; }
}
- 在EntityFrameworkCore项目下,找到DbContext,并添加以下代码:
public DbSet<APJUser> APJUsers { get; set; }
- 执行命令:Add-Migration(注意:要将EntityFrameworkCore设置为启动项目)
再执行命令:Update-Database
会看到在数据库中会多了APJUser这么一张表:
我们往表中插入几条数据,方便我们测试:
INSERT INTO [dbo].[APJUsers]
([Name],[Gender],[Email])
VALUES
('Tim','男','182317676@test.com'),
('Jimmy','男','154317676@test.com'),
('Lily','女','121317676@test.com'),
('Jenny','女','165317676@test.com')
- 在项目Application中,创建以下几个类:
[AutoMapFrom(typeof(APJUser))]
public class APJUserDto: EntityDto
{
public string Name { get; set; }
public string Gender { get; set; }
public string Email { get; set; }
}
[AutoMapTo(typeof(APJUser))]
public class CreateAPJUserDto
{
public string Name { get; set; }
public string Gender { get; set; }
public string Email { get; set; }
}
public class PagedAPJUserResultRequestDto : PagedResultRequestDto
{
public string Keyword { get; set; }
public bool? IsActive { get; set; }
}
public interface IAPJUserAppService : IAsyncCrudAppService<APJUserDto, int, PagedAPJUserResultRequestDto, CreateAPJUserDto, APJUserDto>
{
}
public class APJUserAppService : AsyncCrudAppService<APJUser, APJUserDto, int, PagedAPJUserResultRequestDto, CreateAPJUserDto, APJUserDto>, IAPJUserAppService
{
private IRepository<APJUser, int> _repository;
public APJUserAppService(IRepository<APJUser, int> repository) : base(repository)
{
_repository = repository;
}
}
然后启动项目,看一下预期效果:
ABP会帮助我们自动生成相应的API,实现CRUD的功能,下面我们来调用一下,发现有几个问题。
问题一:GetAll()方法中,KeyWord参数设置无效。
修改方法,在APJUserAppService重写一下CreateFilteredQuery()的方法
protected override IQueryable CreateFilteredQuery(PagedAPJUserResultRequestDto input)
{
return _repository.GetAll().WhereIf(!string.IsNullOrWhiteSpace(input.Keyword), t => t.Name.Contains(input.Keyword));
}
问题二:Update()方法报错。
修改方法
protected override void MapToEntity(APJUserDto updateInput, APJUser entity)
{
entity.Name = updateInput.Name;
entity.Gender = updateInput.Gender;
entity.Email = updateInput.Email;
}
至此,一个简单的CURD功能添加完成。
五、ABP如何分配权限
在Core项目下,找到PermissionNames.cs这个文件:
定义一个PermissionName:
public const string Pages_APJUser_GetAll = "Pages.APJUser.GetAll";
找到Core项目下的AuthorizationProvider.cs文件,并添加一个权限:
context.CreatePermission(PermissionNames.Pages_APJUser_GetAll, L("APJ_User查询权限"));//第二个参数为多资源参数
方法一:
回到APJUserAppService.cs,修改一下查询的方法,在GetAll的方法上添加一个标签:
[AbpAuthorize(PermissionNames.Pages_APJUser_GetAll)]
public override Task<PagedResultDto<APJUserDto>> GetAll(PagedAPJUserResultRequestDto input)
{
return base.GetAll(input);
}
这时,重新编译整个解决方案,并启动项目。由于admin账号,默认是启动所有的权限,所以我们需要先将admin账号的Pages_APJUser_GetAll权限去掉才可以测试。在Role界面,将Pages_APJUser_GetAll权限去掉。
这时再调用这个API,则会提示没有权限:
方法二:
调用PermissionChecker.Authorize()方法
//[AbpAuthorize(PermissionNames.Pages_APJUser_GetAll)]
public override Task<PagedResultDto<APJUserDto>> GetAll(PagedAPJUserResultRequestDto input)
{
PermissionChecker.Authorize(PermissionNames.Pages_APJUser_GetAll);
return base.GetAll(input);
}
方法三:
重写CheckGetAllPermission()方法,我们可以看到,ABP默认帮我们实现了很多方法。其中,部分方法是virtual方法,是可以重写的:
接下来我们重写一下CheckGetAllPermission()方法:
protected override void CheckGetAllPermission()
{
//业务逻辑
throw new AbpAuthorizationException("对不起,你没有这个权限");
}
然后,再GetAll()方法中调用。
//[AbpAuthorize(PermissionNames.Pages_APJUser_GetAll)]
public override Task<PagedResultDto<APJUserDto>> GetAll(PagedAPJUserResultRequestDto input)
{
//PermissionChecker.Authorize(PermissionNames.Pages_APJUser_GetAll);
CheckGetAllPermission();
return base.GetAll(input);
}
六、本地资源化
- 在Core项目下找到Localization\SourceFiles\APJ.xml
- 添加一个新的资源:
<text name="TestResource1">This is a test.</text>
- 实现:
方法一:
在ApplicationService层,注入ILocalizationManager
private ILocalizationManager _localizationManager;
public APJUserAppService(ILocalizationManager localizationManager) : base(repository)
{
_repository = repository;
_localizationManager = localizationManager;
}
定义一个接口方法,并实现它:
string GetLozationString(string key, string language);
public string GetLozationString(string key, string language = "en")
{
return _localizationManager.GetString(APJConsts.LocalizationSourceName, key, new CultureInfo(language));
}
- 调用结果:
方法二:
再构造函数中初始化 LocalizationSourceName,
public APJUserAppService(IRepository<APJUser, int> repository, ILocalizationManager localizationManager) : base(repository)
{
_repository = repository;
_localizationManager = localizationManager;
LocalizationSourceName = APJConsts.LocalizationSourceName;
}
修改获取多资源的方法:
public string GetLozationString(string key = "HomePage", string language = "zh-HK")
{
//return _localizationManager.GetString(APJConsts.LocalizationSourceName, key, new CultureInfo(language));
return L(key, new CultureInfo(language,false));
}
七、记录日志
- 通过依赖注入,注入Logger对象(注意:仅仅能使用public属性):
public ILogger Logger { get; set; }
public APJUserAppService(IRepository<APJUser, int> repository, ILocalizationManager localizationManager) : base(repository)
{
_repository = repository;
_localizationManager = localizationManager;
LocalizationSourceName = APJConsts.LocalizationSourceName;
Logger = NullLogger.Instance;
}
- 调用:
Logger.Info("this is a test");
Logger.Debug("this is debug test");
Logger.Warn("this is warm test");
得到的结果:(放在Logs.txt文件中)
INFO 2019-10-12 12:05:01,346 [6 ] APJ.APJManagement.APJUserAppService - this is a test
DEBUG 2019-10-12 12:05:01,346 [6 ] APJ.APJManagement.APJUserAppService - this is debug test
WARN 2019-10-12 12:05:01,346 [6 ] APJ.APJManagement.APJUserAppService - this is warm test
- 配置log4
找到Web.Host项目下的log4net.config文件,并打开:
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
<file value="App_Data/Logs/Logs.txt" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="10000KB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5level %date [%-5.5thread] %-40.40logger - %message%newline" />
</layout>
</appender>
<root>
<appender-ref ref="RollingFileAppender" />
<level value="DEBUG" />
</root>
</log4net>
详细的配置请参考文章:
https://blog.csdn.net/u013160017/article/details/81392154
https://blog.csdn.net/eagleuniversityeye/article/details/80582140
八、数据验证
方法一:
使用C#自定义标签,如[Required]
[Required]
public string Name { get; set; }
方法二:
自定义数据验证
实现接口ICustomValidate
public class CreateAPJUserDto:ICustomValidate
{
[Required]
public string Name { get; set; }
public string Gender { get; set; }
public string Email { get; set; }
public void AddValidationErrors(CustomValidationContext context)
{
if (Name.Length>10)
{
context.Results.Add(new ValidationResult("Name长度不能大于10"));
}
}
}
结果: