qouoww

质量管理+软件开发=聚焦管理软件的开发与应用

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  • 读取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结果与元数据类的对应关系:

    image

    所有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类型.

    image

    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控件,用于显示所有概念和存储构架的元素.元素按结点层次进行分组显示.例如,实体中的主键属性显示为加粗,外键属性显示为红色加粗.函数返回值和参数列在各自类型下级.

元数据加载后,元数据浏览器类似于模型设计浏览器窗口,如图所示:

image

现在快速地过一下这幅图片.获取的实体,复杂类型,函数以及容器,是不是很直观?在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属性迭代得到的.对每个属性,执行了如下的操作:

image

  • 创建了一个结点,将结果文本采用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));

      }

 

posted on 2012-04-29 17:02  qouoww  阅读(3630)  评论(1编辑  收藏  举报