ASP.NETCORE⼩试⽜⼑:⼲货(完整源码)
扯淡
.NET Core 的推出让开发者欣喜万分,从封闭到拥抱开源⼗分振奋⼈⼼。对跨平台的⽀持,也让咱.NET开发者体验了⼀把 Write once,run any where 的感觉!近期离职后,时间⽐较充裕,我也花了些时间学习了 ASP.NET Core 开发,并且成功将之前的⼀个⼩⽹站极其后台管理移植成 ASP.NET Core,并部署到 linux 上。项⽬完整源码已经提交到,感兴趣的可以看看,希望对⼤家有⽤。
项⽬介绍
前端以 MVVM 框架 knockout.js 为主,jQuery 为辅,css 使⽤ bootstrap。后端就是 ASP.NET Core + AutoMapper + Chloe.ORM,⽇志记录使⽤ NLog。整个项⽬结构如下:
常规的分层,简单介绍下各层:
Ace:项⽬架构基础层,⾥⾯包含了⼀些基础接⼝的定义,如应⽤服务接⼝,以及很多重⽤性⾼的代码。同时,我在这个⽂件夹下建了Ace.Web 和 Ace.Web.Mvc 两个dll,分别是对 asp core 和 asp 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 () {
$firm("确定要删除该条数据吗?", 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();
$("@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();
asp 源代码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 页⾯,我们只管绑定数据即可:
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 类的代码。
View Code
结语
咱做开发的,避免不了千篇⼀律的增删查改,所以,我们要想尽办法 write less,do more!这个项⽬只是⼀个⼊门学习的demo,并没什么特别的技术,但⾥⾯也凝聚了不少LZ这⼏年开发经验的结晶,希望能对⼀些猿友有⽤。⼤家有什么问题或建议可以留⾔讨论,也欢迎各位⼊畅谈.NET复兴⼤计(号见左上⾓)。最后,感谢⼤家阅读⾄此!
该项⽬使⽤的是vs2017开发,数据库默认使⽤ SQLite,配置好 SQLite 的db⽂件即可运⾏。亦⽀持 SqlServer 和 MySql,在项⽬到相应的数据库脚本,运⾏脚本创建相关的表后修改配置⽂件(configs/appsettings.json)内数据库连接配置即可。
源码地址:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论