码迷,mamicode.com
首页 > Web开发 > 详细

ASP.NET CORE小试牛刀:干货(完整源码)

时间:2017-07-17 09:55:33      阅读:264      评论:0      收藏:0      [点我收藏+]

标签:数据库   12px   不能   bootstrap   增删查改   edm   服务层   val   tar   

扯淡

.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。整个项目结构如下: 

技术分享

常规的分层,简单介绍下各层:
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 的一个前端界面:

技术分享

其实,在一些项目中,与上图类似的界面不少。正常情况下,如果我们走拷贝粘贴然后修改的路子,会出现很多重复代码,比如图中各个按钮点击事件绑定,弹框逻辑等等,写多了会非常蛋疼。前面提到过,我们要将拷贝的代码和改动点尽量少!怎么办呢?继承和抽象!我们只要把“重复雷同”的代码放到一个基类里,每个页面的 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")

<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 对象。

对于 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

ASP.NET CORE小试牛刀:干货(完整源码)

标签:数据库   12px   不能   bootstrap   增删查改   edm   服务层   val   tar   

原文地址:http://www.cnblogs.com/so9527/p/7168358.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!