Chapter 12. Exploring EDM metadata

 

This chapter covers

  • Reading the EDM
  • Retrieving entity structures
  • Retrieving function structures
  • Writing generic code

 

Roughly speaking, the EDM consists of three XML files carrying information about classes, database, and their mappings. It’s no surprise that Entity Framework is the first consumer of these XML files, using them to generate the SQL code for CRUD operations, to understand whether a column is an identity on the database, and much more.

Reading XML files with LINQ to XML is easier than ever, but it will never be like having a set of well-designed APIs that access the same data. Entity Framework’s regular need to access EDM metadata led to the need for simplicity, which in turn led to a set of APIs. At the beginning, the APIs were for internal use only, but they were later made public so that everyone could access them.

Metadata retrieval introduces the possibility of writing generic code. By generic, we mean code that works with entities without even knowing what type the entities are. For instance, you could write a clever extension method that takes an entity and decides whether to perform an AddObject or an Attach based on the key value, as we described in chapter 6.

Thanks to metadata, you can discover the key property at runtime, retrieve the value via reflection, and then perform the correct operation. By using custom annotations, you can even specify in the EDM which value causes an AddObject and which causes an Attach. Although this is a simple example, it makes clear why metadata is important.

This chapter discusses all these subjects starting from the basics. First we’ll show how metadata can be reached and what pitfalls you may encounter when accessing it. After that, we’ll take a tour of the API system. Finally, you’ll finally put everything into action by building a metadata viewer and completing an example that chooses between the AddObject and Attach methods.

12.1. Metadata basics

You know where the EDM is, but how are the APIs to access its data exposed? How are the different files in the EDM identified? And when is the metadata loaded?

The APIs are accessible via three different classes: ObjectContext, EntityConnection, and MetadataWorkspace. This may sound confusing, but there’s a good reason for this variety, which we’ll get to shortly.

To identify the different files during data retrieval, you have to specify the dataspace when you query for a certain object, or a list of objects. The same APIs retrieve metadata for both the conceptual and storage models, so you must state each time which file should be inspected. (MSL isn’t available via the APIs; you can only access it via LINQ to XML.)

As for when the metadata is loaded, the CSDL is immediately available, but the SSDL can be inspected only after a request that requires that metadata is triggered (a query, for example).

Those short answers will barely get you started. Let’s look at each of these topics in more detail.

12.1.1. Accessing metadata

The class that exposes APIs to access metadata is MetadataWorkspace. It can be instantiated directly using one of its constructors or accessed through the ObjectContext or EntityConnection class. Manually instantiating the MetadataWorkspace class can be tough because it requires lots of code; you’re better off using one of the other methods.

 

Note

We’ve directly instantiated the MetadataWorkspace class only when generating code via a template (in chapter 13, you’ll see why this is extremely useful). In all other cases, we’ve accessed the Metadata-Workspace class through the context, and only in very rare cases through the connection.

 

Let’s look at how you can access metadata through the ObjectContext.

Accessing Metadata Using the Context

When you work with objects, the context is your gateway to the database. To correctly handle objects, the context works with the conceptual schema of the EDM. It must know whether a property is an identity, which properties are used for concurrency, and so on.

Fortunately, the context constructor already knows about metadata because the connection string, which contains the metadata’s location, is passed in the constructor. That’s why you only need to access the MetadataWorkspace property, as shown in the next snippet:

C#

var ctx = new OrderITEntities();
var mw = ctx.MetadataWorkspace;

VB

Dim ctx As New OrderITEntities()
Dim mw = ctx.MetadataWorkspace

That’s really all you need to do when you have a context around. But this isn’t always the case—you may be working with a connection.

Accessing Metadata Using the Connection

The EntityConnection class exposes metadata through the GetMetadataWorkspace method, which returns a MetadataWorkspace object. After the connection is created, you can invoke the method:

C#

var ctx = new EntityConnection(connString);
var mw = conn.GetMetadataWorkspace();

VB

Dim conn As New EntityConnection(connString)
Dim mw = conn.GetMetadataWorkspace()

 

Note

From now on, we’ll use the variable mw to identify a MetadataWork-space in the code.

 

In chapters 3 and 7, you learned that the context relies on the underlying EntityConnection to access the database. It turns out that the context relies on the connection to access metadata too. When the context is instantiated, it creates the connection and then internally clones the metadata, making it available to the context consumer.

We’ve developed many projects using Entity Framework and have never needed to access metadata without having a context or a connection in place. Obviously, that doesn’t mean it can’t happen to you. In that case, working directly with the Metadata-Workspace class may be the only way.

