2020-06-22

基于领域驱动设计(DDD)超轻量级快速开发架构

 

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) 通常实体就是映射到关系数据库中的表,这里说名一下最佳做法和惯例:
  1. 在域层定义:本项目就是(SmartAdmin.Entity.csproj)
  2. 继承一个基类 Entity,添加必要审计类比如:创建时间,最后修改时间等
  3. 必须要有一个主键最好是GRUID(不推荐复合主键),但本项目使用递增的int类型
  4. 字段不要过多的冗余,可以通过定义关联关系
  5. 字段属性和方法尽量使用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)

我的联系方式,qq群,赞助二维码

基于领域驱动设计(DDD)超轻量级快速开发架构

No comments:

Post a Comment