當前位置:
首頁 > 知識 > ASP.NET CORE小試牛刀:乾貨

ASP.NET CORE小試牛刀:乾貨


扯淡

.NET Core 的推出讓開發者欣喜萬分,從封閉到擁抱開源十分振奮人心。對跨平台的支持,也讓咱.NET開發者體驗了一把 Write once,run any where 的感覺!近期離職後,時間比較充裕,我也花了些時間學習了 ASP.NET Core 開發,並且成功將之前的一個小網站 www.52chloe.com極其後台管理移植成 ASP.NET Core,並部署到 linux 上。項目完整源碼已經提交到github,感興趣的可以看看,希望對大家有用。


項目介紹

前端以 MVVM 框架 knockout.js 為主,jQuery 為輔,css 使用 bootstrap。後端就是 ASP.NET Core + AutoMapper + Chloe.ORM,日誌記錄使用 NLog。整個項目結構如下:

ASP.NET CORE小試牛刀:乾貨

常規的分層,簡單介紹下各層:

Ace:項目架構基礎層,裡面包含了一些基礎介面的定義,如應用服務介面,以及很多重用性高的代碼。同時,我在這個文件夾下建了 Ace.Web 和 Ace.Web.Mvc 兩個dll,分別是對 asp.net core 和 asp.net core mvc 的一些公共擴展和通用的方法。這一層里的東西,基本都是不跟任何業務掛鉤重用度極高的代碼,而且是比較方便移植的。

Application:應(業)用(務)服(邏)務(輯)層。不同模塊業務邏輯可以放在不同的 dll 中。規範是 Ace.Application.{ModuleName},這樣做的目的是隔離不同的功能模塊代碼,避免所有東西都塞在一個 dll 里。

Data:數據層。包含實體類和ORM操作有關的基礎類。不同模塊的實體同樣可以放在不同的 dll 中。

Web:所謂的展示層。

由於LZ個人對開發規範很在(潔)意(癖),多年來一直希望打造一個符合自己的代碼規範。無論是寫前端 js,還是後端 C#。這個項目.NET Framework版本的源碼很早之前就放在 github 上,有一些看過源碼的同學表示看不懂,所以,我也簡單介紹下其中的一些設計思路及風格。


前端freestyle

做開發都知道,很多時候我們都是在寫一些「雷同」的代碼,特別是在做一些後台管理類的項目,基本都是 CRUD,一個功能需求來了,大多時候是將現有的代碼拷貝一遍,改一下。除了這樣貌似也沒什麼好辦法,哈哈。既然避免不了拷貝粘貼,那我們就讓我們要拷貝的代碼和改動點盡量少吧。我們來分析下一個擁有標準 CRUD 的一個前端界面:

ASP.NET CORE小試牛刀:乾貨

其實,在一些項目中,與上圖類似的界面不少。正常情況下,如果我們走拷貝粘貼然後修改的路子,會出現很多重複代碼,比如圖中各個按鈕點擊事件綁定,彈框邏輯等等,寫多了會非常蛋疼。前面提到過,我們要將拷貝的代碼和改動點盡量少!怎麼辦呢?繼承和抽象!我們只要把「重複雷同」的代碼放到一個基類里,每個頁面的 ViewModel 繼承這個基類就好了,開發的時候頁面的 ViewModel 實現變動的邏輯即可 。ViewModelBase 如下:

function ViewModelBase {
var me = this;

me.SearchModel = _ob({});
me.DeleteUrl = null;
me.ModelKeyName = "Id"; /* 實體主鍵名稱 */

/* 如有必要,子類需重寫 DataTable、Dialog */
me.DataTable = new PagedDataTable(me);
me.Dialog = new DialogBase;

/* 添加按鈕點擊事件 */
me.Add = function {
EnsureNotNull(me.Dialog, "Dialog");
me.Dialog.Open(null, "添加");
}

/* 編輯按鈕點擊事件 */
me.Edit = function {
EnsureNotNull(me.DataTable, "DataTable");
EnsureNotNull(me.Dialog, "Dialog");
me.Dialog.Open(me.DataTable.SelectedModel, "修改");
}

/* 刪除按鈕點擊事件 */
me.Delete = function {
$ace.confirm("確定要刪除該條數據嗎?", me.OnDelete);
}

me.OnDelete = function {
DeleteRow;
}
/* 要求每行必須有 Id 屬性,如果主鍵名不是 Id,則需要重寫 me.ModelKeyName */
function DeleteRow {
if (me.DeleteUrl == null)
throw new Error("未指定 DeleteUrl");

var url = me.DeleteUrl;
var params = { id: me.DataTable.SelectedModel[me.ModelKeyName] };
$ace.post(url, params, function (result) {
var msg = result.Msg || "刪除成功";
$ace.msg(msg);
me.DataTable.RemoveSelectedModel;
});
}

/* 搜索按鈕點擊事件 */
me.Search = function {
me.LoadModels;
}

/* 搜索數據邏輯,子類需要重寫 */
me.LoadModels = function {
throw new Error("未重寫 LoadModels 方法");
}

function EnsureNotNull(obj, name) {
if (!obj)
throw new Error("屬性 " + name + " 未初始化");
}
}

ViewModelBase 擁有界面上通用的點擊按鈕事件函數:Add、Edit、Delete以及Search查詢等。Search 方法是界面搜索按鈕點擊時調用的執行事件,內部調用 LoadModels 載入數據,因為每個頁面的查詢邏輯不同, LoadModels 是一個沒有任何實現的方法,因此如果一個頁面有搜索展示數據功能,直接實現該方法即可。這樣,每個頁面的 ViewModel 代碼條理清晰、簡潔:

var _vm;
$(function {
var vm = new ViewModel;
_vm = vm;
vmExtend.call(vm);/* 將 vmExtend 的成員擴展到 vm 對象上 */
ko.applyBindings(vm);
vm.Init;
});

function ViewModel {
var me = this;
ViewModelBase.call(me);
vmExtend.call(me);/* 實現繼承 */

me.DeleteUrl = "@this.Href("~/WikiManage/WikiMenu/Delete")";
me.DataTable = new DataTableBase(me);
me.Dialog = new Dialog(me);

me.RootMenuItems = _oba(@this.RawSerialize( ViewBag.RootMenuItems));
me.Documents = _oba(@this.RawSerialize(ViewBag.Documents));
}

/* ViewModel 的一些私有方法,這裡面的成員會被擴展到 ViewModel 實例上 */
function vmExtend {
var me = this;

me.Init = function {
me.LoadModels;
}

/* 重寫父類方法,載入數據,並綁定到頁面表格上 */
me.LoadModels = function {
me.DataTable.SelectedModel(null);
var data = me.SearchModel;
$ace.get("@this.Href("~/WikiManage/WikiMenu/GetModels")", data, function (result) {
me.DataTable.SetModels(result.Data);
}
);
}
}

/* 模態框 */
function Dialog(vm) {
var me = this;
DialogBase.call(me);

/* 打開模態框時觸發函數 */
me.OnOpen = function {
var model = me.EditModel;
if (model) {
var dataModel = model.Data;
var bindModel = $ko.toJS(dataModel);
me.Model(bindModel);
}
else {
me.EditModel(null);
me.Model({ IsEnabled: true });
}
}
/* 點擊保存按鈕時保存表單邏輯 */
me.OnSave = function {
var model = me.Model;

if (!$("#form1").formValid) {
return false;
}

if (me.EditModel) {
$ace.post("@this.Href("~/WikiManage/WikiMenu/Update")", model, function (result) {
$ace.msg(result.Msg);
me.Close;
vm.LoadModels;
}
);
}
else {
$ace.post("@this.Href("~/WikiManage/WikiMenu/Add")", model, function (result) {
$ace.msg(result.Msg);
me.Close;
vm.LoadModels;
if (!result.Data.ParentId) {
vm.RootMenuItems.push(result.Data);
}
}
);
}
}
}

注意上面代碼:ViewModelBase.call(me); 這句代碼會使是 ViewModel 類繼承前面提到過的 ViewModelBase 基類(確切的說不叫繼承,而是將一個類的成員擴展到另外一個類上),通過這種方式,我們就可以少寫一些重複邏輯了。等等,ViewModel 里的 DataTable 和 Dialog 是幹什麼用的?哈哈,其實我是把界面的表格和模態框做了抽象。大家可以這樣理解,Dialog 是屬於 ViewModel 的,但是 Dialog 里的東西(如表單,保存和關閉按鈕極其事件)是 Dialog 自身擁有的,這些其實也是重複通用的代碼,都封裝在 DialogBase 基類里,代碼就不貼了,感興趣的自個兒翻源碼看就好,DataTable 同理。這應該也算是面向對象開發思想的基本運用吧。通過公共代碼提取和抽象,開發一個新頁面,我們只需要修改變動的邏輯即可。