Accessing Metadata Using the Metadataworkspace Class

When you work with the context or the connection, you always have a connection string. When working with MetadataWorkspace class, metadata in the class can be initialized in the constructor or via specific methods.

When you use the constructor, you have to pass in the paths of the three EDM files plus the assemblies containing the CLR classes. If the three EDM files are embedded in an assembly, that assembly must be passed to the constructor.

Note that if the three EDM files aren’t embedded in an assembly, you have to reference them using their full paths; but if they’re embedded, you can use the same syntax used in the connection string, as the following listing demonstrates.

Listing 12.1. Accessing the metadata using the MetadataWorkspace class

If you opt for creating the MetadataWorkspace class without passing EDM information in the constructor, the job gets harder. MetadataWorkspace internally registers a set of collections for each file type. What you have to do is initialize these collections one by one and register them using the RegisterItemCollection method.

Each collection is of a different type, depending on the EDM file it handles. For instance, if you want to register metadata about the storage layer, you need to create a StoreItemCollection instance and pass in the SSDL file; the EdmItemCollection instance is used for the CSDL.

Even without seeing the code, you can see that this approach isn’t desirable, because you have to write lots of code to instantiate and load the collections. We’ve never used this approach in the past and are unlikely to ever do so in the future. But if you encounter a case where this is the only way to make things work, you now know how to handle it.

We mentioned earlier that the assembly containing the CLR classes must be passed to the MetadataWorkspace class constructor. Have you wondered why? Aren’t EDM metadata only about CSDL, SSDL, and MSL? That’s what we’ll look at next.

12.1.2. How metadata is internally organized

What’s great about the metadata APIs is that they’re reused across the different schemas of the EDM. It doesn’t matter if you’re scanning through the storage or the conceptual model, the APIs you use remain the same. So the question is, how do you specify what schema to look for?

The answer is that the dataspace lets you specify what schema to look in. The dataspace is an enum of type DataSpace (System.Data.Metadata.Edm namespace) that contains the following values:

  • CSpace—Identifies the conceptual schema.
  • SSpace—Identifies the storage schema.
  • CSSpace—Identifies the mapping schema. Unfortunately, support for the mapping schema is minimal; you can’t retrieve anything useful about it using the APIs. The best way to retrieve this metadata is to use LINQ to XML.
  • OSpace—Identifies the CLR classes. This may seem weird, but CLR classes are included in the metadata. That’s why you have to include them when instantiating the MetadataWorkspace. Naturally, only object-model classes are included, and you’ll discover that this is feature is handy.
  • OCSpace—Identifies the mapping between the CLR classes and the CSDL. In .NET Framework 1.0, the mapping between CLR classes and properties with the conceptual schema is based on custom attributes. In .NET Framework 4.0, it’s based on the names of the classes and properties. This mapping information can be queried, but it’s there more for Entity Framework’s internal use than for helping you.

The DataSpace is passed to all MetadataWorkspace methods, as you can see in the following snippets:

C#

mw.GetItems<EntityType>(DataSpace.CSpace);
mw.GetItems<EdmFunction>(DataSpace.SSpace);

VB

mw.GetItems(Of EntityType)(DataSpace.CSpace)
mw.GetItems(Of EdmFunction)(DataSpace.SSpace)

The first method returns all entities in the conceptual space, and the second returns all stored procedures in the storage schema.

 

Note

Keep the EntityType and EdmFunction classes in mind. They are pretty useful, as you’ll discover later in this chapter.

 

You now know how to access the metadata and how to specify what schema you’re extracting data from. The next step is understanding when you can retrieve the data.

12.1.3. Understanding when metadata becomes available

Metadata information is lazily loaded by Entity Framework; because Entity Framework doesn’t need it, you can’t access it. For example, when you instantiate a context, the conceptual schema is immediately loaded because the context works with it. If you try to query the storage schema, you’ll get a runtime InvalidOperationException with the message. “The space ‘SSpace’ has no associated collection.” No query has been performed, so Entity Framework hasn’t needed to access the MSL and SSDL.

Naturally, wherever there’s an inconvenient limitation like this, a workaround is nearby. Most of the MetadataWorkspace methods come with an exception-safe version. For instance, you have GetItem<T> as well as TryGetItem<T>. You can use the Try* methods, and if data the data isn’t ready yet, you can artificially cause it to be loaded.

The easiest way to force loading is to perform a query, but you won’t want to waste a round trip to the database. Fortunately, the ToTraceString method, used in the following listing, does the trick. It causes Entity Framework to load the MSL and SSDL schemas to prepare a query, but it doesn’t actually execute it.

