ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于DDD的文件管理系统:CoreWCF与WebApi多协议宿主架构实践

基于DDD的文件管理系统:CoreWCF与WebApi多协议宿主架构实践 简介这是一份面向C#课程设计、大作业场景的ASP.NET文件管理系统源码包在老师指导下完成并获得高分适合需要完成类似题目的在校生直接参考。压缩包共98个文件以C#源码(.cs)为主同时包含项目工程文件(.csproj/.sln)、数据库脚本(.sql)、配置文件(.config)、可执行文件(.exe)等其中.cs承担核心业务逻辑.sql提供数据库表结构.sln解决方案便于一键加载整个项目整体体积约894KB目录结构清晰便于定位与裁剪。项目采用典型的分层架构包含Domain领域层、Repository数据仓储层、AppService应用服务层及多个Host宿主项目并提供EFCore与EF6两种数据访问实现同时打包了WebApiClient、WCFClient、WebClient等客户端调用示例能帮助读者理解从数据持久化、服务发布到前端调用的完整链路。当前已有882人学习下载对希望提升ASP.NET项目组织能力、快速搭建课程设计框架的读者而言是一份兼顾完整性与实用性的参考资源。1. 为什么 ASP.NET 时代的文件管理系统选择同时跑 WCF 与 WebApi一个文件管理系统如果只实现文件上传、下载和记录管理本质上就是一个磁盘读写工具放在课程设计里只能拿个及格分。但这个项目不一样它的业务逻辑层SD.FileSystem.AppService被四套 Host 同时引用分别跑 CoreWCF、传统 WCF、ASP.NET Core WebApi、ASP.NET WebApi也就是说同一份文件服务的业务代码既能走 SOAP 协议也能走 RESTful 接口。这在课程设计这个尺度上是比较罕见的。它的价值并不在“文件管理”本身而在于你拿到的是一个完整的分层架构领域层、仓储抽象、多套仓储实现、服务契约与部署宿主解耦。适合两类人一类是准备交大作业但不想只堆 CRUD 的在校生另一类是工作中要维护旧 WCF 项目、正在考虑往 Core 架构迁移的在职开发者。2. 领域模型设计SD.FileSystem.Domain 的分层逻辑与实体定义2.1 从解决方案结构反推 DDD 的分层思路打开 SD.FileSystem-master 解决方案项目结构非常清晰。SD.FileSystem.Domain 是领域层SD.FileSystem.Repository(EFCore) 和 SD.FileSystem.Repository(EF6) 是仓储实现SD.FileSystem.IAppService 是服务契约层SD.FileSystem.AppService 是业务实现层再往上就是各种 Host。这正是一个典型的“依赖倒置”结构领域层不依赖任何基础设施仓储接口定义在领域层实现在外层项目里应用服务层只面向接口编程。这个结构的核心好处在于当你把 AppService.Host(WCF) 换成 AppService.Host(CoreWebApi) 时业务代码一行都不用改只需要替换宿主项目和配置文件。这在课程设计答辩时是个很好的讲解点因为大多数同学的项目都是单项目结构你拿一个六七个项目的解决方案出来在架构认知上就已经拉开了差距。2.2 文件实体与值对象设计领域层的核心实体是文件记录。以常见实现为例FileRecord 实体包含文件唯一标识、文件名、存储路径、文件大小、MIME 类型、所属目录、上传人、上传时间等字段。这里有一个容易忽略的设计细节文件内容的存储位置与文件元数据是分离的。元数据进数据库二进制内容进服务器磁盘指定目录二者通过存储路径关联。using System; using System.Collections.Generic; namespace SD.FileSystem.Domain.Entities { /// summary /// 文件记录聚合根 /// /summary public class FileRecord { public Guid Id { get; private set; } public string FileName { get; private set; } public string StoredPath { get; private set; } public long SizeBytes { get; private set; } public string ContentType { get; private set; } public int FolderId { get; private set; } public string UploadedBy { get; private set; } public DateTime UploadedAt { get; private set; } protected FileRecord() { } // EF Core 需要无参构造 public FileRecord(string fileName, string storedPath, long sizeBytes, string contentType, int folderId, string uploadedBy) { Id Guid.NewGuid(); SetFileName(fileName); StoredPath storedPath; SizeBytes sizeBytes; ContentType contentType; FolderId folderId; UploadedBy uploadedBy; UploadedAt DateTime.Now; } public void SetFileName(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException(文件名不能为空, nameof(fileName)); FileName fileName; } public void MoveTo(int newFolderId) { FolderId newFolderId; } } }这里把属性设置为 private set并对外提供行为方法 SetFileName 和 MoveTo属于 DDD 里“充血模型”的写法。优点是业务规则比如文件名非空校验收口在实体内部任何调用方都不能绕过规则直接改字段。EF Core 的无参构造标注为 protected这是 ORM 的映射需求它通过反射来实例化实体不经过业务构造函数。UploadedAt 在构造函数里赋值而不是由调用方传值避免出现“上传时间被篡改”这一类低级问题。2.3 文件夹关联与文件移动的业务约束文件管理系统的领域模型里文件夹和文件之间是典型的一对多关系。在设计时FileRecord 里存了 FolderId但并没有定义导航属性。这里有一个设计取舍如果只做文件管理不涉及文件夹树级联查询用 FolderId 外键关联就足够了不需要引入完整的导航属性。如果需要按目录树检索可以在仓储层通过 JOIN 查询实现而不是在实体上堆导航属性。需要注意的地方是文件移动操作。MoveTo 方法只修改了 FolderId并没有同步更新 Disk 上的目录结构。如果设计上要求文件必须按文件夹物理存放这一步就要进入“领域事件”或“应用服务事务”的范畴了需要先把文件流转到新目录成功后再更新数据库记录。项目里的应用服务层有单独的方法处理这一类场景通常叫 MoveFile它内部先调用文件 IO 操作再调用仓储更新。这个顺序很关键一旦磁盘移动失败数据库记录不会变化保证了数据一致性。3. 仓储抽象与双实现EFCore 与 EF6 并存的切换策略3.1 仓储接口设计SD.FileSystem.Repository(EFCore) 和 SD.FileSystem.Repository(EF6) 这两个项目名直接标明了数据访问技术栈。它们的接口定义在领域层具体实现在各自的项目里依赖关系是“上层依赖抽象不依赖实现”。仓储接口的核心方法一般包括按主键获取、分页查询、插入、更新、删除以及按条件过滤。using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Threading.Tasks; namespace SD.FileSystem.Domain.Repositories { public interface IFileRecordRepository { TaskFileRecord GetByIdAsync(Guid id); TaskIReadOnlyListFileRecord GetByFolderAsync(int folderId, int pageIndex, int pageSize); TaskIReadOnlyListFileRecord QueryAsync(ExpressionFuncFileRecord, bool predicate); Task AddAsync(FileRecord record); Task UpdateAsync(FileRecord record); Task DeleteAsync(FileRecord record); Taskint CountAsync(ExpressionFuncFileRecord, bool predicate); } }接口里全部是异步方法这样设计有两个原因。一是 ASP.NET 场景下使用异步 IO 能显著减少线程池线程占用文件上传下载这种 IO 密集型操作正是吃异步红利的地方二是 EF Core 原生支持异步查询EF6 的 Async 版本也能无缝实现这套接口。QueryAsync 接收 Expression 类型传入的是表达式树而不是委托这样 EF 可以把它翻译成具体的 SQL而不是把整张表捞到内存里再筛选。接口里没有出现“保存”方法仓储只负责持久化单个聚合工作单元UnitOfWork的职责放在应用服务层统一管理。3.2 EFCore 仓储实现要点EFCore 的实现项目里需要配置 DbContext。以常见实现为例上下文里通过 DbSetFileRecord 属性暴露聚合根同时在 OnModelCreating 里做字段映射和索引配置。using Microsoft.EntityFrameworkCore; using SD.FileSystem.Domain.Entities; namespace SD.FileSystem.Repository.EFCore { public class FileSystemDbContext : DbContext { public FileSystemDbContext(DbContextOptionsFileSystemDbContext options) : base(options) { } public DbSetFileRecord FileRecords { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityFileRecord(entity { entity.ToTable(tbl_FileRecord); entity.HasKey(e e.Id); entity.Property(e e.FileName).HasMaxLength(256).IsRequired(); entity.Property(e e.StoredPath).HasMaxLength(512).IsRequired(); entity.Property(e e.ContentType).HasMaxLength(128); entity.HasIndex(e e.FolderId); // 文件夹查询频繁建索引 }); } } }OnModelCreating 里做的事情可以理解成把领域实体的属性映射到数据库列。HasMaxLength 限制了字符串长度StoredPath 设为 512 是为了容纳 Windows 下的长路径HasIndex(FolderId) 是为了把文件夹维度查询的索引建上文件管理系统的查询大头是按目录遍历文件列表这个索引能显著减少扫描行数。EFCore 仓储类本身则注入 DbContext每个方法对应一条查询链路。注意 EFCore 默认快照追踪如果实体是直接从上下文查出来的修改后 SaveChanges 会自动生成 UPDATE如果是新建实体需要调用 Add 或 Update 方法否则 EF 不知道这条记录要插入。3.3 EF6 实现与数据库迁移的差异对比EF6 的仓储实现与 EFCore 在接口拼写上几乎一模一样区别主要在 DbContext 基类和配置方式上。EF6 用 DbConfiguration 或在 app.config/web.config 里配置连接字符串做不到像 EFCore 那样在代码里灵活切换数据库。项目保留两套仓储实现的意义在于EF6 适合连接既有 SQL Server 数据库EFCore 则天然支持跨库迁移你可以把同样的领域模型无缝切到 PostgreSQL、MySQL 或 Sqlite。下表列出了两套仓储在实际使用时的关键差异方便你理解项目的设计边界对比维度EFCoreEF6配置方式代码优先DI 容器注入web.config/app.config 配置跨数据库支持多数据库 Provider以 SQL Server 为主LINQ 翻译能力较完整支持拆分查询有限场景翻译失败实体追踪默认快照追踪同样有追踪机制异步查询原生支持基于 TAP 的 Async 版本在把 AppService.Clients 从 EFCore 切换到 EF6 时只需要在容器注册处替换一行依赖注入代码。EF6 的 SaveChanges 在多实体操作时是自带事务的而 EFCore 在多个 SaveChanges 之间不会自动包裹事务需要显式使用 IDbContextTransaction这是做工单元时必须处理的差异点。4. 应用服务与四套 HostCoreWCF、WCF、WebApi、CoreWebApi 的宿主切换4.1 服务契约为什么单独成项目SD.FileSystem.IAppService 和 SD.FileSystem.AppService 分离是这套方案里最关键的解耦。IAppService 定义接口AppService 提供实现Host 引用 AppService 项目后自己决定以什么协议暴露服务。契约层只有接口、数据传输对象DTO和枚举不引用任何 Web 或 WCF 相关的类型。可以在接口上加 [ServiceContract] 特性使其兼容 WCF同时也能在 WebApi 里按普通接口装配。这样设计的好处在于如果后续要加 gRPC只需要新建一个 Host 项目把 AppService 作为依赖注入进去不需要改动业务实现。接口层的文件上传操作通常会设计成接收字节流加元数据参数而不是直接接收 HttpPostedFileBase这样才能保证同样的契约既能在 WCF 里跑 SOAP 调用又在 WebApi 里走 multipart/form-data。这个细节很关键很多人在做多协议兼容时卡住正是因为接口里混入了 HTTP 上下文相关类型。4.2 CoreWCF Host 的配置与启动逻辑CoreWCF 是微软官方把 WCF 迁移到 .NET Core 的解决方案它保留了服务契约和绑定配置模型。项目里 SD.FileSystem.AppService.Host(CoreWCF) 做的事情是在 ASP.NET Core 管道里把文件服务暴露成 SOAP 端点。using CoreWCF; using CoreWCF.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using SD.FileSystem.AppService; using SD.FileSystem.IAppService; var builder WebApplication.CreateBuilder(args); builder.Services.AddServiceModelServices(); builder.Services.AddServiceModelMetadata(); builder.Services.AddSingletonFileManageService, FileManageService(); var app builder.Build(); app.UseServiceModel(serviceBuilder { serviceBuilder.AddServiceFileManageService(serviceOptions { serviceOptions.BaseAddresses.Add(new Uri(http://localhost:8080/FileService)); }); serviceBuilder.AddServiceEndpointFileManageService, IFileManageService( new BasicHttpBinding(BasicHttpSecurityMode.None), /FileService/BasicHttp); serviceBuilder.AddServiceEndpointFileManageService, IFileManageService( new WSHttpBinding(SecurityMode.None), /FileService/WSHttpBinding); }); app.UseServiceModelMetadata(); app.Run();这段启动代码里做了三件事。AddServiceModelServices() 注册 CoreWCF 运行时所需的服务AddService () 把实现类注册为 WCF 服务宿主AddServiceEndpoint 则为服务绑定两个端点BasicHttpBinding 是为了兼容旧客户端WSHttpBinding 提供基于 SOAP 的会话能力。BaseAddresses 指定服务的基址后续的端点地址都建立在它之上。这里值得留意的坑是CoreWCF 的 BasicHttpBinding 默认消息编码为文本若客户端用的是 HTTP 请求直接 POST 原始 XML必须保证 Content-Type 设置为 text/xml否则请求会在反序列化阶段直接失败。在切换到传统 WCF 宿主时需要在 App.config 里写等效的 serviceModel 配置两种宿主的服务逻辑完全一致只是启动和配置方式不同。4.3 WebApi Host 与路由机制对比CoreWebApi 宿主的实现相对直接简单。FileService 被注册为普通 MVC 控制器或最小 API对外暴露与 WCF 接口同语义的 HTTP 路由。项目 WebApi Host 中的控制器其方法内部同样调用 AppService 中的 FileManageService 实例。这套设计下一个关键点是路由的约定与 WCF 端点的对应关系。比如 WCF 端点在/FileService/Upload那么 WebApi 路由就设计成/api/files/upload两个入口共享同一个上传逻辑只是协议和报文边界不同。当从传统 ASP.NET WebApi 切换到 CoreWebApi 时路由注册从 WebApiConfig.Register 变为 MapControllers。ASP.NET Core 的 Startup 配置里要开放跨域策略这能解决前端上传文件时必须带上 CORS 头的场景而传统 WebApi 里则需要在 GlobalConfiguration 里统一配置。这个差异点同时也是答辩时经常被问起的问题跨域在 WCF 下不存在在 WebApi 下必须显式允许。5. 三种客户端调用与完整上传下载链路验证5.1 WebClient同进程内的 HTTP 调用SD.FileSystem.WebClient 是一个 ASP.NET MVC 客户端站点它的文件列表页面通过 HttpClient 调用 WebApi 宿主获取文件元数据然后渲染成页面。上传时页面先把文件 POST 到本地控制器控制器再把文件流转发到 WebApi 服务端。using Microsoft.AspNetCore.Mvc; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; public class UploadController : Controller { private readonly HttpClient _httpClient; public UploadController(HttpClient httpClient) { _httpClient httpClient; } [HttpPost] public async TaskIActionResult Upload(IFormFile file) { using var content new MultipartFormDataContent(); await using var stream file.OpenReadStream(); var fileContent new StreamContent(stream); fileContent.Headers.ContentType new MediaTypeHeaderValue(file.ContentType); content.Add(fileContent, file, file.FileName); var response await _httpClient.PostAsync(http://localhost:5199/api/files/upload, content); if (response.IsSuccessStatusCode) { return RedirectToAction(Index); } return View(Error); } }IFormFile 是 ASP.NET Core 对上传文件的抽象OpenReadStream 返回的流不能被重复读取。MultipartFormDataContent 的 Add 方法第一个参数是表单字段名必须与 WebApi 端 Upload 方法签名中的参数名一致否则模型绑定会失败。这个代码里的关键限制是 StreamContent 的方式要求文件全部进入内存或临时磁盘超大文件场景下建议改用 PushStreamContent 或 Chunked 编码但课程设计尺度下不需要考虑这个。5.2 WebApiClient 与 WCFClient 的服务引用方式WebApiClient 项目是一个独立控制台或类库通过 HttpClient 远程访问 WebApi 宿主。WCFClient 则通过 ChannelFactory 生成服务代理指向 WCF 宿主。using System.ServiceModel; using System.Threading.Tasks; using SD.FileSystem.IAppService; public class WcfFileClient { private readonly string _endpointAddress http://localhost:8080/FileService/BasicHttp; public async TaskFileUploadResult UploadAsync(byte[] fileBytes, string fileName) { var binding new BasicHttpBinding(BasicHttpSecurityMode.None); var factory new ChannelFactoryIFileManageService(binding, new EndpointAddress(_endpointAddress)); var client factory.CreateChannel(); var request new FileUploadRequest { FileName fileName, ContentBytes fileBytes }; var result await client.UploadAsync(request); factory.Close(); return result; } }ChannelFactory 是 WCF 客户端的标准入口。这里使用 BasicHttpBinding 与前面 CoreWCF 宿主配置的 BasicHttpBinding 对应。上传的方法签名直接使用的是应用服务契约里的类型。注意调用完成后需要关闭 factory如果使用完不关闭通道会长时间占用连接。在实际项目中更稳妥的做法是使用using块或在 finally 里调用 Abort 方法来清理。5.3 联调验证的完整演示路径本地启动四个宿主会产生端口冲突项目设计中通常只会同时启动 WebApi 宿主和 WCF 宿主各一个分别给 WebClient 和 WCFClient 调用。推荐验证顺序是先启动 AppService.Host(CoreWebApi)浏览器访问/swagger页面确认文件上传接口可以访问然后启动 AppService.Host(CoreWCF)用 WCFClient 调一次上传最后启动 WebClient通过页面确认 WebApi 上传记录出现在列表里。三条链路共用同一个数据库所以验证的是协议层差异业务层一致性已经由应用服务统一收口了。如下表验证链路客户端项目目标宿主验证内容HTTP 链路WebClientCoreWebApi页面调用、上传、列表展示SOAP 链路WCFClientCoreWCF服务引用、异步调用客户端链路WebApiClientWebApiHttpClient 直连调用6. 答辩演示时值得展示的三个边界验证技巧第一个技巧是向评委演示“数据库切换”。项目里有 EFCore 和 EF6 两套仓储实现答辩时直接修改依赖注入的注册代码把 EFCore 仓储替换成 EF6 仓储再启动 CoreWebApi 宿主页面数据一样能正常展示。这个操作展示的是抽象接口对实现替换的容忍度比单纯讲“我用了接口”更有说服力。第二个技巧是 WCF 端点的报文抓取。在 WCF 宿主启动后用 Fiddler 打开 HTTP 监听然后调用 WCFClient 上传一个文件可以在 Fiddler 里看到 SOAP 协议的 XML 报文结构。对比这个 XML 与 WebApi 的 JSON 请求体能直观看出两种协议在报文层面的差异。这里要留意的是 WCF 上传文件的 SOAP 报文很大因为字节数组会被 Base64 编码成字符串文件越大 XML 膨胀越明显这也是纯 WCF 方案不适合大文件传输的原因所在。第三个技巧是文件上传大小的边界控制。WebApi 宿主默认有请求体大小上限Kestrel 默认请求体最大约 30MB超过这个值会返回 413 响应。在 Program.cs 里可以配置builder.WebHost.ConfigureKestrel(options { options.Limits.MaxRequestBodySize 100 * 1024 * 1024; // 100MB });MaxRequestBodySize 作用于整个 Kestrel 进程。如果只是希望某个接口放开限制使用[RequestSizeLimit(100_000_000)]特性更灵活。课程设计的演示环境里改全局配置通常就够了。要注意文件上传失败了服务端写入了一半的字节流会残留在磁盘目录中需要在异常处理里做文件清理否则演示多次后会积攒垃圾文件。这个清理逻辑一般放在仓储层事务提交失败后的 catch 块中。本文还有配套的精品资源点击获取
返回列表