ASP.NET CORE小試牛刀:乾貨(完整源碼)

扯淡

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

項目介紹

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

image

常規的分層,簡單介紹下各層:
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 上,有一些看過源碼的同窗表示看不懂,因此,我也簡單介紹下其中的一些設計思路及風格。linux

前端freestyle

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

image

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

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 代碼條理清晰、簡潔:ajax

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 同理。這應該也算是面向對象開發思想的基本運用吧。經過公共代碼提取和抽象,開發一個新頁面,咱們只須要修改變更的邏輯便可。sql

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

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

@this.Partial("Index-js")

<div class="topPanel">
    <div class="toolbar">
        <div class="btn-group">
            <a class="btn btn-primary" onclick="$ace.reload()"><span class="glyphicon glyphicon-refresh"></span></a>
        </div>
        <div class="btn-group">
            <button class="btn btn-primary" data-bind="click:Edit,attr:{disabled:!DataTable.SelectedModel()}"><i class="fa fa-pencil-square-o"></i>修改菜單</button>
            <button class="btn btn-primary" data-bind="click:Delete,attr:{disabled:!DataTable.SelectedModel()}"><i class="fa fa-trash-o"></i>刪除菜單</button>
            <button class="btn btn-primary" data-bind="click:Add"><i class="fa fa-plus"></i>新建菜單</button>
        </div>
    </div>
    <div class="search">
        <table>
            <tr>
                <td>
                    <div class="input-group">
                        <input id="txt_keyword" type="text" class="form-control" placeholder="請輸入要查詢關鍵字" style="width: 200px;" data-bind="value:SearchModel().keyword">
                        <span class="input-group-btn">
                            <button id="btn_search" type="button" class="btn  btn-primary" data-bind="click:Search"><i class="fa fa-search"></i></button>
                        </span>
                    </div>
                </td>
            </tr>
        </table>
    </div>
</div>

<!-- 頁面數據 -->
<div class="table-responsive">
    <table class="table table-hover" data-bind="with:DataTable">
        <thead>
            <tr>
                <th style="width:20px;"></th>
                <th>名稱</th>
                <th>文檔</th>
                <th>文檔標籤</th>
                <th>是否顯示</th>
                <th>排序</th>
            </tr>
        </thead>
        <tbody data-bind="foreach:Models">
            <tr data-bind="click:$parent.SelectRow, attr: { id: $data.Id, 'parent-id': $data.ParentId }">
                <td data-bind="text:$parent.GetOrdinal($index())"></td>
                <td>
                    <!-- ko if: $data.HasChildren -->
                    <div onclick="expandChildren(this);" style="left:0px;cursor:pointer;" class="glyphicon glyphicon-triangle-bottom" data-bind=""></div>
                    <!-- /ko -->
                    <!-- ko if: !$data.HasChildren() -->
                    <div style="width:12px;height:12px;display:inline-block;"></div>
                    <!-- /ko -->
                    <span data-bind="html:appendRetract($data.Level())"></span>
                    <span data-bind="text:$data.Data.Name"></span>
                </td>
                <td>
                    <a href="#" target="_blank" data-bind="text:$ace.getOptionTextByValue($root.Documents(),$data.Data.DocumentId(),'Id','Title'),attr:{href:'@Url.Content("~/WikiManage/WikiDocument/Document?id=")' + $data.Data.DocumentId()}"></a>
                </td>
                <td data-bind="text:$ace.getOptionTextByValue($root.Documents(),$data.Data.DocumentId(),'Id','Tag')"></td>
                <td data-bind="boolString:$data.Data.IsEnabled"></td>
                <td data-bind="boolString:$data.Data.SortCode"></td>
            </tr>
        </tbody>
    </table>
</div>

<!-- 表單模態框 -->
<dialogbox data-bind="with:Dialog">

    <form id="form1">
        <table class="form">
            <tr>
                <td class="formTitle">上級</td>
                <td class="formValue">
                    <select id="ParentId" name="ParentId" class="form-control" data-bind="options:$root.RootMenuItems,optionsText:'Name',optionsValue:'Id', optionsCaption:'-請選擇-',value:Model().ParentId"></select>
                </td>
                <td class="formTitle">名稱</td>
                <td class="formValue">
                    <input id="Name" name="Name" type="text" class="form-control required" placeholder="請輸入名稱" data-bind="value:Model().Name" />
                </td>
            </tr>
            <tr>
                <td class="formTitle">文檔</td>
                <td class="formValue">
                    <select id="DocumentId" name="DocumentId" class="form-control" data-bind="options:$root.Documents,optionsText:'Title',optionsValue:'Id', optionsCaption:'-請選擇-',value:Model().DocumentId"></select>
                </td>

                <td class="formTitle">是否顯示</td>
                <td class="formValue">
                    <label><input type="radio" name="IsEnabled" value="true" data-bind="typedChecked:Model().IsEnabled,dataType:'bool'" /></label>
                    <label><input type="radio" name="IsEnabled" value="false" data-bind="typedChecked:Model().IsEnabled,dataType:'bool'" /></label>
                </td>
            </tr>
            <tr>
                <td class="formTitle">排序</td>
                <td class="formValue">
                    <input id="SortCode" name="SortCode" type="text" class="form-control" placeholder="請輸入排序" data-bind="value:Model().SortCode" />
                </td>
            </tr>
        </table>
    </form>

</dialogbox>
    
View Code

後端freestyle

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

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

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

[HttpPost]
public ActionResult Update(UpdateWikiMenuItemInput input)
{
    IWikiMenuItemAppService service = this.CreateService<IWikiMenuItemAppService>();
    service.Update(input);
    return this.UpdateSuccessMsg();
}
[HttpPost]
public ActionResult Delete(string id)
{
    IWikiMenuItemAppService service = this.CreateService<IWikiMenuItemAppService>();
    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<WikiMenuItem, AddWikiMenuItemInput>(input);
        return entity;
    }

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

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

        this.DbContext.DeleteByKey<WikiMenuItem>(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;
            }
        }

        /// <summary>
        ///  將錯誤記錄進日誌
        /// </summary>
        /// <param name="filterContext"></param>
        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", "#R#").Replace("\n", "#N#").Replace("|", "#VERTICAL#");
        }
    }
View Code

結語

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

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

源碼地址:https://github.com/shuxinqin/Ace

相關文章
相關標籤/搜索