扯淡

.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。下面是添加、更新和删除的示例:

[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)内数据库连接配置即可。

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

ASP.NET CORE小试牛刀:干货(完整源码)的更多相关文章

  1. 各类最新Asp .Net Core 项目和示例源码

    1.网站地址:http://www.freeboygirl.com2.网站Asp .Net Core 资料http://www.freeboygirl.com/blog/tag/asp%20net%2 ...

  2. ASP.NET CORE 启动过程及源码解读

    在这个特殊的春节,大家想必都在家出不了们,远看已经到了回城里上班的日子,但是因为一只蝙蝠的原因导致我们无法回到工作岗位,大家可能有的在家远程办公,有些在家躺着看书,有的是在家打游戏:在这个特殊无聊的日 ...

  3. ASP.Net Core Configuration 理解与源码分析

    Configuration 在ASP.NET Core开发过程中起着很重要的作用,这篇博客主要是理解configuration的来源,以及各种不同类型的configuration source是如何被 ...

  4. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...

  5. teprunner测试平台Django引入pytest完整源码

    本文开发内容 pytest登场!本文将在Django中引入pytest,原理是先执行tep startproject命令创建pytest项目文件,然后从数据库中拉取代码写入文件,最后调用pytest命 ...

  6. 网狐6603 cocos2dx 棋牌、捕鱼、休闲类游戏《李逵捕鱼》手机端完整源码分析及分享

    该资源说明: cocos2d 棋牌.捕鱼.休闲类游戏<李逵捕鱼>手机端完整源码,网狐6603配套手机版源码,可以选桌子,适合新手学习参考,小编已亲测试,绝对完整可编译手机端,下载后将文件考 ...

  7. 在WebBrowser中执行javascript脚本的几种方法整理(execScript/InvokeScript/NavigateScript) 附完整源码

    [实例简介] 涵盖了几种常用的 webBrowser执行javascript的方法,详见示例截图以及代码 [实例截图] [核心代码] execScript方式: 1 2 3 4 5 6 7 8 9 1 ...

  8. 支持语音识别、自然语言理解的微信小程序(“遥知之”智能小秘)完整源码分享

    记录自己搭建https的silk录音文件语音识别服务的调用过程,所有代码可在文中找链接打包下载 >>>>>>>>>>>>> ...

  9. 微信小程序中如何使用WebSocket实现长连接(含完整源码)

    本文由腾讯云技术团队原创,感谢作者的分享. 1.前言   微信小程序提供了一套在微信上运行小程序的解决方案,有比较完整的框架.组件以及 API,在这个平台上面的想象空间很大.腾讯云研究了一番之后,发现 ...

随机推荐

  1. HTML5之2D物理引擎 Box2D for javascript Games 系列 第三部分之创建图腾破坏者的关卡

    创建图腾破坏者的关卡 现在你有能力创建你的第一个游戏原型,我们将从创建图腾破坏者的级别开始. 为了展示我们所做事情的真实性,我们将流行的Flash游戏图腾破坏者的一关作为 我们模仿的对象.请看下面的截 ...

  2. css清除浮动的八大方法

    清除浮动是每一个 web前台设计师必须掌握的机能.css清除浮动大全,共8种方法. 浮动会使当前标签产生向上浮的效果,同时会影响到前后标签.父级标签的位置及 width height 属性.而且同样的 ...

  3. (转)java web 学习之路(学习顺序)

    第一步:学习HTML和CSS HTML(超文本标记语言)是网页的核心,学好HTML是成为Web开发人员的基本条件.HTML很容易学习的,但也很容易误用,要学精还得费点功夫. 随着HTML5的发展和普及 ...

  4. Mac OS X 安装后的简单设置

    让Mac拥有类似apt-get的功能--安装Homebrew Homebrew是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具(比如著名的wget). 国内下载地址:http://ww ...

  5. dd的用法

    1.生成一个空的,大小为1G的文件(有洞的文件)$ dd if=/dev/zero of=winxp.img bs=1k seek=1024k count=1 2.读软盘,并以16进制保存到文件中#d ...

  6. C++汉诺塔递归实现

    程序背景: 汉诺塔(Tower of Hanoi)又称河内塔,问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘.大梵天命 ...

  7. 动态读取文件持续显示在UI上

    private void DisplayLogInfo(FileInfo _LastFile) { if (_LastFile != null) { StreamReader sr = null; t ...

  8. [Linux] PHP程序员玩转Linux系列-Ubuntu配置SVN服务器并搭配域名

    在线上部署网站的时候,大部分人是使用ftp,这样的方式很不方便,现在我要在线上安装上SVN的服务器,直接使用svn部署网站.因为搜盘子的服务器是ubuntu,因此下面的步骤是基于ubuntu的. 安装 ...

  9. input响应慢问题解决办法

    input[file]标签的accept属性可用于指定上传文件的 MIME类型 . 例如,想要实现默认上传图片文件的代码,代码可如下: <input type="file" ...

  10. C++之const限定符

    作者:tongqingliu 转载请注明出处: C++之const限定符 const初始化 const的特点: 用const加以限定的变量,无法改变. 由于const对象定义之后就无法改变,所以必须对 ...