一、ASP.Net MVC簡介
1,什么是ASP.NET MVC?
HttpHandler是ASP.net的底層機制,如果直接使用HttpHandler進行開發難度比較大、工作量大。因此提供了ASP.Net MVC、
ASP.Net WebForm等高級封裝的框架,簡化開發,他們的底層仍然是HttpHandler、HttpRequest等
例如:ASP.NET MVC的核心類仍然是實現了IHttpHandler接口的MVCHandler
2,ASP.NET WebForm和ASP.NET MVC的關系?
兩者都是對HttpHandler的封裝框架,ASP.NET MVC的思想,更適合現代項目的開發,因此會逐步取代WebForm
3,為什么ASP.NET MVC更好
程序員有更強的掌控力,不會產生垃圾代碼;程序員能夠更清晰的控制運行過程,因此更安全、性能和架構等更清晰。
入門“難”,深入“相對比較簡單”
4,什么是MVC模式
模型(Model)、視圖(View)、控制器(Controller)
Model負責在View和控制器之間進行數據的傳遞(用戶輸入的內容封裝成Model對象,發送給Controller);
要顯示的數據由Controller放到Model中,然后扔給View去顯示。
Controller不直接和View交互
5,ASP.Net MVC與“三層架構”沒有任何關系。
唯一的“關系”:三層中的UI層可以用ASP.Net MVC來實現
6,“約定大於配置”:
二、ASP.Net MVC起步
1,項目的創建
新建項目——C#——Web——ASP.NET Web應用程序(不要勾選“將Application Insights添加到項目”)——確定;
選中“Empty”——勾選MVC(不要勾選Host in the cloud)——確定
2,控制器的建立和視圖的建立
在Controller文件夾下右鍵——添加——控制器——選擇“MVC5控制器-空”,
注意:類的名字以Controller結尾,會自動在View文件夾下創建一個對應名字的文件夾(沒有就手動創建文件夾)
在View/文件夾名字 下創建視圖Index(和XXXController的Index方法一致)
注意:添加視圖時,模板選擇Empty,不要勾選創建為分部視圖和使用布局頁
3,新建一個用來收集用戶參數的類
IndexReqModel(類名無所謂,可以隨便起)包含Num1、Num2兩個屬性(只要不重名,大小寫都可以)
然后聲明一個IndexRespModel類用來給view傳遞數據顯示,有Num1、Num2、Result。
也可以同一個類實現,但是這樣寫看起來比較清晰
代碼:
public class TestControler:Controller { public ActionResult Index(IndexReqModel model) { IndexReqModel resq = new IndexReqModel(); resq.num1 = model.Num1; resq.num2 = model.Num2; resq.result = model.Num1 + model.Num2; return View(resq); } }
4,Index.cshtml的代碼
@model Test1.Models.IndexReqModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div> <input type="text" value="@Model.Num1" />+<input type="text" value="@Model.Num2" />=@Model.Result </div> </body> </html>
5,在瀏覽器訪問:http://localhost:56919/Test/Index?num1=1&num2=2
6,執行過程、數據流動分析:
當用戶訪問“Test/Index?num1=1&num2=2”的時候,會找到Controller下的TestController的Index方法去執行,
把請求參數按照名字填充到Index方法的參數對象中(MVC引擎負責創建對象,給數據賦值,並且進行類型的轉換),
return View(resq)就會找到Views下的和自己的“類名、方法名”相對應的Index.cshtml,然后把數據resp給到Index.cshtml去顯示。
注意:
1,@model Test1.Models.IndexReqModel //這里的model要小寫開頭,表示傳遞過來的數據是IndexReqModel類型的
2,@Model指傳遞過來的對象 //這里的Model要大寫開頭
3,cshtml模板就是簡化HTML的拼接的模板,最終還是生成html給瀏覽器顯示,不能直接訪問cshtml文件
三、Razor語法
1,語法簡單:
@啟動的區域為標准的C#代碼,其他部分是普通的html代碼
2,用法:
@{string a = "abc";} @a @{C#代碼塊} //有標簽的就是html代碼
@Model //控制器傳遞來的對象
@Model.dog.Name //控制器傳遞來的dog對象的Name屬性的值
@if(),@foreach()等C#語句
3,在代碼中輸入大段文字
兩種方法:
1,@:大段文字 //不推薦使用了,
代碼:
if(Model.IsOK) { @:文字 }
2,<html標簽>文字</html標簽>
代碼:
if(Model.IsOK) { <span>文字</span> }
razor會智能識別哪塊是C#,哪塊是HTML,HTML中想運行C#代碼就用@,想在C#中代碼中輸入HTML就寫“HTML標簽”。
但是如果由於樣式等原因不想加上額外的標簽,那么可以用<text></text>標記,特殊的<text>不會輸出到Html中。
4,注意:不要在@item后寫分號 //分號會被當成html代碼,原樣輸出
5,razor會自動識別哪塊是普通字符,哪塊是表達式,主要就是根據特殊符號來分辨(“識別到這里是否能被當成一個合法的C#語句”)。
例子:
不能這樣寫 <a href="Course@CourseId.ashx">,否則ashx會被識別為CourseId的一個屬性,
應該加上()強制讓引擎把CourseId識別成一個單獨的語法,<a href="Course(@CourseId).ashx">
技巧:
不確定的地方就加上(),也可以按照編輯器的代碼着色來進行分辨
6,如果不能自動提示,把頁面關掉再打開就可以了。如果還是不能自動提示,只要運行沒問題就行。
cshtml文件中如果有警告甚至錯誤,只要運行沒問題就沒關系
7,<span>333@qq.com</span>,razor會自動識別出來是郵箱,所以razor不會把 @qq.com當成qq對象的com屬性。
但是對於特殊的郵箱或者就是要顯示@,那么可以使用@轉義@,也就是“@@”
<li>item_@item.Length</span>//會把@item.Length識別成郵箱, 因此用上()成為: <li>item_@(item.Length)</span>
8,易錯:
要區分C#代碼和html代碼,
正確的:style='display:(@message.IsHide?"none":"block")'
錯誤的:style="display: (@message.IsHide) ? none : block"
注意:
為了避免C#中的字符串的“”和html的屬性值的“”沖突,建議如果html屬性中嵌入了C#代碼,那么html的屬性的值用單引號
9,為了避免XSS攻擊(跨站腳本攻擊,在輸出對象中嵌入script代碼等惡意代碼),Razor的@會自動把內容進行htmlencode輸出,
如果不想編碼后輸出,使用@Html.Raw()方法
10,Razor的注釋方法
@*要注釋的內容*@
11,Razor中調用泛型方法的時候,由於<>會被認為是html轉回標記模式,因此要用()括起來,比如@(Html.Test<string>)
()可以解決大部分問題,在View中一般不會調用復雜的方法
12,如果cshtml中任何html標簽的屬性中以"~/"開頭,則會自動進行虛擬路徑的處理,
當然一般是給<script>的src屬性、<link>的href屬性、<a>標簽的href屬性、<img>的src屬性用的。
13,html標簽的任何屬性的值如果是C#的值(使用@傳遞過來的值),
如果是bool類型的值,那么如果值是false,則不會渲染這個屬性,如果是true,則會渲染成“屬性名=屬性名”
代碼示例
@{ bool b1 = true; bool b2 = false; } <input type="checkbox" checked="@b1"/>//此時生成的html代碼為:<input type="checkbox" checked="checked">
這個特性避免了進行三元運算符的判斷
14,總結:
1、@就是C#,<aaa></aaa>就是html
2、如果想讓被識別成html的當成C#那就用@()
3、如果想讓被識別成C#的當成html,用<span>等標簽,如果不想生成額外的標簽,就用<text></text>
4、如果不想對內容htmlencode顯示就用@Html.Raw()方法
5、屬性的值如果以"~/"開頭會進行虛擬路徑處理
6、屬性值如果是bool類型,如果是false就不輸出這個屬性,如果true就輸出“屬性名=屬性名”<input type="checkbox" checked="@b1"/>
四、知識點補充和復習
1,dynamic是C#語法中提供的一個語法,實現像JavaScript一樣的動態語言,可以到運行的時候再去發現屬性的值或者調用方法
代碼示例
dynamic p = new dynamic(); p.Name = "rupeng.com"; p.Hello();
注意:即使沒有成員p.Age=3;編譯也不會報錯,只有運行的時候才會報錯
好處是靈活,壞處是不容易在開發的時候發現錯誤、並且性能低
如果dynamic指向System.Dynamic.ExpandoObject()對象,這樣可以給對象動態賦值屬性(不能指向方法):
dynamic p = new System.Dynamic.ExpandoObject(); p.Name = "rupeng.com"; p.Age = 10; Console.WriteLine(p.Name+","+p.Age);
2,var類型推斷
var i = 3; var s ="abc";
編譯器會根據右邊的類型推斷出var是什么類型
var和dynamic的區別:
var是編譯的時候確定的,dynamic是在運行的時候動態確定的
var變量不能指向其他類型,dynamic可以(因為var在編譯的時候已經確定了類型)
3,匿名類型
匿名類型是C#中提供的一個新語法:
var p = new {Age=5,Name="rupeng.com"};//這樣就創建了一個匿名類的對象,這個類沒有名字,所以叫匿名類
原理:
編譯器生成了這個類,這個類是internal、屬性是只讀的、初始值是通過構造函數傳遞的
因此:
因為匿名類的屬性是只讀的,所以匿名類型的屬性是無法賦值的;
因為匿名類型是internal,所以無法跨程序集訪問其成員(只能活在自己當前的程序集內)。
五、Controller給View傳遞數據的方式
1,ViewData:
以ViewData["name"]="rupeng";string s =(string)ViewData["name"]這樣的鍵值對的方式進行數據傳送
2,ViewBag:
ViewBag是dynamic類型的參數,是對ViewData一個動態類型封裝,用起來更方便,和ViewData共同操作一個數據。ViewBag.name="";
@ViewBag.name。
用ViewBag傳遞數據非常方便,但是因為ASP.Net MVC中的“Html輔助類”等對於ViewBag有一些特殊約定,一不小心就跳坑了(http://www.cnblogs.com/rupeng/p/5138575.html),所以盡量不要用ViewBag,而是使用Model。
3、Model:
可以在Controller中通過return View(model)賦值,然后在cshtml中通過Model屬性來訪問這個對象;
如果在cshtml中通過“@model 類型”(注意model小寫)指定類型,則cshtml中的Model就是指定的強類型的,這樣的cshtml叫“強類型視圖”;
如果沒有指定“@model 類型”, 則cshtml中的Model就是dynamic。
六、關於Action的參數
ASP.Net MVC5會自動對參數做類型轉換
對於boolean類型的參數(或者Model的屬性),如果使用checkbox,則value必須是“true”,否則值永遠是false。對於double、int等類型會自動進行類型轉換
1,一個Controller可以有多個方法,這些方法叫Action。通過“Controller名字/方法名”訪問的時候就會執行對應的方法。
2,Action的三種類型的參數:
普通參數、Model類、FormCollection
1,普通參數:
Index(string name,int age)。框架會自動把用戶Get請求的QueryString或者Post表單中的值根據參數名字映射對應參數的值,
適用於查詢參數比較少的情況。
注意:int類型的可空問題
2,Model類。叫ViewModel。
3,FormCollection,采用fc["name"]這種方法訪問,類似於HttpHandler中用context["name"]。
適用於表單元素不確定、動態的情況
3,Action的方法不能重載,所以一個Controller中不能存在兩個同名的Action
錯誤代碼:
public ActionResult T1(string name)和public ActionResult T1(int Age)不能同時存在
特殊情況:
給Action方法上標注[HttpGet]、[HttpPost],注意當發出Get或者Post請求的時候就會執行相應標注的方法,變相實現了同名的Action
常見的應用方法:
把需要展示的初始頁面的Action標注為[HttpGet],把表單提交的標注為[HttpPost]
4,Action參數可以一部分是普通參數,一部分為Model
代碼示例:
public ActionResult T1(string name,Classes className)
5,Action參數如果在請求中沒有對應的值,就會去默認值:
Model類的形式則取默認值:int是0、boolean是false、引用類型是null。
普通參數的形式:取默認值會報錯,如果允許為空,要使用int?,也可以使用C#的可選參數語法來設定默認值
示例代碼:
Index(string name="tom");
6,上傳文件的參數用HttpPostedFileBase類型,
七、View的查找
1,return View()會查找Views的Controller名字的Action的名字的cshtml
2,return View("Action1"),查找Views的Controller名字下的“Action1.cshtml”,如果找不到則到特殊的shared文件夾下找“Action1.cshtml”
3、return View("Action1")中如何傳遞model?return View("Action1",model)。
陷阱:如果model傳遞的是string類型,則需要return View("Action1",(object)str)為什么?看一下重載!
注意:
return View("Action1")不是重定向,瀏覽器和服務器之間只發生了一次交互,地址欄還是舊的Action的地址。
這和重定向return Redirct("/Index/Action1");不一樣
應用:
執行報錯,return View("Error",(object)msg) 通用的報錯頁面。為了防止忘了控制重載,封裝成一個通用方法。
八、其他類型的ActionResult
1,View()是一個方法,它的返回值是ViewResult類型,ViewResult繼承自ActionResult,
如果在確認返回的是View(),返回值寫成ViewResult也行,但是一般沒這個必要,因為那樣就不靈活了。因為ViewResult還有其他子類
2,RedirectResult,重定向,最終就是調用response.Redirect()。
用法:
return Redirect("http://www.rupeng.com");//重定向到rupeng return Redirect("~/1.html");//重定向到
3,ContentResult
返回程序中直接拼接生成的文本內容
return Content(string content,string contentType)
4,文件 return File();
1,return File(byte[] fileContents,string contentType);//返回byte[]格式的數據
2,return File(byte[] fileContents,string contentType,fileDownLoadName);//fileDownLoadName:設定瀏覽器端彈出的建議保存的文件名
3,return File(Stream fileStream, string contentType) 返回Stream類型的數據(框架會幫着Dispose,不用也不能Dispose)
4,FileStreamResult return File(Stream fileStream,string contentType,string fileDownLoadName)
5, File(string fileName, string contentType)// 返回文件名指定的文件,內部還是流方式讀取文件;
6, File(string fileName, string contentType, string fileDownloadName) //如果是返回動態生成的圖片(比如驗證碼),則不用設置fileDownloadName;如果是“導出學生名單”、“下載文檔”等操作則要設定fileDownloadName。
注意:如果在Controller中要使用System.IO下的File類,因為和File方法重名了,所以要用命名空間來引用了。
5,return HttpNotFound();
6,return JavaScript(string script);
返回JavaScript代碼字符串,和return Content("alert('Hello World');","application/x-javascript");效果一樣。
因為違反三層原則,盡量不要使用
7,Json
JsonResult Json(object data) 把data對象序列化為json字符串返回客戶端,並且設置contentType為"application/json"
Json方法默認是禁止Get請求的(主要為了防止CSRF攻擊,舉例:在A網站中嵌入一個請求銀行網站給其他賬號轉賬的Url的img),只能Post請求。所以如果以Get方式訪問是會報錯的。
如果確實需要以Get方式方式,需要調用return Json(data, JsonRequestBehavior.AllowGet)
ASP.NET MVC 默認的Json方法實現有如下的缺點:
1,日期類型的屬性格式化成字符串是“\/Date(1487305054403)\/"這樣的格式,在客戶端要用js代碼格式化處理,很麻煩。
2,json字符串中屬性的名字和C#中的大小寫一樣,不符合js中“小寫開頭、駝峰命名”的習慣。在js中也要用大寫去處理。
3,無法處理循環引用的問題(盡管應該避免循環引用),會報錯“序列化類型為***的對象時檢測到循環引用”
8,重定向
1,Redirect(string url)
2,RedirectToAction(string actionName,string controllerName);//其實就是幫助拼接生成url,最終還是調用Redirect(),
3,兩者的區別:
RedirectToAction是讓客戶端重定向,是一個新的Http請求,所以無法讀取ViewBag中的內容;
return View()是一次服務器一次處理轉移
Redirect和return View 的區別:
1、 Redirect是讓瀏覽器重定向到新的地址;return View是讓服務器把指定的cshtml的內容運行渲染后給到瀏覽器;
2、 Redirect瀏覽器和服務器之間發生了兩次交互;return View瀏覽器和服務器之間發生了1次交互
3、 Redirect由於是兩次請求,所以第一次設置的ViewBag等這些信息,在第二次是取不到;而View則是在同一個請求中,所以ViewBag信息可以取到。
4、 如果用Redirect,則由於是新的對Controller/Action的請求,所以對應的Action會被執行到。如果用View,則是直接拿某個View去顯示,對應的Action是不執行的。
什么情況用View?服務器端產生數據,想讓一個View去顯示的;
什么情況用Redirect?讓瀏覽器去訪問另外一個頁面的時候。
九、雜項Misc
1、TempData
在SendRedirect客戶端重定向或者驗證碼等場景下,由於要跨請求的存取數據,是不能放到ViewBag、Model等中,
需要“暫時存到Session中,用完了刪除”的需求:實現起來也比較簡單:
存入: Session["verifyCode"] = new Random().Next().ToString();
讀取: String code = (string) Session["verifyCode"]; Session["verifyCode"] = null; if(code==model.Code) { //... }
ASP.Net MVC中提供了一個TempData讓這一切更簡單。
在一個Action存入TempData,在后續的Action一旦被讀取一次,數據自動銷毀。
TempData默認就是依賴於Session實現的,所以Session過期以后,即使沒有讀取也會銷毀。
應用場景:驗證碼;
2、HttpContext與HttpContextBase、HttpRequest與HttpRequestBase、HttpPostedFile與HttpPostedFileBase。
注意:進行asp.net mvc開發的時候盡量使用****Base這些類,不要用asp.net內核原生的類。HttpContext.Current(X)
1)在Controller中HttpContext是一個HttpContextBase類型的屬性(真正是HttpContextWrapper類型,是對System.Web.HttpContext的封裝),System.Web.HttpContext是一個類型。這兩個類之間沒有繼承關系。
System.Web.HttpContext類型是原始ASP.Net核心中的類,在ASP.Net MVC中不推薦使用這個類(也可以用)。
2)HttpContextBase能“單元測試”,System.Web.HttpContext不能。
3)怎么樣HttpContextBase.Current?其實是不推薦用Current,而是隨用隨傳遞。
4)HttpContextBase的Request、Response屬性都是HttpRequestBase、HttpResponseBase類型。Session等也如此。
5)如果真要使用HttpContext類的話,就要System.Web.HttpContext
3,Views的web.config中的system.web.webpages.razor的pages/namespaces節點下配置add命名空間,這樣cshtml中就不用using了
示例代碼:
<system.web.webPages.razor> <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" /> <pages pageBaseType="System.Web.Mvc.WebViewPage"> <namespaces> <add namespace="System.Web.Mvc" /> <add namespace="System.Web.Mvc.Ajax" /> <add namespace="System.Web.Mvc.Html" /> <add namespace="System.Web.Routing" /> <add namespace="Test1" /> </namespaces> </pages> </system.web.webPages.razor>
4,Layout布局文件
@RenderBody()渲染正文部分;cshtml的Layout屬性設定Layout頁面地址;
@RenderSection("Footer")用於渲染具體頁面中用@section Footer{}包裹的內容,如果Footer是可選的,那么使用@RenderSection("Footer",false),
可以用IsSectionDefined("Footer")實現“如果沒定義則顯示***”的效果。
5, 可以在Views文件夾下建一個_ViewStart.cshtml文件,在這個文件中定義Layout,這樣不用每個頁面中都設定Layout,
當然具體頁面也可以通過設定Layout屬性來覆蓋默認的實現;
6,@Html.DropDownList
如果在頁面中輸出一個下拉列表或者列表框,就要自己寫foreach拼接html,還要寫if判斷哪項應該處於選中狀態
<select> @foreach(var p in (IEnumerable<Person>)ViewBag.list) { <option selected="@(p.Id==3)">@p.Name</option> } </select>
asp.net mvc中提供了一些“Html輔助方法”(其實就是Controller的Html屬性中的若干方法,其實是擴展方法)用來簡化html代碼的生成。
DropDownList是生成下拉列表的。
1)DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
string name參數用來設定 <select>標簽的name屬性的值,id屬性的值默認和name一致。
下拉列表中的項(<option>)以SelectListItem集合的形式提供,SelectListItem的屬性:
bool Selected:是否選中狀態,也就是是否生成selected="selected"屬性;
string Text:顯示的值,也就是<option>的innerText部分;
string Value:生成的value屬性,注意是string類型;
示例代碼:
List<Person> list = new List<Person>(); list.Add(new Person { Id=1,Name="lily",IsMale=false}); list.Add(new Person { Id = 12, Name = "tom", IsMale = true }); list.Add(new Person { Id = 13, Name = "lucy", IsMale = false }); List<SelectListItem> sliList = new List<SelectListItem>(); foreach (var p in list) { SelectListItem listItem = new SelectListItem(); listItem.Selected = (p.Id==2); listItem.Text = p.Name; listItem.Value = p.Id.ToString(); sliList.Add(listItem); } return View(sliList);
@model IEnumerable<SelectListItem> <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>DDL</title> </head> <body> <div> @Html.DropDownList("pid", Model); </div> </body> </html>
2)DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
htmlAttributes屬性用來生成select標簽的其他屬性,通常以匿名類對象的形式提供,
比如new { onchange = "javascript:alert('ok')", style = "color:red", aaa = "rupeng", id = "yzk",@class="warn error" }
生成的html源碼為:
<select aaa="rupeng" class="warn error" id="yzk" name="pid" onchange="javascript:alert('ok')" style="color:red">
支持自定義屬性,給你原樣輸出,具體什么含義自己定;
由於class是關鍵字,所以不能直接用class="",要加上一個@前綴,這其實是C#中給變量名取名為關鍵字的一種語法;
注意:
id默認和name一致,如果設定了id則覆蓋默認的實現。
3)構造一個特殊的集合類SelectList,他會自動幫着做集合的遍歷
public ActionResult DDL2() { List<Person> list = new List<Person>(); list.Add(new Person { Id=666,Name="zhangsan",IsMale=false}); list.Add(new Person { Id = 222, Name = "tom", IsMale = true }); list.Add(new Person { Id = 333, Name = "lucy", IsMale = false }); SelectList selectList = new SelectList(list, "Id", "Name"); return View(selectList); }
@Html.DropDownList("name",(SelectList)Model);
IEnumerable items參數用來顯示的原始對象數據,string dataValueField為“對象的哪個屬性用做生成value屬性”,
string dataTextField為“對象的哪個屬性用作生成顯示的文本屬性”。
用SelectList的好處是簡單,但是如果說要同時顯示多個屬性的時候,就只能用非SelectList的方式了。
SelectList還可以設定第四個參數:
哪個值被選中:SelectList selectList = new SelectList(list,"Id","Name",222);
一個坑:不能讓cshtml中的DropDownList的第一個name參數和ViewBag中任何一個屬性重名http://www.cnblogs.com/rupeng/p/5138575.html。
建議不要通過ViewBag傳遞,都通過Model傳遞
7,@Html.ListBox()
和@Html.DropDownList()類似
8,為什么不再推薦使用“Html輔助方法”
壞處:因為不符合復雜項目的開發流程(前端程序員可能看不懂),
好處:可以把表單驗證、綁定等充分利用起來,開效率高,
但是在互聯網項目中開發效率並不是唯一關注因素。在asp.net mvc6中已經不再推薦使用html輔助方法的表單了
9,Request.IsAjaxRequest()
判斷是來自於Ajax請求,這樣可以讓ajax請求和非ajax請求響應不同的內容
原理:
Ajax請求的報文頭中有x-requested-with: XMLHttpRequest。
如果使用System.Web.HttpContext,那么是沒有這個方法的,那么自己就從報文頭中取數據判斷。
示例代碼:
public ActionResult Ajax1() { return View(); } public ActionResult Ajax2() { Person p = new Person(); p.Name = "rupeng"; if (Request.IsAjaxRequest()) { return Json(p); } else { return Content(p.Name); } }
10,數據驗證
1,asp.net mvc會自動根據屬性的類型進行基本的校驗,比如如果屬性是int類型的,那么在提交非整數類型的數據的時候就會報錯。
注意ASP.net MVC並不是在請求驗證失敗的時候拋異常,而是把決定權交給程序員,程序員需要決定如何處理數據校驗失敗。
在Action中根據ModelState.IsValid判斷是否驗證通過,如果沒有通過下面的方法拿到報錯信息
示例代碼:
public ActionResult Index(IndexModel model) { if (ModelState.IsValid) { return Content("Age=" + model.Age); } else { return Content("驗證失敗"); } }
在參數很多的情況下使用下面的封裝的方法:
public static string GetValidMsg(ModelStateDictionary modelState) { StringBuilder sb = new StringBuilder(); foreach (var propName in modelState.Keys) { if (modelState[propName].Errors.Count <= 0) { continue; } sb.Append("屬性【").Append(propName).Append("】錯誤:"); foreach (var modelError in modelState[propName].Errors) { sb.AppendLine(modelError.ErrorMessage); } } return sb.ToString(); }
2,ASP.Net MVC提供了在服務器端驗證請求數據的能力。要把對應的Attribute標記到Model的屬性上(標記到方法參數上很多地方不起作用)。
常用驗證Attribute:
a) [Required] 這個屬性是必須的
b) [StringLength(100)], 字符串最大長度100;[StringLength(100,MinimumLength=10)]長度要介於10到100之間
c) [RegularExpression(@"aa(\d)+bb")] 正則表達式
d) [Range(35,88)] 數值范圍。字符串長度范圍的話請使用[StringLength(100,MinimumLength=10)]
e) [Compare("Email")] 這個屬性必須和Email屬性值一樣。
f) [EmailAddress] 要是郵箱地址
g) [Phone] 電話號碼,規則有限
示例代碼:
public class IndexModel { [Required] public int Age { get; set; } public long Id { get; set; } public string Name { get; set; } [StringLength(11)] public string PhoneNum { get; set; } }
3, 驗證Attribute上都有ErrorMessage屬性,用來自定義報錯信息。ErrorMessage中可以用{0}占位符作為屬性名的占位。
示例代碼:
[Required(ErrorMessage="不能為空")] public int Age { get; set; }
4, 數據驗證+Html輔助類高級控件可以實現很多簡化的開發,連客戶端+服務器端校驗都自動實現了,但是有點太“WebForm”了,因此這里先學習核心原理,避免暈菜。
11,自定義驗證規則ValidationAttribute,
自動的驗證規則需要直接或者間接繼承自ValidationAttribute
1,使用正則表達式的校驗,直接從RegularExpressionAttribute繼承
示例代碼: public class QQNumberAttribute : RegularExpressionAttribute { public QQNumberAttribute() : base(@"^\d{5,10}$")//不要忘了^$ { this.ErrorMessage = "{0}屬性不是合法的QQ號,QQ號需要5-10位數字"; //設定ErrorMessage的默認值。使用的人也可以覆蓋這個值 } }
手機號的正則表達式:@"^1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}$"
2,直接繼承自ValidationAttribute,重寫IsValid方法
比如校驗中國電話號碼合法性
public class CNPhoneNumAttribute : ValidationAttribute { public CNPhoneNumAttribute() { this.ErrorMessage = "電話號碼必須是固話或者手機,固話要是3-4位區號開頭,手機必須以13、15、18、17開頭"; } public override bool IsValid(object value) { if (value is string) { string s = (string)value; if (s.Length == 13)//手機號 { if (s.StartsWith("13") || s.StartsWith("15") || s.StartsWith("17") || s.StartsWith("18")) { return true; } else { return false; } } else if (s.Contains("-"))//固話 { string[] strs = s.Split('-'); if (strs[0].Length==3||strs[0].Length==4) { return true; } else { return false; } } else { return false; } } else { return false; } //return base.IsValid(value); } }
3,還可以讓Model類實現IValidatableObject接口,用的比較少
十、過濾器(Filter)
AOP(面向切面編程)是一種架構思想,用於把公共的邏輯放到一個單獨的地方,這樣就不用每個地方都寫重復的代碼了。
比如程序中發生異常,不用每個地方都try...catch...只要在(Global 的Application_Error)中統一進行異常處理。不用每個Action中都檢查當前用戶是否有執行權限,
ASP.net MVC中提供了一個機制,每個Action執行之前都會執行我們的代碼,這樣統一檢查即可。
1,四種Filter
在ASP.Net MVC中提供了四個Filter(過濾器)接口實現了這種AOP機制:
IAuthorizationFilter、IActionFilter、IResultFilter、IExceptionFilter。
1,IAuthorizationFilter
一般用來檢查當前用戶是否有Action的執行權限,在每個Action被執行前執行OnAuthorization方法;
2,IActionFilter
也是在每個Action被執行前執行OnActionExecuting方法,每個Action執行完成后執行OnActionExecuted方法
和IAuthorizationFilter的區別是IAuthorizationFilter在IActionFilter之前執行,檢查權限一般寫到IAuthorizationFilter中;
3,IResultFilter,在每個ActionResult的前后執行IResultFilter。用的很少,后面有一個應用。
4,IExceptionFilter,當Action執行發生未處理異常的時候執行OnException方法。
在ASP.net MVC 中仍然可以使用“Global 的Application_Error”,但是建議用IExceptionFilter。
2、IAuthorizationFilter案例:只有登錄后才能訪問除了LoginController之外的Controller。
1,編寫一個類CheckAuthorFilter,實現IAuthorizationFilter接口(需要引用System.Web.Mvc程序集)
示例代碼:
public class CheckLoginFilter : IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { string ctrlName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; string actionName = filterContext.ActionDescriptor.ActionName; if (ctrlName=="Login"&&(actionName=="Index"||actionName=="Login")) { //什么都不做 } else { if (filterContext.HttpContext.Session["username"]==null) { ContentResult contentResult = new ContentResult(); contentResult.Content = "沒有登錄"; //filterContext.Result = contentResult; filterContext.Result = new RedirectResult("/Login/Index"); } } } }
2,在Globel中注冊這個Filter:GlobalFilters.Filters.Add(new CheckAuthorFilter());
示例代碼:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); GlobalFilters.Filters.Add(new CheckLoginFilter()); }
3,CheckAuthorFilter中實現OnAuthorization方法。
filterContext.ActionDescriptor 可以獲得Action的信息:
filterContext.ActionDescriptor.ActionName 獲得要執行的Action的名字;
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName 為要執行的Controller的名字;
filterContext.ActionDescriptor.ControllerDescriptor.ControllerType 為要執行的Controller的Type;
filterContext.HttpContext 獲得當前請求的HttpContext;
如果給“filterContext.Result”賦值了,那么就不會再執行要執行的Action,而是以“filterContext.Result”的值作為執行結果
(注意如果是執行的filterContext.HttpContext.Response.Redirect(),那么目標Action還會執行的)。
4,檢查當前用戶是否登錄,
如果沒有登錄則filterContext.Result = new ContentResult() { Content = "沒有權限" };
或者filterContext.Result = new RedirectResult("/Login/Index");
(最好不要filterContext.HttpContext.Response.Redirect("/Login/Index");)
5,A用戶有一些Action執行權限,B用戶有另外一些Action的執行權限;
3、IActionFilter案例:日志記錄,記錄登錄用戶執行的Action的記錄,方便跟蹤責任。
4、IExceptionFilter案例:記錄未捕獲異常。
public class ExceptionFilter : IExceptionFilter { public void OnException(ExceptionContext filterContext) { File.AppendAllText("d:/error.log", filterContext.Exception.ToString()); filterContext.ExceptionHandled = true;//如果有其他的IExceptionFilter不再執行 filterContext.Result = new ContentResult() { Content= "error" }; } }
然后:
GlobalFilters.Filters.Add(new ExceptionFilter());
5、總結好處:
一次編寫,其他地方默認就執行了。可以添加多個同一個類型的全局Filter,按照添加的順序執行。
6、(*)非全局Filter:
只要讓實現類繼承自FilterAttribute類,然后該實現哪個Filter接口就實現哪個(四個都支持)。
不添加到GlobalFilters中,而是把這個自定義Attribute添加到Controller類上這樣就只有這個Controller中操作會用到這個Filter。
如果添加到Action方法上,則只有這個Action執行的時候才會用到這個Filter。
Nuget筆記
一、NuGet簡介
我們進行軟件開發的時候,經常會用到第三方的開發包(俗稱dll),比如NPOI、MYSQL ADO.net 驅動等。
如果自己去網上下載的問題:不好搜,不好找;容易下載到錯誤的;要自己進行安裝配置;要選擇和當前環境一致的版本(比如有的開發包在.net 2.0和.net 4.5中要用不同的版本);這個開發包可能還要使用其他的安裝包。
微軟提供了NuGet這個軟件,自動幫我們進行開發包的下載、安裝,並且會根據當前的環境找到合適的版本,下載相關的依賴的開發包,還可以自動更新最新版本。
NuGet在VS2013以上都提供了。有圖形界面和命令行兩種使用方式。
二、Nuget.org尋寶
http://www.nuget.org
三、圖形界面,進行安裝
在項目的“引用”中右鍵——管理Nuget程序包——在瀏覽中輸入程序包的名字(比如NPOI)——搜索——然后進行安裝
由於nuget的服務器在國外,可能你的網絡連不上或者速度慢,可以從其他鏡像站點下
方法:
1,找一個可用的鏡像站點,目前可用的是博客園的鏡像,地址https://nuget.cnblogs.com/v3/index.json
2,在VS的【工具】→【選項】→【NuGet包管理器】→【程序包源】,點擊【添加】
然后在搜索安裝的時候在【程序包源】中選擇我們添加的鏡像
3,在搜索結果中找到合適的結果,點擊后在右側選擇合適的版本
點擊【安裝】以后可能會彈出要求【同意】協議的對話框,點擊【同意】即可。
在【輸出】的【程序包管理器】中出現“========== 已完成 ==========”的時候說明安裝完成,有可能會報錯。
4,會自動添加引用
5,有的安裝包會自動修改APP.config等配置文件
6,Nuget安裝包信息在packages.config中,對應的安裝包在解決方案的packages文件夾下,把項目拷給別人的時候沒必要拷packages文件夾,別人拿到以后會自動下載恢復這些安裝包。
7,如果要刪除某個安裝包,不能只刪除引用,否則還會自動恢復、添加,還要手動刪除packages.config中的內容。最好使用圖形界面的“卸載”功能。
四、命令行的使用
好處:方便、靈活
1,在【程序包管理器控制台】視圖中(如果沒顯示出來,則主菜單【工具】→【NuGet包管理器】→【程序包管理器控制台】)。
輸入:Install-Package 程序包的名字
安裝完成的標志
2,可以在【程序包源】中指定鏡像站點,【默認項目】指的是安裝到哪個項目中
3,指定版本::Install-Package 安裝包 -Version 版本號,
比如:
Install-Package MySql.Data -Version 6.8.8
4,卸載
示例代碼:
UnInstall-Package MySql.Data
Entity Framework筆記
Entity Framework是在.Net平台下進行數據庫開發的框架,ORM框架
一、相關知識復習
1. var類型推斷:
var p =new Person();
2. 匿名類型。
var a =new {p.Name,Age=5,Gender=p.Gender,Name1=a.Name};//{p.Name}=={Name=p.Name}
3. 給新創建對象的屬性賦值的簡化方法:
Person p = new Person{Name="tom",Age=5};
等價於
Person p = new Person();p.Name="tom";p.Age=5;
4. lambda表達式
函數式編程,在Entity Framework編程中用的很多
1,原始樣式
Action<int> a1 = delegate(int i){Console.writeLine(i);};
2,可以簡化為(=>讀作goes to)
Action<int> a2 = (int i)=>{Console.writeLine(i);};
3,還可以省略參數類型(編譯器會自動根據委托類型解析):
Action<int> a3 =(i)=>{Console.writeLine(i);};
4,如果只有一個參數還可以省略參數的小括號(多個參數不行)
Action<int> a4 = i=>{Console.writeLine(i);};
5,如果委托有返回值,並且方法體只有一行代碼,這一行代碼還是返回值,那么就可以連方法的大括號和return都省略:
Func<int,int,string> f1 = delegate(int i,int j){return "結果是"+(i+j);};//原始形式 Func<int,int,string>f2 = (i,j)=>"結果是"+(i+j);//簡化形式
5,集合常用的擴展方法
Where(支持委托)、Select(支持委托)、Max、Min、OrderBy
First(獲取第一個,如果沒有則異常)
FirstOrDefault(獲取第一個,如果沒有則返回默認值)
Single(獲取唯一一個,如果沒有或者多個則異常)
SingleOrDefault(獲取唯一一個,如果沒有則返回默認值,多個則異常)
注意:
lambda中照樣要避免變量名重名的問題:
var p = persons.Where(p=>p.Name=="rupeng.com").First();//錯誤代碼:兩個p重名,修改一個
二、 高級集合擴展方法
准備工作1:創建對象類
//學生 public class Person { public string Name { get; set; } public int Age { get; set; } public bool Gender { get; set; } public int Salary { get; set; } public override string ToString() { return string.Format("Name={0},Age={1},Gender={2},Salary={3}",Name, Age, Gender, Salary); } }
//老師 public class Teacher { public Teacher() { this.Students=new List<Person>(); } public string Name { get; set; } public List<Person> Students { get; set; } }
//准備工作2,在控制台項目中,添加數據
var s0 =new Person { Name="tom",Age=3,Gender=true,Salary=6000}; var s1 = new Person { Name = "jerry", Age = 8, Gender = true, Salary = 5000 }; var s2 = new Person { Name = "jim", Age = 3, Gender = true, Salary = 3000 }; var s3 = new Person { Name = "lily", Age = 5, Gender = false, Salary = 9000 }; var s4 = new Person { Name = "lucy", Age = 6, Gender = false, Salary = 2000 }; var s5 = new Person { Name = "kimi", Age = 5, Gender = true, Salary = 1000 }; List<Person> list = new List<Person>(); list.Add(s0); list.Add(s1); list.Add(s2); list.Add(s3); list.Add(s4); list.Add(s5); Teacher t1 = new Teacher { Name="如鵬網張老師"}; t1.Students.Add(s1); t1.Students.Add(s2); Teacher t2 = new Teacher { Name = "如鵬網劉老師" }; t2.Students.Add(s2); t2.Students.Add(s3); t2.Students.Add(s5); Teacher[] teachers={t1,t2};
//開始展示用法
1,Any(),
判斷集合是否包含元素,返回值是bool,一般比Coun()>0效率高。
Any還可以指定條件表達式。
bool b = list.Any(p => p.Age > 50);等價於bool b = list.Where(p=>p.Age>50).Any();
2,Distinct(),剔除完全重復數據。(*)注意自定義對象的Equals問題:需要重寫Equals和GetHashCode方法來進行內容比較。
3,排序:
升序
list.OrderBy(p=>p.Age);
降序
list.OrderByDescending(p=>p.Age)
指定多個排序規則,不是多個OrderBy,而是:OrderBy..ThenBy
list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary),//也支持ThenByDescending()。注意這些操作不會影響原始的集合數據。
4,Skip(n)
跳過前n條數據;
Take(n)獲取最多n條數據,如果不足n條也不會報錯。
常用來分頁獲取數據。list.Skip(30).Take(20) 跳過前3條數據獲取2條數據。
5,Except(items1)
排除當前集合中在items1中存在的元素
6,Union(items1)
把當前集合和items1中組合
7,Intersect(items1)
把當前集合和items1中取交集
8,分組
foreach(var g in list.GroupBy(p => p.Age)) { Console.WriteLine(g.Key+":"+g.Average(p=>p.Salary)); }
9,SelectMany:
把集合中每個對象的另外集合屬性的值重新拼接為一個新的集合
foreach(var s in teachers.SelectMany(t => t.Students)) { Console.WriteLine(s);//每個元素都是Person }
注意:
不會去重,如果需要去重則要自己再次調用Distinct()
10,Join
//准備工作1:創建類
//Master類 class Master { public long Id{get;set;} public string Name{get;set;} }
//Dog類 class Dog { public long Id { get; set; } public long MasterId { get; set; } public string Name { get; set; } }
//准備工作2,在控制台項目中添加數據
Master m1 = new Master { Id = 1, Name = "楊中科" }; Master m2 = new Master { Id = 2, Name = "比爾蓋茨" }; Master m3 = new Master { Id = 3, Name = "周星馳" }; Master[] masters = { m1,m2,m3}; Dog d1 = new Dog { Id = 1, MasterId = 3, Name = "旺財" }; Dog d2 = new Dog { Id = 2, MasterId = 3, Name = "汪汪" }; Dog d3 = new Dog { Id = 3, MasterId = 1, Name = "京巴" }; Dog d4 = new Dog { Id = 4, MasterId = 2, Name = "泰迪" }; Dog d5 = new Dog { Id = 5, MasterId = 1, Name = "中華田園" }; Dog[] dogs = { d1, d2, d3, d4, d5 };
Join可以實現和數據庫一樣的Join效果,對有關聯關系的數據進行聯合查詢
下面的語句查詢所有Id=1的狗,並且查詢狗的主人的姓名。
var result = dogs.Where(d => d.Id > 1).Join(masters, d => d.MasterId, m => m.Id,(d,m)=>new {DogName=d.Name,MasterName=m.Name}); foreach(var item in result) { Console.WriteLine(item.DogName+","+item.MasterName); }
三、Linq
1,簡介
查詢Id>1的狗有如下兩種寫法:
1)var r1 = dogs.Where(d => d.Id > 1);
2)var r2 = from d in dogs where d.Id>1 select d;
第一種寫法是使用lambda的方式寫的,官方沒有正式的叫法,我們就叫“lambda寫法”;
第二種是使用一種叫Linq(讀作:link)的寫法,是微軟發明的一種類似SQL的語法,給我們一個新選擇。
兩種方法是可以互相替代的,沒有哪個好、哪個壞,看個人習慣。
經驗:
需要join等復雜用法的時候Linq更易懂,一般的時候“lambda寫法”更清晰,更緊湊
2,辟謠
“Linq被淘汰了”是錯誤的說法,應該是“Linq2SQL被淘汰了”。
linq就是微軟發明的這個語法,可以用這種語法操作很多數據,
操作SQL數據就是Linq2SQL,linq操作后面學的EntityFramework就是Linq2Entity,linq操作普通.Net對象就是Linq2Object、Linq操作XML文檔就是Linq2XML。
3,linq基本語法
以from item in items開始,items為待處理的集合,item為每一項的變量名;
最后要加上select,表示結果的數據;記得select一定要最后。這是剛用比較別扭的地方。
用法:
1,var r= from d in dogs select d.Id;
2,var r = from d in dogs select new{d.Id,d.Name,Desc="一條狗"};
3,排序 var items = from d in dogs //orderby d.Age //orderby d.Age descending orderby d.Age,d.MasterId descending select d;
4,join var r9 = from d in dogs join m in masters on d.MasterId equals m1.Id select new { DogName=d.Name,MasterName=m.Name};
注意:
join中相等不要用==,要用equals。
寫join的時候linq比“lambda” 漂亮
5,group by var r1 = from p in list group p by p.Age into g select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() };
4、混用
只有Where,Select,OrderBy,GroupBy,Join等這些能用linq寫法,
如果要用下面的“Max,Min,Count,Average,Sum,Any,First,FirstOrDefault,Single,SingleOrDefault,Distinct,Skip,Take等”則還要用lambda的寫法
(因為編譯后是同一個東西,所以當然可以混用)。
var r1 = from p in list group p by p.Age into g select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() }; int c = r1.Count(); var item = r1.SingleOrDefault(); var c = (from p in list where p.Age>3 select p ).Count();
四、 C#6.0語法
1. 屬性的初始化“public int Age{get;set;}=6”。低版本.Net中怎么辦?構造函數
2. nameof:可以直接獲得變量、屬性、方法等的名字的字符串表現形式。獲取的是最后一段的名稱。如果在低版本中怎么辦?
好處:可以避免寫錯,有利於編譯時查看
應用案例:ASP.Net MVC中的[Compare("BirthDay")]改成[Compare(nameof(BirthDay))]
3,??語法
int j = i ?? 3; 如果i為null則表達式的值為3,否則表達式的值就是i的值。如果在低版本中怎么辦?int j = (i == null)?3:(int)i;
應用案例:
string name = null;Console.WriteLine(name??"未知");
4, ?.語法:
string s8 = null; string s9 = s8?.Trim(); //如果s8為null,則不執行Trim(),讓表達式的結果為null。
在低版本中怎么辦?
string s9 = null; if (s8 != null) { s9 = s8.Trim(); }
五、Entity Framework簡介
1、 ORM:Object Relation Mapping ,通俗說:用操作對象的方式來操作數據庫。
2、 插入數據庫不再是執行Insert,而是類似於
Person p = new Person(); p.Age=3;p.Name="如鵬網"; db.Save(p);
這樣的做法。
3、 ORM工具有很多Dapper、PetaPoco、NHibernate,最首推的還是微軟官方的Entity Framework,簡稱EF。
4、 EF底層仍然是對ADO.Net的封裝。EF支持SQLServer、MYSQL、Oracle、Sqlite等所有主流數據庫。
5、 使用EF進行數據庫開發的時候有兩個東西建:建數據庫(T_Persons),建模型類(Person)。根據這兩種創建的先后順序有EF的三種創建方法
a) DataBase First(數據庫優先):先創建數據庫表,然后自動生成EDM文件,EDM文件生成模型類。簡單展示一下DataBase First的使用。
b) Model First(模型優先):先創建Edm文件,Edm文件自動生成模型類和數據庫;
c) Code First(代碼優先):程序員自己寫模型類,然后自動生成數據庫。沒有Edm。
DataBase First簡單、方便,但是當項目大了之后會非常痛苦;Code First入門門檻高,但是適合於大項目。Model First……
6, Code First的微軟的推薦用法是程序員只寫模型類,數據庫由EF幫我們生成,當修改模型類之后,EF使用“DB Miguration”自動幫我們更改數據庫。
但是這種做法太激進,不適合很多大項目的開發流程和優化,只適合於項目的初始開發階段。
Java的Hibernate中也有類似的DDL2SQL技術,但是也是用的較少。“DB Miguration”也不利於理解EF,因此在初學階段,我們將會禁用“DB Miguration”,采用更實際的“手動建數據庫和模型類”的方式。
7, 如果大家用過NHibernate等ORM工具的話,會發現開發過程特別麻煩,需要在配置文件中指定模型類屬性和數據庫字段的對應關系,哪怕名字完全也一樣也要手動配置。
使用過Java中Struts、Spring等技術的同學也有過類似“配置文件地獄”的感覺。
像ASP.Net MVC一樣,EF也是采用“約定大於配置”這樣的框架設計原則,省去了很多配置,能用約定就不要自己配置。
六、 EF的安裝
1、 基礎階段用控制台項目。使用NuGet安裝EntityFramework。會自動在App.config中中增加兩個entityFramework相關配置段;
2、 在web.config的Connection中配置連接字符串
<add name="conn1" connectionString="Data Source=.;Initial Catalog=test1;User ID=sa;Password=msn@qq888" providerName="System.Data.SqlClient" />
七、 EF簡單DataAnnotations實體配置
1、 數據庫中建表T_Perons,有Id(主鍵,自動增長)、Name、CreateDateTime字段。
2、 創建Person類
[Table("T_Persons")]//因為類名和表名不一樣,所以要使用Table標注 public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
因為EF約定主鍵字段名是Id,所以不用再特殊指定Id是主鍵,如果非要指定就指定[Key]。
因為字段名字和屬性名字一致,所以不用再特殊指定屬性和字段名的對應關系,如果需要特殊指定,則要用[Column("Name")]
(*)必填字段標注[Required]、字段長度[MaxLength(5)]、可空字段用int?、如果字段在數據庫有默認值,則要在屬性上標注[DatabaseGenerated]
注意實體類都要寫成public,否則后面可能會有麻煩。
3,創建DbContext類(模型類、實體類)
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1")//name=conn1表示使用連接字符串中名字為conn1的去連接數據庫 { } public DbSet<Person> Persons { get; set; }//通過對Persons集合的操作就可以完成對T_Persons表的操作 }
4,運行測試
MyDbContext ctx = new MyDbContext(); Person p = new Person(); p.CreateDateTime = DateTime.Now; p.Name = "rupeng"; ctx.Persons.Add(p); ctx.SaveChanges();
注意:
MyDbContext對象是否需要using有爭議,不using也沒事。每次用的時候new MyDbContext就行,不用共享同一個實例,共享反而會有問題。SaveChanges()才會把修改更新到數據庫中。
異常的處理:
如果數據有錯誤可能在SaveChanges()的時候出現異常,一般仔細查看異常信息或者一直深入一層層的鑽InnerException就能發現錯誤信息。
舉例:
創建一個Person對象,不給Name、CreateDateTime賦值就保存。
八、EF模型的兩種配置方式
EF中的模型類的配置有DataAnnotations、FluentAPI兩種。
DataAnnotations:
[Table("T_Persons")]、[Column("Name")]這種在類上或者屬性上標記的方式就叫DataAnnotations
好處與壞處:
這種方式比較方便,但是耦合度太高,不適合大項目開發。
一般的類最好是POCO
(Plain Old C# Object沒有繼承什么特殊的父類,沒有標注什么特殊的Attribute,沒有定義什么特殊的方法,就是一堆普通的屬性);
不符合大項目開發的要求。微軟推薦使用FluentAPI的使用方式,因此后面主要用FluentAPI的使用方式。
九、FluentAPI配置T_Persons的方式
1,數據庫中建表T_Perons,有Id(主鍵,自動增長)、Name、CreateDateTime字段。
2,創建Person類。模型類就是普通C#類
public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
3,創建一個PersonConfig類,放到ModelConfig文件夾下(PersonConfig、EntityConfig這樣的名字都不是必須的)
class PersonConfig: EntityTypeConfiguration<Person> { public PersonConfig() { this.ToTable("T_Persons");//等價於[Table("T_Persons")] } }
4,創建DbContext類
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly()); //代表從這句話所在的程序集加載所有的繼承自EntityTypeConfiguration為模型配置類。 } public DbSet<Person> Persons { get; set; } }
5,運行測試
MyDbContext ctx = new MyDbContext(); Person p = new Person(); p.CreateDateTime = DateTime.Now; p.Name = "rupeng"; ctx.Persons.Add(p); ctx.SaveChanges();
和以前唯一的不同就是:
模型不需要標注Attribute;
編寫一個XXXConfig類配置映射關系;DbContext中override OnModelCreating;
6,多個表怎么辦?
創建多個表的實體類、Config類,並且在DbContext中增加多個DbSet類型的屬性即可。
十、EF的基本增刪改查
獲取DbSet除了可以ctx.Persons之外,還可以ctx.Set<Person>()
1,增加,同上
注意:
如果Id是自動增長的,創建的對象顯然不用指定Id的值,並且在SaveChanges ()后會自動給對象的Id屬性賦值為新增行的Id字段的值。
2,刪除。
先查詢出來要刪除的數據,然后Remove。這種方式問題最少,雖然性能略低,但是刪除操作一般不頻繁,不用考慮性能。后續在“狀態管理”中會講其他實現方法
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)中