1. 程式人生 > >初識ABP vNext(9):ABP模組化開發-檔案管理

初識ABP vNext(9):ABP模組化開發-檔案管理

Tips:本篇已加入系列文章閱讀目錄,可點選檢視更多相關文章。 [TOC] # 前言 在之前的章節中介紹過ABP擴充套件實體,當時在使用者表擴充套件了使用者頭像欄位,使用者頭像就涉及到檔案上傳和檔案儲存。檔案上傳是很多系統都會涉及到的一個基礎功能,在ABP的模組化思路下,檔案管理可以做成一個通用的模組,便於以後在多個專案中複用。單純實現一個檔案上傳的功能並不複雜,本文就藉著這個簡單的功能來介紹一下ABP模組化開發的最基本步驟。 # 開始 ## 建立模組 首先使用ABP CLI建立一個模組:`abp new Xhznl.FileManagement -t module --no-ui` ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910172558639-780117902.png) 建立完成後會得到如下檔案: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910172811988-1656283782.png) 在主專案中新增對應模組的引用,Application=>Application,Domain=>Domain,HttpApi=>HttpApi 等等。例如: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910174159657-385790504.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910174252923-1500539398.png) 需要新增引用的專案:Application、Application.Contracts、Domain、Domain.Shared、EntityFrameworkCore、HttpApi、HttpApi.Client 手動新增這些引用比較麻煩,你可以搭建自己的私有NuGet伺服器,把模組的包釋出到私有NuGet上,然後通過NuGet來安裝引用。兩種方式各有優缺點,具體請參考[自定義現有模組](https://docs.abp.io/zh-Hans/abp/latest/Customizing-Application-Modules-Guide),關於私有NuGet搭建可以參考:[十分鐘搭建自己的私有NuGet伺服器-BaGet](https://www.cnblogs.com/xhznl/p/13426918.html)。 然後給這些專案的模組類新增對應的依賴,例如: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910194927720-1698177383.png) 通過上面的方式引用模組,使用visual studio是無法編譯通過的: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910174502648-1014353291.png) 需要在解決方案目錄下,手動執行`dotnet restore`命令即可: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200910175401258-1508422500.png) ## 模組開發 接下來關於檔案管理功能的開發,都在模組Xhznl.FileManagement中進行,它是一個獨立的解決方案。初學ABP,下面就以儘量簡單的方式來實現這個模組。 ### 應用服務 模組開發通常從Domain層實體建立開始,但是這裡先跳過。先在FileManagement.Application.Contracts專案新增應用服務介面和Dto。 modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\IFileAppService.cs: ```csharp public interface IFileAppService : IApplicationService { Task GetAsync(string name); Task CreateAsync(FileUploadInputDto input); } ``` modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\FileUploadInputDto.cs: ```csharp public class FileUploadInputDto { [Required] public byte[] Bytes { get; set; } [Required] public string Name { get; set; } } ``` 然後是FileManagement.Application專案,實現應用服務,先定義一個配置類。 modules\file-management\src\Xhznl.FileManagement.Application\Files\FileOptions.cs: ```csharp public class FileOptions { /// /// 檔案上傳目錄 ///
public string FileUploadLocalFolder { get; set; } /// /// 允許的檔案最大大小 /// public long MaxFileSize { get; set; } = 1048576;//1MB /// /// 允許的檔案型別 /// public string[] AllowedUploadFormats { get; set; } = { ".jpg", ".jpeg", ".png", "gif", ".txt" }; } ``` modules\file-management\src\Xhznl.FileManagement.Application\Files\FileAppService.cs: ```csharp public class FileAppService : FileManagementAppService, IFileAppService { private readonly FileOptions _fileOptions; public FileAppService(IOptions fileOptions) { _fileOptions = fileOptions.Value; } public Task GetAsync(string name) { Check.NotNullOrWhiteSpace(name, nameof(name)); var filePath = Path.Combine(_fileOptions.FileUploadLocalFolder, name); if (File.Exists(filePath)) { return Task.FromResult(File.ReadAllBytes(filePath)); } return Task.FromResult(new byte[0]); } [Authorize] public Task CreateAsync(FileUploadInputDto input) { if (input.Bytes.IsNullOrEmpty()) { throw new AbpValidationException("Bytes can not be null or empty!", new List { new ValidationResult("Bytes can not be null or empty!", new[] {"Bytes"}) }); } if (input.Bytes.Length > _fileOptions.MaxFileSize) { throw new UserFriendlyException($"File exceeds the maximum upload size ({_fileOptions.MaxFileSize / 1024 / 1024} MB)!"); } if (!_fileOptions.AllowedUploadFormats.Contains(Path.GetExtension(input.Name))) { throw new UserFriendlyException("Not a valid file format!"); } var fileName = Guid.NewGuid().ToString("N") + Path.GetExtension(input.Name); var filePath = Path.Combine(_fileOptions.FileUploadLocalFolder, fileName); if (!Directory.Exists(_fileOptions.FileUploadLocalFolder)) { Directory.CreateDirectory(_fileOptions.FileUploadLocalFolder); } File.WriteAllBytes(filePath, input.Bytes); return Task.FromResult("/api/file-management/files/" + fileName); } } ``` 服務實現很簡單,就是基於本地檔案系統的讀寫操作。 下面是FileManagement.HttpApi專案,新增控制器,暴露服務API介面。 modules\file-management\src\Xhznl.FileManagement.HttpApi\Files\FileController.cs: ```csharp [RemoteService] [Route("api/file-management/files")] public class FileController : FileManagementController { private readonly IFileAppService _fileAppService; public FileController(IFileAppService fileAppService) { _fileAppService = fileAppService; } [HttpGet] [Route("{name}")] public async Task GetAsync(string name) { var bytes = await _fileAppService.GetAsync(name); return File(bytes, MimeTypes.GetByExtension(Path.GetExtension(name))); } [HttpPost] [Route("upload")] [Authorize] public async Task CreateAsync(IFormFile file) { if (file == null) { throw new UserFriendlyException("No file found!"); } var bytes = await file.GetAllBytesAsync(); var result = await _fileAppService.CreateAsync(new FileUploadInputDto() { Bytes = bytes, Name = file.FileName }); return Json(result); } } ``` ### 執行模組 ABP的模板是可以獨立執行的,在FileManagement.HttpApi.Host專案的模組類FileManagementHttpApiHostModule配置FileOptions: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911132722192-254591885.png) 修改FileManagement.HttpApi.Host和FileManagement.IdentityServer專案的資料庫連線配置,然後啟動這2個專案,不出意外的話可以看到如下介面。 FileManagement.HttpApi.Host: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911133218273-1796636936.png) FileManagement.IdentityServer: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911133231215-1422063602.png) 現在你可以使用postman來測試一下File的2個API,當然也可以編寫單元測試。 ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911135241123-1212104739.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911135304435-1298819144.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911135517597-557954077.png) ### 單元測試 更好的方法是編寫單元測試,關於如何做好單元測試可以參考ABP原始碼,下面只做一個簡單示例: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911140231014-126474242.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911140410237-1879670126.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911140453574-1542444913.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911140605769-598857386.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911140922024-290909767.png) ## 模組使用 模組測試通過後,回到主專案。模組引用,模組依賴前面都已經做好了,現在只需配置一下FileOptions,就可以使用了。 ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911144501836-1823056167.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200911144331274-1016096113.png) 目前FileManagement.Domain、FileManagement.Domain.Shared、FileManagement.EntityFrameworkCore這幾個專案暫時沒用到,專案結構也不是固定的,可以根據自己實際情況來調整。 # 最後 本文的模組示例比較簡單,只是完成了一個檔案上傳和顯示的基本功能,關於實體,資料庫,領域服務,倉儲之類的都暫時沒用到。但是相信可以通過這個簡單的例子,感受到ABP外掛式的開發體驗,這是一個好的開始,更多詳細內容後面再做介紹。本文參考了ABP blogging模組的檔案管理,關於檔案儲存,ABP中也有一個BLOB系統可以瞭解一下。