Listing 12.2. Forcing the loading of the storage and mapping schemas

You now have enough background to advance to the next stage: querying. You’ve had a sneak peek with the GetItems<T>, GetItem<T>, TryGetItem<T>, and GetItem-Collection methods, and now it’s time to look at them more closely.

12.2. Retrieving metadata

MetadataWorkspace has many methods for retrieving metadata, but you’ll generally only be using a few of them. All the metadata can be reached using a single generic API, but some types of metadata have dedicated retrieval methods (such as Get-EntityContainer and GetFunctions). We discourage the use of such dedicated methods because using a single generic one makes the code easier to read.

These are the generic methods for accessing metadata:

  • GetItems—Retrieves all items in the specified space
  • GetItems<T>—Retrieves all items of type T in the specified space
  • GetItemCollection and TryGetItemCollection—Retrieve all items in the specified space returning a specialized collection
  • GetItem<T> and TryGetItem<T>—Retrieve a single item in the specified space

The first three of these methods do almost the same thing, with tiny differences. In the example that you’ll create in this chapter, we’ll only use GetItem<T>, TryGet-Item<T>, and GetItems<T>. As you’ll see, the other methods aren’t all that necessary.

You’re probably wondering what type T can be. If you want to retrieve all entities in the conceptual space, what should you pass as T? It’s important to understand this before going deeper into the various retrieval methods.

12.2.1. Understanding the metadata object model

The metadata object model consists of classes that are in the System.Data.Metadata.Edm namespace, but not all classes in this namespace are related to the EDM. For instance, MetadataWorkspace acts as the gateway, but it doesn’t work with metadata.

What we’re interested in are the classes strictly related to metadata. Each node in the CSDL and SSDL has a corresponding class (with some exceptions). What’s good is that in almost all cases, classes have the same name as their corresponding nodes. What’s even better is that because the SSDL and CSDL share the same structure, even the classes are the same. Table 12.1 shows the correspondences between EDM nodes and metadata classes.

Table 12.1. The correspondence between metadata classes and EDM nodes

EDM node

Metadata class name

EntityContainer EntityContainer
EntityType EntityType
ComplexType ComplexType
Function EdmFunction
Association AssociationType

All the other EDM elements you learned about in chapter 5 are exposed as properties of the classes in table 12.1. For instance, the EntityContainer class has a BaseEntitySets property, which lists the AssociationSet and EntitySet elements, and a FunctionImports property, which exposes the FunctionImport elements, all in the EDM’s EntityContainer element.

The EntityType class has a similar structure. It exposes the Properties property, containing all the Property elements inside the EntityType node in the EDM; and KeyMembers, which lists the PropertyRef elements inside the Key element that is nested in the EntityType node of the EDM.

The ComplexType class is pretty simple because it contains only the properties, whereas EdmFunction exposes the Parameters and ReturnParameter properties.

The AssociationType class is the most complex because it exposes the Role property, plus ReferentialConstraint, which has Principal and Dependent properties that in turn have PropertyRef elements.

Now that you know about the classes, it’s time to look closely at the methods for accessing metadata that we introduced before.

12.2.2. Extracting metadata from the EDM

Querying the EDM is just a matter of invoking methods on the MetadataWorkspace class. Let’s analyze these methods one by one; later, you’ll put everything in action. We’ll start with the GetItems method.

Extracting Metadata with Getitems

When you need all the items in a schema, GetItems is the best method to use. Not only does it return objects you’ve defined, but also primitive types and function types that are embedded in the EDM. It’s easy to invoke, as the next snippet shows:

C#

var items = ctx.MetadataWorkspace.GetItems(DataSpace.CSpace);

VB

Dim items = ctx.MetadataWorkspace.GetItems(DataSpace.CSpace)

The variable items contains 272 elements! It contains all EDM primitive types, like Edm.String, Edm.Boolean, and Edm.Int32; primitive functions like Edm.Count, Edm.Sum, and Edm.Average; and the objects you’ve defined.

The preceding query operates on the conceptual schema, but the same considerations hold true for the storage schema. The only difference is that primitive types are in another namespace dedicated to the database. SqlServer.varchar, SqlServer.Bit, SqlServer.int, SqlServer.COUNT, SqlServer.SUM, and SqlServer.AVERAGE are just an example of what you’ll get. Figure 12.1 shows an excerpt from the Visual Studio Quick Watch window containing types from the CSDL and SSDL.

Figure 12.1. On the left side is a snapshot of items returned by GetItems in the CSpace. On the right side are items returned by GetItems in the SSpace.

