拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ASP.NET MVC5+EF6后台框架源码拆解:路由、IOC与工作流实现

简介Ymnets快速开发框架是一套基于ASP.NET MVC5与Entity Framework 6的后台管理系统解决方案面向需要快速搭建企业级后台的.NET开发者适用于OA、信息管理等业务系统开发能有效减少架构搭建与重复编码成本。框架整合了EF6数据访问、IOC依赖注入容器、EasyUI组件库并内置工作流引擎支持分层分模块与面向接口的设计方便团队协作和后续维护工作流可自动化任务流转帮助规范业务流程。资源包为zip压缩格式共3558个文件、约113.59MB核心内容涵盖C#源代码、程序集、视图页面、前端脚本与样式以及配置文件、部署文档和数据字典目录结构清晰。已有1920人学习/下载。借助这套源码可系统理解MVC5EF6框架的完整项目结构与工作流整合思路避免从零搭建底层架构结合部署文档与数据字典能大幅降低二次开发门槛让开发者专注于业务逻辑实现。1. 拿到Ymnets源码先看三处路由、IOC容器与工作流表如果只丢给你一套ASP.NET MVC5EF6后台管理系统源码第一天最容易做的事不是逐行读代码而是先找三个入口App_Start下的路由和容器注册、web.config里的连接串、还有以WF_开头的工作流表。Ymnets快速开发框架就是这套组合的典型代表它把EF6数据访问、EasyUI列表、权限过滤串成了一条完整链路真正值钱的不是登录页和菜单而是基于MVC5路由IOC的工作流骨架。这套代码适合做企业内部管理系统、OA类项目也适合刚接触EF6的团队拿来做脚手架。这里先给出我的拆解顺序后面每一章都可以直接落到编辑器里。2. MVC5路由与Global.asax控制器、通用处理程序和依赖注入如何串起来2.1 从Application_Start看启动顺序Ymnets的启动入口在Global.asax代码结构通常是这样protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); ContainerConfig.RegisterContainer(); }这五行的执行顺序是有讲究的。AreaRegistration先跑后台管理区域的控制器才能被后续路由扫描到FilterConfig注册全局过滤器比如异常日志和登录校验RouteConfig决定URL怎么命中ActionBundleConfig只是压缩CSS和JS最后做依赖注入。我把ContainerConfig放在最后是因为前面的路由和过滤器不依赖容器而控制器激活时才需要IOC接管。2.2 后台管理路由与默认路由的区别项目里有前后台两套路由。前台用MVC5默认路由后台我一般会单独加一个带admin前缀的区域路由routes.MapRoute( name: Admin, url: admin/{controller}/{action}/{id}, defaults: new { controller Home, action Index, id UrlParameter.Optional }, namespaces: new[] { Ymnets.Web.Areas.Admin.Controllers } );url里的admin前缀不是装饰它让所有后台请求都集中在同一段路径下后面做权限过滤时可以直接按URL前缀判断。namespaces参数也值得说明它解决的是同名控制器冲突问题比如前台和Admin区域各有一个HomeController如果不指定namespacesMVC5路由会报“找到多个匹配的控制器”。2.3 IOC容器与构造函数注入Ymnets这一层没有在Controller里到处new Service而是把依赖关系交给容器。常见做法是注册一个IDependencyResolver并把EF6的DbContext生命周期设为每次请求一个实例var container new UnityContainer(); container.RegisterType(typeof(IBaseService), typeof(BaseService), new HierarchicalLifetimeManager()); container.RegisterTypeYmnetsDbContext(new PerRequestLifetimeManager()); DependencyResolver.SetResolver(new UnityDependencyResolver(container));IBaseService 是泛型服务接口BaseService 里封装了增删改查和分页。HierarchicalLifetimeManager表示子容器独立生命周期适合一次请求内多次解析拿到同一实例。这一步解决了两个问题Controller构造函数不再依赖具体Service类单元测试时直接Mock接口DbContext按请求释放避免长生命周期导致EF6上下文跟踪大量实体。2.4 verify_code.ashx与upload_ajax.ashx的定位项目里还能看到verify_code.ashx和upload_ajax.ashx这类通用处理程序它们在MVC5里属于IHttpHandler。验证码处理程序要实现IRequiresSessionState接口否则Session里的验证码取不到public class verify_code : IHttpHandler, IRequiresSessionState { public void ProcessRequest(HttpContext context) { var code GenerateCode(4); context.Session[verify_code] code; context.Response.ContentType image/jpeg; // 这里把code画成图片写入响应流 } }upload_ajax.ashx则负责接收前端上传文件返回给EasyUI的JSON格式通常是{ code: 0, url: /upload/xxx.png }。这两个处理程序不走Controller好处是轻量坏处是无法直接使用IOC注入的Service所以我一般只让它们处理验证码和临时文件不在这里写业务逻辑。2.5 分层边界与依赖方向Ymnets的目录划分值得参考核心是Web、Business、Data三层。Controller属于Web层只做参数接收和结果转换不写SQLBusiness层放业务规则和工作流状态流转Data层只保存EF6的DbContext和实体类。最容易踩的坑是Controller直接调用DbContext或者把EF6的IQueryable传到视图层这样一旦业务规则变化所有调用点都要改。基于接口的设计在这里的真正价值不是多一层抽象而是让依赖方向保持单向。3. EF6的DbContext与数据字典Code First映射、初始化策略和查询编码3.1 YmnetsDbContext的Code First映射EF6使用Code First时核心是一个继承自DbContext的类。Ymnets里常见的写法是这样public class YmnetsDbContext : DbContext { public YmnetsDbContext() : base(nameYmnetsDb) { } public DbSetSysUser SysUsers { get; set; } public DbSetSysOrder SysOrders { get; set; } public DbSetWF_Instance WorkflowInstances { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.EntitySysUser() .ToTable(Sys_User) .Property(u u.LoginName) .HasMaxLength(50) .IsRequired(); modelBuilder.EntitySysOrder() .Property(o o.Amount) .HasPrecision(18, 2); } }构造器里的nameYmnetsDb表示从web.config读取连接串。把表名显式映射成Sys_User而不是默认的SysUsers是为了和数据库里的既有表保持一致。EF6的默认约定是复数表名如果团队习惯用单数或带前缀的表名最好全部在OnModelCreating里统一配置。HasPrecision(18,2)对应SQL Server里的decimal(18,2)如果漏掉这一句金额字段会被映射成decimal(18,2)以外的精度导致Insert时数据溢出。3.2 初始化器与数据库迁移策略EF6有三种常见初始化策略我建议按环境区分策略适用场景风险CreateDatabaseIfNotExists本地第一次跑模型变更不更新库DropCreateDatabaseIfModelChanges开发环境重置数据会清空现有数据MigrateDatabaseToLatestVersion正式环境升级需要保证迁移脚本正确Ymnets如果自带初始化代码通常在Application_Start里这样声明Database.SetInitializer( new MigrateDatabaseToLatestVersionYmnetsDbContext, Migrations.Configuration());但生产环境我一般会把这一行注释掉因为迁移自动执行到生产库有一定风险。稳妥的做法是本地用Update-Database生成SQL脚本DBA审完脚本再手动执行。数据字典和初始化SQL不是一回事数据字典存的是字段含义、类型、长度、备注Ymnets里通常会有一份Excel文档或专用的数据字典表建库前先用它核对表结构避免实体属性名和字典字段名对不上。3.3 数据字典与代码的一致性核对接这种源码后最耗时间的不是跑起来而是核对数据字典。假设Sys_Order表在字典里是这样定义的字段名类型长度允许空备注order_novarchar32否订单号order_stateint-否0草稿 1审批中amountdecimal18,2否金额对照实体时重点检查三处属性名和字段名是否用了不同命名规范允许空的字段在实体里是否为Nullable类型带默认值的字段在数据库里是否设置了DefaultValue。EF6的迁移不关心字段备注所以数据字典里的中文备注和SQL里的扩展属性是两套体系我见过不少项目就因为字典说字段必填、代码模型里却是可空类型导致运行时插入空值才暴露问题。3.4 查询编码延迟加载、AsNoTracking与分页Ymnets列表页最常见的查询写法如下public async TaskPagedResultSysOrder GetPageList(int pageIndex, int pageSize, int state) { var query _db.SysOrders.AsNoTracking() .Where(o o.State state) .OrderByDescending(o o.CreateTime); var total await query.CountAsync(); var list await query .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); return new PagedResultSysOrder { Total total, Rows list }; }AsNoTracking是只读列表的关键。EF6默认会跟踪查询出来的实体列表页一次取几十条没什么感觉但翻页多、列多时跟踪上下文会越来越大。注意加了AsNoTracking后实体上的导航属性再延迟加载就会失效所以如果列表要显示关联表字段要么提前用Include要么在视图模型里手动组装。另一个常见坑是OrderBy之后直接Skip/TakeEF6生成的SQL是有意义的但如果你先ToList再分页那等于全表数据都进了内存数据量一大就卡死。3.5 连接串与EF6性能参数web.config里的连接串经常被忽略实际上生产环境要额外加几个参数connectionStrings add nameYmnetsDb providerNameSystem.Data.SqlClient connectionStringServer.;DatabaseYmnets;User Idsa;Password***;MultipleActiveResultSetstrue; / /connectionStringsMultipleActiveResultSets很重要EF6在同一个DbContext上同时执行多个查询时会用到它不加会报“已有打开的与此Command相关联的DataReader”。另外不要在图省事在连接串里塞Trusted_Connectiontrue这没问题但别把生产密码提交到Git里我这几年看过的后台系统源码最常泄露的不是业务代码而是web.config里的连接串。4. EasyUI列表、权限过滤与轻量级工作流状态机审批流的落地写法4.1 EasyUI DataGrid与后台JSON契约Ymnets前端基于EasyUI它的DataGrid列表页和后端Controller有固定的JSON约定。前端这样配置$(#dg).datagrid({ url: /admin/order/GetPageList, queryParams: { state: 1 }, columns: [[ { field: orderNo, title: 订单号, width: 150 }, { field: createTime, title: 创建时间, width: 150 } ]], pagination: true, pageSize: 20, onDblClickRow: function (index, row) { openApproveDialog(row.id); } });后端返回的数据结构必须是total和rows两个字段否则DataGrid不认public JsonResult GetPageList(int page 1, int rows 20, int state 0) { var result _orderService.GetPageList(page, rows, state); return Json(new { total result.Total, rows result.Rows }, JsonRequestBehavior.AllowGet); }这里最容易出错的是参数名。EasyUI默认提交的参数叫rows后端方法参数也叫rows但返回JSON里的rows是数组两者同名在不同位置新手经常把返回数据结构写成{ total: total, rows: result.Rows }时少一个引用或者把查询条件参数遗漏。我一般建议后端方法第一个参数用page第二个用rows内部再转成pageIndex和pageSize。4.2 基于MVC5 Filter的权限控制Ymnets的权限控制没有依赖复杂的第三方框架而是实现了AuthorizeAttribute的子类public class PermissionFilterAttribute : AuthorizeAttribute { public string PermissionCode { get; set; } protected override bool AuthorizeCore(HttpContextBase httpContext) { var currentUser SessionManager.GetCurrentUser(httpContext.Session); if (currentUser null) return false; return currentUser.Permissions.Contains(PermissionCode); } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { filterContext.Result new JsonResult { Data new { success false, msg 没有权限 }, JsonRequestBehavior JsonRequestBehavior.AllowGet }; } else { filterContext.Result new RedirectResult(/Login/Index); } } }用法是给需要校验的Action打上特性标记[PermissionFilter(PermissionCode order:approve)] public JsonResult Approve(int id) { // 审批逻辑 }需要说明两点。第一AuthorizeAttribute在MVC5里默认就能做登录验证但权限码这种细粒度校验要重写AuthorizeCore第二AJAX请求和普通页面请求要分别处理否则列表页面按钮点击后弹出登录页面用户体验很差。这种过滤器方案比在Action里写if判断强在集中管理但权限码字符串要维护好常见做法是建一个常量类统一存放。4.3 轻量级工作流不依赖Flowable的审批状态机Ymnets带的工作流不是Flowable或Camunda那种完整流程引擎而是一套基于状态的轻量级工作流。它的核心表结构可以精简成两张表CREATE TABLE WF_Instance ( Id INT IDENTITY(1,1) PRIMARY KEY, BizType NVARCHAR(50) NOT NULL, BizId INT NOT NULL, CurrentNode NVARCHAR(50) NOT NULL, State INT NOT NULL DEFAULT 0, CreateUserId INT NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE WF_Step ( Id INT IDENTITY(1,1) PRIMARY KEY, InstanceId INT NOT NULL, FromNode NVARCHAR(50) NOT NULL, ToNode NVARCHAR(50) NOT NULL, OperatorId INT NOT NULL, Comment NVARCHAR(500), OperateTime DATETIME NOT NULL DEFAULT GETDATE() );WF_Instance记录一个业务单据当前在哪个节点WF_Step记录每次流转的痕迹。State字段用整数表示0草稿1审批中2已通过3已驳回。流转逻辑的核心就是状态校验和步骤记录public void Approve(int instanceId, int operatorId, string comment) { using (var tx _db.Database.BeginTransaction()) { var instance _db.WF_Instances.Find(instanceId); if (instance null) throw new BusinessException(工作流实例不存在); if (instance.State ! 1) throw new BusinessException($当前状态({instance.State})不能审批); var node GetNextNode(instance.BizType, instance.CurrentNode); _db.WF_Steps.Add(new WF_Step { InstanceId instanceId, FromNode instance.CurrentNode, ToNode node, OperatorId operatorId, Comment comment, OperateTime DateTime.Now }); instance.CurrentNode node; if (node END) instance.State 2; _db.SaveChanges(); tx.Commit(); } }这段代码的收益在于不引入外部工作流引擎只靠一张流转记录表就能覆盖“提交、审批、驳回、撤回”这类固定流程。但它的边界也在这里如果审批节点是用户在界面上动态配置的那这套轻量级方案就不够用了。我把GetNextNode方法设计成可配置的字典节点的先后关系写在代码里适合节点固定、规则稳定的后台审批比如请假、报销、订单审核。4.4 工作流实例与业务单据怎么绑定工作流表里有一对关键字段BizType和BizId。BizType是业务类型字符串比如order、leaveBizId是业务单据主键。这样工作流表就不需要为每个业务类型单独建一套。审批列表查询时把业务表和工作流表做关联SELECT o.*, w.State AS WfState, w.CurrentNode FROM Sys_Order o LEFT JOIN WF_Instance w ON w.BizType order AND w.BizId o.Id WHERE w.State 1这种绑定方式的好处是业务表干净不做冗余字段代价是每次查列表都要多一次Join。数据量大时我一般会在业务表上冗余一个WfState字段审批结束后回写这样可以避免高频列表查询总是关联工作流表。5. 部署排错与二次开发连接串、工作流并发和新增模块的标准动作5.1 IIS部署时最容易漏的三件事Ymnets部署到IIS时我踩过三次坑都在同样位置。第一应用程序池必须选.NET Framework v4.0集成模式经典模式会导致MVC5路由全部404。第二根目录一定要有Global.asax和bin目录只上传View和Controller不行MVC5不是编译成单个DLL分发时漏掉Global.asax会直接报“无法加载类型Global.asax”。第三Upload和Log目录要给IIS_IUSRS写入权限否则upload_ajax.ashx传文件时报目录不存在。5.2 工作流流转后列表不刷新或并发报错工作流审批后列表状态没变先别怀疑事务没提交多半是前端EasyUI缓存了当前页。审批接口返回成功后执行$(#dg).datagrid(reload)即可。真正的并发问题出现在两个审批人同时点通过时EF6的SaveChanges在同一个DbContext实例里不会冲突但两个请求各自持有工作流实例副本后提交后提交的会覆盖先提交的节点状态。解决方式有两个方向一是给WF_Instance表加RowVersion时间戳列EF6用IsRowVersion()映射后在Update时自动做乐观并发校验二是在事务里先SELECT * FROM WF_Instance WITH (UPDLOCK)锁行再执行流转更新。我的建议是加RowVersion因为UPDLOCK会让工作流引擎的吞吐量下降。5.3 新增一个带审批模块的四个步骤按Ymnets现有结构加新模块我一般按这个顺序操作基本不会漏东西建业务表和WF_Instance约定表更新数据字典。在YmnetsDbContext里添加DbSet实体运行Add-Migration生成迁移脚本但不自动执行。新增Controller和对应的EasyUI列表页面复制一个现有模块改造比从空白页写要快。在权限码常量类里增加两个权限码一个列表查看权限一个审批权限然后给Action打上PermissionFilter特性。最后再提到一个容易被忽略的点新增模块后要把数据字典里的BizType值登记好工作流审批记录、待办列表、已办列表都是按这个BizType去查的。只要WF_Instance的BizType和业务Id绑定一致后续不管加多少审批流引擎本身不需要再改动。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门