1. 程式人生 > >初識ABP vNext(11):聚合根、倉儲、領域服務、應用服務、Blob儲存

初識ABP vNext(11):聚合根、倉儲、領域服務、應用服務、Blob儲存

Tips:本篇已加入系列文章閱讀目錄,可點選檢視更多相關文章。 [TOC] # 前言 在前兩節中介紹了ABP模組開發的基本步驟,試著實現了一個簡單的檔案管理模組;功能很簡單,就是基於本地檔案系統來完成檔案的讀寫操作,資料也並沒有儲存到資料庫,所以之前只簡單使用了應用服務,並沒有用到領域層。而在DDD中領域層是非常重要的一層,其中包含了實體,聚合根,領域服務,倉儲等等,複雜的業務邏輯也應該在領域層來實現。本篇來完善一下檔案管理模組,將檔案記錄儲存到資料庫,並使用ABP BLOB系統來完成檔案的儲存。 # 開始 ## 聚合根 首先從實體模型開始,建立File實體。按照DDD的思路,這裡的File應該是一個**聚合根**。 \modules\file-management\src\Xhznl.FileManagement.Domain\Files\File.cs: ```csharp public class File : FullAuditedAggregateRoot, IMultiTenant { public virtual Guid? TenantId { get; protected set; } [NotNull] public virtual string FileName { get; protected set; } [NotNull] public virtual string BlobName { get; protected set; } public virtual long ByteSize { get; protected set; } protected File() { } public File(Guid id, Guid? tenantId, [NotNull] string fileName, [NotNull] string blobName, long byteSize) : base(id) { TenantId = tenantId; FileName = Check.NotNullOrWhiteSpace(fileName, nameof(fileName)); BlobName = Check.NotNullOrWhiteSpace(blobName, nameof(blobName)); ByteSize = byteSize; } } ``` 在DbContext中**新增DbSet** \modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\IFileManagementDbContext.cs: ```csharp public interface IFileManagementDbContext : IEfCoreDbContext { DbSet Files { get; } } ``` \modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\FileManagementDbContext.cs: ```csharp public class FileManagementDbContext : AbpDbContext, IFileManagementDbContext { public DbSet Files { get; set; } ...... } ``` **配置實體** \modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\FileManagementDbContextModelCreatingExtensions.cs: ```csharp public static void ConfigureFileManagement( this ModelBuilder builder, Action optionsAction = null) { ...... builder.Entity(b => { //Configure table & schema name b.ToTable(options.TablePrefix + "Files", options.Schema); b.ConfigureByConvention(); //Properties b.Property(q => q.FileName).IsRequired().HasMaxLength(FileConsts.MaxFileNameLength); b.Property(q => q.BlobName).IsRequired().HasMaxLength(FileConsts.MaxBlobNameLength); b.Property(q => q.ByteSize).IsRequired(); }); } ``` ## 倉儲 ABP為每個聚合根或實體提供了 **預設的通用(泛型)倉儲** ,其中包含了標準的CRUD操作,注入`IRepository`即可使用。通常來說預設倉儲就夠用了,有特殊需求時也可以自定義倉儲。 定義**倉儲介面** \modules\file-management\src\Xhznl.FileManagement.Domain\Files\IFileRepository.cs: ```csharp public interface IFileRepository : IRepository { Task FindByBlobNameAsync(string blobName); } ``` **倉儲實現** \modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\Files\EfCoreFileRepository.cs: ```csharp public class EfCoreFileRepository : EfCoreRepository, IFileRepository { public EfCoreFileRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { } public async Task FindByBlobNameAsync(string blobName) { Check.NotNullOrWhiteSpace(blobName, nameof(blobName)); return await DbSet.FirstOrDefaultAsync(p => p.BlobName == blobName); } } ``` **註冊倉儲** \modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\FileManagementEntityFrameworkCoreModule.cs: ```csharp public class FileManagementEntityFrameworkCoreModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(options => { options.AddRepository(); }); } } ``` ## 領域服務 定義**領域服務介面** \modules\file-management\src\Xhznl.FileManagement.Domain\Files\IFileManager.cs: ```csharp public interface IFileManager : IDomainService { Task FindByBlobNameAsync(string blobName); Task CreateAsync(string fileName, byte[] bytes); Task GetBlobAsync(string blobName); } ``` 在實現領域服務之前,先來安裝一下ABP Blob系統核心包,因為我要使用blob來儲存檔案,`Volo.Abp.BlobStoring`包是必不可少的。 ### BLOB儲存 BLOB(binary large object):大型二進位制物件;關於BLOB可以參考 [BLOB 儲存](https://docs.abp.io/zh-Hans/abp/latest/Blob-Storing) ,這裡不多介紹。 安裝`Volo.Abp.BlobStoring`,在Domain專案目錄下執行:`abp add-package Volo.Abp.BlobStoring` ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923215317430-1613070966.png) `Volo.Abp.BlobStoring`是BLOB的核心包,它僅包含BLOB的一些基本抽象,想要BLOB系統正常工作,還需要為它配置一個提供程式;這個提供程式暫時不管,將來由模組的具體使用者去提供。這樣的好處是模組不依賴特定儲存提供程式,使用者可以隨意的指定儲存到阿里雲,Azure,或者檔案系統等等。。。 **領域服務實現** \modules\file-management\src\Xhznl.FileManagement.Domain\Files\FileManager.cs: ```csharp public class FileManager : DomainService, IFileManager { protected IFileRepository FileRepository { get; } protected IBlobContainer BlobContainer { get; } public FileManager(IFileRepository fileRepository, IBlobContainer blobContainer) { FileRepository = fileRepository; BlobContainer = blobContainer; } public virtual async Task FindByBlobNameAsync(string blobName) { Check.NotNullOrWhiteSpace(blobName, nameof(blobName)); return await FileRepository.FindByBlobNameAsync(blobName); } public virtual async Task CreateAsync(string fileName, byte[] bytes) { Check.NotNullOrWhiteSpace(fileName, nameof(fileName)); var blobName = Guid.NewGuid().ToString("N"); var file = await FileRepository.InsertAsync(new File(GuidGenerator.Create(), CurrentTenant.Id, fileName, blobName, bytes.Length)); await BlobContainer.SaveAsync(blobName, bytes); return file; } public virtual async Task GetBlobAsync(string blobName) { Check.NotNullOrWhiteSpace(blobName, nameof(blobName)); return await BlobContainer.GetAllBytesAsync(blobName); } } ``` ## 應用服務 接下來修改一下應用服務,應用服務通常沒有太多業務邏輯,其呼叫領域服務來完成業務。 **應用服務介面** \modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\IFileAppService.cs: ```csharp public interface IFileAppService : IApplicationService { Task FindByBlobNameAsync(string blobName); Task CreateAsync(FileDto input); } ``` **應用服務實現** \modules\file-management\src\Xhznl.FileManagement.Application\Files\FileAppService.cs: ```csharp public class FileAppService : FileManagementAppService, IFileAppService { protected IFileManager FileManager { get; } public FileAppService(IFileManager fileManager) { FileManager = fileManager; } public virtual async Task FindByBlobNameAsync(string blobName) { Check.NotNullOrWhiteSpace(blobName, nameof(blobName)); var file = await FileManager.FindByBlobNameAsync(blobName); var bytes = await FileManager.GetBlobAsync(blobName); return new FileDto { Bytes = bytes, FileName = file.FileName }; } [Authorize] public virtual async Task CreateAsync(FileDto input) { await CheckFile(input); var file = await FileManager.CreateAsync(input.FileName, input.Bytes); return file.BlobName; } protected virtual async Task CheckFile(FileDto 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"}) }); } var allowedMaxFileSize = await SettingProvider.GetAsync(FileManagementSettings.AllowedMaxFileSize);//kb var allowedUploadFormats = (await SettingProvider.GetOrNullAsync(FileManagementSettings.AllowedUploadFormats)) ?.Split(",", StringSplitOptions.RemoveEmptyEntries); if (input.Bytes.Length > allowedMaxFileSize * 1024) { throw new UserFriendlyException(L["FileManagement.ExceedsTheMaximumSize", allowedMaxFileSize]); } if (allowedUploadFormats == null || !allowedUploadFormats.Contains(Path.GetExtension(input.FileName))) { throw new UserFriendlyException(L["FileManagement.NotValidFormat"]); } } } ``` **API控制器** 最後記得將服務介面暴露出去,我這裡是自己編寫Controller,你也可以使用ABP的自動API控制器來完成,請參考 [ 自動API控制器](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers) \modules\file-management\src\Xhznl.FileManagement.HttpApi\Files\FileController.cs: ```csharp [RemoteService] [Route("api/file-management/files")] public class FileController : FileManagementController { protected IFileAppService FileAppService { get; } public FileController(IFileAppService fileAppService) { FileAppService = fileAppService; } [HttpGet] [Route("{blobName}")] public virtual async Task GetAsync(string blobName) { var fileDto = await FileAppService.FindByBlobNameAsync(blobName); return File(fileDto.Bytes, MimeTypes.GetByExtension(Path.GetExtension(fileDto.FileName))); } [HttpPost] [Route("upload")] [Authorize] public virtual 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 FileDto() { Bytes = bytes, FileName = file.FileName }); return Json(result); } } ``` ## 單元測試 針對以上內容做一個簡單的測試,首先為Blob系統配置一個提供程式。 我這裡使用最簡單的檔案系統來儲存,所以需要安裝`Volo.Abp.BlobStoring.FileSystem`。在Application.Tests專案目錄下執行:`abp add-package Volo.Abp.BlobStoring.FileSystem` ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923223117152-2113769594.png) **配置預設容器** \modules\file-management\test\Xhznl.FileManagement.Application.Tests\FileManagementApplicationTestModule.cs: ```csharp [DependsOn( typeof(FileManagementApplicationModule), typeof(FileManagementDomainTestModule), typeof(AbpBlobStoringFileSystemModule) )] public class FileManagementApplicationTestModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { Configure(options => { options.Containers.ConfigureDefault(container => { container.UseFileSystem(fileSystem => { fileSystem.BasePath = "D:\\my-files"; }); }); }); base.ConfigureServices(context); } } ``` **測試用例** \modules\file-management\test\Xhznl.FileManagement.Application.Tests\Files\FileAppService_Tests.cs: ```csharp public class FileAppService_Tests : FileManagementApplicationTestBase { private readonly IFileAppService _fileAppService; public FileAppService_Tests() { _fileAppService = GetRequiredService(); } [Fact] public async Task Create_FindByBlobName_Test() { var blobName = await _fileAppService.CreateAsync(new FileDto() { FileName = "微信圖片_20200813165555.jpg", Bytes = await System.IO.File.ReadAllBytesAsync(@"D:\WorkSpace\WorkFiles\雜項\圖片\微信圖片_20200813165555.jpg") }); blobName.ShouldNotBeEmpty(); var fileDto = await _fileAppService.FindByBlobNameAsync(blobName); fileDto.ShouldNotBeNull(); fileDto.FileName.ShouldBe("微信圖片_20200813165555.jpg"); } } ``` **執行測試** ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200924130852505-278784871.png) 測試通過,blob也已經存入D:\\my-files: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200924131054161-1906173417.png) ## 模組引用 下面回到主專案,前面的章節中已經介紹過,模組的引用依賴都已經新增完成,下面就直接從資料庫遷移開始。 \src\Xhznl.HelloAbp.EntityFrameworkCore.DbMigrations\EntityFrameworkCore\HelloAbpMigrationsDbContext.cs: ```csharp public class HelloAbpMigrationsDbContext : AbpDbContext { public HelloAbpMigrationsDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { ...... builder.ConfigureFileManagement(); ...... } } ``` 開啟程式包管理器控制檯,執行以下命令: `Add-Migration "Added_FileManagement"` `Update-Database` ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923231607988-1470111310.png) 此時資料庫已經生成了File表: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923231839341-235042846.png) 還有記得在HttpApi.Host專案配置你想要的blob提供程式。 最後結合前端測試一下吧: ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923232348431-2045452685.png) ![](https://img2020.cnblogs.com/blog/610959/202009/610959-20200923232306418-1872529958.png) # 最後 以上就是本人所理解的abp模組開發一個相對完整的流程,還有些概念後面再做補充。因為這個例子比較簡單,文中有些環節是不必要的,需要結合實際情況去取捨。程式碼地址:https://github.com/xiajingren/HelloAbp