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

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模組開發一個相對完整的流程,還有些概念後面再做補充。因為這個例子比較簡單,文中有些環節是不必要的,需要結合實際情況去取捨。程式碼地址://github.com/xiajingren/HelloAbp