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','男','[email protected]'),
('Jimmy','男','[email protected]'),
('Lily','女','[email protected]'),
('Jenny','女','[email protected]')
- 在項目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"));
}
}
}
結果: