初识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模块开发一个相对完整的流程,还有些概念后面再做补充。因为这个例子比较简单,文中有些环节是不必要的,需要结合实际情况去取舍。代码地址:https://github.com/xiajingren/HelloAbp
初识ABP vNext(11):聚合根、仓储、领域服务、应用服务、Blob存储的更多相关文章
- Abp vNext 自定义 Ef Core 仓储引发异常
问题 在使用自定义 Ef Core 仓储和 ABP vNext 注入的默认仓储时,通过两个 Repository 进行 Join 操作,提示 Cannot use multiple DbContext ...
- ABP VNext从单体切换到微服务
注:此处的微服务只考虑服务部分,不考虑内外层网关.认证等. ABP VNext从单体切换到微服务,提供了相当大的便利性,对于各模块内部不要做任何调整,仅需要调整承载体即可. ABP can help ...
- Abp 领域事件简单实践 <四> 聚合根的领域事件
聚合根有个 DomainEvents 属性. 首先聚合根是一个实体.这个实体的仓储有变化(增删改)的时候,会触发这个DomainEvents 里的事件.就像EventBus.Trigger一样. pu ...
- 初识ABP vNext(1):开篇计划&基础知识
目录 前言 开始 审计(Audit) 本地化(Localization) 事件总线(Event Bus) 多租户(multi-tenancy technology) DDD分层 实体(Entity) ...
- 初识ABP vNext(9):ABP模块化开发-文件管理
Tips:本篇已加入系列文章阅读目录,可点击查看更多相关文章. 目录 前言 开始 创建模块 模块开发 应用服务 运行模块 单元测试 模块使用 最后 前言 在之前的章节中介绍过ABP扩展实体,当时在用户 ...
- 持续提升程序员幸福指数——使用abp vnext设计一款面向微服务的单体架构
可能你会面临这样一种情况,在架构设计之前,你对业务不甚了解,需求给到的也模棱两可,这个时候你既无法明确到底是要使用单体架构还是使用微服务架构,如果使用单体,后续业务扩展可能带来大量修改,如果使用微服务 ...
- [Abp vNext 源码分析] - 23. 二进制大对象系统(BLOB)
一.简介 ABP vNext 在 v 2.9.x 版本当中添加了 BLOB 系统,主要用于存储大型二进制文件.ABP 抽象了一套通用的 BLOB 体系,开发人员在存储或读取二进制文件时,可以忽略具体实 ...
- 初识ABP vNext(2):ABP启动模板
目录 前言 开始 AbpHelper 模块安装 最后 前言 上一篇介绍了ABP的一些基础知识,本篇继续介绍ABP的启动模板.使用ABP CLI命令就可以得到这个启动模板,其中包含了一些基础功能模块,你 ...
- 初识ABP vNext(3):vue对接ABP基本思路
目录 前言 开始 登录 权限 本地化 创建项目 ABP vue-element-admin 最后 前言 上一篇介绍了ABP的启动模板以及AbpHelper工具的基本使用,这一篇将进入项目实战部分.因为 ...
随机推荐
- c++: internal compiler error: Killed (program cc1plus)
转自https://blog.csdn.net/qq_27148893/article/details/88936044 这是在开发板上编译opencv的时候报了一个错,主要是在编译过程中,内存不够造 ...
- 如何在Linux上使用xargs命令
大家好,我是良许. 在使用 Linux 时,你是否遇到过需要将一些命令串在一起,但是其中一个命令不接受管道输入的情况呢?在这种情况下,我们就可以使用 xargs 命令.xargs 可以将一个命令的输出 ...
- sql 游标(理论)
游标是处理结果集的一种机制 --声明游标 --ISO 语法 DECLARE cursor_name [ INSENSITIVE ] [ SCROLL ] CURSOR FOR select_state ...
- [CSP-S2019]Emiya 家今天的饭 题解
CSP-S2 2019 D2T1 很不错的一题DP,通过这道题学到了很多. 身为一个对DP一窍不通的蒟蒻,在考场上还挣扎了1h来推式子,居然还有几次几乎推出正解,然而最后还是只能打个32分的暴搜滚粗 ...
- 专为seo新手准备的百度分享工具教程
http://www.wocaoseo.com/thread-178-1-1.html 百度分享工具是目前seo站长最为常用的工具之一,主要用来让用户分享来提高网站的流量,同时他也有很多实际有效的方式 ...
- C#封装定时执行任务类
a.日常开发中经常会遇到定时去执行一些操作,比如定时更新数据.A类需要做我们写个Timer定时去取数据,这时候B类,C类也需要做这样的事情,是不是需要写三次重复代码? 这时候把timer封装成一个帮助 ...
- Vue根据条件添加 click 事件
方式一:在绑定事件中直接添加标示量clickFlag <div @click="clickFlag && addGoodsHandler()"> XXX ...
- vue路由守卫+cookie实现页面跳转时验证用户是否登录----(二)设置路由守卫
上一篇我们已经封装好了cookie方法,登录成功之后也可以吧用户信息存到cookie中,接下来需要在router/index.js中引入一下cookie.js文件 然后继续添加以下代码 /* * be ...
- Flink自定义Sink
Flink自定义Sink Flink 自定义Sink,把socket数据流数据转换成对象写入到mysql存储. #创建Student类 public class Student { private i ...
- 揭秘 Kubernetes attach/detach controller 逻辑漏洞致使 pod 启动失败
前言 本文主要通过深入学习k8s attach/detach controller源码,了解现网案例发现的attach/detach controller bug发生的原委,并给出解决方案. 看完本文 ...