The objects returned by GetItems are of type ReadOnlyCollection<GlobalItem>, which implements IEnumerable<T>. That means you can query the data using LINQ. For instance, if you want to retrieve all primitive types, you can perform the following query:

C#

ctx.MetadataWorkspace.GetItems(DataSpace.CSpace)
  .Where(i => i.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType);

VB

ctx.MetadataWorkspace.GetItems(DataSpace.CSpace).
  Where(Function(i) i.BuiltInTypeKind = BuiltInTypeKind.PrimitiveType)

Like GetItems, GetItemCollection returns all the items that belong to a specific schema, but with a subtle difference: the type returned is different.

Extracting Metadata with Getitemcollection and Trygetitemcollection

The GetItemCollection method retrieves all items in the specified schema, and it returns them in an ItemCollection instance. ItemCollection inherits from Read-OnlyCollection<GlobalItem>, meaning that you can use LINQ to query its data, and it adds some convenient methods. Its use is shown in the following snippet:

C#

var items = ctx.MetadataWorkspace.GetItemCollection(DataSpace.CSpace);

VB

Dim items = ctx.MetadataWorkspace.GetItemCollection(DataSpace.CSpace)

The items variable contains the same data extracted by GetItems. The difference is that now you can call additional methods like GetItems<T>, GetFunctions, and others that are also exposed by the MetadataWorkspace class. These methods act on the data extracted by GetItemCollection, whereas the methods invoked on Metadata-Workspace need the schema to scan through.

These additional methods exposed by the ItemCollection class don’t particularly simplify development. What really helps development is the TryGetItemCollection method, which you can use to check whether metadata in that space is loaded, as you saw in listing 12.2.

Both GetItems and GetItemCollection return all data from the queried dataspace. If you want to filter it, you have to use LINQ or, in the case of GetItem-Collection, some specific methods. Either way, the core behavior doesn’t change; you retrieve all the data and then pick up what you need.

But wouldn’t it be better to only have the items you wanted, without having to filter them after retrieving them?

Extracting Metadata with Getitems<T>

The GetItems<T> method enables you to retrieve items of a certain type immediately. No additional methods and no LINQ queries are needed: just a method call. Isn’t that better?

GetItems<T> returns a ReadOnlyCollection<T>, where T is the type searched for. If you searched all entities in the conceptual schema, the result would be a ReadOnly-Collection<EntityType>. The following example shows how to use this method:

C#

var items = ctx.MetadataWorkspace.GetItems<EntityType>(DataSpace.CSpace);

VB

Dim items = ctx.MetadataWorkspace.GetItems(Of EntityType)(DataSpace.CSpace)

Because ReadOnlyCollection<T> implements IEnumerable<T>, you can perform LINQ queries on the result returned by GetItems<T>. This is full control.

Unfortunately, GetItems<T> doesn’t have an exception-safe version, meaning that the metadata for the searched space must be loaded, or this method will raise an exception.

All the methods we’ve seen so far return a list of items, but often you’ll just need one item. For instance, you may need to inspect the Supplier item to validate the IBAN property with a regular expression (remember the example in chapter 5?).

Extracting Metadata with Getitem<T> and Trygetitem<T>

The GetItem<T> method retrieves a single entity. It accepts the dataspace and a string representing the full name of the entity.

It’s important that you understand what we mean by the full name. When you search through the CSpace or the SSpace, the namespace is the one specified in the Schema element. When you search through OSpace, the namespace is the CLR class namespace that doesn’t necessarily match with its counterpart in the CSDL. In the following listing, you can see the difference in how data from different spaces is retrieved.

Listing 12.3. Using GetItem<T> to retrieve metadata

C#

var csItem = ctx.MetadataWorkspace.GetItem<EntityType>
  ("OrderITModel.Supplier", DataSpace.CSpace);
var osItem = ctx.MetadataWorkspace.GetItem<EntityType>
  ("OrderIT.Model.Supplier", DataSpace.OSpace);

VB

Dim csItem = ctx.MetadataWorkspace.GetItem(Of EntityType)(
  "OrderITModel.Supplier", DataSpace.CSpace)
Dim osItem = ctx.MetadataWorkspace.GetItem(Of EntityType)(
  "OrderIT.Model.Supplier", DataSpace.OSpace)

The object returned by GetItem<T> is of the type specified in the generic parameter. In this listing, csItem and osItem are of EntityType type.

If an element isn’t found, or if the metadata for the dataspace isn’t loaded, Get-Item<T> throws an exception. If you’re sure that the metadata is loaded and that the item exists in the space, GetItem<T> is for you; otherwise, the exception-safe version TryGetItem<T>, used in the following listing, is a better choice.

Listing 12.4. Using TryGetItem<T> to retrieve metadata

C#

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);

VB

Dim osItem As EntityType = Nothing
Dim csItem As EntityType = Nothing
Dim csloaded = ctx.MetadataWorkspace.TryGetItem(Of EntityType)(
  ("OrderITModel.Supplier", DataSpace.CSpace, csItem)
Dim osloaded = ctx.MetadataWorkspace.TryGetItem(Of EntityType)(
  "OrderIT.Model.Supplier", DataSpace.CSpace, osItem)

Congratulations! You’re close to becoming a metadata Jedi. You have the knowledge; now it’s time to see how to use it. Let’s look at how you can build powerful stuff using what you’ve learned.

12.3. Building a metadata explorer

The best way to use all you’ve seen in this chapter is to create a metadata explorer. The metadata explorer is a simple form with a tree view showing all elements defined in the conceptual and storage schemas. The elements are grouped in nodes, and the most important properties of each type of element are shown.

For instance, for each entity, all properties are shown with the primary-key properties displayed in bold and the foreign-key properties shown in bold and red. Function return values and parameters are listed along with their types.

After the metadata is loaded, the metadata explorer form is very similar to the designer’s Model Browser window. Figure 12.2 shows the metadata browser you’re going to build.

Figure 12.2. The tree view where metadata is shown. The conceptual side is divided into entities, complex types, functions, and containers. The same is done for the storage side.

Take a quick look at the tree in figure 12.2. Doesn’t it remind you the structure of the conceptual and storage schemas? You have entities, complex types, functions, and containers. It’s much easier to read than raw XML, isn’t it?

Representing both the conceptual and the storage schemas takes about 120 lines of code in the editor (but only about 80 if you don’t count blank lines and lines with only curly brackets). In the tree view, each schema is represented with two root nodes, labeled Conceptual Side and Storage Side, with four inner nodes: Entities, ComplexTypes (only for the conceptual schema), Functions, and Containers. Let’s see how you can populate the tree view, starting with the Entities node.

12.3.1. Populating entities and complex types

The entities node has a child for each entity in the schema. Listing all the entities is just a matter of using GetItems<T>, passing EntityType as the generic parameter, and creating a node for each item.

Each entity node has three children:

  • Base types—Contains all classes that the entity inherits from
  • Derived types—Contains all classes that inherit from the entity
  • Properties—Contains all entity properties

The following code creates the entities node.

Listing 12.5. Creating the entities node

So far, everything is pretty easy. The core code lies in the three internal methods WriteTypeBaseTypes, WriteTypeDerivedTypes, and WriteProperties, whose names explain perfectly what they do.

Retrieving Entity Base Types

The WriteTypeBaseTypes method retrieves the base types. It uses the BaseType property of EntityType, which points to another EntityType object representing the base class. For instance, the entity type representing Order has the BaseType property set to null, and the one representing Customer has the property set to Company. Here’s the code.

Listing 12.6. Creating the base types nodes

C#

private void WriteTypeBaseTypes(TreeNode currentTreeNode, EntityType item)
{
  var node = currentTreeNode.Nodes.Add("Base types");
  if (item.BaseType != null)
    node.Nodes.Add(item.BaseType.FullName);
}

VB

Private Sub WriteTypeBaseTypes(ByVal currentTreeNode As TreeNode,
  ByVal item As EntityType)
  Dim node = currentTreeNode.Nodes.Add("Base types")
  If item.BaseType IsNot Nothing Then
    node.Nodes.Add(item.BaseType.FullName)
  End If
End Sub

As you can see, looking for the base type is trivial. Finding types that inherit from an entity is a little more complicated.

Retrieving Entity-Derived Entities

Retrieving which entities inherit from the current one requires a simple LINQ query that searches for entities whose base type matches the current entity. The Write-TypeDerivedTypes method that retrieves this data is shown in this listing.

Listing 12.7. Creating the derived type nodes

The LINQ query is fairly simple . You just match the FullName property of the current entity with the FullName property of all entities’ base types. That’s all.

So far, everything has been simple. The next, and last, method retrieves properties and their information. That’s a bit harder, compared with what you’ve done so far.

Retrieving Properties

The method that writes properties for the current entity in the tree view is Write-Properties. This method accepts the entity to be inspected as a StructuralType instance. Because EntityType inherits from StructuralType, passing the parameter as an EntityType instance is perfectly valid.

StructuralType has a property named Member that lists all the entity’s properties. Highlighting the primary key ones is a simple matter of checking whether the current property name is included in the KeyMembers property, which is a list of primary-key properties.

Determining whether a property is a foreign-key property is a bit more complex. It requires a LINQ query that looks for foreign-key associations where the end role is the current entity and the dependent properties contain the current property. These and other features are shown in the following listing.

Listing 12.8. Creating the properties nodes

Wow, that’s a huge amount of code. But don’t worry. It’s the only long function in this chapter.

As we mentioned before, the input entity is of type StructuralType and not EntityType. This happens because this function can be used to write complex types too, and both ComplexType and EntityType inherit from StructuralType.

In the code, all properties are iterated via the Members property . For each property, the following actions are taken:

  • A node is created, setting the text with the result of the GetElementNameWith-Type method, which returns the property name and its type . The code for this method isn’t shown here because it’s of no interest for this discussion. You can look at it in the book’s source code.
  • If the input item is an entity, a check is performed to see whether the property is part of the primary key and if it’s a foreign key .
  • A node for each facet is added. The facets are the attributes of the property node (nullable, maxLength, and so on) .
  • A node for each metadata property is shown .

It’s important to note that metadata properties are exposed by every EDM-related class and that they contain the custom nodes you may add to an element. Do you remember the custom node that expresses the validation of the IBAN property of Supplier? You can see it in figure 12.3.

Figure 12.3. The regular expression of the IBAN is reachable through the property metadata.

Now that the entities are described, it’s time to talk about complex types.

Retrieving Complex Types

Complex types are similar to entities, but they’re simpler to manage because they can’t be inherited, which means there are no Base Type and no Derived type nodes, and they have no primary or foreign keys. Only the Properties node is shared between entities and complex types.

The method that writes properties is already aware of complex types, so you can reuse it as the following listing demonstrates (note that in this listing and the rest of the chapter, the tree variable refers to the tree view in the form).

Listing 12.9. Creating the complex types nodes

C#

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

VB

For Each item In ctx.MetadataWorkspace.GetItems(Of ComplexType)(
  DataSpace.CSpace)
  Dim currentTreeNode = tree.Nodes(0).Nodes(2).Nodes.Add(item.FullName)
  WriteProperties(currentTreeNode, item, ctx, DataSpace.CSpace)
Next

That’s it! You retrieve complex types and then call WriteProperties for each of them. Cool, isn’t it?

The second step is populating the functions. What’s nice here is that you only have parameters and results to show, so there’s not a lot to learn.

12.3.2. Populating functions

The functions node contains a child for each function, which in turn has a child node for each parameter. The text shown in the root node for each function is formatted with the name followed by the return type.

The technique is always the same: you retrieve all the functions using Get-Items<EdmFunction>, and for each of them you call a method that populates the parameters and return types. Here’s the code that does that.

Listing 12.10. Creating the functions nodes

Notice the filter applied to the items returned by GetItems<EdmFunction> . It’s applied because primitive functions are returned too, and the namespace is the only way to differentiate them from those defined by you (which are the ones you’re interested in).

The WriteFunctionParameters method is trivial, as you can see in the following listing. It iterates over function parameters, leveraging the GetElementNameWithType method to get their names and types, and adding the directions: In, Out, or InOut.

Listing 12.11. Creating parameter nodes for each function

The code is self-explanatory. We can move on and talk about the last part of the conceptual schema: the containers.

12.3.3. Populating containers

The containers node contains a child node for each container found. Each container has three children: entity sets, association sets, and function imports. Association sets and entity sets are very similar and, in terms of metadata, share the base class. Function imports are identical to functions, so this listing reuses the code from the previous section.

Listing 12.12. Creating the containers nodes

Here you retrieve all the containers, using GetItems<EntityContainer>, and then invoke the methods that build the inner nodes. Those methods are the interesting part, as you can see in the next in listing.

Listing 12.13. Creating the entity sets and function imports nodes

C#

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

VB

Private Sub WriteEntitySets(Of T As EntitySetBase)(
  ByVal currentTreeNode As TreeNode, ByVal item As EntityContainer)
  Dim entitySetsNode = currentTreeNode.Nodes.Add(
    IIf(GetType(T) = GetType(EntitySet), "Entity sets","Association sets"))
    For Each bes In item.BaseEntitySets.OfType(Of T)()
      Dim node = entitySetsNode.Nodes.Add((bes.Name & ": ") &
        bes.ElementType)
    Next
End Sub

Private Sub WriteFunctionImports(ByVal currentTreeNode As TreeNode,
  ByVal item As EntityContainer)
  Dim funcsNode = currentTreeNode.Nodes.Add("FunctionImports")
  For Each func In item.FunctionImports
    Dim funcNode = funcsNode.Nodes.Add(func.Name)
    WriteFunctionParameters(funcNode, func.Parameters, DataSpace.CSpace)
  Next
End Sub

The WriteEntitySets method is the most interesting. The classes representing entity sets and association sets are EntitySet and AssociationSet, both of which inherit from the EntitySetBase class. The EntityContainer class has a BaseEntitySets property that exposes both sets via their base class. To show them in different nodes, you pass the type as a generic parameter and then use the OfType<T> LINQ method to extract only the sets for that type. Then for each set, a node with the name and base type is created.

The WriteFunctionImports method is less complex. It creates a node for each function and then describes it using the WriteFunctionParameters method you’ve seen before.

The conceptual node is now populated. It contains all the conceptual schema items, so it’s time to move on to the storage-schema representation. Fortunately, because CSDL and SSDL share the same schema, all the functions can be reused.

12.3.4. Populating storage nodes

Populating the storage-side nodes is slightly easier than populating the conceptual-side modes. In a database, you don’t have complex types; there’s only one container because you can’t describe more databases, and there’s no function-import concept. These differences lead to simpler code, as you can see in the following listing.

Listing 12.14. Creating the containers nodes

C#

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);

