- 讀取EDM
- 獲取實體結構
- 獲取功能結構
- 編寫泛型代碼
一般來說,EDM由三個XML文件組成,分別包含類,數據庫以及類與數據間映射的相關信息。毫無疑問,EF是這些XML第一個訪問者,通過它們生成CRUD操作的SQL代碼,識別某一列是否為數據庫的標識列,以及很多其他功能的實現。
使用Linq To XML讀取XML文件是很容易的,但是這樣就不能再使用一系列用於訪問這些數據的經過良好設計的API了。EF通常需要訪問EDM元數據,這就需要一套簡單的API。開始,這些API只供內部使用,但現在已經公開了,所有人都可以通過這些API來訪問EDM元數據。
通過獲取元數據,使編寫范型代碼成為可能。泛型的應用使得即使不知道實體的類型也可以對實體進行編碼工作。例如,可以編寫一個擴展方法,根據關鍵值來確定一個實體是執行AddObject或Attach方法。
幸虧有了metadata(元數據),可以在運行時查詢關鍵值的屬性,通過反射來獲取值,然后就可以執行正確的操作。通過使用定制的特性標識,甚至還可以在EDM中指定哪個值引發AddObject方法,哪個引發Attach方法。盡管這是一個小例子,已經清楚地表明元數據是相當重要的。
我們先從基礎的主題開始。首先展示如何讀取元數據以及在訪問這些數據時可能會遇到的問題。然后,對API系統進行概覽。最后,將學習的知識進行綜合來完成選擇AddObject與Attach方法的例子。
1、元數據基礎
EDM在哪每個人都知道(編者注:將EDMX文件用XML編輯器打開就可以看到),但API是怎樣訪問其暴露的數據呢?如何識別EDM中的不同文件?元數據何時加載?
API訪問EDM通過三個類:ObjectContext,EntityConnection和MetadataWorkspace。
為了在獲取數據時區分不同的文件,需要在查詢某對象或對象列表時要指定dataspace。同一API即可以獲取概念模型,也可以獲取存儲模型,因此每次都要指定訪問的是哪一個文件。
當元數據加載以后,CSDL就立即可以使用的,但是SSDL只有在請求元數據的操作被觸發(比如有一個查詢操作)時才能獲取。
上面只是一些簡知的回答,下面我們來詳細地分析每個議題。
1)訪問元數據
暴露API以訪問元數據的類是MetadataWorkspace。這一類可以直接通過構造器被實例化,也可以通過ObjectContext或EntityConnection類進行訪問。手工實例化MetadataWorkspace類比較麻煩,需要大量代碼;因此最好使用其他方法。
注意:只有在通過模板生成代碼時才會直接實例化MetadataWorkspace類。在其他場合,都是通過上下文來訪問,很少的情況是通過連接來訪問的。
下面來看看如何通過ObjectContext來訪問元數據。
通過上下文來訪問元數據
使用對象進行工作的時候,上下文是其與數據庫溝通橋梁。為了正確地處理對象,上下文在EDM的概念模型下進行工作。上下文必須知道某個屬性是否為標識屬性,以用於並發處理,以及種種諸如此類的問題。
幸好,上下文類的構造器已經通過連接字符串獲得了元數據的存在,就可以任由我們對其進行訪問了,如下代碼片段:
var ctx=new OrderITEntites(); var mw=ctx.MetadataWorkspace;
如果有了上下文類,這就是所要做的全部.當然也可以使用連接獲取.使用連接訪問元數據EntityConnection類通過GetMetadataWorkspace方法獲取元數據,這一方法返回一個MetadataWorkspace對象.連接創建以后,可以用此方法進行調用:
var ctx=new EntityConnection(connString); var mw=conn.GetMetadatWorkspace();
上下文依賴EntityConnection訪問數據庫,也依賴這一對象訪問元數據.上下文實例化以后,就創建了連接和元數據的內部克隆,可以用來進行讀取和使用.我們已經使用EF開發了很多項目,從未在沒有上下文或連接的情況下訪問過元數據,實際上,這並非不能辦到,這種情況下直接使用MetadataWorkspace是唯一的辦法了.
使用MetadataWorkspace訪問元數據
使用MetadataWorkspace類,可以在構造器或通過指方法來實例化類.
使用構造器,需要傳遞三個EDM文件的路徑和包含CLR類的程序集.如果三個EDM文件封裝為一個程序集,該程序集必須傳遞給構造器.
注意如果三個EDM文件沒有封閉在一個程序集里,必須使用全路徑進行引用;而如果已經封裝,可以使用與連接字符串相同的語法 ,見下代碼:
Assembly asm = Assembly.LoadFile("C:\\OrderIT\\OrderITModel.dll"); var mw1 = new MetadataWorkspace Embedded files (new string[] { "res://*/ Model.csdl", "res://*/ Model.ssdl" }, new Assembly[] { asm }); var mw2 = new MetadataWorkspace Plain files (new string[] { "C:\\OrderIT\\Model.csdl", "C:\\OrderIT\\Model.ssdl" }, new Assembly[] { asm });
如果選擇不在構造用EDM信息創建MetadataWorkspace類,工作就會變得很復雜.MetadataWorkspace類內部為每個文件類型注冊了一系列集合.你需要一個一個地實現這些集合並使用RegisterItemCollection方法進行注冊.每個集合都是一個不同的類型,取決於所處理的EDM文件.比如,如果為存儲層注冊元數據,需要創建StoreItemCollection實例然后傳進SSDL文件里;面CSDL則需要EdmItemCollection集合實例.即使沒有看到代碼,也能想像實現這一目標的困難程度,這為需要寫大量實例化和載入集合的代碼.這種方法我們盡量不用.但是在遇到只有這種方法可用的時候,你能想起來如何處理它.前面我們提到包含CLR類的程序集必須傳遞給MetadataWorkspace類的構造器.你可以能會疑惑,這是為什么呢?難道元數據不只CSDL,SSDL以及MSL嗎?下面我們來看看問題的答案:2)元數據的內部組織元數據API的強大之處在於可以在不同的EDM架構下進行重用.不管是在存儲模型還是在概念模型中掃描,都可以使用相同的API.現在問題來了,如果指定所查詢的架構呢?答案就是指定所查詢構架的dataspace.dataspace是一個DataSpace的枚舉類型(System.Data.Metadata.Edm 名稱空間),包含下列值:CSPace--概念構架SSpace--存儲構架CSSpace--映射構架,對該構架的支持很少;無法使用API獲得有用的信息.獲取此類元數據最好直接使用Linq To XML;OSpace--識別CLR類.這有點怪,但是CLR確實包含在元數據里.這就是為什么在實例化MetadataWorkspace的時候要包含程序集的原因.自然地,只有對象模型類包含在內,很快你就會發現這一特征的靈活之處;OCSpace--識別CLR類與CSDL間的映射.在.Net FrameWork 1.0,CLR與屬性間的映射是基於定制特性的.在.Net Framework 4.0,這一映射調整為基於類和屬性的名稱.這一映射信息可以進行查詢,但多用於EF的內部.DataSpace傳遞給所有的MetadataWorkspace方法,如下代碼段所示:mw.GetItems<EntityType>(DataSpace.CSpace); mw.GetItems<EdmFunction>(DataSpace.SSpace);
第一個方法返回概念模型的所有實體,第二個返回存儲構架的所有存儲過程.請記住EntityType和EdmFunction類,在后面你會發現這非常有用.現在已經掌握了如何訪問元數據,也清楚如何指定哪種構架來進行訪問.下一步就是理解何時能夠獲取這些數據.3)理解何時元數據可以使用元數據信息由EF延遲加載;EF不需要這些信息的時候,就不能訪問它們.比如,當你實例化了一個上下文對象,概念構架立即加載.如果此時試圖查詢存儲構架,就會拋出InvalidOperationException異常,信息表明:“The space ‘SSpace’ has no associated collection.” 不執行查詢,EF也就不需要訪問MSL和SSDL.自然而然地,我們需要一個變通的方法來突破這一限制.大多數MetadataWorkspace方法都有一個異常安全的版本.比如GetItem<T> 還有一個TryGetItem<T> .可以使用這此方法,如果數據尚未准備好,可以進行人工加載.最簡單的方法是通過查詢來強制加載,但你可能不想因此而浪費一次訪問數據庫的循環.幸好有ToTraceString方法,可以讓EF加載MSL和SSDL構架以備查詢,但並不真的執行:ItemCollection coll=null; var loaded = mw.TryGetItemCollection(DataSpace.SSpace,out coll); if(!loaded) ctx.Orders.TOTraceString();
我們已經對GetItems<T>,GetItem<T>, TryGetItem<T>和 GetItemCollection 這幾個方法有了初步的了解,現在對其進行深入解析.
2/獲取元數據
MetadataWorkspace有許多用於獲取元數據的方法,但是通常只使用其中幾個.所有的元數據都可以使用單一的通用API來獲取.但有一些元數據還有其獨特的獲取方法(比如GetEntityContainer和GetFunctions).不推薦使用這些獨特的方法因為使用通用方法可以使代碼更易讀.
下面是訪問元數據的通用方法列表:
- GetItems---獲取指定空間的所有項目
- GetItems<T>---獲取指定空間里類型為T的所有項目
- GetItemCollection和TryGetItemsCollection---獲取指定空間的所有項目,返回一個指定的集合對象
- GetItem<T>和TryGetItem<T>--獲取指定空間指定類型的單一項目
前三個方法幾乎做同樣的事情,區別很小.所面我們只使用GetItem<T>, TryGetItem<T>和GetItems<T>.你可能會疑惑T類型是什么.如果想要獲取概念空間里的所有實體,應傳遞給T什么參數?在進入各種獲取方法之前,有必要對此作些解釋.
1)理解元數據對象模型
元數據對象模型包含了位於System.Data.Metadata.Edm名稱空間的一系列類,但不是所有的類.比如,MetadataWorkspace僅作為橋梁,並不使用元數據進行工作.
我們感興趣的是與元數據據嚴格相關的類.在CSDL和SSDL的第一個節點都有一個相應類與之匹配.好在是幾乎在所有情況里,類都有與相應節點相同的命名.更重要的是由於SSDL,CSDL共享相同的結構,甚至類也是相同的.下表顯示了DEM結果與元數據類的對應關系:
所有DEM元素都暴露為屬性.比如,EntityContainer類有一個BaseEntitySets屬性.包含AssociationSet與EntitySet元素的列表,有一個FunctionImports,暴露了所有EntityContainer里的EDMFunctionImport元素.
EntityType類具有相類似的結構.暴露了Properties屬性,包含了EntityType結點的所有Property元素;KeyMembers,列出了嵌套在EDM的EntityType結點的Key元素里的PropertyRef元素列表.
ComplexType類很簡單因為只包含屬性,而EdmFunction暴露Parameters和ReturnParameter屬性.
AssociationType類是最復雜的,因為它暴露了Role屬性和ReferentialConstraint對象,后面的對象為每個PropertyRef元素暴露Principal和Dependent屬性.
現在對類已經了解,下面對元數據的訪問方法作一介紹.
2)從EDM中獲取元數據
查詢EDM只需要調用MeatadaWorkspace類上的方法.我們來一個一個分析這些方法;
使用GetItems方法
獲取架構內的所有項目,最好使用GetItems方法.不僅可以返回已經定義過的對象,還可以返回所有封裝在EDM里的primitive類開和function類型.
var items=ctx.MetadataWorkspace.GetItems(DataSpace.CSpace);
-
變量items包含272個元素!包含了所有EDM基本類型,像Edm.String, Edm.Boolean, and Edm.Int32; 基本函數如Edm.Count,Edm.Sum, and Edm.Average
以及所定義的對象.
前述代碼是對概念架構的查詢,也可以用同樣的方法對存付構架進行查詢,不同之處返回的基本類型都是有關數據庫的:SqlServer.varchar, SqlServer.Bit,等下圖展示了VS快速查詢窗口可以查看到的CSDL和SSDL類型.
GetItems返回的對象類型為ReadOnlyCollection<GlobalItem>,實現了IEnumerable<T>接口.這意味着可以LInq進行數據查詢.例如想要查詢所有基本類型,可以使用如下查詢代碼:
ctx.MetadataWorkspace.GetItems(DataSpace.CSpace) .Where(c=>c.BuiltInTypeKind== BuiltInTypeKind.PrimitiveType);
-
像GetItems一樣,GetItemCollection也可以返回所有指定構架的項目,不同之處是返回的類型.
-
使用GetItemCollection和TryGetItemCollection獲取元數據
GetItemCollection方法返回ItemCollection實例.ItemCollection繼承自ReadOnlyCollection<GlobalItem>,也可以使用Linq查詢,並且還添加了很多實用的方法.用法如下:
var items = ctx.MetadataWorkspace.GetItemCollection(DataSpace.CSpace);
items變量包含有與GetItems獲取到的相同的數據.區別在於現在還可以調用附加的方法如GetItems<T>.GetFunctions,以及其他由MetadataWorkspace類暴露的方法.這些方法基於GetItemCollection獲取的數據.
這些附加的方法並不會簡化開發.唯一真正有用的是TryGetItemCollection方法,可以用於檢查是否元數據已經加載,這一方法在前面已經介紹過了.
GetItems和GetItemCollection都返回所查詢數據空間的所有數據.如果想要進行篩選,必須使用LINQ獲取所需要的數據.
但是篩選就不如只選擇想要的數據,對不對?
使用GetItems<T>獲取元數據
GetItems<T>方法可以立即獲取某定類型的項目,不需要使用附加方法,也不需要LINQ語句:只需要調用方法即可.這是不是更好?
GetItems<T>返回ReadOnlyCollection<T>,這里的T是待查詢的類型.如果在概念構架下查找所有實體,結果就是ReadOnlyCollection<EntityType>.如下代碼展示了這一方法的使用:
var items=ctx.MetadataWorkspace.GetItems<EntityType>(DataSpace.CSpace);
由於ReadOnlyCollection<T> 實現了IEnumerable<T>接口,也可以對GetItems<T>的返回結果執行LINQ查詢.
Getitems<t>並沒有異常安全的版本,也就是說所查詢空間的元數據必須進行加載,否則會拋出異常.
以上所有的方法都是返回項目列表,但是通常只需要其中的一項.例如,想要檢查Supplier項以驗證IBAN屬性是否符合正則表達式要求,就是針對Supplier這一項而進行的.
使用GETITEM<T> AND TRYGETITEM<T>獲限數據
GetItem<T>方法獲取單一實體.這個方法接受dataspace和以字符串代表實體全名.
對全名的理解很重要.如果 在CSpace或SSpace中搜索,名稱空間指定為架構的元素.如果從OSpace中搜索,名換空間是CLR類的名換空間,並不一定與CSDL的相應值匹配.在如下列表中岢以看到數據如何從不同的空間中獲取:
var csItem = ctx.MetadataWorkspace.GetItem<EntityType> ("OrderITModel.Supplier", DataSpace.CSpace); var osItem = ctx.MetadataWorkspace.GetItem<EntityType> ("OrderIT.Model.Supplier", DataSpace.OSpace);
由GetItem<T>返回的類型由泛型參數指定,在如下列表中,csItem和osItem都是EntityType類型.
如果元素未找到,或者如果數據空間中的元數據未加載,GetItem<T>會拋出異常.可以使用異常安全的TryGetItem<T>代替,見如下代碼:
EntityType osItem = null, csItem = null; var csloaded = ctx.MetadataWorkspace.TryGetItem<EntityType> ("OrderITModel.Supplier", DataSpace.CSpace, out csItem); var osloaded = ctx.MetadataWorkspace.TryGetItem<EntityType> ("OrderIT.Model.Supplier", DataSpace.CSpace, out osItem);
3. 構建元數據瀏覽器
元數據瀏覽器是一個簡單的Form,使用了TreeView控件,用於顯示所有概念和存儲構架的元素.元素按結點層次進行分組顯示.例如,實體中的主鍵屬性顯示為加粗,外鍵屬性顯示為紅色加粗.函數返回值和參數列在各自類型下級.
元數據加載后,元數據瀏覽器類似於模型設計瀏覽器窗口,如圖所示:
現在快速地過一下這幅圖片.獲取的實體,復雜類型,函數以及容器,是不是很直觀?在TreeView控件上,每個構架由兩個根結點代表,分別標識為Conceptual Side 和Storage Side,下面有四個子結點:Entities,ComplexTypes(僅適用於Conceptual構架),Functions和Containers.下面介紹如何填充這個TreeView控件.
1)填充實體及復雜類型
Entities為構架的每個實體都創建了子結點.列出所有的實體只需要調用GetItems<T>方法,傳遞范型以EntityType參數,並為每個項目創建一個結點:
每實體結點又有三個子結點:
- Base typs--包含所有實體可繼承的類
- Derived types---包含所有繼承自實體的類
- Properties----包含所有實體屬性
如下代碼用於創建entites結點:
var entities = ctx.MetadataWorkspace.GetItems<EntityType> (DataSpace.CSpace); foreach (var item in entities) { var currentTreeNode = tree.Nodes[0].Nodes[0] .Nodes.Add(item.FullName); WriteTypeBaseTypes(currentTreeNode, item); WriteTypeDerivedTypes(currentTreeNode, item, entities); WriteProperties(currentTreeNode, item, ctx, DataSpace.CSpace); }
獲取實體基類型
WriteTypeBaseTypes方法獲取基類型.使用了EntityType的BaseType屬性,指向另一個代表基類型的EntityType對象 .例如,實體類型 Order的BaseType屬性設置為null,而Customer的BaseType則設置為Company.下面是該方法的代碼:
private void WriteTypeBaseTypes(TreeNode currentTreeNode, EntityType item) { var node = currentTreeNode.Nodes.Add("Base types"); if (item.BaseType != null) node.Nodes.Add(item.BaseType.FullName); }
獲取Entity-Derived實體
獲取繼承自當前實體的實體需要簡單的LINQ查詢以確定哪個基類型匹配當前實體.WriteTypeDerivedTypes方法用於解決這一問題,代碼如下:
private void WriteTypeDerivedTypes(TreeNode currentTreeNode, EntityType item, ReadOnlyCollection<EntityType> entities) { var node = currentTreeNode.Nodes.Add("Derived types"); var derivedTypes = entities .Where(e => e.BaseType != null && e.BaseType.FullName == item.FullName); foreach (var entity in derivedTypes) { node.Nodes.Add(entity.FullName); } }
LInq查詢是很簡單的.只需要將當前實體的全名與基類型的全名屬性進行比較匹配即可.
獲取屬性
獲取當前實體的屬性並顯示在TreeView控件上的方法是WriteProperties.這一方法視實體為StructuralType的實例.由於EntityType繼承自StructuralType,直接將EntityType作為參數傳入也是合法的.
StructuralType有一個屬性名為Member可以列出實體的所有屬性.高亮顯示主鍵只需要檢查當前屬性名是否包含在KeyMember屬性里,這一屬性是主鍵屬性的列表.
確定屬性是否為外鍵屬性稍微復雜一點,需要使用LINQ查詢外鍵關系中的end role是否為當前實體,並且依賴 屬性是否包含當前屬性.綜合代碼如下:
private void WriteProperties(TreeNode currentTreeNode, StructuralType item, OrderITEntities ctx, DataSpace space) { var node = currentTreeNode.Nodes.Add( (space == DataSpace.CSpace) ? "Properties" : "Columns"); foreach(var prop in item.Members) { var propNode = node.Nodes.Add( GetElementNameWithType(prop.Name, prop.TypeUsage, space)); var entityItem = item as EntityType; if (entityItem != null) { if (entityItem.KeyMembers .Any(p => p.Name == prop.Name)) { propNode.NodeFont = new Font(this.Font, FontStyle.Bold); } if (ctx.MetadataWorkspace .GetItems<AssociationType>(space) .Where(a => a.IsForeignKey).Any(a => a.ReferentialConstraints[0] .ToProperties[0].Name == prop.Name && a.ReferentialConstraints[0] .ToRole.Name == item.Name)) { propNode.NodeFont = new Font(this.Font, FontStyle.Bold); propNode.ForeColor = Color.Red; } } var metaNode = propNode.Nodes.Add("Metadata Properties"); foreach (var facet in prop.TypeUsage.Facets) { propNode.Nodes.Add(facet.Name + ": " + facet.Value); } foreach (var meta in prop.MetadataProperties){ metaNode.Nodes.Add(meta.Name + ": " + meta.Value); } } }
前面提到,輸入的實體是StructuralType類型而不是EntityType類型.這是因為這一函數還可以用於復雜類型, ComplexType and EntityType都是繼承自StructuralType.
在代碼里,所有屬性都是通過Member屬性迭代得到的.對每個屬性,執行了如下的操作:
創建了一個結點,將結果文本采用GetElementNameWithType方法進行格式化,返回屬性名和類型.該方法的代碼未在此展示,原因是對於本文的議題無關.可以在后附的源代碼中找到.
如果輸入項是一個實體,會對其進行檢查看看是否屬性為主鍵或外鍵;
為每個facet添加結點.factes是屬性結點的特性(nullable,maxLength等);
為每個元數據屬性顯示結點.
獲取復雜類型
復雜類型類似於實體,但相對簡單因為不能被繼承,也就無所謂Base Type和Derived Type結點.也沒有主鍵外鍵之分.復雜屬性只與實體共享Properties結點.
前面獲取屬性的代碼中已經考慮到了復雜類型,因上可以重用這些代碼(注意tree變量指的是TreeView控件):
foreach (var item in ctx.MetadataWorkspace.GetItems<ComplexType> (DataSpace.CSpace)) { var currentTreeNode = tree.Nodes[0].Nodes[2].Nodes.Add(item.FullName); WriteProperties(currentTreeNode, item, ctx, DataSpace.CSpace); }
2)填充fuctions結點
functions結點的子結點包括所有的函數,每個函數的子結點還包含該函數的參數 .根結點的文本格式化為函數名+返回類型.
技術還是一樣的:使用GetItems<EdmFunction>方法獲取所有function,然后為每個函數調用填充參數和返回類型的方法,見下代碼:
var functions = ctx.MetadataWorkspace.GetItems<EdmFunction> (DataSpace.CSpace) .Where(i => i.NamespaceName != "Edm"); foreach (var item in functions) { var currentTreeNode = tree.Nodes[0].Nodes[1].Nodes.Add( GetElementNameWithType(item.FullName, item.ReturnParameter.TypeUsage, DataSpace.CSpace)); WriteFunctionParameters(currentTreeNode, item.Parameters, DataSpace.CSpace); }
注意items的篩選器,使用它是因為GetItems<EdmFunction>也會返回基本函數,名稱空間是區分自定義函數與基本函數的途徑.
WriteFunctionParameters方法很簡單,如下所示.
private void WriteFunctionParameters(TreeNode parentNode, ReadOnlyMetadataCollection<FunctionParameter> parameters, DataSpace space) { foreach (var param in parameters) parentNode.Nodes.Add( GetElementNameWithType(param.Name, param.TypeUsage, space) + ": " + param.Mode); }
3)獲取Container數據
containers結點下是所有找到的container。每個container有三個子結點:entity sets,association sets 和function imports。Association sets和Entity Set從元數據的形式上來講是類似的,都共享基類。Function Imports用於識別函數,所以可以重用前面的的代碼。
var containers = ctx.MetadataWorkspace.GetItems<EntityContainer> (DataSpace.CSpace); foreach (var item in containers) { var currentTreeNode=tree.Nodes[0].Nodes[3].Nodes.Add(item.Name); WriteFunctionImports(currentTreeNode,item); WriteEntitySets<EntitySet>(currenTreeNode,item); WriteEntitySets<AssociationSet> (currentTreeNode,item); }
在此想要獲取所有containers.需要使用GetItems<EntityContainer>方法,然后調用生成內部結點的方法引用這些containers.下面的代碼列出了這些生成結點的方法.
private void WriteEntitySets<T>(TreeNode currentTreeNode, EntityContainer item) where T: EntitySetBase { var entitySetsNode = currentTreeNode.Nodes.Add( typeof(T) == typeof(EntitySet) ? "Entity sets" : "Association sets"); foreach (var bes in item.BaseEntitySets.OfType<T>()) { var node = entitySetsNode.Nodes.Add(bes.Name + ": " + bes.ElementType); } } private void WriteFunctionImports(TreeNode currentTreeNode, EntityContainer item) { var funcsNode = currentTreeNode.Nodes.Add("FunctionImports"); foreach (var func in item.FunctionImports) { var funcNode = funcsNode.Nodes.Add(func.Name); WriteFunctionParameters(funcNode, func.Parameters, DataSpace.CSpace); } }
WriteEntitySets方法介紹一下.代表entity sets和association sets的類分別為EntitySet和AssociationSet,這兩個類繼承自EntitySetBase類.而EntityContainer類有一個BaseEntitySets屬性,該屬性包含了EntitySet和AssociationSet的數據.為了區分,使用了一個范型作為參數,然后使用OfType<T>結合LINQ來獲取指定類型的sets.然后為每個結點創建帶有名稱和基類型的文本輸出.
WriteFunctionImports 方法沒有那么復雜.該方法為每個函數創建了一個結點,並使用前面的WriteFunctionParameters方法來對結點進行修飾.
概念構架結點現在填充完畢.現在對存儲構架進行討論.幸好,CSDL與SSDL共享同一構架,所有的函數都可以重用.
4)填充存儲結點
填充存儲相關的結點比概念模型要簡單.在數據庫里,沒有復雜類型,只有一種container,因為數據庫不必進行更多的描述,也沒有function-improt概念.這樣代碼就簡化了:
foreach (var item in ctx.MetadataWorkspace.GetItems<EntityType>(DataSpace.SSpace)) { var currentTreeNode = tree.Nodes[1].Nodes[0].Nodes.Add(item.ToString()); WriteProperties(currentTreeNode, item, ctx, DataSpace.SSpace); } foreach (var item in ctx.MetadataWorkspace.GetItems<EdmFunction>(DataSpace.SSpace) .Where(i => i.NamespaceName != "SqlServer")) { var currentTreeNode = tree.Nodes[1].Nodes[1].Nodes.Add(item.ToString()); WriteFunctionParameters(currentTreeNode, item.Parameters, DataSpace.SSpace); } var container = ctx.MetadataWorkspace.GetItems<EntityContainer> (DataSpace.SSpace).First(); var currentNode = tree.Nodes[1].Nodes[2].Nodes.Add(container.ToString());WriteEntitySets<EntitySet>(currentNode, container); WriteEntitySets<AssociationSet>(currentNode, container);與前面的代碼相比,區別很小:
CSpace換成了SSpace,用於從存儲構架獲取項目
基本函數名稱空間是SqlServer而不是Edm.
剩余的代碼只是對已有方法的擴展性重用,就不多說了.
現在應該已經掌握了元數據的獲取技術了.下面我們來看看如何讓元數據在你的真實代碼中發揮作用.
4/使用元數據來編寫范型代碼
使用元數據.可以只寫一個方法處理任何類.
另一個有趣的方法是可以只通過關鍵詞來獲取 實體的任意類型.幾乎每個實體都有一個GetById方法.這需要為屬性類型和返回類型進行大量的編碼.使用元數據.可以只寫一個通用的方法,從而節少了大量代碼.
1)根據定制的特性標識添加或附加對象
假定正在添加一個新的customer.其CompanyId屬性為0,因為實際值是在數據庫中計算得到的.如果需要更新一個customer,CompanyId屬性已經被設置了一個值需要在數據庫中通過該值進行匹配.
前面我們創建了一個方法,根據主鍵屬性的值來確定是附加還是添加一個實體到上下文里.如果值為0,就是添加實體,否則就是附加實體.如果根據EDM中的值配置來確定是添加還是附加,不是一個更好的方法嗎?
1)在EDM中添加一個定制的特性標識到鍵屬性上,指出哪個值會導致AddObject方法得以調用;
2)創建一個擴展方法(比如SmartAttached)以接受實體.方法會檢查急鍵屬性的值,如果與定制特性標識相匹配,就調用AddObject方法,反之調用Attached方法.
第一點不用過多解釋.只需要添加efex(或者任何你喜歡的名字)名稱空間,然后使用InsertWhen元素旋轉在CompanyId屬性里:
<Schema xmlns:efex="http://efex" ...> ... <EntityType Name="Company" Abstract="true"> <Property Name="CompanyId" ...> <efex:InsertWhen>0</efex:InsertWhen> </Property> ... </EntityType> ... </Schema>
第二部分如下代碼所示:
public static void SmartAttach<T>(this ObjectSet<T> es, T input) where T : class { var objectType = ObjectContext.GetObjectType(input.GetType()); var osItem = es.Context.MetadataWorkspace.GetItem<EntityType>(objectType.FullName, DataSpace.OSpace); var csItem = (EntityType)es.Context.MetadataWorkspace.GetEdmSpaceType(osItem); var value = ((XElement)(csItem.KeyMembers.First().MetadataProperties.First(p => p.Name == "http://efex:InsertWhen").Value)).Value; var idType = input.GetType().GetProperty(csItem.KeyMembers.First().Name).PropertyType; var id = input.GetType().GetProperty(csItem.KeyMembers.First().Name).GetValue(input, null); if (id.Equals(Convert.ChangeType(value, idType))) { es.AddObject(input); } else { es.Attach(input); } }
該方法很簡單,擴展了ObjectSet<T>類並接受實體用於添加或附加.
前兩個表達式獲取實體的POCO類型,然后在元數據的對象空間中查找實體.獲取的對象接着被傳遞給GetEdmSpaceType方法以獲取概念空間的副本.
然后,key屬性就被獲取了,並且其定制的特性標識也被解析出來,該特性標識的值用於確定實體應被標記為哪種添加方式 .第一個需要注意的事情是定制特性標識需要全名獲取,比如http://efex:InsertWhen,而不能使用命名空間的別名進行獲取.第二個需要注意的是定制標識是以XML片段的形式暴露的,首先要用XElement進行選擇然后才能獲取所需的數值.
最后,來自於元數據的值被轉換為鍵屬性類型並與鍵屬性值進行比較.如果匹配,實體就被標記為added;反之為attached.
2)創建泛型GetById方法
ObjectContext有一個GetObjectByKey方法,可以使用鍵值來查詢對象.這一個方法有兩個缺點:返回object類型實例需要轉換為實體類型;需要EntityKey實例作為輸入.這些缺點使得GetObjectByKey方法很不好用.現在我們創建一個好用一點的方法.
GetById<T>方法克服了GetObjectBy方法的局限.首先,GetById<T>方法使用梵行,省去了將結果轉換為所需要類型的麻煩.其次,GetByID<T>方法只接受鍵屬性的值,因此方法的接口也非常簡單.
GetById<T>在內部使用GetObjectByKey,這一方法需要一個EntityKey實例作為參數 ,GetById<T>自動通過元數據獲取必要的值業創建EntityKey實例然后傳遞給GetObjectByKey就去.
GetById<T>方法的代碼如下:
public static T GetById<T>(this ObjectContext ctx, object id) { var container = ctx.MetadataWorkspace.GetEntityContainer(ctx.DefaultContainerName, DataSpace.CSpace); var osItem = ctx.MetadataWorkspace.GetItem<EntityType>(typeof(T).Name, DataSpace.OSpace); var csItem = (EdmType)ctx.MetadataWorkspace.GetEdmSpaceType(osItem); while (csItem.BaseType != null) csItem = csItem.BaseType; var esName = container.BaseEntitySets .First(s => s.ElementType.FullName == csItem.FullName).Name; var fullEsName = container.Name + "." + esName; var keyNames = ((EntityType)csItem).KeyMembers.First().Name; return (T)ctx.GetObjectByKey(new EntityKey(fullEsName, keyNames, id)); }