初识ABP vNext(9):ABP模块化开发-文件管理
- 创建模块
- 模块开发
- 应用服务
- 运行模块
- 单元测试
- 模块使用
前言
在之前的章节中介绍过ABP扩展实体,当时在用户表扩展了用户头像字段,用户头像就涉及到文件上传和文件存储。文件上传是很多系统都会涉及到的一个基础功能,在ABP的模块化思路下,文件管理可以做成一个通用的模块,便于以后在多个项目中复用。单纯实现一个文件上传的功能并不复杂,本文就借着这个简单的功能来介绍一下ABP模块化开发的最基本步骤。
开始
创建模块
首先使用ABP CLI创建一个模块:abp new Xhznl.FileManagement -t module --no-ui
创建完成后会得到如下文件:
在主项目中添加对应模块的引用,Application=>Application,Domain=>Domain,HttpApi=>HttpApi 等等。例如:
需要添加引用的项目:Application、Application.Contracts、Domain、Domain.Shared、EntityFrameworkCore、HttpApi、HttpApi.Client
手动添加这些引用比较麻烦,你可以搭建自己的私有NuGet服务器,把模块的包发布到私有NuGet上,然后通过NuGet来安装引用。两种方式各有优缺点,具体请参考自定义现有模块[1],关于私有NuGet搭建可以参考:十分钟搭建自己的私有NuGet服务器-BaGet[2]。
然后给这些项目的模块类添加对应的依赖,例如:
通过上面的方式引用模块,使用visual studio是无法编译通过的:
需要在解决方案目录下,手动执行dotnet restore
命令即可:
模块开发
接下来关于文件管理功能的开发,都在模块Xhznl.FileManagement中进行,它是一个独立的解决方案。初学ABP,下面就以尽量简单的方式来实现这个模块。
应用服务
模块开发通常从Domain层实体建立开始,但是这里先跳过。先在FileManagement.Application.Contracts项目添加应用服务接口和Dto。
modulesfile-managementsrcXhznl.FileManagement.Application.ContractsFilesIFileAppService.cs:
public interface IFileAppService : IApplicationService
{
Task<byte[]> GetAsync(string name);
Task<string> CreateAsync(FileUploadInputDto input);
}
modulesfile-managementsrcXhznl.FileManagement.Application.ContractsFilesFileUploadInputDto.cs:
public class FileUploadInputDto
{
[Required]
public byte[] Bytes { get; set; }
[Required]
public string Name { get; set; }
}
然后是FileManagement.Application项目,实现应用服务,先定义一个配置类。
modulesfile-managementsrcXhznl.FileManagement.ApplicationFilesFileOptions.cs:
public class FileOptions
{
/// <summary>
/// 文件上传目录
/// </summary>
public string FileUploadLocalFolder { get; set; }
/// <summary>
/// 允许的文件最大大小
/// </summary>
public long MaxFileSize { get; set; } = 1048576;//1MB
/// <summary>
/// 允许的文件类型
/// </summary>
public string[] AllowedUploadFormats { get; set; } = { ".jpg", ".jpeg", ".png", "gif", ".txt" };
}
modulesfile-managementsrcXhznl.FileManagement.ApplicationFilesFileAppService.cs:
public class FileAppService : FileManagementAppService, IFileAppService
{
private readonly FileOptions _fileOptions;
public FileAppService(IOptions<FileOptions> fileOptions)
{
_fileOptions = fileOptions.Value;
}
public Task<byte[]> 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<string> CreateAsync(FileUploadInputDto 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"})
});
}
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接口。
modulesfile-managementsrcXhznl.FileManagement.HttpApiFilesFileController.cs:
[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<FileResult> GetAsync(string name)
{
var bytes = await _fileAppService.GetAsync(name);
return File(bytes, MimeTypes.GetByExtension(Path.GetExtension(name)));
}
[HttpPost]
[Route("upload")]
[Authorize]
public 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 FileUploadInputDto()
{
Bytes = bytes,
Name = file.FileName
});
return Json(result);
}
}
运行模块
ABP的模板是可以独立运行的,在FileManagement.HttpApi.Host项目的模块类FileManagementHttpApiHostModule配置FileOptions:
修改FileManagement.HttpApi.Host和FileManagement.IdentityServer项目的数据库连接配置,然后启动这2个项目,不出意外的话可以看到如下界面。
FileManagement.HttpApi.Host:
FileManagement.IdentityServer:
现在你可以使用postman来测试一下File的2个API,当然也可以编写单元测试。
单元测试
更好的方法是编写单元测试,关于如何做好单元测试可以参考ABP源码,下面只做一个简单示例:
模块使用
模块测试通过后,回到主项目。模块引用,模块依赖前面都已经做好了,现在只需配置一下FileOptions,就可以使用了。
目前FileManagement.Domain、FileManagement.Domain.Shared、FileManagement.EntityFrameworkCore这几个项目暂时没用到,项目结构也不是固定的,可以根据自己实际情况来调整。
最后
本文的模块示例比较简单,只是完成了一个文件上传和显示的基本功能,关于实体,数据库,领域服务,仓储之类的都暂时没用到。但是相信可以通过这个简单的例子,感受到ABP插件式的开发体验,这是一个好的开始,更多详细内容后面再做介绍。本文参考了ABP blogging模块的文件管理,关于文件存储,ABP中也有一个BLOB系统可以了解一下。
参考资料
[1]
自定义现有模块: https://docs.abp.io/zh-Hans/abp/latest/Customizing-Application-Modules-Guide
[2]
十分钟搭建自己的私有NuGet服务器-BaGet: https://www.cnblogs.com/xhznl/p/13426918.html
如果本文对您有用,
不妨点个“在看”或者转发朋友圈支持一下
- viewgroup实现item拖动效果
- Android之ExpandableListView下拉分组的实现
- 粗略的物体碰撞预测及检测
- Regionserver频繁挂掉故障处理实践
- 【翻译】GeoJSON格式规范-RFC7946
- [机器学习]-[数据预处理]-中心化 缩放 KNN(二)
- 基于Spring Cloud 几行配置完成单点登录开发
- 深入浅出全栈工程师: Web编程基础
- android之动画popowindows
- surfaceview详解
- android几种常见的启动模式
- 文件图片上传
- 深度学习代码系列之——Deep Learning Toolbox For Matlab
- Java加载js
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- SAP Spartacus如何创建自定义route页面
- SAP Spartacus的url parameter
- 来讲讲你对ThreadLocal的理解
- 用了这个jupyter插件,我已经半个月没打开过excel了
- vue接入腾讯地图(二)【标注&定位实战】
- 图像处理笔记(4)----OpenCV对象追踪
- MySQL 数据恢复
- 【从0到1学习边缘容器系列2】之 边缘应用管理
- 【从0到1学习边缘容器系列-3】应用容灾之边缘自治
- Hacking with iOS: SwiftUI Edition - 里程碑:项目 13 - 15
- HDU 1896 优先队列用法
- 蓝桥杯省内模拟赛C++
- C++ STL (标准模板库) 详细内容讲解
- 蓝桥杯 试题 基础练习 分解质因数
- 蓝桥杯 试题 基础练习 FJ的字符串