上述提到的 ViewModelBase 和 DialogBase 基類都會放在一個公共的 js 文件里,我們在頁面中引用(布局頁_LayoutPage里)。而 html 頁面,我們只管綁定數據即可:

@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_LayoutPage.cshtml";
}

@this.Partial("Index-js")



名稱 文檔 文檔標籤 是否顯示 排序





上級 名稱
文檔 是否顯示
排序

View Code


後端freestyle

後端核心其實就展示層(控制器層)和應用服務層(業務邏輯層),展示層通過應用服務層定義一些業務介面來交互,他們之間的數據傳輸通過 dto 對象。

對於 post 請求的數據,有一些同學為了圖方便,直接用實體來接收前端數據,不建議大家這麼做。我們是規定必須建一個 model 類來接收,也就是 dto。下面是添加、更新和刪除的示例:

[HttpPost]
public ActionResult Add(AddWikiMenuItemInput input)
{
IWikiMenuItemAppService service = this.CreateService;
WikiMenuItem entity = service.Add(input);
return this.AddSuccessData(entity);
}

[HttpPost]
public ActionResult Update(UpdateWikiMenuItemInput input)
{
IWikiMenuItemAppService service = this.CreateService;
service.Update(input);
return this.UpdateSuccessMsg;
}
[HttpPost]
public ActionResult Delete(string id)
{
IWikiMenuItemAppService service = this.CreateService;
service.Delete(id);
return this.DeleteSuccessMsg;
}

AddWikiMenuItemInput 類:

[MapToType(typeof(WikiMenuItem))]
public class AddWikiMenuItemInput : ValidationModel
{
public string ParentId { get; set; }
[RequiredAttribute(ErrorMessage = "名稱不能為空")]
public string Name { get; set; }
public string DocumentId { get; set; }
public bool IsEnabled { get; set; }
public int? SortCode { get; set; }
}

數據校驗我們使用 .NET 自帶的 Validator,所以我們可以在 dto 的成員上打一些驗證標記,同時要繼承我們自定義的一個類,ValidationModel,這個類有一個 Validate 方法,我們驗證數據是否合法的時候只需要調用下這個方法就好了:dto.Validate。按照常規做法,數據校驗應該在控制器的 Action 里,但目前我是將這個校驗操作放在了應用服務層里。

對於 dto,最終是要與實體建立映射關係的,所以,我們還要給 dto 打個 [MapToType(typeof(WikiMenuItem))] 標記,表示這個 dto 類映射到 WikiMenuItem 實體類。

應用服務層添加、更新和刪除數據實現:

public class WikiMenuItemAppService : AdminAppService, IWikiMenuItemAppService
{
public WikiMenuItem Add(AddWikiMenuItemInput input)
{
input.Validate;
WikiMenuItem entity = this.DbContext.InsertFromDto(input);
return entity;
}

public void Update(UpdateWikiMenuItemInput input)
{
input.Validate;
this.DbContext.UpdateFromDto(input);
}
public void Delete(string id)
{
id.NotNullOrEmpty;

bool existsChildren = this.DbContext.Query(a => a.ParentId == id).Any;
if (existsChildren)
throw new InvalidDataException("刪除失敗!操作的對象包含了下級數據");

this.DbContext.DeleteByKey(id);
}
}

DbContext.InsertFromDto 和 DbContext.UpdateFromDto 是 ORM 擴展的方法,通用的,定義好 dto,並給 dto 標記好映射實體,調用這兩個方法時傳入 dto 對象就可以插入和更新。從 dto 到將數據插進資料庫,有數據校驗,也不用拼 sql!這都是基於 ORM 和 AutoMapper 的配合。

日常開發中,頻繁的寫 try catch 代碼是件很蛋疼的事,因此,我們可以定義一個全局異常處理的過濾器去記錄錯誤信息,配合 NLog 組件,MVC中任何錯誤都會被記錄進文件。所以,如果下載了源碼你會發現,項目中幾乎沒有 try catch 類的代碼。

