如鵬網學習筆記(十五)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)中
























免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM