Tips:本篇已加入系列文章閱讀目錄,可點選檢視更多相關文章。
前言
在前兩節中介紹了ABP模組開發的基本步驟,試著實現了一個簡單的檔案管理模組;功能很簡單,就是基於本地檔案系統來完成檔案的讀寫操作,資料也並沒有儲存到資料庫,所以之前只簡單使用了應用服務,並沒有用到領域層。而在DDD中領域層是非常重要的一層,其中包含了實體,聚合根,領域服務,倉儲等等,複雜的業務邏輯也應該在領域層來實現。本篇來完善一下檔案管理模組,將檔案記錄儲存到資料庫,並使用ABP BLOB系統來完成檔案的儲存。
開始
聚合根
首先從實體模型開始,建立File實體。按照DDD的思路,這裡的File應該是一個聚合根。
\modules\file-management\src\Xhznl.FileManagement.Domain\Files\File.cs:
public class File : FullAuditedAggregateRoot<Guid>, 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:
public interface IFileManagementDbContext : IEfCoreDbContext
{
DbSet<File> Files { get; }
}
\modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\FileManagementDbContext.cs:
public class FileManagementDbContext : AbpDbContext<FileManagementDbContext>, IFileManagementDbContext
{
public DbSet<File> Files { get; set; }
......
}
配置實體
\modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\EntityFrameworkCore\FileManagementDbContextModelCreatingExtensions.cs:
public static void ConfigureFileManagement(
this ModelBuilder builder,
Action<FileManagementModelBuilderConfigurationOptions> optionsAction = null)
{
......
builder.Entity<File>(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<TEntity, TKey>
即可使用。通常來說預設倉儲就夠用了,有特殊需求時也可以自定義倉儲。
定義倉儲介面
\modules\file-management\src\Xhznl.FileManagement.Domain\Files\IFileRepository.cs:
public interface IFileRepository : IRepository<File, Guid>
{
Task<File> FindByBlobNameAsync(string blobName);
}
倉儲實現
\modules\file-management\src\Xhznl.FileManagement.EntityFrameworkCore\Files\EfCoreFileRepository.cs:
public class EfCoreFileRepository : EfCoreRepository<IFileManagementDbContext, File, Guid>, IFileRepository
{
public EfCoreFileRepository(IDbContextProvider<IFileManagementDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async Task<File> 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:
public class FileManagementEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<FileManagementDbContext>(options =>
{
options.AddRepository<File, EfCoreFileRepository>();
});
}
}
領域服務
定義領域服務介面
\modules\file-management\src\Xhznl.FileManagement.Domain\Files\IFileManager.cs:
public interface IFileManager : IDomainService
{
Task<File> FindByBlobNameAsync(string blobName);
Task<File> CreateAsync(string fileName, byte[] bytes);
Task<byte[]> GetBlobAsync(string blobName);
}
在實現領域服務之前,先來安裝一下ABP Blob系統核心包,因為我要使用blob來儲存檔案,Volo.Abp.BlobStoring
包是必不可少的。
BLOB儲存
BLOB(binary large object):大型二進位制物件;關於BLOB可以參考 BLOB 儲存 ,這裡不多介紹。
安裝Volo.Abp.BlobStoring
,在Domain專案目錄下執行:abp add-package Volo.Abp.BlobStoring
Volo.Abp.BlobStoring
是BLOB的核心包,它僅包含BLOB的一些基本抽象,想要BLOB系統正常工作,還需要為它配置一個提供程式;這個提供程式暫時不管,將來由模組的具體使用者去提供。這樣的好處是模組不依賴特定儲存提供程式,使用者可以隨意的指定儲存到阿里雲,Azure,或者檔案系統等等。。。
領域服務實現
\modules\file-management\src\Xhznl.FileManagement.Domain\Files\FileManager.cs:
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<File> FindByBlobNameAsync(string blobName)
{
Check.NotNullOrWhiteSpace(blobName, nameof(blobName));
return await FileRepository.FindByBlobNameAsync(blobName);
}
public virtual async Task<File> 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<byte[]> GetBlobAsync(string blobName)
{
Check.NotNullOrWhiteSpace(blobName, nameof(blobName));
return await BlobContainer.GetAllBytesAsync(blobName);
}
}
應用服務
接下來修改一下應用服務,應用服務通常沒有太多業務邏輯,其呼叫領域服務來完成業務。
應用服務介面
\modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\IFileAppService.cs:
public interface IFileAppService : IApplicationService
{
Task<FileDto> FindByBlobNameAsync(string blobName);
Task<string> CreateAsync(FileDto input);
}
應用服務實現
\modules\file-management\src\Xhznl.FileManagement.Application\Files\FileAppService.cs:
public class FileAppService : FileManagementAppService, IFileAppService
{
protected IFileManager FileManager { get; }
public FileAppService(IFileManager fileManager)
{
FileManager = fileManager;
}
public virtual async Task<FileDto> 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<string> 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<ValidationResult>
{
new ValidationResult("Bytes can not be null or empty!", new[] {"Bytes"})
});
}
var allowedMaxFileSize = await SettingProvider.GetAsync<int>(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控制器
\modules\file-management\src\Xhznl.FileManagement.HttpApi\Files\FileController.cs:
[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<FileResult> 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<JsonResult> 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
配置預設容器
\modules\file-management\test\Xhznl.FileManagement.Application.Tests\FileManagementApplicationTestModule.cs:
[DependsOn(
typeof(FileManagementApplicationModule),
typeof(FileManagementDomainTestModule),
typeof(AbpBlobStoringFileSystemModule)
)]
public class FileManagementApplicationTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBlobStoringOptions>(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:
public class FileAppService_Tests : FileManagementApplicationTestBase
{
private readonly IFileAppService _fileAppService;
public FileAppService_Tests()
{
_fileAppService = GetRequiredService<IFileAppService>();
}
[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");
}
}
執行測試
測試通過,blob也已經存入D:\my-files:
模組引用
下面回到主專案,前面的章節中已經介紹過,模組的引用依賴都已經新增完成,下面就直接從資料庫遷移開始。
\src\Xhznl.HelloAbp.EntityFrameworkCore.DbMigrations\EntityFrameworkCore\HelloAbpMigrationsDbContext.cs:
public class HelloAbpMigrationsDbContext : AbpDbContext<HelloAbpMigrationsDbContext>
{
public HelloAbpMigrationsDbContext(DbContextOptions<HelloAbpMigrationsDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
......
builder.ConfigureFileManagement();
......
}
}
開啟程式包管理器控制檯,執行以下命令:
Add-Migration "Added_FileManagement"
Update-Database
此時資料庫已經生成了File表:
還有記得在HttpApi.Host專案配置你想要的blob提供程式。
最後結合前端測試一下吧:
最後
以上就是本人所理解的abp模組開發一個相對完整的流程,還有些概念後面再做補充。因為這個例子比較簡單,文中有些環節是不必要的,需要結合實際情況去取捨。程式碼地址:https://github.com/xiajingren/HelloAbp