VB

For Each item In ctx.MetadataWorkspace.GetItems(Of EntityType)
  (DataSpace.SSpace)
  Dim currentTreeNode = tree.Nodes(1).Nodes(0).Nodes.Add(item.ToString())
  WriteProperties(currentTreeNode, item, ctx, DataSpace.SSpace)
Next
For Each item In ctx.MetadataWorkspace.GetItems(Of EdmFunction)
  (DataSpace.SSpace).Where(Function(i) i.NamespaceName <> "SqlServer")
  Dim currentTreeNode = tree.Nodes(1).Nodes(1).Nodes.Add(item.ToString())
  WriteFunctionParameters(currentTreeNode, item.Parameters,
    DataSpace.SSpace)
Next
Dim container = ctx.MetadataWorkspace.GetItems(Of EntityContainer)
  (DataSpace.SSpace).First()
Dim currentNode = tree.Nodes(1).Nodes(2).Nodes.Add(container.ToString())
WriteEntitySets(Of EntitySet)(currentNode, container)
WriteEntitySets(Of AssociationSet)(currentNode, container)

As you can see, the differences are small:

  • The SSpace value, instead of CSpace, is used to extract items from the storage side.
  • The namespace for removing primitive functions is SqlServer, not Edm.

The rest of the code makes extensive reuse of the existing methods, making everything easier than you may have thought.

We’ve covered a lot, but now you’ve mastered metadata. This academic exercise was a good way to learn a new feature, but you may find a bite of the real world more interesting. In the next section, we’ll show you how metadata can positively affect your real code.

 

Why there’s no mapping representation

You surely have noticed that there’s no MSL metadata in the tree. That’s because there are no methods to access the mapping schema. Although there’s the CSSpace value in the DataSpace enum, it’s of absolutely no use. The classes representing the mapping metadata aren’t exposed by Entity Framework, so you can’t access them.

If you think there’s some esoteric reason for this lack, you’re wrong. There has just been no developer interest in having such APIs. The Entity Framework team decided to keep the APIs internal, suggesting that you use LINQ to XML to search for the necessary information. It’s not nice, but it’s not difficult to do once you get used to the mapping structure.

 

12.4. Writing generic code with metadata

In chapter 6, we introduced a smart method that adds or attaches an entity to the context depending on the value of the primary key. That method works with a fixed entity, meaning that if you have 100 entities, you have to duplicate it 100 times. That’s unbearable. Fortunately, thanks to metadata, you can write a single method that can handle any class.

Another interesting method is the one that allows you to retrieve any type of entity using just its keys. Every project has a GetById method for almost every entity. This requires some coding for the types of the properties and the returning types. Using metadata, you can write a generic method and save some lines of code.

We’ll look at these examples in the next sections so you can understand how to use metadata in such real-world scenarios.

12.4.1. Adding or attaching an object based on custom annotations

Let’s assume you’re adding a new customer. Its CompanyId property is 0 because the real value is calculated on the database. If you need to update a customer, the CompanyId property is already set to a value that matches the value in the database (a value that is higher than 0).

In chapter 6, you created a method that decided whether to attach or add an entity to the context based on the value of the primary key properties. If the value is 0, the entity is added; otherwise, it’s attached. Wouldn’t it be good if such a method could decide whether to add or to attach an entity based on a value configured in the EDM?

