初識ABP vNext(9):ABP模組化開發-文件管理

Tips:本篇已加入系列文章閱讀目錄,可點擊查看更多相關文章。

前言

在之前的章節中介紹過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來安裝引用。兩種方式各有優缺點,具體請參考自定義現有模組,關於私有NuGet搭建可以參考:十分鐘搭建自己的私有NuGet伺服器-BaGet

然後給這些項目的模組類添加對應的依賴,例如:

通過上面的方式引用模組,使用visual studio是無法編譯通過的:

需要在解決方案目錄下,手動執行dotnet restore命令即可:

模組開發

接下來關於文件管理功能的開發,都在模組Xhznl.FileManagement中進行,它是一個獨立的解決方案。初學ABP,下面就以盡量簡單的方式來實現這個模組。

應用服務

模組開發通常從Domain層實體建立開始,但是這裡先跳過。先在FileManagement.Application.Contracts項目添加應用服務介面和Dto。

modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\IFileAppService.cs:

public interface IFileAppService : IApplicationService
{
    Task<byte[]> GetAsync(string name);

    Task<string> CreateAsync(FileUploadInputDto input);
}

modules\file-management\src\Xhznl.FileManagement.Application.Contracts\Files\FileUploadInputDto.cs:

public class FileUploadInputDto
{
    [Required]
    public byte[] Bytes { get; set; }

    [Required]
    public string Name { get; set; }
}

然後是FileManagement.Application項目,實現應用服務,先定義一個配置類。

modules\file-management\src\Xhznl.FileManagement.Application\Files\FileOptions.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" };
}

modules\file-management\src\Xhznl.FileManagement.Application\Files\FileAppService.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介面。

modules\file-management\src\Xhznl.FileManagement.HttpApi\Files\FileController.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系統可以了解一下。