smartadmin.core.urf 這個項目是基於asp.net core 3.1(最新)基礎上參照領域驅動設計(DDD)的理念,並參考目前最為了流行的abp架構開發的一套輕量級的快速開發web application 技術架構,專註業務核心需求,減少重覆代碼,開始構建和發佈,讓初級程式員也能開 ...
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 else 105 { 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 method 118 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 else 107 { 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/:id 116 [HttpGet] 117 public async Task<JsonResult> Delete(int id) 118 { 119 try 120 { 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 try 136 { 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 try 156 { 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 } 16