[.Net]使用Soa库+Abp搭建微服务项目框架(二):面向服务体系的介绍

本文探讨了在Abp框架下,传统开发方式可能导致的领域层和服务之间的耦合问题。通过实例展示了两种常见做法:直接在应用层获取数据和在领域层处理数据,但两者均造成耦合。为了解耦,提出了采用面向服务架构的思路,通过接口化改造将MainService、Service1和Service2解耦。下一部分将详细介绍如何在Abp框架中实施接口化改造。

上一章我们建立了一个典型的面向领域设计的Abp小项目,如果按照常规的开发方式,会遇到什么问题呢?

先来完善一下这个小项目,在定义好各实体类后,运行Miguration并向数据库里写入一些初始数据。

现在整个项目的依赖引用图如下,每一个都有独立的引用路线,互不干涉。

简略图如下

假设现在有一个需求,MainService业务需要用到Service1和Service2 中的数据,如何操作?

在使用Abp框架时,传统开发方式是先建立领域层服务,应用层中调用领域层服务(Manager)并返回给UI层,完成整个业务流程;或者更简单方式是直接在应用层注入仓储对象,拿到实体类做数据操作。

我们分别来完成这两个方式实现这一需求。

首先在MatoProject.MainService.Application项目中建立一个应用层服务MainService.cs并继承AsyncCrudAppService。

建立一个方法GetExtends,用于查询Entity1,Entity2 中的Type和Num字段。

 方式1: 模拟传统方式在应用层获取数据:

看看依赖情况:

 若是使用方式1, 在注入仓储对象的时候,势必直接引用了这两个服务的领域层,造成了耦合。

方式2: 模拟传统方式在领域层获取数据:

在Domain层中分别建立两个领域服务(Manager),并且定义GetType和GetNum方法,分别用于获取Entity1,Entity2 中的Type和Num字段

 在MainService.cs的GetExtends方法更改分别调用两个Manager 的GetType与GetNum方法,从中获取Type和Num值。

拼接完成后返回结果

 看看依赖情况:

  若是使用方式2, 在注入领域服务(Manager)时,也势必直接引用了这两个服务的领域层,造成了项目之间的耦合

若是在MainService服务层应用了Service1 和 Service2 的话,则项目的各个模块边界将变得不明晰,上一章所讨论的依据上下文边界划分服务,也将变得没有意义。

简略图如下 

 现在,我们需要考虑用面向服务体系的架构改造,将MainService,Service1, Service2 解耦。

若要独立成服务,则要考虑引用其抽象,也就是要做接口化的改造,使其之间的状态变为弱关联

下一章节将介绍如何结合Abp,进行接口化改造

 项目地址

jevonsflash/Soa: (github.com)

