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。整个项目结构如下:
常规的分层,简单介绍下各层:
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小试牛刀:干货(完整源码)的更多相关文章
- 各类最新Asp .Net Core 项目和示例源码
1.网站地址:http://www.freeboygirl.com2.网站Asp .Net Core 资料http://www.freeboygirl.com/blog/tag/asp%20net%2 ...
- ASP.NET CORE 启动过程及源码解读
在这个特殊的春节,大家想必都在家出不了们,远看已经到了回城里上班的日子,但是因为一只蝙蝠的原因导致我们无法回到工作岗位,大家可能有的在家远程办公,有些在家躺着看书,有的是在家打游戏:在这个特殊无聊的日 ...
- ASP.Net Core Configuration 理解与源码分析
Configuration 在ASP.NET Core开发过程中起着很重要的作用,这篇博客主要是理解configuration的来源,以及各种不同类型的configuration source是如何被 ...
- 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)
本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...
- teprunner测试平台Django引入pytest完整源码
本文开发内容 pytest登场!本文将在Django中引入pytest,原理是先执行tep startproject命令创建pytest项目文件,然后从数据库中拉取代码写入文件,最后调用pytest命 ...
- 网狐6603 cocos2dx 棋牌、捕鱼、休闲类游戏《李逵捕鱼》手机端完整源码分析及分享
该资源说明: cocos2d 棋牌.捕鱼.休闲类游戏<李逵捕鱼>手机端完整源码,网狐6603配套手机版源码,可以选桌子,适合新手学习参考,小编已亲测试,绝对完整可编译手机端,下载后将文件考 ...
- 在WebBrowser中执行javascript脚本的几种方法整理(execScript/InvokeScript/NavigateScript) 附完整源码
[实例简介] 涵盖了几种常用的 webBrowser执行javascript的方法,详见示例截图以及代码 [实例截图] [核心代码] execScript方式: 1 2 3 4 5 6 7 8 9 1 ...
- 支持语音识别、自然语言理解的微信小程序(“遥知之”智能小秘)完整源码分享
记录自己搭建https的silk录音文件语音识别服务的调用过程,所有代码可在文中找链接打包下载 >>>>>>>>>>>>> ...
- 微信小程序中如何使用WebSocket实现长连接(含完整源码)
本文由腾讯云技术团队原创,感谢作者的分享. 1.前言 微信小程序提供了一套在微信上运行小程序的解决方案,有比较完整的框架.组件以及 API,在这个平台上面的想象空间很大.腾讯云研究了一番之后,发现 ...
随机推荐
- HTML5之2D物理引擎 Box2D for javascript Games 系列 第三部分之创建图腾破坏者的关卡
创建图腾破坏者的关卡 现在你有能力创建你的第一个游戏原型,我们将从创建图腾破坏者的级别开始. 为了展示我们所做事情的真实性,我们将流行的Flash游戏图腾破坏者的一关作为 我们模仿的对象.请看下面的截 ...
- css清除浮动的八大方法
清除浮动是每一个 web前台设计师必须掌握的机能.css清除浮动大全,共8种方法. 浮动会使当前标签产生向上浮的效果,同时会影响到前后标签.父级标签的位置及 width height 属性.而且同样的 ...
- (转)java web 学习之路(学习顺序)
第一步:学习HTML和CSS HTML(超文本标记语言)是网页的核心,学好HTML是成为Web开发人员的基本条件.HTML很容易学习的,但也很容易误用,要学精还得费点功夫. 随着HTML5的发展和普及 ...
- Mac OS X 安装后的简单设置
让Mac拥有类似apt-get的功能--安装Homebrew Homebrew是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具(比如著名的wget). 国内下载地址:http://ww ...
- dd的用法
1.生成一个空的,大小为1G的文件(有洞的文件)$ dd if=/dev/zero of=winxp.img bs=1k seek=1024k count=1 2.读软盘,并以16进制保存到文件中#d ...
- C++汉诺塔递归实现
程序背景: 汉诺塔(Tower of Hanoi)又称河内塔,问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘.大梵天命 ...
- 动态读取文件持续显示在UI上
private void DisplayLogInfo(FileInfo _LastFile) { if (_LastFile != null) { StreamReader sr = null; t ...
- [Linux] PHP程序员玩转Linux系列-Ubuntu配置SVN服务器并搭配域名
在线上部署网站的时候,大部分人是使用ftp,这样的方式很不方便,现在我要在线上安装上SVN的服务器,直接使用svn部署网站.因为搜盘子的服务器是ubuntu,因此下面的步骤是基于ubuntu的. 安装 ...
- input响应慢问题解决办法
input[file]标签的accept属性可用于指定上传文件的 MIME类型 . 例如,想要实现默认上传图片文件的代码,代码可如下: <input type="file" ...
- C++之const限定符
作者:tongqingliu 转载请注明出处: C++之const限定符 const初始化 const的特点: 用const加以限定的变量,无法改变. 由于const对象定义之后就无法改变,所以必须对 ...