The steps are pretty simple:

1.  Add a custom annotation to the key properties in the EDM, indicating what value causes the AddObject method to be invoked.

2.  Create an extension method (say, SmartAttach) that accepts the entity. It then checks the key properties’ values, and if they match the ones expressed in the custom annotation, it invokes the AddObject method; otherwise it invokes the Attach method.

The first point needs no explanation. We covered it in depth in chapter 5. You just need to add the efex (or any name you like) namespace, and then use the Insert-When element inside the CompanyId attribute:

<Schema xmlns:efex="http://efex" ...>
  ...
  <EntityType Name="Company" Abstract="true">
    <Property Name="CompanyId" ...>
      <efex:InsertWhen>0</efex:InsertWhen>
    </Property>
    ...
  </EntityType>
  ...
</Schema>

The fun part is the second point. Look at the following listing.

Listing 12.15. Adding or attaching an entity depending on the value in the EDM

The method signature is simple; it extends the ObjectSet<T> class and accepts the entity to be added or attached.

The first two statements retrieve the POCO type of the entity (remember that it could be a proxied instance) and look for the entity in the object space of the metadata. The object retrieved is then passed to the GetEdmSpaceType method to obtain its conceptual space counterpart .

Next, the key property is retrieved, and its custom annotation value is extracted, obtaining the value that determines whether the entity should be marked for addition . The first thing to notice here is that the custom annotation is retrieved using its full name, http://efex:InsertWhen, and not using the namespace alias. The second thing is that the custom annotation is exposed as an XML fragment. It’s first cast to XElement, and then the value is extracted.

Finally, the value from the metadata is converted to a key property type and compared with the key property value . If they match, the entity is marked as added; otherwise, it’s attached.

 

Note

In this example, we’ve made the assumption that the entity has only one key property. Naturally, this isn’t always the case, because you may have many key properties. Accommodating this case doesn’t change the code a lot; instead of dealing with a single property, you must deal with an array of properties. Have fun.

 

The next example we’ll look at involves a generic GetById<T> method. If you’ve used NHibernate, you’ll know that this method is already provided by the session. Let’s realize it using Entity Framework.

12.4.2. Building a generic GetById method

ObjectContext has a GetObjectByKey method that searches for an object using its key. This method has two drawbacks: it returns an object instance that requires casting to the actual entity type, and it requires an EntityKey instance as input. These drawbacks make the GetObjectByKey method awkward. Let’s see how to create a smarter method.

The GetById<T> method you’ll create in this section overcomes the limitations of the GetObjectByKey method. First, the GetById<T> method makes use of generics, eliminating the need for casting the result to the entity type you need. Second, the GetById<T> method accepts only the key properties’ values, so that the method interface is extremely simple.

GetById<T> internally uses GetObjectByKey, and GetObjectByKey needs an EntityKey instance as parameter. GetById<T> automatically retrieves, through metadata, the values necessary to create the EntityKey instance that is then passed to GetObjectByKey.

The code of the GetById<T> method is shown in the following listing.

Listing 12.16. The GetById<T> implementation

At this point in the chapter, you should have no difficulty understanding this method. The first part retrieves the container and the entity in the metadata. They’re necessary to get the entity set name that is then used to create the EntityKey instance.

Because the entity set points to the base entity of an inheritance hierarchy, the base class for the input entity is retrieved through the BaseType property . The loop is required because the inheritance can be more than one level deep. When BaseType is null, the base class has been reached.

The full entity set name is required by GetObjectByKey, so the entity set name is appended to the container name. After that, the key property name is retrieved from the entity. Now every parameter necessary for GetObjectByKey to work has been retrieved. You just invoke it, cast the result to the generic type, and then return it to the caller.

Hopefully these examples have given you an idea of what you can do with metadata. You may still be somewhat uncertain of its usefulness, but if you give metadata a chance, you won’t regret it.

12.5. Summary

In this chapter, you’ve learned about a powerful, often underestimated, feature of the Entity Framework: metadata retrieval.

You’ve learned how to read the conceptual and storage schemas, and you’ve learned what the object space is useful for and why the mapping layer doesn’t have a set of public APIs. All this knowledge makes you a Padawan of metadata.

What takes you to the Jedi Master level is the ability to find places where metadata helps you cut down on the code. We presented two examples at the end of the chapter, but there are many others, and your duty is to find them.

Understanding metadata is also vital to code and database generation, which we’ll discuss in the next chapter. Code generation uses metadata to create CLR classes from the model, and database generation creates mapping schema, storage schema, (and the SQL DDL from this last schema) starting from the conceptual model.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.142.194