public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly IHostingEnvironment _env;

public HttpGlobalExceptionFilter(IHostingEnvironment env)
{
this._env = env;
}

public ContentResult FailedMsg(string msg = null)
{
Result retResult = new Result(ResultStatus.Failed, msg);
string json = JsonHelper.Serialize(retResult);
return new ContentResult { Content = json };
}
public void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
return;

//執行過程出現未處理異常
Exception ex = filterContext.Exception;

#if DEBUG
if (filterContext.HttpContext.Request.IsAjaxRequest)
{
string msg = null;

if (ex is Ace.Exceptions.InvalidDataException)
{
msg = ex.Message;
filterContext.Result = this.FailedMsg(msg);
filterContext.ExceptionHandled = true;
return;
}
}

this.LogException(filterContext);
return;
#endif

if (filterContext.HttpContext.Request.IsAjaxRequest)
{
string msg = null;

if (ex is Ace.Exceptions.InvalidDataException)
{
msg = ex.Message;
}
else
{
this.LogException(filterContext);
msg = "伺服器錯誤";
}

filterContext.Result = this.FailedMsg(msg);
filterContext.ExceptionHandled = true;
return;
}
else
{
//對於非 ajax 請求

this.LogException(filterContext);
return;
}
}

///

/// 將錯誤記錄進日誌
///

/// void LogException(ExceptionContext filterContext)
{
ILoggerFactory loggerFactory = filterContext.HttpContext.RequestServices.GetService(typeof(ILoggerFactory)) as ILoggerFactory;
ILogger logger = loggerFactory.CreateLogger(filterContext.ActionDescriptor.DisplayName);

logger.LogError("Error: {0}, {1}", ReplaceParticular(filterContext.Exception.Message), ReplaceParticular(filterContext.Exception.StackTrace));
}

static string ReplaceParticular(string s)
{
if (string.IsNullOrEmpty(s))
return s;

return s.Replace("
", "#R#").Replace("
", "#N#").Replace("|", "#VERTICAL#");
}
}

View Code


結語

咱做開發的,避免不了千篇一律的增刪查改,所以,我們要想盡辦法 write less,do more!這個項目只是一個入門學習的demo,並沒什麼特別的技術,但裡面也凝聚了不少LZ這幾年開發經驗的結晶,希望能對一些猿友有用。大家有什麼問題或建議可以留言討論,也歡迎各位入群暢談.NET復興大計(群號見左上角)。最後,感謝大家閱讀至此!

該項目使用的是vs2017開發,資料庫默認使用 SQLite,配置好 SQLite 的db文件即可運行。亦支持 SqlServer 和 MySql,在項目找到相應的資料庫腳本,運行腳本創建相關的表後修改配置文件(configs/appsettings.json)內資料庫連接配置即可。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 達人科技 的精彩文章:

業餘草教你解讀Spark源碼閱讀之HistoryServer
「PHP」PHP面向對象編程——phpOOP入門
將git版本號編譯進程序

TAG:達人科技 |

您可能感興趣

ASP.NET的簡介
ASP.NET Core Web API與SSL
徠卡APO-SUMMICRON-SL 75MM F/2 ASPH
ASP.NET編程
松下發布LEICA DG SUMMILUX 25mm F1.4 II ASPH.鏡頭
LEICA SUMMILUX-SL 50MM F/1.4 ASPH 測評:徠卡光學新標準
VAN TJALLE EN JASPER燈具荷蘭進口時尚吊燈-義大利之家
吃貨會不會心動?ASPHALTGOLD X REEBOK 全新「披薩」聯名
ASP.NET Core MVC+EF Core從開發到部署
ASP.NET Web Pages-HTML 表單
ASP - AJAX 與 ASP
ASP.NET Core MVC 2.1 頂級參數驗證
ASP.NET Web Forms XML 文件
ASP.NET Web Forms編程HTML 頁面
ORICO硬碟盒:JMS578主控,UASP+TRIM雙加速,僅售59元你想要嗎
微軟宣布 ASP.NET Core 3.0 只運行在.NET Core 上
微軟宣布 ASP.NET Core 3.0 只運行在 .NET Core 上
ASP.NET Web Forms 編程HTML 表單
盛世英雄匯,ASPENCORE全球CEO峰會報道紀實(上)
ASP.NET Core URL Rewrite中間件