- 读取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)); }