smartadmin.core.urf 这个项目是基于asp.net core 3.1(最新)基础上参照领域驱动设计(DDD)的理念,并参考目前最为了流行的abp架构开发的一套轻量级的快速开发web application 技术架构,专注业务核心需求,减少重复代码,开始构建和发布,让初级程序员也能开发出专业并且漂亮的Web应用程序
域驱动设计(DDD)是一种通过将实现与不断发展的模型相连接来满足复杂需求的软件开发方法。域驱动设计的前提如下:
- 将项目的主要重点放在核心领域和领域逻辑上;
- 将复杂的设计基于领域模型;
- 启动技术专家和领域专家之间的创造性合作,以迭代方式完善解决特定领域问题的概念模型。
最终的核心思想还是SOLID,只是实现的方式有所不同,ABP可能目前对DDD设计理念最好的实现方式。但对于小项目我还是更喜欢 URF.Core https://github.com/urfnet/URF.Core 这个超轻量级的实现。
同时这个项目也就是我2年前的一个开源项目 ASP.NET MVC 5 SmartCode Scaffolding for Visual Studio.Net 的升级版,支持.net core.目前没有把所有功能都迁移到.net core,其中最重要的就是代码生成这块。再接下来的时间里主要就是完善代码生成的插件。当然也要看是否受欢迎,如果反应一般,我可能不会继续更新。
Demo 网站
演示站点
账号:demo 密码:123456
GitHub 源代码 https://github.com/neozhu/smartadmin.core.urf
喜欢请给个 Star 每一颗Star都是鼓励我继续更新的动力 谢谢
如果你用于自己公司及盈利性的项目,希望给与金钱上的赞助,并且保留原作者的版权
分层
smartadmin.core.urf遵行DDD设计模式来实现应用程序的四层模型
- 表示层(Presentation Layer):用户操作展示界面,使用SmartAdmin - Responsive WebApp模板+Jquery EasyUI
- 应用层(Application Layer):在表示层与域层之间,实现具体应用程序逻辑,业务用例,Project:StartAdmin.Service.csproj
- 域层(Domain Layer):包括业务对象(Entity)和核心(域)业务规则,应用程序的核心,使用EntityFrmework Core Code-first + Repository实现
- 基础结构层(Infrastructure Layer):提供通用技术功能,这些功能主要有第三方库来支持,比如日志:Nlog,服务发现:Swagger UI,事件总线(EventBus):dotnetcore/CAP,认证与授权:Microsoft.AspNetCore.Identity,后面会具体介绍
内容
域层(Domain Layer)
- 实体(Entity,BaseEntity) 通常实体就是映射到关系数据库中的表,这里说名一下最佳做法和惯例:
- 在域层定义:本项目就是(SmartAdmin.Entity.csproj)
- 继承一个基类 Entity,添加必要审计类比如:创建时间,最后修改时间等
- 必须要有一个主键最好是GRUID(不推荐复合主键),但本项目使用递增的int类型
- 字段不要过多的冗余,可以通过定义关联关系
- 字段属性和方法尽量使用virtual关键字。有些ORM和动态代理工具需要
- 存储库(Repositories) 封装基本数据操作方法(CRUD),本项目应用 URF.Core实现
- 域服务
- 技术指标
应用层
- 应用服务:用于实现应用程序的用例。它们用于将域逻辑公开给表示层,从表示层(可选)使用DTO(数据传输对象)作为参数调用应用程序服务。它使用域对象执行某些特定的业务逻辑,并(可选)将DTO返回到表示层。因此,表示层与域层完全隔离。对应本项目:(SmartAdmin.Service.csproj)
- 数据传输对象(DTO):用于在应用程序层和表示层或其他类型的客户端之间传输数据,通常,使用DTO作为参数从表示层(可选)调用应用程序服务。它使用域对象执行某些特定的业务逻辑,并(可选)将DTO返回到表示层。因此,表示层与域层完全隔离.对应本项目:(SmartAdmin.Dto.csproj)
- Unit of work:管理和控制应用程序中操作数据库连接和事务 ,本项目使用 URF.Core实现
基础服务层
- UI样式定义:根据用户喜好选择多种页面显示模式
- 租户管理:使用EntityFrmework Core提供的Global Filter实现简单多租户应用
- 账号管理: 对登录系统账号维护,注册,注销,锁定,解锁,重置密码,导入、导出等功能
- 角色管理:使用Microsoft身份库管理角色,用户及其权限管理
- 导航菜单:系统主导航栏配置
- 角色授权:配置角色显示的菜单
- 键值对配置:常用的数据字典维护,如何正确使用和想法后面会介绍
- 导入&导出配置:使用Excel导入导出做一个可配置的功能
- 系统日志:asp.net core 自带的日志+Nlog把所有日志保存到数据库方便查询和分析
- 消息订阅:集中订阅CAP分布式事件总线的消息
- WebApi: Swagger UI Api服务发现和在线调试工具
- CAP: CAP看板查看发布和订阅的消息
快速上手开发
- 开发环境
- Visual Studio .Net 2019
- .Net Core 3.1
- Sql Server(LocalDb)
- 附加数据库
使用SQL Server Management Studio 附加.\src\SmartAdmin.Data\db\smartadmindb.mdf 数据库(如果是localdb,那么不需要修改数据库连接配置)
- 打开解决方案
第一个简单的需求开始
新增 Company 企业信息 完成CRUD 导入导出功能
- 新建实体对象(Entity)
在SmartAdmin.Entity.csproj项目的Models目录下新增一个Company.cs类
1 //记住:定义实体对象最佳做法,继承基类,使用virtual关键字,尽可能的定义每个属性,名称,类型,长度,校验规则,索引,默认值等 2 namespace SmartAdmin.Data.Models 3 { 4 public partial class Company : URF.Core.EF.Trackable.Entity 5 { 6 [Display(Name = "企业名称", Description = "归属企业名称")] 7 [MaxLength(50)] 8 [Required] 9 //[Index(IsUnique = true)]10 public virtual string Name { get; set; }11 [Display(Name = "组织代码", Description = "组织代码")]12 [MaxLength(12)]13 //[Index(IsUnique = true)]14 [Required]15 public virtual string Code { get; set; }16 [Display(Name = "地址", Description = "地址")]17 [MaxLength(128)]18 [DefaultValue("-")]19 public virtual string Address { get; set; }20 [Display(Name = "联系人", Description = "联系人")]21 [MaxLength(12)]22 public virtual string Contect { get; set; }23 [Display(Name = "联系电话", Description = "联系电话")]24 [MaxLength(20)]25 public virtual string PhoneNumber { get; set; }26 [Display(Name = "注册日期", Description = "注册日期")]27 [DefaultValue("now")]28 public virtual DateTime RegisterDate { get; set; }29 }30 }31 //在 SmartAdmin.Data.csproj 项目 SmartDbContext.cs 添加32 public virtual DbSet<Company> Companies { get; set; }View Code
- 添加服务对象 Service
在项目 SmartAdmin.Service.csproj 中添加ICompanyService.cs,CompanyService.cs 就是用来实现业务需求 用例的地方
1 //ICompany.cs 2 //根据实际业务用例来创建方法,默认的CRUD,增删改查不需要再定义 3 namespace SmartAdmin.Service 4 { 5 // Example: extending IService<TEntity> and/or ITrackableRepository<TEntity>, scope: ICustomerService 6 public interface ICompanyService : IService<Company> 7 { 8 // Example: adding synchronous Single method, scope: ICustomerService 9 Company Single(Expression<Func<Company, bool>> predicate); 10 Task ImportDataTableAsync(DataTable datatable); 11 Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc"); 12 } 13 } 14 // 具体实现接口的方法 15 namespace SmartAdmin.Service 16 { 17 public class CompanyService : Service<Company>, ICompanyService 18 { 19 private readonly IDataTableImportMappingService mappingservice; 20 private readonly ILogger<CompanyService> logger; 21 public CompanyService( 22 IDataTableImportMappingService mappingservice, 23 ILogger<CompanyService> logger, 24 ITrackableRepository<Company> repository) : base(repository) 25 { 26 this.mappingservice = mappingservice; 27 this.logger = logger; 28 } 29 30 public async Task<Stream> ExportExcelAsync(string filterRules = "", string sort = "Id", string order = "asc") 31 { 32 var filters = PredicateBuilder.FromFilter<Company>(filterRules); 33 var expcolopts = await this.mappingservice.Queryable() 34 .Where(x => x.EntitySetName == "Company") 35 .Select(x => new ExpColumnOpts() 36 { 37 EntitySetName = x.EntitySetName, 38 FieldName = x.FieldName, 39 IgnoredColumn = x.IgnoredColumn, 40 SourceFieldName = x.SourceFieldName 41 }).ToArrayAsync(); 42 43 var works = (await this.Query(filters).OrderBy(n => n.OrderBy(sort, order)).SelectAsync()).ToList(); 44 var datarows = works.Select(n => new 45 { 46 Id = n.Id, 47 Name = n.Name, 48 Code = n.Code, 49 Address = n.Address, 50 Contect = n.Contect, 51 PhoneNumber = n.PhoneNumber, 52 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss") 53 }).ToList(); 54 return await NPOIHelper.ExportExcelAsync("Company", datarows, expcolopts); 55 } 56 57 public async Task ImportDataTableAsync(DataTable datatable) 58 { 59 var mapping = await this.mappingservice.Queryable() 60 .Where(x => x.EntitySetName == "Company" && 61 (x.IsEnabled == true || (x.IsEnabled == false && x.DefaultValue != null)) 62 ).ToListAsync(); 63 if (mapping.Count == 0) 64 { 65 throw new NullReferenceException("没有找到Work对象的Excel导入配置信息,请执行[系统管理/Excel导入配置]"); 66 } 67 foreach (DataRow row in datatable.Rows) 68 { 69 70 var requiredfield = mapping.Where(x => x.IsRequired == true && x.IsEnabled == true && x.DefaultValue == null).FirstOrDefault()?.SourceFieldName; 71 if (requiredfield != null || !row.IsNull(requiredfield)) 72 { 73 var item = new Company(); 74 foreach (var field in mapping) 75 { 76 var defval = field.DefaultValue; 77 var contain = datatable.Columns.Contains(field.SourceFieldName ?? ""); 78 if (contain && !row.IsNull(field.SourceFieldName)) 79 { 80 var worktype = item.GetType(); 81 var propertyInfo = worktype.GetProperty(field.FieldName); 82 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 83 var safeValue = (row[field.SourceFieldName] == null) ? null : Convert.ChangeType(row[field.SourceFieldName], safetype); 84 propertyInfo.SetValue(item, safeValue, null); 85 } 86 else if (!string.IsNullOrEmpty(defval)) 87 { 88 var worktype = item.GetType(); 89 var propertyInfo = worktype.GetProperty(field.FieldName); 90 if (string.Equals(defval, "now", StringComparison.OrdinalIgnoreCase) && (propertyInfo.PropertyType == typeof(DateTime) || propertyInfo.PropertyType == typeof(Nullable<DateTime>))) 91 { 92 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 93 var safeValue = Convert.ChangeType(DateTime.Now, safetype); 94 propertyInfo.SetValue(item, safeValue, null); 95 } 96 else if (string.Equals(defval, "guid", StringComparison.OrdinalIgnoreCase)) 97 { 98 propertyInfo.SetValue(item, Guid.NewGuid().ToString(), null); 99 }100 else if (string.Equals(defval, "user", StringComparison.OrdinalIgnoreCase))101 {102 propertyInfo.SetValue(item, "", null);103 }104 else105 {106 var safetype = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;107 var safeValue = Convert.ChangeType(defval, safetype);108 propertyInfo.SetValue(item, safeValue, null);109 }110 }111 }112 this.Insert(item);113 }114 }115 }116 117 // Example, adding synchronous Single method118 public Company Single(Expression<Func<Company, bool>> predicate)119 {120 121 return this.Repository.Queryable().Single(predicate);122 123 }124 }125 }View Code
- 添加Controller
MVC Controller
1 namespace SmartAdmin.WebUI.Controllers 2 { 3 public class CompaniesController : Controller 4 { 5 private readonly ICompanyService companyService; 6 private readonly IUnitOfWork unitOfWork; 7 private readonly ILogger<CompaniesController> _logger; 8 private readonly IWebHostEnvironment _webHostEnvironment; 9 public CompaniesController(ICompanyService companyService, 10 IUnitOfWork unitOfWork, 11 IWebHostEnvironment webHostEnvironment, 12 ILogger<CompaniesController> logger) 13 { 14 this.companyService = companyService; 15 this.unitOfWork = unitOfWork; 16 this._logger = logger; 17 this._webHostEnvironment = webHostEnvironment; 18 } 19 20 // GET: Companies 21 public IActionResult Index()=> View(); 22 //datagrid 数据源 23 public async Task<JsonResult> GetData(int page = 1, int rows = 10, string sort = "Id", string order = "asc", string filterRules = "") 24 { 25 try 26 { 27 var filters = PredicateBuilder.FromFilter<Company>(filterRules); 28 var total = await this.companyService 29 .Query(filters) 30 .AsNoTracking() 31 .CountAsync() 32 ; 33 var pagerows = (await this.companyService 34 .Query(filters) 35 .AsNoTracking() 36 .OrderBy(n => n.OrderBy(sort, order)) 37 .Skip(page - 1).Take(rows) 38 .SelectAsync()) 39 .Select(n => new 40 { 41 Id = n.Id, 42 Name = n.Name, 43 Code = n.Code, 44 Address = n.Address, 45 Contect = n.Contect, 46 PhoneNumber = n.PhoneNumber, 47 RegisterDate = n.RegisterDate.ToString("yyyy-MM-dd HH:mm:ss") 48 }).ToList(); 49 var pagelist = new { total = total, rows = pagerows }; 50 return Json(pagelist); 51 } 52 catch(Exception e) { 53 throw e; 54 } 55 56 } 57 //编辑 58 [HttpPost] 59 [ValidateAntiForgeryToken] 60 public async Task<JsonResult> Edit(Company company) 61 { 62 if (ModelState.IsValid) 63 { 64 try 65 { 66 this.companyService.Update(company); 67 68 var result = await this.unitOfWork.SaveChangesAsync(); 69 return Json(new { success = true, result = result }); 70 } 71 catch (Exception e) 72 { 73 return Json(new { success = false, err = e.GetBaseException().Message }); 74 } 75 } 76 else 77 { 78 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage))); 79 return Json(new { success = false, err = modelStateErrors }); 80 //DisplayErrorMessage(modelStateErrors); 81 } 82 //return View(work); 83 } 84 //新建 85 [HttpPost] 86 [ValidateAntiForgeryToken] 87 88 public async Task<JsonResult> Create([Bind("Name,Code,Address,Contect,PhoneNumber,RegisterDate")] Company company) 89 { 90 if (ModelState.IsValid) 91 { 92 try 93 { 94 this.companyService.Insert(company); 95 await this.unitOfWork.SaveChangesAsync(); 96 return Json(new { success = true}); 97 } 98 catch (Exception e) 99 {100 return Json(new { success = false, err = e.GetBaseException().Message });101 }102 103 //DisplaySuccessMessage("Has update a Work record");104 //return RedirectToAction("Index");105 }106 else107 {108 var modelStateErrors = string.Join(",", this.ModelState.Keys.SelectMany(key => this.ModelState[key].Errors.Select(n => n.ErrorMessage)));109 return Json(new { success = false, err = modelStateErrors });110 //DisplayErrorMessage(modelStateErrors);111 }112 //return View(work);113 }114 //删除当前记录115 //GET: Companies/Delete/:id116 [HttpGet]117 public async Task<JsonResult> Delete(int id)118 {119 try120 {121 await this.companyService.DeleteAsync(id);122 await this.unitOfWork.SaveChangesAsync();123 return Json(new { success = true });124 }125 126 catch (Exception e)127 {128 return Json(new { success = false, err = e.GetBaseException().Message });129 }130 }131 //删除选中的记录132 [HttpPost]133 public async Task<JsonResult> DeleteChecked(int[] id)134 {135 try136 {137 foreach (var key in id)138 {139 await this.companyService.DeleteAsync(key);140 }141 await this.unitOfWork.SaveChangesAsync();142 return Json(new { success = true });143 }144 catch (Exception e)145 {146 return Json(new { success = false, err = e.GetBaseException().Message });147 }148 }149 //保存datagrid编辑的数据150 [HttpPost]151 public async Task<JsonResult> AcceptChanges(Company[] companies)152 {153 if (ModelState.IsValid)154 {155 try156 {157 foreach (var item in companies)158 {159 this.companyService.ApplyChanges(item);160 }161 var result = await this.unitOfWork.SaveChangesAsync();162 return Json(new { success = true, result });163 }164 catch (Exception e)165 {166 return Json(new { success = false, err = e.GetBaseException().Message });167 }168 }169 else170 {171 var modelStateErrors = string.Join(",", ModelState.Keys.SelectMany(key => ModelState[key].Errors.Select(n => n.ErrorMessage)));172 return Json(new { success = false, err = modelStateErrors });173 }174 175 }176 //导出Excel177 [HttpPost]178 public async Task<IActionResult> ExportExcel(string filterRules = "", string sort = "Id", string order = "asc")179 {180 var fileName = "compnay" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";181 var stream = await this.companyService.ExportExcelAsync(filterRules, sort, order);182 return File(stream, "application/vnd.ms-excel", fileName);183 }184 //导入excel185 [HttpPost]186 public async Task<IActionResult> ImportExcel()187 {188 try189 {190 var watch = new Stopwatch();191 watch.Start();192 var total = 0;193 if (Request.Form.Files.Count > 0)194 {195 for (var i = 0; i < this.Request.Form.Files.Count; i++)196 {197 var model = Request.Form["model"].FirstOrDefault() ?? "company";198 var folder = Request.Form["folder"].FirstOrDefault() ?? "company";199 var autosave = Convert.ToBoolean(Request.Form["autosave"].FirstOrDefault());200 var properties = (Request.Form["properties"].FirstOrDefault()?.Split(','));201 var file = Request.Form.Files[i];202 var filename = file.FileName;203 var contenttype = file.ContentType;204 var size = file.Length;205 var ext = Path.GetExtension(filename);206 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, "UploadFiles", folder);207 if (!Directory.Exists(path))208 {209 Directory.CreateDirectory(path);210 }211 var datatable = await NPOIHelper.GetDataTableFromExcelAsync(file.OpenReadStream(), ext);212 await this.companyService.ImportDataTableAsync(datatable);213 await this.unitOfWork.SaveChangesAsync();214 total = datatable.Rows.Count;215 if (autosave)216 {217 var filepath = Path.Combine(path, filename);218 file.OpenReadStream().Position = 0;219 220 using (var stream = System.IO.File.Create(filepath))221 {222 await file.CopyToAsync(stream);223 }224 }225 226 }227 }228 watch.Stop();229 return Json(new { success = true, total = total, elapsedTime = watch.ElapsedMilliseconds });230 }231 catch (Exception e) {232 this._logger.LogError(e, "Excel导入失败");233 return this.Json(new { success = false, err = e.GetBaseException().Message });234 }235 }236 //下载模板237 public async Task<IActionResult> Download(string file) {238 239 this.Response.Cookies.Append("fileDownload", "true");240 var path = Path.Combine(this._webHostEnvironment.ContentRootPath, file);241 var downloadFile = new FileInfo(path);242 if (downloadFile.Exists)243 {244 var fileName = downloadFile.Name;245 var mimeType = MimeTypeConvert.FromExtension(downloadFile.Extension);246 var fileContent = new byte[Convert.ToInt32(downloadFile.Length)];247 using (var fs = downloadFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))248 {249 await fs.ReadAsync(fileContent, 0, Convert.ToInt32(downloadFile.Length));250 }251 return this.File(fileContent, mimeType, fileName);252 }253 else254 {255 throw new FileNotFoundException($"文件 {file} 不存在!");256 }257 }258 259 }260 }View Code
- 新建 View
MVC Views\Companies\Index
1 @model SmartAdmin.Data.Models.Company 2 @{ 3 ViewData["Title"] = "企业信息"; 4 ViewData["PageName"] = "Companies_Index"; 5 ViewData["Heading"] = "<i class='fal fa-window text-primary'></i> 企业信息"; 6 ViewData["Category1"] = "组织架构"; 7 ViewData["PageDescription"] = ""; 8 } 9 <div class="row"> 10 <div class="col-lg-12 col-xl-12"> 11 <div id="panel-1" class="panel"> 12 <div class="panel-hdr"> 13 <h2> 14 公司信息 15 </h2> 16 <div class="panel-toolbar"> 17 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-collapse" data-toggle="tooltip" data-offset="0,10" data-original-title="Collapse"><i class="fal fa-window-minimize"></i></button> 18 <button class="btn btn-panel bg-transparent fs-xl w-auto h-auto rounded-0" data-action="panel-fullscreen" data-toggle="tooltip" data-offset="0,10" data-original-title="Fullscreen"><i class="fal fa-expand"></i></button> 19 </div> 20 21 </div> 22 <div class="panel-container show"> 23 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade "> 24 <div class="row no-gutters align-items-center"> 25 <div class="col"> 26 <!-- 开启授权控制请参考 @@if (Html.IsAuthorize("Create") --> 27 <div class="btn-group btn-group-sm"> 28 <button onclick="append()" class="btn btn-default"> 29 <span class="fal fa-plus mr-1"></span> 新增 30 </button> 31 </div> 32 <div class="btn-group btn-group-sm"> 33 <button name="deletebutton" disabled onclick="removeit()" class="btn btn-default"> 34 <span class="fal fa-times mr-1"></span> 删除 35 </button> 36 </div> 37 <div class="btn-group btn-group-sm"> 38 <button name="savebutton" disabled onclick="acceptChanges()" class="btn btn-default"> 39 <span class="fal fa-save mr-1"></span> 保存 40 </button> 41 </div> 42 <div class="btn-group btn-group-sm"> 43 <button name="cancelbutton" disabled onclick="rejectChanges()" class="btn btn-default"> 44 <span class="fal fa-ban mr-1"></span> 取消 45 </button> 46 </div> 47 <div class="btn-group btn-group-sm"> 48 <button onclick="reload()" class="btn btn-default"> <span class="fal fa-search mr-1"></span> 查询 </button> 49 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 50 <span class="sr-only">Toggle Dropdown</span> 51 </button> 52 <div class="dropdown-menu dropdown-menu-animated"> 53 <a class="dropdown-item js-waves-on" href="javascript:void()"> 我的记录 </a> 54 <div class="dropdown-divider"></div> 55 <a class="dropdown-item js-waves-on" href="javascript:void()"> 自定义查询 </a> 56 </div> 57 </div> 58 <div class="btn-group btn-group-sm hidden-xs"> 59 <button type="button" onclick="importExcel.upload()" class="btn btn-default"><span class="fal fa-cloud-upload mr-1"></span> 导入 </button> 60 <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split waves-effect waves-themed" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> 61 <span class="sr-only">Toggle Dropdown</span> 62 </button> 63 <div class="dropdown-menu dropdown-menu-animated"> 64 <a class="dropdown-item js-waves-on" href="javascript:importExcel.downloadtemplate()"> 65 <span class="fal fa-download"></span> 下载模板 66 </a> 67 </div> 68 </div> 69 <div class="btn-group btn-group-sm hidden-xs"> 70 <button onclick="exportexcel()" class="btn btn-default"> 71 <span class="fal fa-file-export mr-1"></span> 导出 72 </button> 73 </div> 74 75 </div> 76 77 </div> 78 79 </div> 80 <div class="panel-content"> 81 <div class="table-responsive"> 82 <table id="companies_datagrid"> 83 </table> 84 </div> 85 </div> 86 </div> 87 </div> 88 </div> 89 </div> 90 <!-- 弹出窗体form表单 --> 91 <div id="companydetailwindow" class="easyui-window" 92 title="明细数据" 93 data-options="modal:true, 94 closed:true, 95 minimizable:false, 96 collapsible:false, 97 maximized:false, 98 iconCls:'fal fa-window', 99 onBeforeClose:function(){100 var that = $(this);101 if(companyhasmodified()){102 $.messager.confirm('确认','你确定要放弃保存修改的记录?',function(r){103 if (r){104 var opts = that.panel('options');105 var onBeforeClose = opts.onBeforeClose;106 opts.onBeforeClose = function(){};107 that.panel('close');108 opts.onBeforeClose = onBeforeClose;109 hook = false;110 }111 });112 return false;113 }114 },115 onOpen:function(){116 $(this).window('vcenter');117 $(this).window('hcenter');118 },119 onRestore:function(){120 },121 onMaximize:function(){122 }123 " style="width:820px;height:420px;display:none">124 <!-- toolbar -->125 <div class="panel-content py-2 rounded-bottom border-faded border-left-0 border-right-0 text-muted bg-subtlelight-fade sticky-top">126 <div class="d-flex flex-row-reverse pr-4">127 <div class="btn-group btn-group-sm mr-1">128 <button name="saveitembutton" onclick="savecompanyitem()" class="btn btn-default">129 <i class="fal fa-save"></i> 保存130 </button>131 </div>132 <div class="btn-group btn-group-sm mr-1" id="deleteitem-btn-group">133 <button onclick="deletecompanyitem()" class="btn btn-danger">134 <i class="fal fa-trash-alt"></i> 删除135 </button>136 </div>137 </div>138 </div>139 <div class="panel-container show">140 <div class="container">141 <div class="panel-content">142 <form id="company_form"143 class="easyui-form form-horizontal p-1"144 method="post"145 data-options="novalidate:true,146 onChange: function(target){147 hook = true;148 $('button[name*=\'saveitembutton\']').prop('disabled', false);149 },150 onLoadSuccess:function(data){151 hook = false;152 $('button[name*=\'saveitembutton\']').prop('disabled', true);153 }">154 @Html.AntiForgeryToken()155 <!--Primary Key-->156 @Html.HiddenFor(model => model.Id)157 <fieldset class="form-group">158 <!-- begin row -->159 <!--名称-->160 <div class="row h-100 justify-content-center align-items-center">161 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Name)</label>162 <div class="col-md-4 mb-1 pl-1">163 <input id="@Html.IdFor(model => model.Name)"164 name="@Html.NameFor(model => model.Name)"165 value="@Html.ValueFor(model => model.Name)"166 tabindex="0" required167 class="easyui-textbox"168 style="width:100%"169 type="text"170 data-options="prompt:'@Html.DescriptionFor(model => model.Name)',171 required:true,172 validType: 'length[0,50]'173 " />174 </div>175 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.Code)</label>176 <div class="col-md-4 mb-1 pl-1">177 <input id="@Html.IdFor(model => model.Code)"178 name="@Html.NameFor(model => model.Code)"179 value="@Html.ValueFor(model => model.Code)"180 tabindex="1" required181 class="easyui-textbox"182 style="width:100%"183 type="text"184 data-options="prompt:'@Html.DescriptionFor(model => model.Code)',185 required:true,186 validType: 'length[0,12]'187 " />188 </div>189 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Address)</label>190 <div class="col-md-4 mb-1 pl-1">191 <input id="@Html.IdFor(model => model.Address)"192 name="@Html.NameFor(model => model.Address)"193 value="@Html.ValueFor(model => model.Address)"194 tabindex="2"195 class="easyui-textbox"196 style="width:100%"197 type="text"198 data-options="prompt:'@Html.DescriptionFor(model => model.Address)',199 required:false,200 validType: 'length[0,50]'201 " />202 </div>203 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.Contect)</label>204 <div class="col-md-4 mb-1 pl-1">205 <input id="@Html.IdFor(model => model.Contect)"206 name="@Html.NameFor(model => model.Contect)"207 value="@Html.ValueFor(model => model.Contect)"208 tabindex="3"209 class="easyui-textbox"210 style="width:100%"211 type="text"212 data-options="prompt:'@Html.DescriptionFor(model => model.Contect)',213 required:false,214 validType: 'length[0,12]'215 " />216 </div>217 <label class="col-md-2 pr-1 form-label text-right">@Html.DisplayNameFor(model => model.PhoneNumber)</label>218 <div class="col-md-4 mb-1 pl-1">219 <input id="@Html.IdFor(model => model.PhoneNumber)"220 name="@Html.NameFor(model => model.PhoneNumber)"221 value="@Html.ValueFor(model => model.PhoneNumber)"222 tabindex="4"223 class="easyui-textbox"224 style="width:100%"225 type="text"226 data-options="prompt:'@Html.DescriptionFor(model => model.PhoneNumber)',227 required:false,228 validType: 'length[0,20]'229 " />230 </div>231 <label class="col-md-2 pr-1 form-label text-right text-danger">@Html.DisplayNameFor(model => model.RegisterDate)</label>232 <div class="col-md-4 mb-1 pl-1">233 <input id="@Html.IdFor(model => model.RegisterDate)"234 name="@Html.NameFor(model => model.RegisterDate)"235 value="@Html.ValueFor(model => model.RegisterDate)"236 tabindex="5" required237 class="easyui-datebox"238 style="width:100%"239 type="text"240 data-options="prompt:'@Html.DescriptionFor(model => model.RegisterDate)',241 required:true,242 formatter:dateformatter" />243 </div>244 </div>245 </fieldset>246 </form>247 </div>248 </div>249 </div>250 </div>251 252 253 @await Component.InvokeAsync("ImportExcel", new ImportExcelOptions { entity="Company",254 folder="Companies",255 url="/Companies/ImportExcel",256 tpl="/Companies/Download"257 258 259 })260 261 @section HeadBlock {262 <link href="~/css/notifications/toastr/toastr.css" rel="stylesheet" asp-append-version="true" />263 <link href="~/css/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.css" rel="stylesheet" asp-append-version="true" />264 <link href="~/js/easyui/themes/insdep/easyui.css" rel="stylesheet" asp-append-version="true" />265 }266 @section ScriptsBlock {267 <script src="~/js/dependency/moment/moment.js" asp-append-version="true"></script>268 <script src="~/js/notifications/toastr/toastr.js"></script>269 <script src="~/js/formplugins/bootstrap-daterangepicker/bootstrap-daterangepicker.js" asp-append-version="true"></script>270 <script src="~/js/easyui/jquery.easyui.min.js" asp-append-version="true"></script>271 <script src="~/js/easyui/plugins/datagrid-filter.js" asp-append-version="true"></script>272 <script src="~/js/easyui/plugins/columns-ext.js" asp-append-version="true"></script>273 <script src="~/js/easyui/plugins/columns-reset.js" asp-append-version="true"></script>274 <script src="~/js/easyui/locale/easyui-lang-zh_CN.js" asp-append-version="true"></script>275 <script src="~/js/easyui/jquery.easyui.component.js" asp-append-version="true"></script>276 <script src="~/js/plugin/filesaver/FileSaver.js" asp-append-version="true"></script>277 <script src="~/js/plugin/jquery.serializejson/jquery.serializejson.js" asp-append-version="true"></script>278 <script src="~/js/jquery.custom.extend.js" asp-append-version="true"></script>279 <script src="~/js/jquery.extend.formatter.js" asp-append-version="true"></script>280 <script>281 var $dg = $('#companies_datagrid');282 var EDITINLINE = true;283 var company = null;284 var editIndex = undefined;285 //下载Excel导入模板286 287 //执行导出下载Excel288 function exportexcel() {289 const filterRules = JSON.stringify($dg.datagrid('options').filterRules);290 console.log(filterRules);291 $.messager.progress({ title: '请等待',msg:'正在执行导出...' });292 let formData = new FormData();293 formData.append('filterRules', filterRules);294 formData.append('sort', 'Id');295 formData.append('order', 'asc');296 $.postDownload('/Companies/ExportExcel', formData).then(res => {297 $.messager.progress('close');298 toastr.success('导出成功!');299 }).catch(err => {300 //console.log(err);301 $.messager.progress('close');302 $.messager.alert('导出失败', err.statusText, 'error');303 });304 305 }306 //弹出明细信息307 function showdetailswindow(id, index) {308 const company = $dg.datagrid('getRows')[index];309 opencompanydetailwindow(company, 'Modified');310 }311 function reload() {312 $dg.datagrid('uncheckAll');313 $dg.datagrid('reload');314 }315 //新增记录316 function append() {317 company = {318 Address: '-',319 RegisterDate: moment().format('YYYY-MM-DD HH:mm:ss'),320 };321 if (!EDITINLINE) {322 //弹出新增窗口323 opencompanydetailwindow(company, 'Added');324 } else {325 if (endEditing()) {326 //对必填字段进行默认值初始化327 $dg.datagrid('insertRow',328 {329 index: 0,330 row: company331 });332 editIndex = 0;333 $dg.datagrid('selectRow', editIndex)334 .datagrid('beginEdit', editIndex);335 hook = true;336 }337 }338 }339 //删除编辑的行340 function removeit() {341 if (this.$dg.datagrid('getChecked').length <= 0 && EDITINLINE) {342 if (editIndex !== undefined) {343 const delindex = editIndex;344 $dg.datagrid('cancelEdit', delindex)345 .datagrid('deleteRow', delindex);346 hook = true;347 } else {348 const rows =$dg.datagrid('getChecked');349 rows.slice().reverse().forEach(row => {350 const rowindex =$dg.datagrid('getRowIndex', row);351 $dg.datagrid('deleteRow', rowindex);352 hook = true;353 });354 }355 } else {356 deletechecked();357 }358 }359 //删除该行360 function deleteRow(id) {361 $.messager.confirm('确认', '你确定要删除该记录?', result => {362 if (result) {363 dodeletechecked([id]);364 }365 })366 }367 //删除选中的行368 function deletechecked() {369 const id =$dg.datagrid('getChecked').filter(item => item.Id != null && item.Id > 0).map(item => {370 return item.Id;371 });372 if (id.length > 0) {373 $.messager.confirm('确认', `你确定要删除这 <span class='badge badge-icon position-relative'>${id.length} </span> 行记录?`, result => {374 if (result) {375 dodeletechecked(id);376 }377 });378 } else {379 $.messager.alert('提示', '请先选择要删除的记录!', 'question');380 }381 }382 //执行删除383 function dodeletechecked(id) {384 $.post('/Companies/DeleteChecked', { id: id })385 .done(response => {386 if (response.success) {387 toastr.error(`成功删除[${id.length}]行记录`);388 reload();389 } else {390 $.messager.alert('错误', response.err, 'error');391 }392 })393 .fail((jqXHR, textStatus, errorThrown) => {394 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');395 });396 }397 //开启编辑状态398 function onClickCell(index, field) {399 400 company = $dg.datagrid('getRows')[index];401 const _actions = ['action', 'ck'];402 if (!EDITINLINE || $.inArray(field, _actions) >= 0) {403 return;404 }405 406 if (editIndex !== index) {407 if (endEditing()) {408 $dg.datagrid('selectRow', index)409 .datagrid('beginEdit', index);410 hook = true;411 editIndex = index;412 const ed = $dg.datagrid('getEditor', { index: index, field: field });413 if (ed) {414 ($(ed.target).data('textbox') ? $(ed.target).textbox('textbox') : $(ed.target)).focus();415 }416 } else {417 $dg.datagrid('selectRow', editIndex);418 }419 }420 }421 //关闭编辑状态422 function endEditing() {423 424 if (editIndex === undefined) {425 return true;426 }427 if (this.$dg.datagrid('validateRow', editIndex)) {428 $dg.datagrid('endEdit', editIndex);429 return true;430 } else {431 const invalidinput = $('input.validatebox-invalid', $dg.datagrid('getPanel'));432 const fieldnames = invalidinput.map((index, item) => {433 return $(item).attr('placeholder') || $(item).attr('id');434 });435 $.messager.alert('提示', `${Array.from(fieldnames)} 输入有误.`, 'error');436 return false;437 }438 }439 //提交保存后台数据库440 function acceptChanges() {441 if (endEditing()) {442 if ($dg.datagrid('getChanges').length > 0) {443 const inserted = $dg.datagrid('getChanges', 'inserted').map(item => {444 item.TrackingState = 1;445 return item;446 });447 const updated = $dg.datagrid('getChanges', 'updated').map(item => {448 item.TrackingState = 2449 return item;450 });451 const deleted = $dg.datagrid('getChanges', 'deleted').map(item => {452 item.TrackingState = 3453 return item;454 });455 //过滤已删除的重复项456 const changed = inserted.concat(updated.filter(item => {457 return !deleted.includes(item);458 })).concat(deleted);459 //$.messager.progress({ title: '请等待', msg: '正在保存数据...', interval: 200 });460 $.post('/Companies/AcceptChanges', { companies: changed })461 .done(response => {462 //$.messager.progress('close');463 //console.log(response);464 if (response.success) {465 toastr.success('保存成功');466 $dg.datagrid('acceptChanges');467 reload();468 hook = false;469 } else {470 $.messager.alert('错误', response.err, 'error');471 }472 })473 .fail((jqXHR, textStatus, errorThrown) => {474 //$.messager.progress('close');475 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');476 });477 }478 }479 }480 function rejectChanges() {481 $dg.datagrid('rejectChanges');482 editIndex = undefined;483 hook = false;484 }485 $(document).ready(function () {486 //定义datagrid结构487 $dg.datagrid({488 rownumbers: true,489 checkOnSelect: false,490 selectOnCheck: false,491 idField: 'Id',492 sortName: 'Id',493 sortOrder: 'desc',494 remoteFilter: true,495 singleSelect: true,496 method: 'get',497 onClickCell: onClickCell,498 clientPaging: false,499 pagination: true,500 striped: true,501 filterRules: [],502 onHeaderContextMenu: function (e, field) {503 e.preventDefault();504 $(this).datagrid('columnMenu').menu('show', {505 left: e.pageX,506 top: e.pageY507 });508 },509 onBeforeLoad: function () {510 const that = $(this);511 document.addEventListener('panel.onfullscreen', () => {512 setTimeout(() => {513 that.datagrid('resize');514 }, 200)515 })516 },517 onLoadSuccess: function (data) {518 editIndex = undefined;519 $("button[name*='deletebutton']").prop('disabled', true);520 $("button[name*='savebutton']").prop('disabled', true);521 $("button[name*='cancelbutton']").prop('disabled', true);522 },523 onCheck: function () {524 $("button[name*='deletebutton']").prop('disabled', false);525 },526 onUncheck: function () {527 const checked = $(this).datagrid('getChecked').length > 0;528 $("button[name*='deletebutton']").prop('disabled', !checked);529 },530 onSelect: function (index, row) {531 company = row;532 },533 onBeginEdit: function (index, row) {534 //const editors = $(this).datagrid('getEditors', index);535 536 },537 onEndEdit: function (index, row) {538 editIndex = undefined;539 },540 onBeforeEdit: function (index, row) {541 editIndex = index;542 row.editing = true;543 $("button[name*='deletebutton']").prop('disabled', false);544 $("button[name*='cancelbutton']").prop('disabled', false);545 $("button[name*='savebutton']").prop('disabled', false);546 $(this).datagrid('refreshRow', index);547 },548 onAfterEdit: function (index, row) {549 row.editing = false;550 editIndex = undefined;551 $(this).datagrid('refreshRow', index);552 },553 onCancelEdit: function (index, row) {554 row.editing = false;555 editIndex = undefined;556 $("button[name*='deletebutton']").prop('disabled', true);557 $("button[name*='savebutton']").prop('disabled', true);558 $("button[name*='cancelbutton']").prop('disabled', true);559 $(this).datagrid('refreshRow', index);560 },561 frozenColumns: [[562 /*开启CheckBox选择功能*/563 { field: 'ck', checkbox: true },564 {565 field: 'action',566 title: '操作',567 width: 85,568 sortable: false,569 resizable: true,570 formatter: function showdetailsformatter(value, row, index) {571 if (!row.editing) {572 return `<div class="btn-group">\573 <button onclick="showdetailswindow('${row.Id}', ${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="查看明细" ><i class="fal fa-edit"></i> </button>\574 <button onclick="deleteRow('${row.Id}',${index})" class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" title="删除记录" ><i class="fal fa-times"></i> </button>\575 </div>`;576 } else {577 return `<button class="btn btn-primary btn-sm btn-icon waves-effect waves-themed" disabled title="查看明细" ><i class="fal fa-edit"></i> </button>`;578 }579 }580 }581 ]],582 columns: [[583 584 { /*名称*/585 field: 'Name',586 title: '<span >@Html.DisplayNameFor(model => model.Name)</span>',587 width: 200,588 hidden: false,589 editor: {590 type: 'textbox',591 options: { prompt: '@Html.DescriptionFor(model => model.Name)', required: true, validType: 'length[0,50]' }592 },593 sortable: true,594 resizable: true595 },596 { /*组织代码*/597 field: 'Code',598 title: '<span >@Html.DisplayNameFor(model => model.Code)</span>',599 width: 120,600 hidden: false,601 editor: {602 type: 'textbox',603 options: { prompt: '@Html.DescriptionFor(model => model.Code)', required: true, validType: 'length[0,12]' }604 },605 sortable: true,606 resizable: true607 },608 { /*地址*/609 field: 'Address',610 title: '@Html.DisplayNameFor(model => model.Address)',611 width: 200,612 hidden: false,613 editor: {614 type: 'textbox',615 options: { prompt: '@Html.DescriptionFor(model => model.Address)', required: false, validType: 'length[0,50]' }616 },617 sortable: true,618 resizable: true619 },620 { /*联系人*/621 field: 'Contect',622 title: '@Html.DisplayNameFor(model => model.Contect)',623 width: 120,624 hidden: false,625 editor: {626 type: 'textbox',627 options: { prompt: '@Html.DescriptionFor(model => model.Contect)', required: false, validType: 'length[0,12]' }628 },629 sortable: true,630 resizable: true631 },632 { /*联系电话*/633 field: 'PhoneNumber',634 title: '@Html.DisplayNameFor(model => model.PhoneNumber)',635 width: 120,636 hidden: false,637 editor: {638 type: 'textbox',639 options: { prompt: '@Html.DescriptionFor(model => model.PhoneNumber)', required: false, validType: 'length[0,20]' }640 },641 sortable: true,642 resizable: true643 },644 { /*注册日期*/645 field: 'RegisterDate',646 title: '<span >@Html.DisplayNameFor(model => model.RegisterDate)</span>',647 width: 140,648 align: 'right',649 hidden: false,650 editor: {651 type: 'datebox',652 options: { prompt: '@Html.DescriptionFor(model => model.RegisterDate)', required: true }653 },654 formatter: dateformatter,655 sortable: true,656 resizable: true657 },658 ]]659 }).datagrid('columnMoving')660 .datagrid('resetColumns')661 .datagrid('enableFilter', [662 { /*Id*/663 field: 'Id',664 type: 'numberbox',665 op: ['equal', 'notequal', 'less', 'lessorequal', 'greater', 'greaterorequal']666 },667 { /*注册日期*/668 field: 'RegisterDate',669 type: 'dateRange',670 options: {671 onChange: value => {672 $dg.datagrid('addFilterRule', {673 field: 'RegisterDate',674 op: 'between',675 value: value676 });677 678 $dg.datagrid('doFilter');679 }680 }681 },682 ])683 .datagrid('load', '/Companies/GetData');684 }685 );686 687 </script>688 <script type="text/javascript">689 //判断新增编辑状态690 var MODELSTATE = 'Added';691 var companyid = null;692 function opencompanydetailwindow(data, state) {693 MODELSTATE = state;694 initcompanydetailview();695 companyid = (data.Id || 0);696 $("#companydetailwindow").window("open");697 $('#company_form').form('reset');698 $('#company_form').form('load', data);699 }700 //删除当前记录701 function deletecompanyitem() {702 $.messager.confirm('确认', '你确定要删除该记录?', result => {703 if (result) {704 const url = `/Companies/Delete/${companyid}`;705 $.get(url).done(res => {706 if (res.success) {707 toastr.success("删除成功");708 $("#companydetailwindow").window("close");709 reload();710 } else {711 $.messager.alert("错误", res.err, "error");712 }713 });714 }715 });716 }717 //async 保存数据718 async function savecompanyitem() {719 const $companyform = $('#company_form');720 if ($companyform.form('enableValidation').form('validate')) {721 let company = $companyform.serializeJSON();722 let url = '/Companies/Edit';723 //判断是新增或是修改方法724 if (MODELSTATE === 'Added') {725 url = '/Companies/Create';726 }727 var token = $('input[name="__RequestVerificationToken"]', $companyform).val();728 //$.messager.progress({ title: '请等待', msg: '正在保存数据...', interval: 200 });729 $.ajax({730 type: "POST",731 url: url,732 data: {733 __RequestVerificationToken: token,734 company: company735 },736 dataType: 'json',737 contentType: 'application/x-www-form-urlencoded; charset=utf-8'738 })739 .done(response => {740 //$.messager.progress('close');741 if (response.success) {742 hook = false;743 $companyform.form('disableValidation');744 $dg.datagrid('reload');745 $('#companydetailwindow').window("close");746 toastr.success("保存成功");747 } else {748 $.messager.alert("错误", response.err, "error");749 }750 })751 .fail((jqXHR, textStatus, errorThrown) => {752 //$.messager.progress('close');753 $.messager.alert('异常', `${jqXHR.status}: ${jqXHR.statusText} `, 'error');754 });755 }756 }757 //关闭窗口758 function closecompanydetailwindow() {759 $('#companydetailwindow').window('close');760 }761 762 //判断是否有没有保存的记录763 function companyhasmodified() {764 return hook;765 }766 767 768 function initcompanydetailview() {769 //判断是否显示功能按钮770 if (MODELSTATE === 'Added') {771 $('#deleteitem-btn-group').hide();772 } else {773 $('#deleteitem-btn-group').show();774 }775 776 //回车光标移动到下个输入控件777 //日期类型 注册日期778 $('#RegisterDate').datebox('textbox').bind('keydown', function (e) {779 if (e.keyCode == 13) {780 $(e.target).emulateTab();781 }782 });783 }784 </script>785 }View Code
上面View层的代码非常的复杂,但都是固定格式,可以用scaffold快速生成
- 配置依赖注入(DI),注册服务
打开 startup.cs 在 public void ConfigureServices(IServiceCollection services) 注册服务 services.AddScoped<IRepositoryX, RepositoryX>();
services.AddScoped<ICustomerService, CustomerService>();
- 更新数据库
EF Core Code-First 同步更新数据库
在 Visual Studio.Net
Package Manager Controle 运行
PM>:add-migration create_Company
PM>:update-database
PM>:更新完成
- Debug 运行项目
高级应用
CAP 分布式事务的解决方案及应用场景
nuget 安装组件
PM> Install-Package DotNetCore.CAP
PM> Install-Package DotNetCore.CAP.RabbitMQ
PM> Install-Package DotNetCore.CAP.SqlServer \
- 配置Startup.cs
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddCap(x => 4 { 5 x.UseEntityFramework<SmartDbContext>(); 6 x.UseRabbitMQ("127.0.0.1"); 7 x.UseDashboard(); 8 x.FailedRetryCount = 5; 9 x.FailedThresholdCallback = failed =>10 {11 var logger = failed.ServiceProvider.GetService<ILogger<Startup>>();12 logger.LogError($@"A message of type {failed.MessageType} failed after executing {x.FailedRetryCount} several times, 13 requiring manual troubleshooting. Message name: {failed.Message.GetName()}");14 };15 });16 }View Code
- 发布消息
- 订阅消息
roadmap
- 完善主要的开发文档
- 支持My SQL数据库
- 还会继续重构和完善代码
- 开发Scaffold MVC模板,生成定制化的Controller 和 View 减少开发人员重复工作
- 完善授权访问策略(policy-based authorization)
- 开发Visual Sutdio.net代码生成插件(类似国内做比较好的52abp)
No comments:
Post a Comment