一、ASP.Net MVC简介
1,什么是ASP.NET MVC?
HttpHandler是ASP.net的底层机制,如果直接使用HttpHandler进行开发难度比较大、工作量大。因此提供了ASP.Net MVC、
ASP.Net WebForm等高级封装的框架,简化开发,他们的底层仍然是HttpHandler、HttpRequest等
例如:ASP.NET MVC的核心类仍然是实现了IHttpHandler接口的MVCHandler
2,ASP.NET WebForm和ASP.NET MVC的关系?
两者都是对HttpHandler的封装框架,ASP.NET MVC的思想,更适合现代项目的开发,因此会逐步取代WebForm
3,为什么ASP.NET MVC更好
程序员有更强的掌控力,不会产生垃圾代码;程序员能够更清晰的控制运行过程,因此更安全、性能和架构等更清晰。
入门“难”,深入“相对比较简单”
4,什么是MVC模式
模型(Model)、视图(View)、控制器(Controller)
Model负责在View和控制器之间进行数据的传递(用户输入的内容封装成Model对象,发送给Controller);
要显示的数据由Controller放到Model中,然后扔给View去显示。
Controller不直接和View交互
5,ASP.Net MVC与“三层架构”没有任何关系。
唯一的“关系”:三层中的UI层可以用ASP.Net MVC来实现
6,“约定大于配置”:
二、ASP.Net MVC起步
1,项目的创建
新建项目——C#——Web——ASP.NET Web应用程序(不要勾选“将Application Insights添加到项目”)——确定;
选中“Empty”——勾选MVC(不要勾选Host in the cloud)——确定
2,控制器的建立和视图的建立
在Controller文件夹下右键——添加——控制器——选择“MVC5控制器-空”,
注意:类的名字以Controller结尾,会自动在View文件夹下创建一个对应名字的文件夹(没有就手动创建文件夹)
在View/文件夹名字 下创建视图Index(和XXXController的Index方法一致)
注意:添加视图时,模板选择Empty,不要勾选创建为分部视图和使用布局页
3,新建一个用来收集用户参数的类
IndexReqModel(类名无所谓,可以随便起)包含Num1、Num2两个属性(只要不重名,大小写都可以)
然后声明一个IndexRespModel类用来给view传递数据显示,有Num1、Num2、Result。
也可以同一个类实现,但是这样写看起来比较清晰
代码:
public class TestControler:Controller { public ActionResult Index(IndexReqModel model) { IndexReqModel resq = new IndexReqModel(); resq.num1 = model.Num1; resq.num2 = model.Num2; resq.result = model.Num1 + model.Num2; return View(resq); } }
4,Index.cshtml的代码
@model Test1.Models.IndexReqModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> <input type="text" value="@Model.Num1" />+<input type="text" value="@Model.Num2" />=@Model.Result </div> </body> </html>
5,在浏览器访问:http://localhost:56919/Test/Index?num1=1&num2=2
6,执行过程、数据流动分析:
当用户访问“Test/Index?num1=1&num2=2”的时候,会找到Controller下的TestController的Index方法去执行,
把请求参数按照名字填充到Index方法的参数对象中(MVC引擎负责创建对象,给数据赋值,并且进行类型的转换),
return View(resq)就会找到Views下的和自己的“类名、方法名”相对应的Index.cshtml,然后把数据resp给到Index.cshtml去显示。
注意:
1,@model Test1.Models.IndexReqModel //这里的model要小写开头,表示传递过来的数据是IndexReqModel类型的
2,@Model指传递过来的对象 //这里的Model要大写开头
3,cshtml模板就是简化HTML的拼接的模板,最终还是生成html给浏览器显示,不能直接访问cshtml文件
三、Razor语法
1,语法简单:
@启动的区域为标准的C#代码,其他部分是普通的html代码
2,用法:
@{string a = "abc";} @a @{C#代码块} //有标签的就是html代码
@Model //控制器传递来的对象
@Model.dog.Name //控制器传递来的dog对象的Name属性的值
@if(),@foreach()等C#语句
3,在代码中输入大段文字
两种方法:
1,@:大段文字 //不推荐使用了,
代码:
if(Model.IsOK) { @:文字 }
2,<html标签>文字</html标签>
代码:
if(Model.IsOK) { <span>文字</span> }
razor会智能识别哪块是C#,哪块是HTML,HTML中想运行C#代码就用@,想在C#中代码中输入HTML就写“HTML标签”。
但是如果由于样式等原因不想加上额外的标签,那么可以用<text></text>标记,特殊的<text>不会输出到Html中。
4,注意:不要在@item后写分号 //分号会被当成html代码,原样输出
5,razor会自动识别哪块是普通字符,哪块是表达式,主要就是根据特殊符号来分辨(“识别到这里是否能被当成一个合法的C#语句”)。
例子:
不能这样写 <a href="Course@CourseId.ashx">,否则ashx会被识别为CourseId的一个属性,
应该加上()强制让引擎把CourseId识别成一个单独的语法,<a href="Course(@CourseId).ashx">
技巧:
不确定的地方就加上(),也可以按照编辑器的代码着色来进行分辨
6,如果不能自动提示,把页面关掉再打开就可以了。如果还是不能自动提示,只要运行没问题就行。
cshtml文件中如果有警告甚至错误,只要运行没问题就没关系
7,<span>333@qq.com</span>,razor会自动识别出来是邮箱,所以razor不会把 @qq.com当成qq对象的com属性。
但是对于特殊的邮箱或者就是要显示@,那么可以使用@转义@,也就是“@@”
<li>item_@item.Length</span>//会把@item.Length识别成邮箱, 因此用上()成为: <li>item_@(item.Length)</span>
8,易错:
要区分C#代码和html代码,
正确的:style='display:(@message.IsHide?"none":"block")'
错误的:style="display: (@message.IsHide) ? none : block"
注意:
为了避免C#中的字符串的“”和html的属性值的“”冲突,建议如果html属性中嵌入了C#代码,那么html的属性的值用单引号
9,为了避免XSS攻击(跨站脚本攻击,在输出对象中嵌入script代码等恶意代码),Razor的@会自动把内容进行htmlencode输出,
如果不想编码后输出,使用@Html.Raw()方法
10,Razor的注释方法
@*要注释的内容*@
11,Razor中调用泛型方法的时候,由于<>会被认为是html转回标记模式,因此要用()括起来,比如@(Html.Test<string>)
()可以解决大部分问题,在View中一般不会调用复杂的方法
12,如果cshtml中任何html标签的属性中以"~/"开头,则会自动进行虚拟路径的处理,
当然一般是给<script>的src属性、<link>的href属性、<a>标签的href属性、<img>的src属性用的。
13,html标签的任何属性的值如果是C#的值(使用@传递过来的值),
如果是bool类型的值,那么如果值是false,则不会渲染这个属性,如果是true,则会渲染成“属性名=属性名”
代码示例
@{ bool b1 = true; bool b2 = false; } <input type="checkbox" checked="@b1"/>//此时生成的html代码为:<input type="checkbox" checked="checked">
这个特性避免了进行三元运算符的判断
14,总结:
1、@就是C#,<aaa></aaa>就是html
2、如果想让被识别成html的当成C#那就用@()
3、如果想让被识别成C#的当成html,用<span>等标签,如果不想生成额外的标签,就用<text></text>
4、如果不想对内容htmlencode显示就用@Html.Raw()方法
5、属性的值如果以"~/"开头会进行虚拟路径处理
6、属性值如果是bool类型,如果是false就不输出这个属性,如果true就输出“属性名=属性名”<input type="checkbox" checked="@b1"/>
四、知识点补充和复习
1,dynamic是C#语法中提供的一个语法,实现像JavaScript一样的动态语言,可以到运行的时候再去发现属性的值或者调用方法
代码示例
dynamic p = new dynamic(); p.Name = "rupeng.com"; p.Hello();
注意:即使没有成员p.Age=3;编译也不会报错,只有运行的时候才会报错
好处是灵活,坏处是不容易在开发的时候发现错误、并且性能低
如果dynamic指向System.Dynamic.ExpandoObject()对象,这样可以给对象动态赋值属性(不能指向方法):
dynamic p = new System.Dynamic.ExpandoObject(); p.Name = "rupeng.com"; p.Age = 10; Console.WriteLine(p.Name+","+p.Age);
2,var类型推断
var i = 3; var s ="abc";
编译器会根据右边的类型推断出var是什么类型
var和dynamic的区别:
var是编译的时候确定的,dynamic是在运行的时候动态确定的
var变量不能指向其他类型,dynamic可以(因为var在编译的时候已经确定了类型)
3,匿名类型
匿名类型是C#中提供的一个新语法:
var p = new {Age=5,Name="rupeng.com"};//这样就创建了一个匿名类的对象,这个类没有名字,所以叫匿名类
原理:
编译器生成了这个类,这个类是internal、属性是只读的、初始值是通过构造函数传递的
因此:
因为匿名类的属性是只读的,所以匿名类型的属性是无法赋值的;
因为匿名类型是internal,所以无法跨程序集访问其成员(只能活在自己当前的程序集内)。
五、Controller给View传递数据的方式
1,ViewData:
以ViewData["name"]="rupeng";string s =(string)ViewData["name"]这样的键值对的方式进行数据传送
2,ViewBag:
ViewBag是dynamic类型的参数,是对ViewData一个动态类型封装,用起来更方便,和ViewData共同操作一个数据。ViewBag.name="";
@ViewBag.name。
用ViewBag传递数据非常方便,但是因为ASP.Net MVC中的“Html辅助类”等对于ViewBag有一些特殊约定,一不小心就跳坑了(http://www.cnblogs.com/rupeng/p/5138575.html),所以尽量不要用ViewBag,而是使用Model。
3、Model:
可以在Controller中通过return View(model)赋值,然后在cshtml中通过Model属性来访问这个对象;
如果在cshtml中通过“@model 类型”(注意model小写)指定类型,则cshtml中的Model就是指定的强类型的,这样的cshtml叫“强类型视图”;
如果没有指定“@model 类型”, 则cshtml中的Model就是dynamic。
六、关于Action的参数
ASP.Net MVC5会自动对参数做类型转换
对于boolean类型的参数(或者Model的属性),如果使用checkbox,则value必须是“true”,否则值永远是false。对于double、int等类型会自动进行类型转换
1,一个Controller可以有多个方法,这些方法叫Action。通过“Controller名字/方法名”访问的时候就会执行对应的方法。
2,Action的三种类型的参数:
普通参数、Model类、FormCollection
1,普通参数:
Index(string name,int age)。框架会自动把用户Get请求的QueryString或者Post表单中的值根据参数名字映射对应参数的值,
适用于查询参数比较少的情况。
注意:int类型的可空问题
2,Model类。叫ViewModel。
3,FormCollection,采用fc["name"]这种方法访问,类似于HttpHandler中用context["name"]。
适用于表单元素不确定、动态的情况
3,Action的方法不能重载,所以一个Controller中不能存在两个同名的Action
错误代码:
public ActionResult T1(string name)和public ActionResult T1(int Age)不能同时存在
特殊情况:
给Action方法上标注[HttpGet]、[HttpPost],注意当发出Get或者Post请求的时候就会执行相应标注的方法,变相实现了同名的Action
常见的应用方法:
把需要展示的初始页面的Action标注为[HttpGet],把表单提交的标注为[HttpPost]
4,Action参数可以一部分是普通参数,一部分为Model
代码示例:
public ActionResult T1(string name,Classes className)
5,Action参数如果在请求中没有对应的值,就会去默认值:
Model类的形式则取默认值:int是0、boolean是false、引用类型是null。
普通参数的形式:取默认值会报错,如果允许为空,要使用int?,也可以使用C#的可选参数语法来设定默认值
示例代码:
Index(string name="tom");
6,上传文件的参数用HttpPostedFileBase类型,
七、View的查找
1,return View()会查找Views的Controller名字的Action的名字的cshtml
2,return View("Action1"),查找Views的Controller名字下的“Action1.cshtml”,如果找不到则到特殊的shared文件夹下找“Action1.cshtml”
3、return View("Action1")中如何传递model?return View("Action1",model)。
陷阱:如果model传递的是string类型,则需要return View("Action1",(object)str)为什么?看一下重载!
注意:
return View("Action1")不是重定向,浏览器和服务器之间只发生了一次交互,地址栏还是旧的Action的地址。
这和重定向return Redirct("/Index/Action1");不一样
应用:
执行报错,return View("Error",(object)msg) 通用的报错页面。为了防止忘了控制重载,封装成一个通用方法。
八、其他类型的ActionResult
1,View()是一个方法,它的返回值是ViewResult类型,ViewResult继承自ActionResult,
如果在确认返回的是View(),返回值写成ViewResult也行,但是一般没这个必要,因为那样就不灵活了。因为ViewResult还有其他子类
2,RedirectResult,重定向,最终就是调用response.Redirect()。
用法:
return Redirect("http://www.rupeng.com");//重定向到rupeng return Redirect("~/1.html");//重定向到
3,ContentResult
返回程序中直接拼接生成的文本内容
return Content(string content,string contentType)
4,文件 return File();
1,return File(byte[] fileContents,string contentType);//返回byte[]格式的数据
2,return File(byte[] fileContents,string contentType,fileDownLoadName);//fileDownLoadName:设定浏览器端弹出的建议保存的文件名
3,return File(Stream fileStream, string contentType) 返回Stream类型的数据(框架会帮着Dispose,不用也不能Dispose)
4,FileStreamResult return File(Stream fileStream,string contentType,string fileDownLoadName)
5, File(string fileName, string contentType)// 返回文件名指定的文件,内部还是流方式读取文件;
6, File(string fileName, string contentType, string fileDownloadName) //如果是返回动态生成的图片(比如验证码),则不用设置fileDownloadName;如果是“导出学生名单”、“下载文档”等操作则要设定fileDownloadName。
注意:如果在Controller中要使用System.IO下的File类,因为和File方法重名了,所以要用命名空间来引用了。
5,return HttpNotFound();
6,return JavaScript(string script);
返回JavaScript代码字符串,和return Content("alert('Hello World');","application/x-javascript");效果一样。
因为违反三层原则,尽量不要使用
7,Json
JsonResult Json(object data) 把data对象序列化为json字符串返回客户端,并且设置contentType为"application/json"
Json方法默认是禁止Get请求的(主要为了防止CSRF攻击,举例:在A网站中嵌入一个请求银行网站给其他账号转账的Url的img),只能Post请求。所以如果以Get方式访问是会报错的。
如果确实需要以Get方式方式,需要调用return Json(data, JsonRequestBehavior.AllowGet)
ASP.NET MVC 默认的Json方法实现有如下的缺点:
1,日期类型的属性格式化成字符串是“\/Date(1487305054403)\/"这样的格式,在客户端要用js代码格式化处理,很麻烦。
2,json字符串中属性的名字和C#中的大小写一样,不符合js中“小写开头、驼峰命名”的习惯。在js中也要用大写去处理。
3,无法处理循环引用的问题(尽管应该避免循环引用),会报错“序列化类型为***的对象时检测到循环引用”
8,重定向
1,Redirect(string url)
2,RedirectToAction(string actionName,string controllerName);//其实就是帮助拼接生成url,最终还是调用Redirect(),
3,两者的区别:
RedirectToAction是让客户端重定向,是一个新的Http请求,所以无法读取ViewBag中的内容;
return View()是一次服务器一次处理转移
Redirect和return View 的区别:
1、 Redirect是让浏览器重定向到新的地址;return View是让服务器把指定的cshtml的内容运行渲染后给到浏览器;
2、 Redirect浏览器和服务器之间发生了两次交互;return View浏览器和服务器之间发生了1次交互
3、 Redirect由于是两次请求,所以第一次设置的ViewBag等这些信息,在第二次是取不到;而View则是在同一个请求中,所以ViewBag信息可以取到。
4、 如果用Redirect,则由于是新的对Controller/Action的请求,所以对应的Action会被执行到。如果用View,则是直接拿某个View去显示,对应的Action是不执行的。
什么情况用View?服务器端产生数据,想让一个View去显示的;
什么情况用Redirect?让浏览器去访问另外一个页面的时候。
九、杂项Misc
1、TempData
在SendRedirect客户端重定向或者验证码等场景下,由于要跨请求的存取数据,是不能放到ViewBag、Model等中,
需要“暂时存到Session中,用完了删除”的需求:实现起来也比较简单:
存入: Session["verifyCode"] = new Random().Next().ToString();
读取: String code = (string) Session["verifyCode"]; Session["verifyCode"] = null; if(code==model.Code) { //... }
ASP.Net MVC中提供了一个TempData让这一切更简单。
在一个Action存入TempData,在后续的Action一旦被读取一次,数据自动销毁。
TempData默认就是依赖于Session实现的,所以Session过期以后,即使没有读取也会销毁。
应用场景:验证码;
2、HttpContext与HttpContextBase、HttpRequest与HttpRequestBase、HttpPostedFile与HttpPostedFileBase。
注意:进行asp.net mvc开发的时候尽量使用****Base这些类,不要用asp.net内核原生的类。HttpContext.Current(X)
1)在Controller中HttpContext是一个HttpContextBase类型的属性(真正是HttpContextWrapper类型,是对System.Web.HttpContext的封装),System.Web.HttpContext是一个类型。这两个类之间没有继承关系。
System.Web.HttpContext类型是原始ASP.Net核心中的类,在ASP.Net MVC中不推荐使用这个类(也可以用)。
2)HttpContextBase能“单元测试”,System.Web.HttpContext不能。
3)怎么样HttpContextBase.Current?其实是不推荐用Current,而是随用随传递。
4)HttpContextBase的Request、Response属性都是HttpRequestBase、HttpResponseBase类型。Session等也如此。
5)如果真要使用HttpContext类的话,就要System.Web.HttpContext
3,Views的web.config中的system.web.webpages.razor的pages/namespaces节点下配置add命名空间,这样cshtml中就不用using了
示例代码:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="Test1" /> </namespaces> </pages> </system.web.webPages.razor>
4,Layout布局文件
@RenderBody()渲染正文部分;cshtml的Layout属性设定Layout页面地址;
@RenderSection("Footer")用于渲染具体页面中用@section Footer{}包裹的内容,如果Footer是可选的,那么使用@RenderSection("Footer",false),
可以用IsSectionDefined("Footer")实现“如果没定义则显示***”的效果。
5, 可以在Views文件夹下建一个_ViewStart.cshtml文件,在这个文件中定义Layout,这样不用每个页面中都设定Layout,
当然具体页面也可以通过设定Layout属性来覆盖默认的实现;
6,@Html.DropDownList
如果在页面中输出一个下拉列表或者列表框,就要自己写foreach拼接html,还要写if判断哪项应该处于选中状态
<select> @foreach(var p in (IEnumerable<Person>)ViewBag.list) { <option selected="@(p.Id==3)">@p.Name</option> } </select>
asp.net mvc中提供了一些“Html辅助方法”(其实就是Controller的Html属性中的若干方法,其实是扩展方法)用来简化html代码的生成。
DropDownList是生成下拉列表的。
1)DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
string name参数用来设定 <select>标签的name属性的值,id属性的值默认和name一致。
下拉列表中的项(<option>)以SelectListItem集合的形式提供,SelectListItem的属性:
bool Selected:是否选中状态,也就是是否生成selected="selected"属性;
string Text:显示的值,也就是<option>的innerText部分;
string Value:生成的value属性,注意是string类型;
示例代码:
List<Person> list = new List<Person>(); list.Add(new Person { Id=1,Name="lily",IsMale=false}); list.Add(new Person { Id = 12, Name = "tom", IsMale = true }); list.Add(new Person { Id = 13, Name = "lucy", IsMale = false }); List<SelectListItem> sliList = new List<SelectListItem>(); foreach (var p in list) { SelectListItem listItem = new SelectListItem(); listItem.Selected = (p.Id==2); listItem.Text = p.Name; listItem.Value = p.Id.ToString(); sliList.Add(listItem); } return View(sliList);
@model IEnumerable<SelectListItem> <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>DDL</title> </head> <body> <div> @Html.DropDownList("pid", Model); </div> </body> </html>
2)DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
htmlAttributes属性用来生成select标签的其他属性,通常以匿名类对象的形式提供,
比如new { onchange = "javascript:alert('ok')", style = "color:red", aaa = "rupeng", id = "yzk",@class="warn error" }
生成的html源码为:
<select aaa="rupeng" class="warn error" id="yzk" name="pid" onchange="javascript:alert('ok')" style="color:red">
支持自定义属性,给你原样输出,具体什么含义自己定;
由于class是关键字,所以不能直接用class="",要加上一个@前缀,这其实是C#中给变量名取名为关键字的一种语法;
注意:
id默认和name一致,如果设定了id则覆盖默认的实现。
3)构造一个特殊的集合类SelectList,他会自动帮着做集合的遍历
public ActionResult DDL2() { List<Person> list = new List<Person>(); list.Add(new Person { Id=666,Name="zhangsan",IsMale=false}); list.Add(new Person { Id = 222, Name = "tom", IsMale = true }); list.Add(new Person { Id = 333, Name = "lucy", IsMale = false }); SelectList selectList = new SelectList(list, "Id", "Name"); return View(selectList); }
@Html.DropDownList("name",(SelectList)Model);
IEnumerable items参数用来显示的原始对象数据,string dataValueField为“对象的哪个属性用做生成value属性”,
string dataTextField为“对象的哪个属性用作生成显示的文本属性”。
用SelectList的好处是简单,但是如果说要同时显示多个属性的时候,就只能用非SelectList的方式了。
SelectList还可以设定第四个参数:
哪个值被选中:SelectList selectList = new SelectList(list,"Id","Name",222);
一个坑:不能让cshtml中的DropDownList的第一个name参数和ViewBag中任何一个属性重名http://www.cnblogs.com/rupeng/p/5138575.html。
建议不要通过ViewBag传递,都通过Model传递
7,@Html.ListBox()
和@Html.DropDownList()类似
8,为什么不再推荐使用“Html辅助方法”
坏处:因为不符合复杂项目的开发流程(前端程序员可能看不懂),
好处:可以把表单验证、绑定等充分利用起来,开效率高,
但是在互联网项目中开发效率并不是唯一关注因素。在asp.net mvc6中已经不再推荐使用html辅助方法的表单了
9,Request.IsAjaxRequest()
判断是来自于Ajax请求,这样可以让ajax请求和非ajax请求响应不同的内容
原理:
Ajax请求的报文头中有x-requested-with: XMLHttpRequest。
如果使用System.Web.HttpContext,那么是没有这个方法的,那么自己就从报文头中取数据判断。
示例代码:
public ActionResult Ajax1() { return View(); } public ActionResult Ajax2() { Person p = new Person(); p.Name = "rupeng"; if (Request.IsAjaxRequest()) { return Json(p); } else { return Content(p.Name); } }
10,数据验证
1,asp.net mvc会自动根据属性的类型进行基本的校验,比如如果属性是int类型的,那么在提交非整数类型的数据的时候就会报错。
注意ASP.net MVC并不是在请求验证失败的时候抛异常,而是把决定权交给程序员,程序员需要决定如何处理数据校验失败。
在Action中根据ModelState.IsValid判断是否验证通过,如果没有通过下面的方法拿到报错信息
示例代码:
public ActionResult Index(IndexModel model) { if (ModelState.IsValid) { return Content("Age=" + model.Age); } else { return Content("验证失败"); } }
在参数很多的情况下使用下面的封装的方法:
public static string GetValidMsg(ModelStateDictionary modelState) { StringBuilder sb = new StringBuilder(); foreach (var propName in modelState.Keys) { if (modelState[propName].Errors.Count <= 0) { continue; } sb.Append("属性【").Append(propName).Append("】错误:"); foreach (var modelError in modelState[propName].Errors) { sb.AppendLine(modelError.ErrorMessage); } } return sb.ToString(); }
2,ASP.Net MVC提供了在服务器端验证请求数据的能力。要把对应的Attribute标记到Model的属性上(标记到方法参数上很多地方不起作用)。
常用验证Attribute:
a) [Required] 这个属性是必须的
b) [StringLength(100)], 字符串最大长度100;[StringLength(100,MinimumLength=10)]长度要介于10到100之间
c) [RegularExpression(@"aa(\d)+bb")] 正则表达式
d) [Range(35,88)] 数值范围。字符串长度范围的话请使用[StringLength(100,MinimumLength=10)]
e) [Compare("Email")] 这个属性必须和Email属性值一样。
f) [EmailAddress] 要是邮箱地址
g) [Phone] 电话号码,规则有限
示例代码:
public class IndexModel { [Required] public int Age { get; set; } public long Id { get; set; } public string Name { get; set; } [StringLength(11)] public string PhoneNum { get; set; } }
3, 验证Attribute上都有ErrorMessage属性,用来自定义报错信息。ErrorMessage中可以用{0}占位符作为属性名的占位。
示例代码:
[Required(ErrorMessage="不能为空")] public int Age { get; set; }
4, 数据验证+Html辅助类高级控件可以实现很多简化的开发,连客户端+服务器端校验都自动实现了,但是有点太“WebForm”了,因此这里先学习核心原理,避免晕菜。
11,自定义验证规则ValidationAttribute,
自动的验证规则需要直接或者间接继承自ValidationAttribute
1,使用正则表达式的校验,直接从RegularExpressionAttribute继承
示例代码: public class QQNumberAttribute : RegularExpressionAttribute { public QQNumberAttribute() : base(@"^\d{5,10}$")//不要忘了^$ { this.ErrorMessage = "{0}属性不是合法的QQ号,QQ号需要5-10位数字"; //设定ErrorMessage的默认值。使用的人也可以覆盖这个值 } }
手机号的正则表达式:@"^1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}$"
2,直接继承自ValidationAttribute,重写IsValid方法
比如校验中国电话号码合法性
public class CNPhoneNumAttribute : ValidationAttribute { public CNPhoneNumAttribute() { this.ErrorMessage = "电话号码必须是固话或者手机,固话要是3-4位区号开头,手机必须以13、15、18、17开头"; } public override bool IsValid(object value) { if (value is string) { string s = (string)value; if (s.Length == 13)//手机号 { if (s.StartsWith("13") || s.StartsWith("15") || s.StartsWith("17") || s.StartsWith("18")) { return true; } else { return false; } } else if (s.Contains("-"))//固话 { string[] strs = s.Split('-'); if (strs[0].Length==3||strs[0].Length==4) { return true; } else { return false; } } else { return false; } } else { return false; } //return base.IsValid(value); } }
3,还可以让Model类实现IValidatableObject接口,用的比较少
十、过滤器(Filter)
AOP(面向切面编程)是一种架构思想,用于把公共的逻辑放到一个单独的地方,这样就不用每个地方都写重复的代码了。
比如程序中发生异常,不用每个地方都try...catch...只要在(Global 的Application_Error)中统一进行异常处理。不用每个Action中都检查当前用户是否有执行权限,
ASP.net MVC中提供了一个机制,每个Action执行之前都会执行我们的代码,这样统一检查即可。
1,四种Filter
在ASP.Net MVC中提供了四个Filter(过滤器)接口实现了这种AOP机制:
IAuthorizationFilter、IActionFilter、IResultFilter、IExceptionFilter。
1,IAuthorizationFilter
一般用来检查当前用户是否有Action的执行权限,在每个Action被执行前执行OnAuthorization方法;
2,IActionFilter
也是在每个Action被执行前执行OnActionExecuting方法,每个Action执行完成后执行OnActionExecuted方法
和IAuthorizationFilter的区别是IAuthorizationFilter在IActionFilter之前执行,检查权限一般写到IAuthorizationFilter中;
3,IResultFilter,在每个ActionResult的前后执行IResultFilter。用的很少,后面有一个应用。
4,IExceptionFilter,当Action执行发生未处理异常的时候执行OnException方法。
在ASP.net MVC 中仍然可以使用“Global 的Application_Error”,但是建议用IExceptionFilter。
2、IAuthorizationFilter案例:只有登录后才能访问除了LoginController之外的Controller。
1,编写一个类CheckAuthorFilter,实现IAuthorizationFilter接口(需要引用System.Web.Mvc程序集)
示例代码:
public class CheckLoginFilter : IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { string ctrlName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; string actionName = filterContext.ActionDescriptor.ActionName; if (ctrlName=="Login"&&(actionName=="Index"||actionName=="Login")) { //什么都不做 } else { if (filterContext.HttpContext.Session["username"]==null) { ContentResult contentResult = new ContentResult(); contentResult.Content = "没有登录"; //filterContext.Result = contentResult; filterContext.Result = new RedirectResult("/Login/Index"); } } } }
2,在Globel中注册这个Filter:GlobalFilters.Filters.Add(new CheckAuthorFilter());
示例代码:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); GlobalFilters.Filters.Add(new CheckLoginFilter()); }
3,CheckAuthorFilter中实现OnAuthorization方法。
filterContext.ActionDescriptor 可以获得Action的信息:
filterContext.ActionDescriptor.ActionName 获得要执行的Action的名字;
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName 为要执行的Controller的名字;
filterContext.ActionDescriptor.ControllerDescriptor.ControllerType 为要执行的Controller的Type;
filterContext.HttpContext 获得当前请求的HttpContext;
如果给“filterContext.Result”赋值了,那么就不会再执行要执行的Action,而是以“filterContext.Result”的值作为执行结果
(注意如果是执行的filterContext.HttpContext.Response.Redirect(),那么目标Action还会执行的)。
4,检查当前用户是否登录,
如果没有登录则filterContext.Result = new ContentResult() { Content = "没有权限" };
或者filterContext.Result = new RedirectResult("/Login/Index");
(最好不要filterContext.HttpContext.Response.Redirect("/Login/Index");)
5,A用户有一些Action执行权限,B用户有另外一些Action的执行权限;
3、IActionFilter案例:日志记录,记录登录用户执行的Action的记录,方便跟踪责任。
4、IExceptionFilter案例:记录未捕获异常。
public class ExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext filterContext) { File.AppendAllText("d:/error.log", filterContext.Exception.ToString()); filterContext.ExceptionHandled = true;//如果有其他的IExceptionFilter不再执行 filterContext.Result = new ContentResult() { Content= "error" }; } }
然后:
GlobalFilters.Filters.Add(new ExceptionFilter());
5、总结好处:
一次编写,其他地方默认就执行了。可以添加多个同一个类型的全局Filter,按照添加的顺序执行。
6、(*)非全局Filter:
只要让实现类继承自FilterAttribute类,然后该实现哪个Filter接口就实现哪个(四个都支持)。
不添加到GlobalFilters中,而是把这个自定义Attribute添加到Controller类上这样就只有这个Controller中操作会用到这个Filter。
如果添加到Action方法上,则只有这个Action执行的时候才会用到这个Filter。
Nuget笔记
一、NuGet简介
我们进行软件开发的时候,经常会用到第三方的开发包(俗称dll),比如NPOI、MYSQL ADO.net 驱动等。
如果自己去网上下载的问题:不好搜,不好找;容易下载到错误的;要自己进行安装配置;要选择和当前环境一致的版本(比如有的开发包在.net 2.0和.net 4.5中要用不同的版本);这个开发包可能还要使用其他的安装包。
微软提供了NuGet这个软件,自动帮我们进行开发包的下载、安装,并且会根据当前的环境找到合适的版本,下载相关的依赖的开发包,还可以自动更新最新版本。
NuGet在VS2013以上都提供了。有图形界面和命令行两种使用方式。
二、Nuget.org寻宝
http://www.nuget.org
三、图形界面,进行安装
在项目的“引用”中右键——管理Nuget程序包——在浏览中输入程序包的名字(比如NPOI)——搜索——然后进行安装
由于nuget的服务器在国外,可能你的网络连不上或者速度慢,可以从其他镜像站点下
方法:
1,找一个可用的镜像站点,目前可用的是博客园的镜像,地址https://nuget.cnblogs.com/v3/index.json
2,在VS的【工具】→【选项】→【NuGet包管理器】→【程序包源】,点击【添加】
然后在搜索安装的时候在【程序包源】中选择我们添加的镜像
3,在搜索结果中找到合适的结果,点击后在右侧选择合适的版本
点击【安装】以后可能会弹出要求【同意】协议的对话框,点击【同意】即可。
在【输出】的【程序包管理器】中出现“========== 已完成 ==========”的时候说明安装完成,有可能会报错。
4,会自动添加引用
5,有的安装包会自动修改APP.config等配置文件
6,Nuget安装包信息在packages.config中,对应的安装包在解决方案的packages文件夹下,把项目拷给别人的时候没必要拷packages文件夹,别人拿到以后会自动下载恢复这些安装包。
7,如果要删除某个安装包,不能只删除引用,否则还会自动恢复、添加,还要手动删除packages.config中的内容。最好使用图形界面的“卸载”功能。
四、命令行的使用
好处:方便、灵活
1,在【程序包管理器控制台】视图中(如果没显示出来,则主菜单【工具】→【NuGet包管理器】→【程序包管理器控制台】)。
输入:Install-Package 程序包的名字
安装完成的标志
2,可以在【程序包源】中指定镜像站点,【默认项目】指的是安装到哪个项目中
3,指定版本::Install-Package 安装包 -Version 版本号,
比如:
Install-Package MySql.Data -Version 6.8.8
4,卸载
示例代码:
UnInstall-Package MySql.Data
Entity Framework笔记
Entity Framework是在.Net平台下进行数据库开发的框架,ORM框架
一、相关知识复习
1. var类型推断:
var p =new Person();
2. 匿名类型。
var a =new {p.Name,Age=5,Gender=p.Gender,Name1=a.Name};//{p.Name}=={Name=p.Name}
3. 给新创建对象的属性赋值的简化方法:
Person p = new Person{Name="tom",Age=5};
等价于
Person p = new Person();p.Name="tom";p.Age=5;
4. lambda表达式
函数式编程,在Entity Framework编程中用的很多
1,原始样式
Action<int> a1 = delegate(int i){Console.writeLine(i);};
2,可以简化为(=>读作goes to)
Action<int> a2 = (int i)=>{Console.writeLine(i);};
3,还可以省略参数类型(编译器会自动根据委托类型解析):
Action<int> a3 =(i)=>{Console.writeLine(i);};
4,如果只有一个参数还可以省略参数的小括号(多个参数不行)
Action<int> a4 = i=>{Console.writeLine(i);};
5,如果委托有返回值,并且方法体只有一行代码,这一行代码还是返回值,那么就可以连方法的大括号和return都省略:
Func<int,int,string> f1 = delegate(int i,int j){return "结果是"+(i+j);};//原始形式 Func<int,int,string>f2 = (i,j)=>"结果是"+(i+j);//简化形式
5,集合常用的扩展方法
Where(支持委托)、Select(支持委托)、Max、Min、OrderBy
First(获取第一个,如果没有则异常)
FirstOrDefault(获取第一个,如果没有则返回默认值)
Single(获取唯一一个,如果没有或者多个则异常)
SingleOrDefault(获取唯一一个,如果没有则返回默认值,多个则异常)
注意:
lambda中照样要避免变量名重名的问题:
var p = persons.Where(p=>p.Name=="rupeng.com").First();//错误代码:两个p重名,修改一个
二、 高级集合扩展方法
准备工作1:创建对象类
//学生 public class Person { public string Name { get; set; } public int Age { get; set; } public bool Gender { get; set; } public int Salary { get; set; } public override string ToString() { return string.Format("Name={0},Age={1},Gender={2},Salary={3}",Name, Age, Gender, Salary); } }
//老师 public class Teacher { public Teacher() { this.Students=new List<Person>(); } public string Name { get; set; } public List<Person> Students { get; set; } }
//准备工作2,在控制台项目中,添加数据
var s0 =new Person { Name="tom",Age=3,Gender=true,Salary=6000}; var s1 = new Person { Name = "jerry", Age = 8, Gender = true, Salary = 5000 }; var s2 = new Person { Name = "jim", Age = 3, Gender = true, Salary = 3000 }; var s3 = new Person { Name = "lily", Age = 5, Gender = false, Salary = 9000 }; var s4 = new Person { Name = "lucy", Age = 6, Gender = false, Salary = 2000 }; var s5 = new Person { Name = "kimi", Age = 5, Gender = true, Salary = 1000 }; List<Person> list = new List<Person>(); list.Add(s0); list.Add(s1); list.Add(s2); list.Add(s3); list.Add(s4); list.Add(s5); Teacher t1 = new Teacher { Name="如鹏网张老师"}; t1.Students.Add(s1); t1.Students.Add(s2); Teacher t2 = new Teacher { Name = "如鹏网刘老师" }; t2.Students.Add(s2); t2.Students.Add(s3); t2.Students.Add(s5); Teacher[] teachers={t1,t2};
//开始展示用法
1,Any(),
判断集合是否包含元素,返回值是bool,一般比Coun()>0效率高。
Any还可以指定条件表达式。
bool b = list.Any(p => p.Age > 50);等价于bool b = list.Where(p=>p.Age>50).Any();
2,Distinct(),剔除完全重复数据。(*)注意自定义对象的Equals问题:需要重写Equals和GetHashCode方法来进行内容比较。
3,排序:
升序
list.OrderBy(p=>p.Age);
降序
list.OrderByDescending(p=>p.Age)
指定多个排序规则,不是多个OrderBy,而是:OrderBy..ThenBy
list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary),//也支持ThenByDescending()。注意这些操作不会影响原始的集合数据。
4,Skip(n)
跳过前n条数据;
Take(n)获取最多n条数据,如果不足n条也不会报错。
常用来分页获取数据。list.Skip(30).Take(20) 跳过前3条数据获取2条数据。
5,Except(items1)
排除当前集合中在items1中存在的元素
6,Union(items1)
把当前集合和items1中组合
7,Intersect(items1)
把当前集合和items1中取交集
8,分组
foreach(var g in list.GroupBy(p => p.Age)) { Console.WriteLine(g.Key+":"+g.Average(p=>p.Salary)); }
9,SelectMany:
把集合中每个对象的另外集合属性的值重新拼接为一个新的集合
foreach(var s in teachers.SelectMany(t => t.Students)) { Console.WriteLine(s);//每个元素都是Person }
注意:
不会去重,如果需要去重则要自己再次调用Distinct()
10,Join
//准备工作1:创建类
//Master类 class Master { public long Id{get;set;} public string Name{get;set;} }
//Dog类 class Dog { public long Id { get; set; } public long MasterId { get; set; } public string Name { get; set; } }
//准备工作2,在控制台项目中添加数据
Master m1 = new Master { Id = 1, Name = "杨中科" }; Master m2 = new Master { Id = 2, Name = "比尔盖茨" }; Master m3 = new Master { Id = 3, Name = "周星驰" }; Master[] masters = { m1,m2,m3}; Dog d1 = new Dog { Id = 1, MasterId = 3, Name = "旺财" }; Dog d2 = new Dog { Id = 2, MasterId = 3, Name = "汪汪" }; Dog d3 = new Dog { Id = 3, MasterId = 1, Name = "京巴" }; Dog d4 = new Dog { Id = 4, MasterId = 2, Name = "泰迪" }; Dog d5 = new Dog { Id = 5, MasterId = 1, Name = "中华田园" }; Dog[] dogs = { d1, d2, d3, d4, d5 };
Join可以实现和数据库一样的Join效果,对有关联关系的数据进行联合查询
下面的语句查询所有Id=1的狗,并且查询狗的主人的姓名。
var result = dogs.Where(d => d.Id > 1).Join(masters, d => d.MasterId, m => m.Id,(d,m)=>new {DogName=d.Name,MasterName=m.Name}); foreach(var item in result) { Console.WriteLine(item.DogName+","+item.MasterName); }
三、Linq
1,简介
查询Id>1的狗有如下两种写法:
1)var r1 = dogs.Where(d => d.Id > 1);
2)var r2 = from d in dogs where d.Id>1 select d;
第一种写法是使用lambda的方式写的,官方没有正式的叫法,我们就叫“lambda写法”;
第二种是使用一种叫Linq(读作:link)的写法,是微软发明的一种类似SQL的语法,给我们一个新选择。
两种方法是可以互相替代的,没有哪个好、哪个坏,看个人习惯。
经验:
需要join等复杂用法的时候Linq更易懂,一般的时候“lambda写法”更清晰,更紧凑
2,辟谣
“Linq被淘汰了”是错误的说法,应该是“Linq2SQL被淘汰了”。
linq就是微软发明的这个语法,可以用这种语法操作很多数据,
操作SQL数据就是Linq2SQL,linq操作后面学的EntityFramework就是Linq2Entity,linq操作普通.Net对象就是Linq2Object、Linq操作XML文档就是Linq2XML。
3,linq基本语法
以from item in items开始,items为待处理的集合,item为每一项的变量名;
最后要加上select,表示结果的数据;记得select一定要最后。这是刚用比较别扭的地方。
用法:
1,var r= from d in dogs select d.Id;
2,var r = from d in dogs select new{d.Id,d.Name,Desc="一条狗"};
3,排序 var items = from d in dogs //orderby d.Age //orderby d.Age descending orderby d.Age,d.MasterId descending select d;
4,join var r9 = from d in dogs join m in masters on d.MasterId equals m1.Id select new { DogName=d.Name,MasterName=m.Name};
注意:
join中相等不要用==,要用equals。
写join的时候linq比“lambda” 漂亮
5,group by var r1 = from p in list group p by p.Age into g select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() };
4、混用
只有Where,Select,OrderBy,GroupBy,Join等这些能用linq写法,
如果要用下面的“Max,Min,Count,Average,Sum,Any,First,FirstOrDefault,Single,SingleOrDefault,Distinct,Skip,Take等”则还要用lambda的写法
(因为编译后是同一个东西,所以当然可以混用)。
var r1 = from p in list group p by p.Age into g select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() }; int c = r1.Count(); var item = r1.SingleOrDefault(); var c = (from p in list where p.Age>3 select p ).Count();
四、 C#6.0语法
1. 属性的初始化“public int Age{get;set;}=6”。低版本.Net中怎么办?构造函数
2. nameof:可以直接获得变量、属性、方法等的名字的字符串表现形式。获取的是最后一段的名称。如果在低版本中怎么办?
好处:可以避免写错,有利于编译时查看
应用案例:ASP.Net MVC中的[Compare("BirthDay")]改成[Compare(nameof(BirthDay))]
3,??语法
int j = i ?? 3; 如果i为null则表达式的值为3,否则表达式的值就是i的值。如果在低版本中怎么办?int j = (i == null)?3:(int)i;
应用案例:
string name = null;Console.WriteLine(name??"未知");
4, ?.语法:
string s8 = null; string s9 = s8?.Trim(); //如果s8为null,则不执行Trim(),让表达式的结果为null。
在低版本中怎么办?
string s9 = null; if (s8 != null) { s9 = s8.Trim(); }
五、Entity Framework简介
1、 ORM:Object Relation Mapping ,通俗说:用操作对象的方式来操作数据库。
2、 插入数据库不再是执行Insert,而是类似于
Person p = new Person(); p.Age=3;p.Name="如鹏网"; db.Save(p);
这样的做法。
3、 ORM工具有很多Dapper、PetaPoco、NHibernate,最首推的还是微软官方的Entity Framework,简称EF。
4、 EF底层仍然是对ADO.Net的封装。EF支持SQLServer、MYSQL、Oracle、Sqlite等所有主流数据库。
5、 使用EF进行数据库开发的时候有两个东西建:建数据库(T_Persons),建模型类(Person)。根据这两种创建的先后顺序有EF的三种创建方法
a) DataBase First(数据库优先):先创建数据库表,然后自动生成EDM文件,EDM文件生成模型类。简单展示一下DataBase First的使用。
b) Model First(模型优先):先创建Edm文件,Edm文件自动生成模型类和数据库;
c) Code First(代码优先):程序员自己写模型类,然后自动生成数据库。没有Edm。
DataBase First简单、方便,但是当项目大了之后会非常痛苦;Code First入门门槛高,但是适合于大项目。Model First……
6, Code First的微软的推荐用法是程序员只写模型类,数据库由EF帮我们生成,当修改模型类之后,EF使用“DB Miguration”自动帮我们更改数据库。
但是这种做法太激进,不适合很多大项目的开发流程和优化,只适合于项目的初始开发阶段。
Java的Hibernate中也有类似的DDL2SQL技术,但是也是用的较少。“DB Miguration”也不利于理解EF,因此在初学阶段,我们将会禁用“DB Miguration”,采用更实际的“手动建数据库和模型类”的方式。
7, 如果大家用过NHibernate等ORM工具的话,会发现开发过程特别麻烦,需要在配置文件中指定模型类属性和数据库字段的对应关系,哪怕名字完全也一样也要手动配置。
使用过Java中Struts、Spring等技术的同学也有过类似“配置文件地狱”的感觉。
像ASP.Net MVC一样,EF也是采用“约定大于配置”这样的框架设计原则,省去了很多配置,能用约定就不要自己配置。
六、 EF的安装
1、 基础阶段用控制台项目。使用NuGet安装EntityFramework。会自动在App.config中中增加两个entityFramework相关配置段;
2、 在web.config的Connection中配置连接字符串
<add name="conn1" connectionString="Data Source=.;Initial Catalog=test1;User ID=sa;Password=msn@qq888" providerName="System.Data.SqlClient" />
七、 EF简单DataAnnotations实体配置
1、 数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime字段。
2、 创建Person类
[Table("T_Persons")]//因为类名和表名不一样,所以要使用Table标注 public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
因为EF约定主键字段名是Id,所以不用再特殊指定Id是主键,如果非要指定就指定[Key]。
因为字段名字和属性名字一致,所以不用再特殊指定属性和字段名的对应关系,如果需要特殊指定,则要用[Column("Name")]
(*)必填字段标注[Required]、字段长度[MaxLength(5)]、可空字段用int?、如果字段在数据库有默认值,则要在属性上标注[DatabaseGenerated]
注意实体类都要写成public,否则后面可能会有麻烦。
3,创建DbContext类(模型类、实体类)
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1")//name=conn1表示使用连接字符串中名字为conn1的去连接数据库 { } public DbSet<Person> Persons { get; set; }//通过对Persons集合的操作就可以完成对T_Persons表的操作 }
4,运行测试
MyDbContext ctx = new MyDbContext(); Person p = new Person(); p.CreateDateTime = DateTime.Now; p.Name = "rupeng"; ctx.Persons.Add(p); ctx.SaveChanges();
注意:
MyDbContext对象是否需要using有争议,不using也没事。每次用的时候new MyDbContext就行,不用共享同一个实例,共享反而会有问题。SaveChanges()才会把修改更新到数据库中。
异常的处理:
如果数据有错误可能在SaveChanges()的时候出现异常,一般仔细查看异常信息或者一直深入一层层的钻InnerException就能发现错误信息。
举例:
创建一个Person对象,不给Name、CreateDateTime赋值就保存。
八、EF模型的两种配置方式
EF中的模型类的配置有DataAnnotations、FluentAPI两种。
DataAnnotations:
[Table("T_Persons")]、[Column("Name")]这种在类上或者属性上标记的方式就叫DataAnnotations
好处与坏处:
这种方式比较方便,但是耦合度太高,不适合大项目开发。
一般的类最好是POCO
(Plain Old C# Object没有继承什么特殊的父类,没有标注什么特殊的Attribute,没有定义什么特殊的方法,就是一堆普通的属性);
不符合大项目开发的要求。微软推荐使用FluentAPI的使用方式,因此后面主要用FluentAPI的使用方式。
九、FluentAPI配置T_Persons的方式
1,数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime字段。
2,创建Person类。模型类就是普通C#类
public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
3,创建一个PersonConfig类,放到ModelConfig文件夹下(PersonConfig、EntityConfig这样的名字都不是必须的)
class PersonConfig: EntityTypeConfiguration<Person> { public PersonConfig() { this.ToTable("T_Persons");//等价于[Table("T_Persons")] } }
4,创建DbContext类
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly()); //代表从这句话所在的程序集加载所有的继承自EntityTypeConfiguration为模型配置类。 } public DbSet<Person> Persons { get; set; } }
5,运行测试
MyDbContext ctx = new MyDbContext();
Person p = new Person();
p.CreateDateTime = DateTime.Now;
p.Name = "rupeng";
ctx.Persons.Add(p);
ctx.SaveChanges();
和以前唯一的不同就是:
模型不需要标注Attribute;
编写一个XXXConfig类配置映射关系;DbContext中override OnModelCreating;
6,多个表怎么办?创建多个表的实体类、Config类,并且在DbContext中增加多个DbSet类型的属性即可。
十、EF的基本增删改查
获取DbSet除了可以ctx.Persons之外,还可以ctx.Set<Person>()
1,增加,同上
注意:
如果Id是自动增长的,创建的对象显然不用指定Id的值,并且在SaveChanges ()后会自动给对象的Id属性赋值为新增行的Id字段的值。
2,删除。
先查询出来要删除的数据,然后Remove。这种方式问题最少,虽然性能略低,但是caozuo.html" target="_blank">删除操作一般不频繁,不用考虑性能。后续在“状态管理”中会讲其他实现方法
MyDbContext ctx = new MyDbContext();
var p1= ctx.Persons.Where(p => p.Id == 3).SingleOrDefault();//先查询出来
if(p1==null)
{
Console.WriteLine("没有id=3的人");
}
else
{
ctx.Persons.Remove(p1);//进行删除
}
ctx.SaveChanges();//删除后进行保存
批量删除:
怎么批量删除,比如删除Id>3的?查询出来一个个Remove。性能坑爹。如果操作不频繁或者数据量不大不用考虑性能,如果需要考虑性能就直接执行sql语句(后面讲)
3,修改:先查询出来要修改的数据,然后修改,然后SaveChanges()
MyDbContext ctx = new MyDbContext();
var ps = ctx.Persons.Where(p => p.Id > 3);
foreach(var p in ps)
{
p.CreateDateTime = p.CreateDateTime.AddDays(3);
p.Name = "haha";
}
ctx.SaveChanges();
4,查。
因为DbSet实现了IQueryable接口,而IQueryable接口继承了IEnumerable接口,所以可以使用所有的linq、lambda操作。给表增加一个Age字段,然后举例orderby、groupby、where操作、分页等。一样一样的。
5,查询order by的一个细节
EF调用Skip之前必须调用OrderBy:
如下调用var items = ctx.Persons.Skip(3).Take(5); 会报错“The method 'OrderBy' must be called before the method 'Skip'.)”,
要改成:var items = ctx.Persons.OrderBy(p=>p.CreateDateTime).Skip(3).Take(5);
这也是一个好习惯,因为以前就发生过(写原始sql):
分页查询的时候没有指定排序规则,以为默认是按照Id排序,其实有的时候不是,就造成数据混乱。写原始SQL的时候也要注意一定要指定排序规则。
十一、EF原理及SQL监控
EF会自动把Where()、OrderBy()、Select()等这些编译成“表达式树(Expression Tree)”,然后会把表达式树翻译成SQL语句去执行。
(编译原理,AST)因此不是“把数据都取到内存中,然后使用集合的方法进行数据过滤”,因此性能不会低。但是如果这个操作不能被翻译成SQL语句,则或者报错,或者被放到内存中操作,性能就会非常低。
怎么查看真正执行的SQL是什么样呢?
DbContext有一个Database属性,其中的Log属性,是Action<String>委托类型,也就是可以指向一个void A(string s)方法,其中的参数就是执行的SQL语句,每次EF执行SQL语句的时候都会执行Log。因此就可以知道执行了什么SQL。
EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select查询,ToList()内部也是遍历结果集形成List。
(如果要立刻开始执行,可以在后面加上ToList(),因为它会遍历集合)
查看Update操作,会发现只更新了修改的字段。
观察一下前面学学习时候执行的SQL是什么样的。Skip().Take()被翻译成了?Count()被翻译成了?
var result = ctx.Persons.Where(p => p.Name.StartsWith("rupeng"));看看翻译成了什么? like语句
var result = ctx.Persons.Where(p => p.Name.Contains("com"));呢? %com%
var result = ctx.Persons.Where(p => p.Name.Length>5); 呢?
var result = ctx.Persons.Where(p => p.CreateDateTime>DateTime.Now); 呢?
再看看(好牛):
long[] ids = { 2,5,6};//不要写成int[]
var result = ctx.Persons.Where(p => ids.Contains(p.Id));
EF中还可以多次指定where来实现动态的复合检索:
//必须写成IQueryable<Person>,如果写成IEnumerable就会在内存中取后续数据
IQueryable<Person> items = ctx.Persons;//为什么把IQueryable<Person>换成var会编译出错
items = items.Where(p=>p.Name=="rupeng");
items = items.Where(p=>p.Id>5);
(*)EF是跨数据库的,如果迁移到MYSQL上,就会翻译成MYSQL的语法。要配置对应数据库的Entity Framework Provider。
细节:
每次开始执行的__MigrationHistory等这些SQL语句是什么?
是DBMigration用的,也就是由EF帮我们建数据库,现在我们用不到,用下面的代码禁用:
Database.SetInitializer<XXXDbContext>(null);
XXXDbContext就是项目DbContext的类名。一般建议放到XXXDbContext构造函数中。
注意这里的Database是System.Data.Entity下的类,不是DbContext的Database属性。如果写到DbContext中,最好用上全名,防止出错。
十二、执行原始的SQL
在一些特殊场合,需要执行原生SQL。
执行非查询语句,调用DbContext 的Database属性的ExecuteSqlCommand方法,可以通过占位符的方式传递参数:
ctx.Database.ExecuteSqlCommand("update T_Persons set Name={0},CreateDateTime=GetDate()", "rupeng.com");
占位符的方式不是字符串拼接,经过观察生成的SQL语句,发现仍然是参数化查询,因此不会有SQL注入漏洞。
示例代码:
var q1 = ctx.Database.SqlQuery<Item1>("select Name,Count(*) Count from T_Persons where Id>{0} and CreateDateTime<={1} group by Name",2, DateTime.Now); //返回值是DbRawSqlQuery<T> 类型,也是实现了IEnumerable接口
foreach(var item in q1)
{
Console.WriteLine(item.Name+":"+item.Count);
}
class Item1
{
public string Name { get; set; }
public int Count { get; set; }
}
类似于ExecuteScalar的操作比较麻烦:
int c = ctx.Database.SqlQuery<int>("select count(*) from T_Persons").SingleOrDefault();
十三、不是所有lambda写法都能被支持
下面想把Id转换为字符串比较一下是否为"3"(别管为什么):
var result = ctx.Persons.Where(p => Convert.ToString(p.Id)=="3");
运行会报错(也许高版本支持了就不报错了),这是一个语法、逻辑上合法的写法,但是EF目前无法把他解析为一个SQL语句。
出现“System.NotSupportedException”异常一般就说明你的写法无法翻译成SQL语句。
想获取创建日期早于当前时间一小时以上的数据:
var result = ctx.Persons.Where(p => (DateTime.Now - p.CreateDateTime).TotalHours>1);
同样也可能会报错。
怎么解决?
尝试其他替代方案(没有依据,只能乱试):
var result = ctx.Persons.Where(p => p.Id==3);
EF中提供了一个SQLServer专用的类SqlFunctions,对于EF不支持的函数提供了支持,比如:
var result = ctx.Persons.Where(p =>SqlFunctions.DateDiff("hour",p.CreateDateTime,DateTime.Now)>1);
十四、EF对象的状态
1,简介
为什么查询出来的对象Remove()、再SaveChanges()就会把数据删除。而自己new一个Person()对象,然后Remove()不行?
为什么查询出来的对象修改属性值后、再SaveChanges()就会把数据库中的数据修改。
因为EF会跟踪对象状态的改变。
2,EF中中对象有五个状态:Detached(游离态,脱离态)、Unchanged(未改变)、Added(新增)、Deleted(删除)、Modified(被修改)。
3,状态转换
Add()、Remove()修改对象的状态。所有状态之间几乎都可以通过:Entry(p).State=xxx的方式 进行强制状态转换。
状态改变都是依赖于Id的(Added除外)
4,应用
当SavaChanged()方法执行期间,会查看当前对象的EntityState的值,决定是去新增(Added)、修改(Modified)、删除(Deleted)或者什么也不做(UnChanged)。
下面的做法不推荐,在旧版本中一些写法不被支持,到新版EF中可能也会不支持。
ObjectStateManager
1,不先查询再修改保存,而是直接更新部分字段的方法:
var p = new Person();
p.Id = 2;
ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged;
p.Name = "adfad";
ctx.SaveChanges();
也可以:
var p = new Person();
p.Id = 5;
p.Name = "yzk";
ctx.Persons.Attach(p);//等价于ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged;
ctx.Entry(p).Property(a => a.Name).IsModified = true;
ctx.SaveChanges();
2,不先查询再Remove再保存,而是直接根据Id删除的方法:
var p = new Person();
p.Id = 2;
ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted;
ctx.SaveChanges();
注意下面的做法并不会删除所有Name="rupeng.com" 的,因为更新、删除等都是根据Id进行的:
var p = new Person();
p.Name = "rupeng.com";
ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted;
ctx.SaveChanges();
上面其实是在:delete * from t_persons where Id=0
5,EF优化的一个技巧
如果查询出来的对象只是供显示使用,不会修改、删除后保存,那么可以使用AsNoTracking()来使得查询出来的对象是Detached状态,这样对对象的修改也还是Detached状态,EF不再跟踪这个对象状态的改变,能够提升性能。
示例代码:
原来的:
var p1 = ctx.Persons.Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);
修改为:
var p1 = ctx.Persons.AsNoTracking().Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);
因为AsNoTracking()是DbQuery类(DbSet的父类)的方法,所以要先在DbSet后调用AsNoTracking()。
十五、Fluent API更多配置
基本EF配置只要配置实体类和表、字段的对应关系、表间关联关系即可。
如果利用EF的高级配置,可以达到更多效果:
如果数据错误(比如字段不能为空、字符串超长等),会在EF层就会报错,而不会被提交给数据库服务器再报错;如果使用自动生成数据库,也能帮助EF生成更完美的数据库表。
这些配置方法无论是DataAnnotations、FluentAPI都支持,下面讲FluentAPI的用法,DataAnnotations感兴趣的自己查(http://blog.csdn.net/beglorious/article/details/39637475)。
尽量用约定,EF配置越少越好。Simple is best 参考资料:http://www.cnblogs.com/nianming/archive/2012/11/07/2757997.html
1, HasMaxLength设定字段的最大长度
public PersonConfig()
{
this.ToTable("T_Persons");
this.Property(p => p.Name).HasMaxLength(50);//长度为50
}
如果插入一个Person对象,Name属性的值非常长,保存的时候就会报DbEntityValidationException异常,这个异常的Message中看不到详细的报错消息,要看EntityValidationErrors属性的值。
var p = new Person();
p.Name = "非常长的字符串";
ctx.Persons.Add(p);
try
{
ctx.SaveChanges();
}
catch(DbEntityValidationException ex)
{
StringBuilder sb = new StringBuilder();
foreach(var ve in ex.EntityValidationErrors.SelectMany(eve=>eve.ValidationErrors))
{
sb.AppendLine(ve.PropertyName+":"+ve.ErrorMessage);
}
Console.WriteLine(sb);
}
2, (有用)字段是否可空:
this.Property(p => p.Name).IsRequired() 属性不能为空;
this.Property(p => p.Name).IsOptional() 属性可以为空;
默认规则是“主键属性不允许为空,引用类型允许为空,可空的值类型long?等允许为空,值类型不允许为空。
”基于“尽量少配置”的原则:如果属性是值类型并且允许为null,就声明成long?等,否则声明成long等;
如果属性属性值是引用类型,只有不允许为空的时候设置IsRequired()。
3, 其他一般不用设置的(了解即可)
a) 主键:this.HasKey(p => p.Id);
b) 某个字段不参与映射数据库:this.Ignore(p => p.Name1);
c) this.Property(p => p.Name).IsFixedLength(); 是否对应固定长度
d) this.Property(p => p.Name).IsUnicode(false) 对应的数据库类型是varchar类型,而不是nvarchar
e) this.Property(p => p.Id).HasColumnName("Id"); Id列对应数据库中名字为Id的字段
f) this.Property(p => p.Id).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity) 指定字段是自动增长类型。
4,流动起来
因为ToTable()、Property()、IsRequired()等方法的还是配置对象本身,因此可以实现类似于StringBuilder的链式编程,这就是“Fluent”一词的含义
下面的写法可以被简化:
public PersonConfig()
{
this.ToTable("T_Persons");
this.HasKey(p=>p.Id);
this.Ignore(p=>p.Name2);
this.Property(p=>p.Name).HasMaxLength(50);
this.Property(p=>p.Name).IsRequired();
this.Property(p=>p.CreateDateTime).HasColumnName("CreateDateTime");
this.Property(p=>p.Name).IsRequired();
}
可以被简化为:
public PersonConfig()
{
this.ToTable("T_Persons").HasKey(p=>p.Id).Ignore(p=>p.Name2);
this.Property(p=>p.Name).HasMaxLength(50).IsRequired();
this.Property(p=>p.CreateDateTime).HasColumnName("CreateDateTime").IsRequired();
}
十六、一对多关系映射
EF最有魅力的地方在于对于多表间关系的映射,可以简化工作。
复习一下表间关系:
1) 一对多(多对一):
一个班级对应着多个学生,一个学生对着一个班级。一方是另外一方的唯一。
在多端有一个指向一端的外键。
举例:班级表:T_Classes(Id,Name) 学生表T_Students(Id,Name,Age,ClassId)
2) 多对多:
一个老师对应多个学生,一个学生对于多个老师。
任何一方都不是对方的唯一。需要一个中间关系表。
具体:学生表T_Students(Id,Name,Age,ClassId),老师表 T_Teachers(Id,Name,PhoneNum),关系表T_StudentsTeachers(Id,StudentId,TeacherId)
和关系映射相关的方法:
1) 基本套路this.Has****(p=>p.AAA).With***()
当前这个表和AAA属性的表的关系是Has定义,With定义的是AAA表和这个表的关系。
2) HasOptional() 有一个可选的(可以为空的)
3) HasRequired() 有一个必须的(不能为空的)
4) HasMany() 有很多的
5) WithOptional() 可选的
6) WithRequired() 必须的
7) WithMany() 很多的
举例:
在AAA实体中配置this. HasRequired(p=>p.BBB).WithMany();是什么意思?
在AAA实体中配置this. HasRequired(p=>p.BBB). WithRequired ();是什么意思?
十七、配置一对多关系
1,先按照正常的单表配置把Student、Class配置起来,T_Students的ClassId字段就对应Student类的ClassId属性。WithOptional()
using(MyDbContext ctx = new MyDbContext())
{
Class c1 = new Class{Name="三年二班"};
ctx.SaveChanges();
Student s1 = new Student{Age = 11, Name = "张三", ClassId=c1.Id};
Student s2 = new Student{Name="李四",ClassId=c1.Id};
ctx.Students.Add(s1);
ctx.Students.Add(s2);
ctx.SaveChanges();
}
2, 给Student类增加一个Class类型、名字为Class(不一定非叫这个,但是习惯是:外键名去掉Id)的属性,要声明成virtual
3,然后就可以实现各种对象间的操作了:
a) Console.WriteLine(ctx.Students.First().Class.Name)
b) 然后数据插入也变得简单了,不用再考虑“先保存Class,生成Id,再保存Student”了。这样就是纯正的“面向对象模型”,ClassId属性可以删掉。
Class c1 = new Class { Name = "五年三班" };
ctx.Classes.Add(c1);
Student s1 = new Student { Age = 11, Name = "皮皮虾"};
Student s2 = new Student { Name = "巴斯"};
s1.Class = c1;
s2.Class = c1;
ctx.Students.Add(s1);
ctx.Students.Add(s2);
ctx.Classes.Add(c1);
ctx.SaveChanges();
4, 如果ClassId字段可空怎么办?
直接把ClassId属性设置为long?
5, 还可以在Class中配置一个public virtual ICollection<Student> Students { get; set; } = new List<Student>(); 属性。
最好给这个属性初始化一个对象。注意是virtual。这样就可以获得所有指向了当前对象的Stuent集合,也就是这个班级的所有学生。
我个人不喜欢这个属性,业界的大佬也是建议“尽量不要设计双向关系”,
因为可以通过Class clz = ctx.Classes.First(); var students = ctx.Students.Where(s => s.ClassId == clz.Id);来查询获取到,思路更清晰。
不过有了这样的集合属性之后一个方便的地方:
Class c1 = new Class { Name = "五年三班" };
ctx.Classes.Add(c1);
Student s1 = new Student { Age = 11, Name = "皮皮虾" };
Student s2 = new Student { Name = "巴斯" };
c1.Students.Add(s1);//注意要在Students属性声明的时候= new List<Student>();或者在之前赋值
c1.Students.Add(s2);
ctx.Classes.Add(c1);
ctx.SaveChanges();
EF会自动追踪对象的关联关系,给那些有关联的对象也自动进行处理。
一对多深入:
1、 默认约定配置即可,如果非要配置,可以在StudentConfig中如下配置:
this.HasRequired(s => s.Class).WithMany().HasForeignKey(s => s.ClassId);;
表示“我需要(Require)一个Class,Class有很多(Many)的Student;ClassId是这样一个外键”。
如果ClassId可空,那么就要写成:this. HasOptional (s => s.Class).WithMany().HasForeignKey(s => s.ClassId);
2、 一对多的关系在一端配置就可以了,当然两边都配也不错。思考:如果把一对多的关系配置到ClassConfig中(不建议这么搞)怎么配?
3、 如果一张表中有两个指向另外一个表的外键怎么办?比如学生有“正常班级Class”(不能空)和“小灶班级XZClass”(可以空)两个班。如果用默认约定就会报错,怎么办?
this.HasRequired(s => s.Class).WithMany().HasForeignKey(s => s.ClassId);
this. HasOptional (s => s.XZClass).WithMany().HasForeignKey(s => s.XZClassId);
十八、多对多关系配置
老师和学生:
class Student
{
public long Id { set; get; }
public string Name { get; set; }
public virtual ICollection<Teacher> Teachers { get; set; }=new List<Teacher>();
}
class Teacher
{
public long Id { set; get; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }=new List< Student >();
}
class StudentConfig : EntityTypeConfiguration<Student>
{
public StudentConfig()
{
ToTable("T_Students");
}
}
class TeacherConfig : EntityTypeConfiguration<Teacher>
{
public TeacherConfig()
{
ToTable("T_Teachers");
this.HasMany(e => e.Students).WithMany(e => e.Teachers)
.Map(m => m.ToTable("T_TeacherStudentRelations").MapLeftKey("TeacherId").MapRightKey("StudentId"));
}
}
这样不用中间表建实体(也可以为中间表建立一个实体,其实思路更清晰),就可以完成多对多映射。当然如果中间关系表还想有其他字段,则要必须为中间表建立实体类。
测试:
Teacher t1 = new Teacher();
t1.Name = "张老师";
t1.Students = new List<Student>();
Teacher t2 = new Teacher();
t2.Name = "王老师";
t2.Students = new List<Student>();
Student s1 = new Student();
s1.Name = "tom";
s1.Teachers = new List<Teacher>();
Student s2 = new Student();
s2.Name = "jerry";
s2.Teachers = new List<Teacher>();
t1.Students.Add(s1);
把中间表也建成一个实体了。
t1.Students.Add(s2);
s1.Teachers.Add(t1);
s2.Teachers.Add(t1);
ctx.Students.Add(s1);
ctx.Students.Add(s2);
ctx.SaveChanges();
十九、延迟加载(LazyLoad)
如果public virtual Class Class { get; set; }把virtual去掉,那么下面的代码就会报空引用异常
var s = ctx.Students.First();
Console.WriteLine(s.Class.Name);
强调:如果要使用延迟加载,类必须是public,关联属性必须是virtual。
延迟加载(LazyLoad)的优点:
用到的时候才加载,没用到的时候才加载,因此避免了一次性加载所有数据,提高了加载的速度。
缺点:
如果不用延迟加载,就可以一次数据库查询就可以把所有数据都取出来(使用join实现),用了延迟加载就要多次执行数据库操作,提高了数据库服务器的压力。
因此:如果关联的属性几乎都要读取到,那么就不要用延迟加载;如果关联的属性只有较小的概率(比如年龄大于7岁的学生显示班级名字,否则就不显示)则可以启用延迟加载。
这个概率到底是多少是没有一个固定的值,和数据、业务、技术架构的特点都有关系,这是需要经验和直觉,也需要测试和平衡的。
注意:启用延迟加载的时候拿到的对象是动态生成类的对象,是不可序列化的,因此不能直接放到进程外Session、Redis等中,要转换成DTO(后面讲)再保存。
二十、不延迟加载,怎么样一次性加载
使用Include()方法:
var s = ctx.Students.Include("Class").First();//
观察生成的SQL语句,会发现只执行一个使用join的SQL就把所有用到的数据取出来了。当然拿到的对象还是Student的子类对象,但是不会延迟加载。(不用研究“怎么让他返回Student对象”)
Include("Class")的意思是直接加载Student的Class属性的数据。注意只有关联的对象属性次啊可以用Include,普通字段不可以
直接写"Class"可能拼写错误,如果用C#6.0,可以使用nameof语法解决问这个问题:
var s = ctx.Students.Include(nameof(Student.Class)).First();
也可以using System.Data.Entity;然后var s = ctx.Students.Include(e=>e.Class).First(); 推荐这种做法。
如果有多个属性需要一次性加载,也可以写多个Include:
var s = ctx.Students.Include(e=>e.Class) .Include(e=>e.Teacher).First();
如果Class对象还有一个School属性,也想把School对象的属性也加载,就要:
var s = ctx.Students.Include("Class").Include("Class. School").First();
或者更好的
var s = ctx.Students.Include(nameof(Student.Class))
二十一、延迟加载的一些坑
1,DbContext销毁后就不能再延迟加载了,因为数据库连接已经断开
下面的代码最后一行会报错:
Student s;
using (MyDbContext ctx = new MyDbContext())
{
s = ctx.Students.First();
}
Console.WriteLine(s.Class.Name);
两种解决方法:
1,用Include,不延迟加载(推荐)
Student s;
using (MyDbContext ctx = new MyDbContext())
{
s = ctx.Students.Include(t=>t.Class).First();
}
Console.WriteLine(s.Class.Name);
2,关闭前把要用到的数据取出来
Class c;
using (MyDbContext ctx = new MyDbContext())
{
Student s = ctx.Students.Include(t=>t.Class).First();
c = s.Class;
}
Console.WriteLine(c.Name);
2,两个取数一起使用
下面的程序会报错:
已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭。
foreach(var s in ctx.Students)
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
因为EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select查询,而由于延迟加载的存在到s.Class.Name也会再次执行查询。ADO.Net中默认是不能同时遍历两个DataReader。因此就报错。
三种解决方式:
1,允许多个DataReader一起执行:在连接字符串上加上MultipleActiveResultSets=true,但只适用于SQL 2005以后的版本。其他数据库不支持。
2,执行一下ToList(),因为ToList()就遍历然后生成List:
foreach(var s in ctx.Students.ToList())
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
3,推荐做法:用Include预先加载:
foreach(var s in ctx.Students.Include(e=>e.Class))
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
二十二、实体类的继承
所有实体类都会有一些公共属性,可以把这些属性定义到一个父类中。比如:
public abstract class BaseEntity
{
public long Id { get; set; } //主键
public bool IsDeleted { get; set; } = false; //软删除
public DateTime CreateDateTime { get; set; } = DateTime.Now;//创建时间
public DateTime DeleteDateTime { get; set; } //删除时间
}
使用公共父类的好处不仅是写实体类简单了,而且可以提供一个公共的Entity操作类:
class BaseDAO<T> where T:BaseEntity
[
private MyDbContext ctx;//不自己维护MyDbContext而是由调用者传递,因为调用者可以要执行很多操作,由调用者决定什么时候销毁。
public BaseDAO (MyDbContext ctx)
{
this.ctx = ctx;
}
public IQueryable<T> GetAll()//获得所有数据(不要软删除的)
{
return ctx.Set<T>().Where(t=>t.IsDeleted==false);//这样自动处理软删除,避免了忘了过滤软删除的数据
}
public IQueryable<T> GetAll(int start,int count) //分页获得所有数据(不要软删除的)
{
return GetAll().Skip(start).Take(count);
}
public long GetTotalCount()//获取所有数据的条数
{
return GetAll().LongCount();
}
public T GetById(long id)//根据id获取
{
return GetAll().Where(t=>t.Id==id).SingleOrDefault();
}
public void MarkDeleted(long id)//软删除
{
T en = GetById(id);
if(en!=null)
{
en.IsDeleted = true;
en.DeleteDateTime = DateTime.Now;
ctx.SaveChanges();
}
}
]
下面的代码会报错:
using (MyDbContext ctx = new MyDbContext())
{
BaseDAO<Student> dao = new BaseDAO<Student>(ctx);
foreach(var s in dao.GetAll())
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
}
原因是什么?
怎么Include?需要using System.Data.Entity;
using (MyDbContext ctx = new MyDbContext())
{
BaseDAO<Student> dao = new BaseDAO<Student>(ctx);
foreach(var s in dao.GetAll().Include(t=>t.Class))
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
}
有两个版本的Include、AsNoTracking:
1) DbQuery中的:
DbQuery<TResult> AsNoTracking()、DbQuery<TResult> Include(string path)
2) QueryableExtensions中的扩展方法:
AsNoTracking<T>(this IQueryable<T> source) 、Include<T>(this IQueryable<T> source, string path)、Include<T, TProperty>(this IQueryable<T> source, Expression<Func<T, TProperty>> path)
DbSet继承自DbQuery;Where()、Order、Skip()等这些方法返回的是IQueryable接口。因此如果在IQueryable接口类型的对象上调用Include、AsNoTracking就要using System.Data.Entity
二十三、其他
还有其他优秀的ORM框架:NHibernate、Dapper、PetaPoco、IBatis.Net;
ASP.Net MVC+Entity Framework的架构
一、了解一些不推荐的做法
有的项目里是直接把EF代码写到ASP.Net MVC的Controller中,这样做其实不符合分层的原则。ASP.Net MVC是UI层的框架,EF是数据访问的逻辑。
如果就要这么做怎么做的呢?
如果在Controller中using DbContext,把查询的结果的对象放到cshtml中显示,那么一旦在cshtml中访问关联属性,那么就会报错。因为关联属性可以一直关联下去,很诱惑人,include也来不及。
如果不using也没问题,因为会自动回收。但是这是打开了“潘多拉魔盒”,甚至可以在UI层更新数据。相当于把数据逻辑写到了UI层。
有的三层架构中用实体类做Model,这样也是不好的,因为实体类属于DAL层的逻辑。
二、EO、DTO、ViewModel
EO(Entity Object,实体对象)就是EF中的实体类,对EO的操作会对数据库产生影响。EO不应该传递到其他层。
DTO(Data Transfer Object,数据传输对象),用于在各个层之间传递数据的普通类。
DTO有哪些属性取决于其他层要什么数据。DTO一般是“扁平类”,也就是没有关联属性,都是普通类型属性。
一些复杂项目中,数据访问层(DAL)和业务逻辑层(BLL)直接传递用一个DTO类,UI层和BLL层之间用一个新的DTO类。简单的项目共用同一个DTO。DTO类似于三层架构中的Model。
ViewModel(视图模型),用来组合来自其他层的数据显示到UI层。简单的数据可能可以直接把DTO交给界面显示,一些复杂的数据可以要从新转换为ViewModel对象。
三、多层架构
搭建一个ASP.Net 三层架构项目:DAL、BLL、DTO、UI(asp.net mvc)。
UI、DAL、BLL都引用DTO;BLL引用DAL;EF中的所有代码都定义到DAL中,BLL中只访问DTO、BLL中不要引用DAL中的EF相关的类、不要在BLL中执行Include等操作、所有数据的准备工作都在DAL中完成。
注意:.Net中配置文件都是加载UI项目(ASP.net MVC)的,而不是加载DAL中的配置文件,因此EF的配置、连接字符串应该挪到UI项目中。
没有“正确的架构”,“错误的架构”,
只有“合适的架构” : 能够满足当前项目的要求,并且适当的考虑以后项目的发展,不要想的“太远”,不要“过度架构”;让新手能够非常快的上手
CRUD例子,带关联关系。班级管理、学生管理、民族
UI项目虽然不直接访问EF中的类,但是仍然需要在UI项目的App.config(Web.config)中对EF做配置,也要在项目中通过Nuget安装EF,并且要把连接字符串也配置到UI项目的App.config(Web.config)中