ASP.NET CORE小试牛刀:干货

ASP.NET CORE小试牛刀:干货

  • 商品编号:
    #12507736_125
    • 原价:
      免费
    • 会员价:
      免费
  • 分类:
    • V完整版
  • 数量:
    限购1件

购物车中已存在此商品(限购一件),单击跳转购物车

  • 开发者:so_while
  • 开发语言:.net core
  • 开发环境:Visual Studio 2017
  • 数据库:SQLite/MySql/SqlServer
  • 商品架构:B/S
  • 代码管理工具:github
  • 大小(M):0 M
  • 编码格式:utf-8
  • 是否开源:是
  • 开源协议:BSD

扯淡

.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>

后端freestyle

后端核心其实就展示层(控制器层)和应用服务层(业务逻辑层),展示层通过应用服务层定义一些业务接口来交互,他们之间的数据传输通过 dto 对象。

对于 post 请求的数据,有一些同学为了图方便,直接用实体来接收前端数据,不建议大家这么做。我们是规定必须建一个 model 类来接收,也就是 dto。下面是添加、更新和删除的示例:

AddWikiMenuItemInput 类:[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#");
        }
    }

结语

咱做开发的,避免不了千篇一律的增删查改,所以,我们要想尽办法 write less,do more!这个项目只是一个入门学习的demo,并没什么特别的技术,但里面也凝聚了不少LZ这几年开发经验的结晶,希望能对一些猿友有用。大家有什么问题或建议可以留言讨论,也欢迎各位入群畅谈.NET复兴大计(群号见左上角)。最后,感谢大家阅读至此!

该项目使用的是vs2017开发,数据库默认使用 SQLite,配置好 SQLite 的db文件即可运行。亦支持 SqlServer 和 MySql,在项目找到相应的数据库脚本,运行脚本创建相关的表后修改配置文件(configs/appsettings.json)内数据库连接配置即可。


权利声明:本站所有商品信息、客户评价等信息是初心商城重要的数据资源,未经许可,禁止非法转载使用。 注:本站商品信息均来自初心商城,其真实性、准确性和合法性由初心商城负责。

                  初心源说明:初心商城主要为程序员提供开发基础的代码源以及成熟项目,网站中所有的商品有提供收费版本的, 也有提供免费版本的,按照大家各自不同的需求进行购买。实实在在的让程序员只用专注于自己的业务实现你的小梦想, 如果您对我们的成果表示认同并且觉得对你有所帮助我们愿意接受来自各方面的支持^_^。

                  支持:用手机扫描二维码支付

                  支付宝支持我们 微信支持我们

                  您的支持将被用于:
                  1、持续深入的上传更多更好的源代码
                  2、建立更加完善的技术社区
                  3、完善现在系统出现各种问题
                  4、购买域名和租赁服务器

                  1、交易规则

                  2、发货方式

                  1、自动:在上方保障服务中标有自动发货的商品,拍下后,将会自动收到来自卖家的商品获取(下载)链接

                  2、手动:在上方保障服务中标有手动发货的商品,拍下后,卖家会收到邮件,也可通过QQ或订单中的电话联系对方。

                  3、退款说明

                  1、描述:源码描述(含标题)与实际源码不一致的(例:描述PHP实际为ASP、描述的功能实际缺少、版本不符等)

                  2、演示:有演示站时,与实际源码小于95%一致的(但描述中有"不保证完全一样、有变化的可能性"类似显著声明的除外)

                  3、发货:手动发货源码,在卖家未发货前,已申请退款的

                  4、服务:卖家不提供安装服务或需额外收费的(但描述中有显著声明的除外)

                  5、其它:如质量方面的硬性常规问题等

                  备注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。交易中的商品,卖家无法对描述进行修改!

                  4、注意事项

                  1、客户买完之后未确认收货,将不会收到下载地址和下载码,确认收货之后才能收到下载地址和下载码。

                  2、在未拍下前,双方在QQ上所商定的内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准);

                  3、在商品同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外);

                  4、在没有"无任何正当退款依据"的前提下,写有"一旦售出,概不支持退款"等类似的声明,视为无效声明;

                  5、虽然交易产生纠纷的几率很小,但请尽量保留如聊天记录这样的重要信息,以防产生纠纷时出现问题不明确的情况。

                  5、交易声明

                  1、本站作为直卖平台,依据交易合同(商品描述、交易前商定的内容)来保障交易的安全及买卖双方的权益;

                  2、非平台线上交易的商品,出现任何后果均与本站无关;无论卖家以何理由要求线下交易的,请联系管理举报。

                  初心Logo

                  初心商城| 初心系列| 初心博客| 初心历史| 系统反馈

                  chuxinm.com 京ICP备16055626号 © 2016-2018 山西米立信息技术有限公司 保留所有权利
                  违法和不良信息举报电话:186-2950-9347,本网站所列数据,除特殊说明,所有数据均出自我工作室
                  本网站兼容所有主流浏览器,不支持手机自适应