EF Core与CQRS架构下在CreateHandler中实现FTP文件上传的咨询
CQRS架构下EF Core项目整合FTP文件上传到SliderCreateHandler的实现方案
1. 先改造现有FTP上传帮助类
原有帮助类存在3个问题:
UploadFtp为私有方法,外部无法调用- FTP账号、密码、地址硬编码在代码中,不可配置
- 同步方法会阻塞请求线程,影响接口性能
改造后的代码如下:
namespace Medoper.Application.Helpers { // 新增接口方便依赖注入 public interface IFileUpload { // 支持传入文件流直接上传,无需存本地临时文件 Task<string> UploadFtpAsync(string fileName, Stream fileStream); // 兼容原有本地文件路径上传的逻辑 Task<string> UploadFtpAsync(string fileName, string filePath); } public class FileUpload : IFileUpload { // 从配置读取FTP参数,不要硬编码 private readonly FtpConfig _ftpConfig; public FileUpload(IOptions<FtpConfig> ftpConfig) { _ftpConfig = ftpConfig.Value; } public async Task<string> UploadFtpAsync(string fileName, Stream fileStream) { var ftpFullPath = $"{_ftpConfig.CdnAddress}/asd/{fileName}"; FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpFullPath); request.Method = WebRequestMethods.Ftp.UploadFile; request.Credentials = new NetworkCredential(_ftpConfig.CdnName, _ftpConfig.CdnPassword); using (Stream requestStream = request.GetRequestStream()) { await fileStream.CopyToAsync(requestStream); } using (FtpWebResponse response = (FtpWebResponse)await request.GetResponseAsync()) { // 返回可访问的文件CDN地址,用于存到数据库 return $"{_ftpConfig.CdnPublicDomain}/asd/{fileName}"; } } public async Task<string> UploadFtpAsync(string fileName, string filePath) { using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); return await UploadFtpAsync(fileName, fileStream); } } // FTP配置类,对应appsettings.json中的节点 public class FtpConfig { public string CdnName { get; set; } public string CdnPassword { get; set; } public string CdnAddress { get; set; } // CDN公网访问域名,用户打开浏览器访问文件用的地址 public string CdnPublicDomain { get; set; } } }
然后在appsettings.json中新增FTP配置节点:
"FtpConfig": { "CdnName": "你的FTP账号", "CdnPassword": "你的FTP密码", "CdnAddress": "ftp://你的FTP服务器地址", "CdnPublicDomain": "https://你的CDN访问域名" }
2. 注册服务到DI容器
在Program.cs中添加如下代码:
// 绑定FTP配置 builder.Services.Configure<FtpConfig>(builder.Configuration.GetSection("FtpConfig")); // 注册文件上传服务 builder.Services.AddScoped<IFileUpload, FileUpload>();
3. 修改SliderCreate请求类
新增文件接收属性,让Swagger支持上传文件:
public class SliderCreate : IRequest<ApiResponse<SliderResponse>> { // 原有其他字段不变 public IFormFile SliderImage { get; set; } }
4. 改造SliderCreateHandler整合上传逻辑
修改后的Handler代码如下:
public class SliderCreateHandler : IRequestHandler<SliderCreate, ApiResponse<SliderResponse>> { private readonly IUnitOfWork _repo; private readonly IMapper _mapper; // 注入文件上传服务 private readonly IFileUpload _fileUpload; public SliderCreateHandler(IUnitOfWork repo, IMapper mapper, IFileUpload fileUpload) { _repo = repo; _mapper = mapper; _fileUpload = fileUpload; } public async Task<ApiResponse<SliderResponse>> Handle(SliderCreate request, CancellationToken cancellationToken) { // 1. 先校验文件是否存在 if (request.SliderImage == null || request.SliderImage.Length == 0) { return new ErrorApiResponse<SliderResponse>("请上传轮播图文件"); } // 2. 生成唯一文件名,避免重名覆盖 var fileExt = Path.GetExtension(request.SliderImage.FileName); var uniqueFileName = $"{Guid.NewGuid():N}{fileExt}"; string filePublicUrl; try { // 3. 直接读取文件流上传FTP,无需存本地临时文件 using var fileStream = request.SliderImage.OpenReadStream(); filePublicUrl = await _fileUpload.UploadFtpAsync(uniqueFileName, fileStream); } catch (Exception ex) { // 上传失败直接返回错误,不执行后续数据库操作 return new ErrorApiResponse<SliderResponse>($"文件上传失败:{ex.Message}"); } // 4. 映射实体,把上传后的文件地址赋值到实体字段 var mapped = _mapper.Map<Slider>(request); // 假设Slider实体有ImageUrl字段存文件地址 mapped.ImageUrl = filePublicUrl; if (mapped == null) return new ErrorApiResponse<SliderResponse>(ResultMessage.NotCreatedBlog); // 5. 保存到数据库 var model = await _repo.Sliders.AddAsync(mapped); // 这里可以加SaveChangesAsync的异常捕获,捕获到的话可以调用FTP删除逻辑回滚上传的文件 await _repo.SaveChangesAsync(cancellationToken); var response = _mapper.Map<SliderResponse>(model); return new SuccessApiResponse<SliderResponse>(response); } }
注意事项
- 如果要求数据一致性更高,可以在
SaveChangesAsync抛出异常时,新增FTP删除方法删除已上传的文件,避免产生垃圾文件 - 可以新增文件格式、大小校验逻辑,避免上传非法文件
- 生产环境建议给FTP请求加超时时间,避免请求长时间挂起
内容的提问来源于stack exchange,提问作者Burcu
相关产品推荐
相关产品推荐

