如鹏网学习笔记(十五)ASP.NET MVC核心基础笔记

一、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();

  1return File(byte[] fileContents,string contentType);//返回byte[]格式的数据
  2return File(byte[] fileContents,string contentType,fileDownLoadName);//fileDownLoadName:设定浏览器端弹出的建议保存的文件名
  3return 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(&#39;ok&#39;)" 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的狗有如下两种写法:

      1var r1 = dogs.Where(d => d.Id > 1);
      2var 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一定要最后。这是刚用比较别扭的地方。

    用法:

    1var r= from d in dogs
        select d.Id;
    2var 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。这种方式问题最少,虽然性能略低,但是删除操作一般不频繁,不用考虑性能。后续在“状态管理”中会讲其他实现方法

    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)中























posted on 2017-08-22 16:49  半路和尚  阅读(4139)  评论(4编辑  收藏  举报

导航