使用 **.NET 9 + ABP Framework (ASP.NET Boilerplate 或 Abp vNext) + SQL Server** 的后端架构中,保存文件或图片时通常**不推荐直接将文件存储在数据中(如使用 `varbinary(max)`)**,而是建议将文件**保存到磁盘、网络路径或对象存储(如 Azure Blob、AWS S3)中**,然后在数据中仅保存文件的**元信息(如路径、名称、大小、MIME 类型等)**。 但根据你的需求,我会详细介绍两种方式: --- ### ✅ 推荐方案:文件存磁盘/云存储,数据只存元数据 #### 1. 创建文件实体(Entity) ```csharp using System; using Abp.Domain.Entities.Auditing; public class AppFile : FullAuditedEntity<long> { public string FileName { get; set; } // 原始文件名 public string StoredFileName { get; set; } // 存储时的唯一文件名(如 GUID) public string FilePath { get; set; } // 相对路径或 URL public long FileSize { get; set; } // 文件大小(字节) public string MimeType { get; set; } // MIME 类型,如 image/jpeg public string FileType { get; set; } // 自定义分类(如 Avatar, Document 等) } ``` #### 2. 在 `.DbContext` 中注册实体 ```csharp public class YourAppDbContext : AbpZeroCoreDbContext<YourAppDbContext> { public DbSet<AppFile> AppFiles { get; set; } public YourAppDbContext(DbContextOptions<YourAppDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<AppFile>(e => { e.ToTable("AppFiles"); e.Property(x => x.FileName).HasMaxLength(255); e.Property(x => x.StoredFileName).HasMaxLength(255); e.Property(x => x.FilePath).HasMaxLength(1000); e.Property(x => x.MimeType).HasMaxLength(100); e.Property(x => x.FileType).HasMaxLength(50); }); } } ``` #### 3. 文件上传服务(IFileAppService) ```csharp using Microsoft.AspNetCore.Http; using System.IO; using System.Threading.Tasks; using Abp.Application.Services; using Abp.Authorization; using Abp.Domain.Repositories; [AbpAuthorize] public class FileAppService : ApplicationService, IFileAppService { private readonly IRepository<AppFile, long> _fileRepository; private readonly IWebHostEnvironment _env; public FileAppService(IRepository<AppFile, long> fileRepository, IWebHostEnvironment env) { _fileRepository = fileRepository; _env = env; } public async Task<long> UploadFileAsync(IFormFile file, string fileType = "") { if (file == null || file.Length == 0) throw new UserFriendlyException("文件为空"); var extension = Path.GetExtension(file.FileName); var storedFileName = $"{Guid.NewGuid()}{extension}"; // 避免重名 var uploadFolder = Path.Combine(_env.WebRootPath, "uploads"); // wwwroot/uploads var filePath = Path.Combine(uploadFolder, storedFileName); // 确保目录存在 if (!Directory.Exists(uploadFolder)) Directory.CreateDirectory(uploadFolder); using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } var appFile = new AppFile { FileName = file.FileName, StoredFileName = storedFileName, FilePath = $"/uploads/{storedFileName}", // 可通过 HTTP 访问 FileSize = file.Length, MimeType = file.ContentType, FileType = fileType }; await _fileRepository.InsertAsync(appFile); await CurrentUnitOfWork.SaveChangesAsync(); return appFile.Id; } } ``` #### 4. 配置静态文件访问(`Program.cs`) ```csharp var builder = WebApplication.CreateBuilder(args); // 其他配置... var app = builder.Build(); // 启用静态文件中间件(用于访问 /uploads 下的图片) app.UseStaticFiles(); // 允许访问 wwwroot 下的静态文件 app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( Path.Combine(builder.Environment.WebRootPath, "uploads")), RequestPath = "/uploads" }); app.MapControllers(); app.Run(); ``` --- ### 🔺 可选方案:将文件以进制形式存入 SQL Server > ⚠️ 不推荐大文件使用此方式,会影响数据性能和备份。 #### 修改实体: ```csharp public class AppFile : FullAuditedEntity<long> { public string FileName { get; set; } public string MimeType { get; set; } public byte[] Content { get; set; } // 文件内容 public long FileSize { get; set; } } ``` #### 修改上传逻辑(小文件适用): ```csharp public async Task<long> UploadFileToDatabaseAsync(IFormFile file) { if (file == null || file.Length == 0) throw new UserFriendlyException("文件为空"); byte[] content; using (var memoryStream = new MemoryStream()) { await file.CopyToAsync(memoryStream); content = memoryStream.ToArray(); } var appFile = new AppFile { FileName = file.FileName, MimeType = file.ContentType, Content = content, FileSize = file.Length }; await _fileRepository.InsertAsync(appFile); await CurrentUnitOfWork.SaveChangesAsync(); return appFile.Id; } ``` > 查询时直接返回 `byte[]` 即可用于下载。 --- ### 📌 如何提供文件下载? ```csharp [HttpGet("download/{id}")] public async Task<FileResult> DownloadFile(long id) { var file = await _fileRepository.FirstOrDefaultAsync(id); if (file == null || file.Content == null) throw new UserFriendlyException("文件不存在"); return File(file.Content, file.MimeType, file.FileName); } ``` --- ### ✅ 最佳实践总结 | 方式 | 优点 | 缺点 | 推荐场景 | |------|------|------|----------| | 存磁盘/CDN/Blob | 性能好、易扩展、节省 DB 资源 | 需管理路径一致性、备份分离 | ✅ 所有场景优先选择 | | 存数据(varbinary) | 事务一致、易于备份 | 性能差、膨胀数据 | ❌ 仅小文件、强一致性要求 | --- ### 💡 补充建议 - 使用 **Azure Blob Storage** 或 **MinIO/AWS S3** 更适合生产环境。 - 可结合 ABP 的模块化设计,封装成独立的 `FileManagementModule`。 - 添加权限控制:谁可以上传/下载? - 添加文件清理任务(定期删除过期文件)。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值