Chapter 17. Controlling Objects with ObjectStateManager and MetadataWorkspace

It’s time to delve deeper into the Entity Framework and work directly with its core components: the ObjectStateManager and MetadataWorkspace APIs.

Object Services uses the classes in these two APIs extensively to interact with objects at a granular level. Most of the classes and methods are public, so you can also take advantage of them to control and manipulate entity objects at a low level.

In this chapter, you will build some pretty intensive code samples that will give you great hands-on experience working at this level. You will also get some ideas of what you can achieve with ObjectStateManager and MetadataWorkspace. In fact, most of the object interaction under the covers occurs using ObjectStateEntries and MetadataWorkspace. You have direct access to the same capabilities.

Combined, these two classes not only allow you to manipulate objects, but also enable you to write generic methods that you can use on various Entity Framework object types, as well as dynamically create objects at runtime without depending on the generated entity classes.

There are many benefits to writing generic code for entity objects. It allows you to write reusable code that can handle any EntityObject, whether processing entities in SavingChanges, writing utilities, or even, as you’ll see in this chapter, writing code that can dynamically create entities.

Managing ObjectStateEntry Objects with ObjectStateManager

In Chapter 9, you got an introduction to ObjectStateEntry objects, which contain the value and state information for every object in the cache. Objects enter the cache in two distinct ways: as the result of a query or as the result of an explicit code instruction to add or attach an entity that is already in memory.

Anytime an entity enters the cache, the ObjectStateManager creates an ObjectStateEntry for that object. The type of ObjectStateEntry for an entity is called an EntityEntry. When any new relationships are created between attached entities, either automatically by the context or explicitly by code, the ObjectStateManager creates an ObjectStateEntry of the type RelationshipEntry to represent the relationship object (see Figure 17-1).

ObjectStateEntries created by the ObjectContext for each entity and relationship it is managing

Figure 17-1. ObjectStateEntries created by the ObjectContext for each entity and relationship it is managing

Anytime an entity leaves the cache, its ObjectStateEntry is automatically destroyed as well as any RelationshipEntry objects that are bound to that entity.

An ObjectStateEntry Refresher

Because ObjectStateEntry is critical to most of what you will be learning in this chapter, it might be helpful to have a quick refresher on this class.

The information that an entity’s ObjectStateEntry exposes includes the following:

  • An array of original values (the values when the entity was attached to the context)

  • An array of current values

  • An array of names of properties that have been modified

  • A pointer back to the entity

  • EntityKey

  • EntityState

  • The EntitySet name

  • ObjectStateManager

ObjectStateEntry also has an IsRelationship property to determine whether it’s an EntityEntry or a RelationshipEntry.

Most of the ObjectStateEntry properties are null when the entry is a RelationshipEntry. However, these properties are still relevant and will be populated:

  • EntityState

  • EntitySet

  • ObjectStateManager

The pointer back to the EntitySet is important because the EntitySet itself provides all of the information regarding the two ends of the relationship.

You may recall that the debugger doesn’t show all of the ObjectStateEntry information. You can access some of it only at runtime by calling properties that are not exposed in the debugger.

Getting an ObjectStateManager and Its Entries

Every ObjectContext has its own ObjectStateManager, which you can access using the ObjectContext.ObjectStateManager property.

The ObjectStateManager itself doesn’t have properties. It only manages the ObjectStateEntry objects; therefore, it has methods to return those entries.

Getting Groups of Entries with GetObjectStateEntries

ObjectStateManager.GetObjectStateEntries returns an IEnumerable collection of entries. As you learned in the brief introduction to GetObjectStateEntries in Chapter 10, you must specify one or more EntityState enums to determine which types of entities to return. It won’t just return all entries by default.

For example, to get all Unchanged entries that are currently in the context, you can use the code in Example 17-1.

Example 17-1. Retrieving Unchanged ObjectStateEntry entries

VB
context.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged)
C#
context.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged)

You can specify multiple states by separating the enums with the Or operator, as shown in Example 17-2.

Example 17-2. Specifying more than one EntityState for GetObjectStateEntries

VB
context.ObjectStateManager.GetObjectStateEntries _
(EntityState.Added Or EntityState.Unchanged)
C#
context.ObjectStateManager.GetObjectStateEntries
 (EntityState.Added | EntityState.Unchanged).

Overloading GetObjectStateEntries using extension methods

If you use GetObjectStateEntries frequently, you are sure to find the use of the EntityState parameters annoying. Sometimes you’ll want all of the entries and you’ll need to type in each EntityState over and over. Other times you’ll want to find entries of a certain type.

Note

.NET’s extension methods allow you to add functionality to internal classes. You can take advantage of this in many places with the Entity Framework. If you want to learn more about extension methods, check the MSDN topics for C# (http://msdn.microsoft.com/en-us/library/bb383977.aspx/) and for Visual Basic (http://msdn.microsoft.com/en-us/library/bb384936.aspx/).

The following examples represent a set of three extension methods to make the use of GetObjectStateEntries more convenient. The extension method in Example 17-3 takes no parameters, and returns all of the entries regardless of EntityState.

Example 17-3. GetObjectStateEntries overload to return all entries regardless of their EntityState

VB
<Extension()> _
Public Function GetObjectStateEntries _
     (ByVal osm As Objects.ObjectStateManager) _ 
      As IEnumerable(Of Objects.ObjectStateEntry)
  Dim typeEntries = From entry In osm.GetObjectStateEntries
                    (EntityState.Added Or EntityState.Deleted _
                     Or EntityState.Modified Or EntityState.Unchanged)
  Return typeEntries
End Function
C#
public static IEnumerable<ObjectStateEntry>
 GetObjectStateEntries(this ObjectStateManager osm)
{
  var typeEntries = from entry in osm.GetObjectStateEntries
                     (EntityState.Added | EntityState.Deleted
                      | EntityState.Modified | EntityState.Unchanged)
                    select entry;
  return typeEntries;
}

This definitely beats having to specify four entity state enums in the frequent cases where you’ll want to do that.

The extension method in Example 17-4 returns all objects of a particular type by taking advantage of generics.

Example 17-4. GetObjectStateEntries overload to return all entries of a particular entity type

VB
<Extension()> _
Public Function GetObjectStateEntries(Of TEntity) _
   (ByVal osm As Objects.ObjectStateManager) _
    As IEnumerable(Of Objects.ObjectStateEntry)

 'this method takes advantage of the previous overload to return
 'all EntitytStates
  Dim typeEntries = From entry As Objects.ObjectStateEntry _
                    In osm.GetObjectStateEntries() _
                    Where entry.Entity Is TEntity
  Return typeEntries
End Function
C#
public static IEnumerable<ObjectStateEntry>
 GetObjectStateEntries<TEntity>(this ObjectStateManager osm)
{
 //this method takes advantage of the previous overload to return
 // all EntityStates
  var typeEntries =
      from  entry in osm.GetObjectStateEntries()
      where entry.Entity is TEntity
      select entry;
  return typeEntries;
}

Now you can get all entities of a particular type without having to build a LINQ query. The following code demonstrates how to call the overload:

VB
GetObjectStateEntries(Of Customer)
C#
GetObjectStateEntries<Customer>

The extension method in Example 17-5 takes the EntityState parameters and filters on a particular type using generics again.

Example 17-5. GetObjectStateEntries overload to return all entries of a particular entity type and EntityState

VB
<Extension()> _
Public Function GetObjectStateEntries(Of TEntity) _
   (ByVal osm As Objects.ObjectStateManager, _
    ByVal state As EntityState) _
    As IEnumerable(Of Objects.ObjectStateEntry)

  Dim typeEntries = From entry As Objects.ObjectStateEntry _
                    In osm.GetObjectStateEntries(state) _
                    Where entry.Entity Is TEntity
  Return typeEntries
End Function
C#
public static IEnumerable<ObjectStateEntry>
  GetObjectStateEntries<TEntity>
 (this ObjectStateManager osm, EntityState state)
{
  var typeEntries =
      from  entry in osm.GetObjectStateEntries(state)
      where entry.Entity is TEntity
      select entry;
  return typeEntries;
}

The code in Example 17-6 calls each new GetObjectStateEntries overload.

Example 17-6. Calling all three GetObjectStateEntries overloads

VB
'query for some contacts
Dim contacts = context.Contacts _
                      .Where(Function(c) c.Addresses _
                             .Any(Function(a) a.CountryRegion = "UK")) _
                      .ToList()
'Get all entries in the context, even relationships
Dim allOses=context.ObjectStateManager.GetObjectStateEntries()
'Get all Customer entries
Dim custOses=context.ObjectStateManager _
                           .GetObjectStateEntries(Of Customer)()
'Get only Modified Customer entries
Dim modifiedCustOses=context.ObjectStateManager _
         .GetObjectStateEntries(Of Customer)(EntityState.Modified)
C#
//query for some contacts
 var contacts = context.Contacts
                       .Where(c => c.Addresses
                                    .Any(a => a.CountryRegion == "UK"))
                       .ToList();
//Get all entries in the context, even relationships
 var allOses=context.ObjectStateManager.GetObjectStateEntries();
//Get all Customer entries
var custOses=context.ObjectStateManager.GetObjectStateEntries<Customer>();
//Get only Modified Customer entries
var modifiedCustOses=context.ObjectStateManager _
          .GetObjectStateEntries<Customer>(EntityState.Modified);

Getting a Single Entry with GetObjectStateEntry and TryGetObjectStateEntry

You can also retrieve a single entry from the ObjectStateManager using either GetObjectStateEntry or its counterpart, TryGetObjectStateEntry. These methods will look in the context to return an entry. They each have two overloads that let you use either an entity or an EntityKey as a parameter. If you pass in the entire entity, the method will extract its EntityKey and use that to find the entry. Example 17-7 uses an entity to find its related ObjectStateEntry, whereas Example 17-8 uses an EntityKey to find an ObjectStateEntry.

Example 17-7. Using an entity to find its related ObjectStateEntry

VB
GetObjectStateEntry(myReservation) 
C#
GetObjectStateEntry(myReservation) 

Example 17-8. Using an EntityKey to find an ObjectStateEntry

VB
GetObjectStateEntry(New EntityKey("BAEntities.Reservations",
   "ReservationID",10)
C#
GetObjectStateEntry(new EntityKey("BAEntities.Reservations",
   "ReservationID",10)

If the entry cannot be found (meaning that the object doesn’t exist in the context), an InvalidOperationException will be thrown.

TryGetObjectStateEntry is safer than GetObjectStateEntry. TryGetObjectStateEntry emulates the TryParse and TryCast methods in the .NET Framework. Rather than throwing an exception, it will return a Boolean if the entry is not found. You need to create a variable in advance for the entry and pass that into the method to be populated. Again, you can pass in either the entity or the EntityKey. You can then use the Boolean to determine whether the operation succeeded or failed, and have your code smoothly handle a failure, as shown in Example 17-9.

Example 17-9. Using TryGetObjectStateEntry to avoid an exception

VB
Dim osm=context.ObjectStateManager
Dim ose As Objects.ObjectStateEntry
If osm.TryGetObjectStateEntry(myReservation, ose) Then
  'is the method returned True, then ose is populated
  'continue working with the entry
Else
  'logic here to gracefully deal with entry not being found
End If
C#
Var osm=context.ObjectStateManager;
ObjectStateEntry ose;
if (osm.TryGetObjectStateEntry(res,out ose))
 // success logic 
else
 //failure logic

Digging Through ObjectStateEntry

Once you have an ObjectStateEntry for an entity in hand, you can view some of its details in the debugger watch window. However, the debug view doesn’t show much more than what you can already get from the entity itself (see Figure 17-2).

An ObjectStateEntry in debug view

Figure 17-2. An ObjectStateEntry in debug view

The real information comes through the methods and properties that are not exposed in the debugger. The C# debug view shows some of these private properties, but you can’t expand them to see the actual data.

Once you know what the methods and properties are, you can type them directly into the debugger to see their results.

CurrentValues and OriginalValues

The CurrentValues property returns a CurrentValueRecord (an enhanced version of a DbDataRecord), which is an ObjectStateEntryDbUpdatableDataRecord, and it contains three things:

  • An array of the property values for the entity

  • A FieldCount property

  • A DataRecordInfo object containing the metadata about the entity, such as the name and type of each property

The OriginalValues property returns a DbDataRecord that contains the array of original property values and a FieldCount property. It does not include a DataRecordInfo object. Under the covers, this is an ObjectStateEntryDbDataRecord. The word Updatable is missing from this type. You can modify the current values but not the original values.

Warning

Entities in the Added state do not have any original values. In fact, calling OriginalValues will throw an exception.

The value array contains scalar property values of the entity. If the property is a complex type, the value is a nested DbDataRecord.

Note

Remember that the ObjectContext has a different definition of original than you may have. Although the original values are typically the database values, they are reset using the current values anytime you attach the entity to the ObjectContext. So, if you have detached and reattached an entity, there’s no longer a guarantee that the values are what originally came from the database.

The way to access the values is through the Item property or one of the many casting methods such as GetString or GetByte. You can’t expand the array in the debugger, and no property returns the entire array. If you are familiar with working with DbDataReaders, the properties are exposed in the same way.

The code in Example 17-10 grabs an entry for a Customer that is in the context and displays its property values.

Example 17-10. Reading the CurrentValues of an ObjectStateEntry

VB
Dim objectStateEntry = osm.GetObjectStateEntry(customer.EntityKey)
Dim currentValues = objectStateEntry.CurrentValues
For i = 0 To currentValues.FieldCount - 1
  Console.WriteLine("Field {0}: {1}", i, currentValues.Item(i))
Next
C#
var objectStateEntry = osm.GetObjectStateEntry(customer.EntityKey);
var currentValues = objectStateEntry.CurrentValues;
for (var i = 0; i < currentValues.FieldCount; i++)
{
  Console.WriteLine("Field {0}: {1}", i, currentValues [i]);
}

The example code returns the following:

Field 0: 16
Field 1: Christopher
Field 2: Beck
Field 3: Mr.
Field 4: 9/4/2004 2:58:25 AM
Field 5: 8/7/2008 8:27:07 AM
Field 6: System.Byte[]

Even if the reservations or other related data for the customer was in the context, it won’t be listed here. No navigation properties are retained in an ObjectStateEntry for an entity. However, it is possible to identify the RelationshipEntries for this entity, and from those you can locate the related entities. In this way, you can identify or interact with the graph, if you need to do so from this direction.

The ObjectStateEntry Visualizer extension method you looked at briefly in Chapter 9 takes advantage of inspecting an entity in a generic way using information from the ObjectStateEntry. Although it doesn’t inspect relationships, it does use a few more features of ObjectStateEntry, so let’s look at those before looking at the method extension.

CurrentValueRecord.DataRecordInfo

The DataRecordInfo that is returned by CurrentValues provides two important functions. The first is that it enables you to access the metadata about the entity: property names, EDM types, and more. Additionally, it allows you “back-door” access to edit the entity objects. This is especially useful in scenarios where you don’t have specific references to entities that are being managed by the context. You can grab an ObjectStateEntry from the context and then get the entity from there. This allows you to work directly with the entity after all.

OriginalValues does not expose a DataRecordInfo property. You can see OriginalValues.DataRecordInfo in the debugger, but you can’t access it in code. If you need the metadata information, use CurrentValues to get the DataRecordInfo. Also, it’s not possible to update the original values. The only time you would explicitly impact the original values is if you call AcceptAllChanges on the ObjectContext, forcing the original values to be updated with the current values.

Figure 17-3 displays the debug window for the CurrentValueRecord of a Reservation entity. In Example 17-10, the values were retrieved using the Item property combined with the FieldCount. The FieldMetadata lists details for each field. The first is expanded a bit and highlighted. Notice that you can see the property name, ReservationID, here. Now you have a way to align the value of the first item with the property name of the first field, and you can conclude that ReservationID=1.

The FieldMetadata value of CurrentValues, which lets you discover plenty of information about each property

Figure 17-3. The FieldMetadata value of CurrentValues, which lets you discover plenty of information about each property

The properties and methods of ObjectStateEntry give you direct access to some of the metadata without having to use the MetadataWorkspace. This is the tip of the iceberg in terms of what you can achieve when coding directly with the MetadataWorkspace.

Building the ObjectStateEntry Visualizer

Now let’s look at the tool for visualizing an ObjectStateEntry, which you saw briefly in Chapter 9. This tool reads information from the ObjectStateEntry and displays it on a Windows form. I have found it to be a handy tool to use when debugging Entity Framework applications.

The visualizer is an extension method for an EntityKey because EntityKeys are serializable. In addition, it takes an ObjectContext as a parameter so that it can search the context to find the ObjectStateEntry.

Note

For those who are familiar with debugger visualizers, introduced in Visual Studio 2005, the ObjectStateEntry visualizer is not a debugger visualizer. Debugger visualizers require the target object to be serializable so that it can be moved to the debugger process. However, like ObjectStateManager, ObjectStateEntry classes are not serializable. In fact, if you do want to serialize them, you will need to deconstruct them and reconstruct them using the tools you are learning about in this chapter. Instead, this visualizer will be wrapped into an extension method with an attribute that makes it available only during debugging.

Although the tool is handy to have, the lessons you will learn by writing this code will be valuable. The code provides a practical demonstration of inspecting and extracting details of an ObjectStateEntry using its properties and methods.

Setting Up the Project and Code File

You can download the code for the visualizer from the book’s website. If you want to build it while walking through the explanation in this chapter, you’ll need to create a new class library project with a reference to System.Data.Entity. In the primary code file, add Imports or Using statements for the following namespaces:

  • System.Runtime.CompilerServices

  • System.Data.Objects

  • System.Data

  • System.Data.Common

  • System.Windows.Forms

Add a Windows form to the project you’ll work on after you have created the extension method. Name the form debuggerForm.

In VB, extension methods are housed in modules. The shell for your code should look like Example 17-11. If you haven’t written an extension method before, the first parameter defines the class that the method will extend.

Example 17-11. Base module and method for the Visual Basic version of the visualizer

VB
<Extension()> _
Public Module Visualizers

<ConditionalAttribute("DEBUG")> _
<Extension()> _
Public Sub VisualizeObjectStateEntry +
 (ByVal eKey As EntityKey, ByVal context As ObjectContext)

 'code will go here

End Sub

In C#, you can create the visualizer extension method within a class:

C#
namespace EFExtensionMethods
{
  public static class Visualizer
  {

    public static void VisualizeObjectStateEntry
      (this EntityKey eKey, ObjectContext context)
    {
       //code will go here
    }
  }
}

Retrieving an ObjectStateEntry Using an EntityKey

The visualizer’s first task is to use the EntityKey to retrieve the ObjectStateEntry from the context (see Example 17-12). If the entity is detached, there will be no entry in the context, so you can use TryGetObjectStateEntry to be safe.

Note

The visualizer displays its results in a Windows form; therefore, you should already be in the correct environment for displaying a MessageBox.

Example 17-12. Getting the ObjectStateEntry

VB
Dim ose As ObjectStateEntry = Nothing
'If object is Detached, there will be no Entry in the ObjectStateManager
If Not context.ObjectStateManager.TryGetObjectStateEntry(eKey, ose) Then
  MessageBox.Show( _
    "Object is not currently being change tracked and no " & _
    "ObjectStateEntry exists.", "ObjectStateEntryVisualizer", _
    MessageBoxButtons.OK, MessageBoxIcon.Warning)
Else
C#
ObjectStateEntry ose = null;
/If object is Detached, there will be no Entry in the ObjectStateManager

if (!(context.ObjectStateManager.TryGetObjectStateEntry(eKey, out ose)))
  MessageBox.Show
    ("Object is not currently being change tracked " +
     "and no ObjectStateEntry exists.", "ObjectStateEntry Visualizer",
     MessageBoxButtons.OK, MessageBoxIcon.Warning);
else
{

Reading the OriginalValues and CurrentValues of an ObjectStateEntry

If the entry exists, the next step is to retrieve the current and original values from the entry. However, there’s a potential problem with OriginalValues. As noted earlier, entities in the “Added” state do not have original values and the property will throw an exception. Therefore, you’ll declare a variable to contain the OriginalValues and populate it only if the state is not Added (see Example 17-13).

Note

Because of the length of code for the visualizer, this walkthrough will highlight critical pieces of code but will not display the complete code listing. You can download the C# and VB code for the visualizer from the book’s website.

Example 17-13. Getting the CurrentValues and OriginalValues

VB
Dim currentValues = ose.CurrentValues
Dim originalValues As DbDataRecord = Nothing
If ose.State <> EntityState.Added Then
  originalValues = ose.OriginalValues
End If
C#
var currentValues = ose.CurrentValues;
DbDataRecord originalValues = null;
if (ose.State != EntityState.Added)
  originalValues = ose.OriginalValues;

Next, create an array to store the data you’ll be collecting for each property. The visualizer will need to not only display the current and original values, but also retrieve the property name by drilling into the metadata.

Iterate through the items in CurrentValues, picking up the value and the property as well as its related item value in the OriginalValues array. The values are captured in a number of variables and at the end will be pushed into the new array. Example 17-14 shows how DataRecordInfo is used to drill into the metadata to get the field names. For added records, you’ll use a default of “n/a” in place of the nonexistent original value.

Example 17-14. Reading through the value arrays

VB
'create an array to store the data for the form
 Dim valueArray As New ArrayList
'walk through value arrays to get the values
 For i = 0 To currentValues.FieldCount - 1
   'FieldMetadata provides field names
    Dim sName = currentValues.DataRecordInfo.FieldMetadata(i) _
                                            .FieldType.Name
    Dim sCurrVal = currentValues.Item(i)
    Dim sOrigVal As Object
    If originalValues Is Nothing Then
      sOrigVal = "n/a" 'this will be for Added entities
    Else
      sOrigVal = originalValues.Item(i)
    End If
C#
//walk through arrays to get the values
var valueArray = new System.Collections.ArrayList();
for (var i = 0; i < currentValues.FieldCount; i++)
{
  //metadata provides field names
  var sName = currentValues.DataRecordInfo.FieldMetadata[i]
                                          .FieldType.Name;
  var sCurrVal = currentValues[i];
  object sOrigVal = null;
  if (originalValues == null)
    sOrigVal = "n/a"; //this will be for Added entities
  else
    sOrigVal = originalValues[i];

Determining Whether a Property Has Been Modified

Although you could just compare original to current values to determine whether the property has been modified, ObjectStateEntry has a method called GetModifiedProperties that returns an array of strings listing the names of any properties that have changed. Example 17-15 uses a LINQ to Objects query to check whether the current property is in that list.

Example 17-15. Determining whether the value has changed

VB
Dim changedProp = (From prop In ose.GetModifiedProperties _
                 Where prop = sName).FirstOrDefault
Dim propModified As String
propModified = If(changedProp = Nothing, "", "X")
C#
string changedProp = (from prop in ose.GetModifiedProperties()
                      where prop == sName
                      select prop).FirstOrDefault();
string propModified;
if(changedProp == null)
   propModified= "";
else
  propModified="X";

Finally, gather all of the information you just collected regarding that item and place it into the array you created at the start (see Example 17-16).

Example 17-16. Pushing the property information into the array

VB
valueArray.Add(New With {.Index = i.ToString, .Property = sName, _
                       .Current = sCurrVal, .Original = sOrigVal, _
                       .ValueModified = propModified})
Next 'this closes the For Each loop
C#
valueArray.Add(new { _Index = i.ToString(), _Property = sName,
                     Current = sCurrVal, Original = sOrigVal,
                     ValueModified = propModified });
} //this closes the foreach loop

Displaying the ObjectStateEntry’s State and Entity Type

When this is complete, the array is passed into a Windows form and is displayed in a grid.

Two more pieces of data are sent along as well: the ObjectStateEntry.State and ObjectStateEntry.Entity.ToString properties. ObjectStateEntry.Entity.ToString returns the fully qualified name of the entity’s type (see Example 17-17). You can see the results in Figure 17-4.

The visualizer populated with ObjectStateEntry information

Figure 17-4. The visualizer populated with ObjectStateEntry information

Note

The code in Example 17-17 assumes you have added the appropriate labels and a DataGridView to the form. To access the controls from the class, you will need to set their Modifiers property to Friend in Visual Basic and to Internal in C#.

Example 17-17. Pushing the values into the form

VB
Dim frm As New debuggerForm
With frm
  .DataGridView1().DataSource = valueArray
  .lblState.Text = ose.State.ToString
  .lblType.Text = ose.Entity.ToString
  .ShowDialog()
End With
C#
debuggerForm frm = new debuggerForm();
frm.dataGridView1.DataSource = valueArray;
frm.lblState.Text = ose.State.ToString();
frm.lblType.Text = ose.Entity.ToString();
frm.ShowDialog();

Getting ComplexType Properties Out of ObjectStateEntry

There’s one more twist that the preceding code doesn’t take into account: the possibility of a complex type in your properties.

If the entity contains a complex type, the value of the item will be a DbDataRecord, not a normal scalar value. Using the preceding solution, this will display in the grid as System.Data.Objects.ObjectStateEntryDbUpdatableDataRecord. Instead, you’ll need to read the array values of the complex type.

Your first step is to determine whether the property is a complex type. The simple way to do this is to look for a DbDataRecord type using a type comparison, as shown in Example 17-18.

Example 17-18. Testing to see whether a property is a complex type

VB
If TypeOf (currentValues(i)) Is DbDataRecord Then
C#
if (currentValues[i] is DbDataRecord)

No other property types will render a DbDataRecord, so this will do the trick.

Although it is not practical for this example, it is possible, as shown in Example 17-19, to get much more granular by drilling much deeper into the entry where you can use the metadata to identify the complex type, or any other entity type, for that matter.

Note

You can compare the BuiltInTypeKind property to the BuiltInTypeKind enumerator. You can use BuiltInTypeKind to identify any one of 40 schema types in an EDM, beginning alphabetically with AssociationEndMember.

Example 17-19. An alternative way to check for a complex type

VB
If currentValues.DataRecordInfo.FieldMetadata(i).FieldType _
      .TypeUsage.EdmType.BuiltInTypeKind =  _
           System.Data.Metadata.Edm.BuiltInTypeKind.ComplexType Then
C#
if (currentValues.DataRecordInfo.FieldMetadata[i].FieldType
      .TypeUsage.EdmType.BuiltInTypeKind ==
      System.Data.Metadata.Edm.BuiltInTypeKind.ComplexType)

Your code can then return the scalar item or, if it is a complex type, further process the item to extract its values. The visualizer uses a separate function, ComplexTypeString, for that task.

ComplexTypeString takes the DbDataRecord and returns a string with the internal values of the complex value, as shown in the following code:

VB
Private Function ComplexTypeString(ByVal item As DbDataRecord) As String
  Dim dbRecString = New StringBuilder
  For i = 0 To item.FieldCount - 1
    If item(i) Is DBNull.Value Then
      dbRecString.AppendLine("")
    Else
      dbRecString.AppendLine(CType(item(i), String))
    End If
  Next
  Return dbRecString.ToString
End Function
C#
private string ComplexTypeString(DbDataRecord item)
{
  var dbRecString = new StringBuilder();
  for (var i = 0; i < item.FieldCount; i++)
  {
    if (item[i] == DBNull.Value)
    {
      dbRecString.AppendLine("");
    }
    else
    {
      dbRecString.AppendLine((String)(item[i]));
    }
  }
  return dbRecString.ToString();
}

You could take this a step further and find the property names of the complex type. You probably don’t want to attempt to find these from within the DataRecordInfo. It would be much simpler to use the MetadataWorkspace API directly to read the Conceptual Schema Definition Layer (CSDL) and determine the property name of the complex type—in this case, AddressDetail. You can discover that name through the same TypeUsage property you used earlier to identify that this was a ComplexType:

VB
currentValues.DataRecordInfo.FieldMetadata(i)
             .FieldType.TypeUsage.EdmType.Name
C#
currentValues.DataRecordInfo.FieldMetadata[i]
             .FieldType.TypeUsage.EdmType.Name

Shortly, you’ll see how to perform the next steps with the MetaDataWorkspace API.

Figure 17-5 displays the results (without the additional property names of the complex type).

Note

You can download the visualizer’s code from the book’s website.

An Address entity with a ComplexType property displayed in the visualizer by reading the ObjectStateEntry

Figure 17-5. An Address entity with a ComplexType property displayed in the visualizer by reading the ObjectStateEntry

Modifying Values with ObjectStateManager

Because the CurrentValues property returns an updatable DbDataRecord, it is possible to modify the values directly through the ObjectStateManager.

Like the various accessors for a DbDataReader and a DbDataRecord, CurrentValues allows you to change a value using SetValue or one of the type-specific setters such as SetString, SetInt32, or even SetDBNull.

The signature for these methods is to pass in the value to be used for updating and the index of the item in the array. Again, remember that you can do this directly only with the scalar values. If you need to change relationships, more work is involved.

Example 17-20 shows the signature for CurrentValueRecord.SetBoolean.

Example 17-20. Changing a Boolean property with SetBoolean

VB
ObjectStateEntry.CurrentValues.SetBoolean(3,False)
C#
ObjectStateEntry.CurrentValues.SetBoolean(3,false)

The plural SetValues lets you pass in an array to update all of the values, as shown in Example 17-21. SetValues requires that you know the order and types of the properties. There are two fields that you don’t want to change, however: the ContactID and TimeStamp values. Those fields will just have their current values passed back in.

Example 17-21. Changing all of the values with SetValues

VB
currentValues.SetValues(currentValues(0), "Pablo", "Castro", "Sr.", _
 DateTime.Now, DateTime.Now, currentValues(6))
C#
currentValues.SetValues(currentValues[0],"Pablo","Castro","Sr.",
                       DateTime.Now,DateTime.Now, currentValues[6]);

Working with Relationships in ObjectStateManager

You’ve seen the RelationshipEntry in ObjectStateManager. As with EntityEntry types, the debugger doesn’t provide a lot of information, especially critical information, to help you identify which entities the relationship is for.

You can access this information using CurrentValues, which returns EntityKeys in the first and second index positions. Because a relationship has two ends, each set has only two fields containing the EntityKey of the entity on each end of the relationship.

Note

Although you will also find the EntityKeys in the OriginalValues (unless the relationship is Added), the OriginalValues are not truly viable. The property exists because it is there for all EntityStateObjects, but you should not rely on it for RelationshipEntries. Stick with the CurrentValues.

Because the RelationshipEntry describes a relationship between two entities, the EntityKeys found within the CurrentValues will match up with EntityKeys of ObjectStateEntries in the context. Figure 17-6 shows a RelationshipEntry that defines the relationship between a Customer and a Reservation.

A RelationshipEntry defining a relationship between two entities using EntityKeys

Figure 17-6. A RelationshipEntry defining a relationship between two entities using EntityKeys

Object Services uses the values in the RelationshipEntry to determine how to build graphs with the entities in the context. It also uses this information to build commands that involve foreign keys when SaveChanges is called. If you need to work with code generically, you can take advantage of the RelationshipEntries as well.

RelationshipEntry EntityState

When a new relationship is created between entities, a RelationshipEntry is created and its EntityState is Added.

The EntityState of a RelationshipEntry that is created of a graph being added to the context is Unchanged. This is also true for related entities that were returned from a query.

When a relationship is removed (e.g., you remove a Reservation from a Customer’s Reservations collection), the existing RelationshipEntry’s EntityState becomes Deleted.

If you change a relationship (e.g., move a payment from one reservation to another), the existing relationship is marked Deleted and a new relationship is created with its EntityState set to Added.

A RelationshipEntry will never have a Modified EntityState.

Inspecting the RelationshipEntries

You can filter the RelationshipEntries using the IsRelationship property. Then you can start digging into the values for each end of the current state of the relationship and the original state. Example 17-22 uses the GetObjectStateEntries overload to return entries from an already populated context, regardless of their EntityState. Then it filters for only those that are relationships.

Example 17-22. Inspecting the RelationshipEntry objects

VB
For Each relEntry In _
   (From ose In context.ObjectStateManager.GetObjectStateEntries() _
    Where ose.IsRelationship)
  Dim currRelEndA As EntityKey = relEntry.CurrentValues(0)
  Dim currRelEndB As EntityKey = relEntry.CurrentValues(1)
Next
C#
foreach (var relEntry in
    (from ose in context.ObjectStateManager.GetObjectStateEntries()
     where ose.IsRelationship
     select ose))
{
  EntityKey currRelEndA = relEntry.CurrentValues[0];
  EntityKey currRelEndB = relEntry.CurrentValues[1];
}

Figure 17-7 shows the last value, the second end of the original values of the relationship, which, as promised, is an EntityKey. You can see that the EntityKey is for a CustomerType. The second CurrentValues item contains an EntityKey for the Customer entity, which is attached to this CustomerType in this particular relationship.

An EntityKey that is the result of the RelationshipEntry’s CurrentValues property requested in

Figure 17-7. An EntityKey that is the result of the RelationshipEntry’s CurrentValues property requested in Example 17-22

The EntityKeys provide the common thread throughout the graph. Now, given an entity, you can take its key, query the RelationshipEntries to find the relationships that it is part of, and from those relationships, find the other ends.

Locating relationships for an entity

You can search relationship entries for an EntityKey to see which relationships a particular entity is involved in. Example 17-23 is a handy extension method for ObjectStateEntry that searches a relationship for a given EntityKey. If the EntityKey does not exist, the result will be null. If it does exist, rather than just returning a Boolean of True, the result will be the ordinal representing the position (0 or 1) of the EntityKey in CurrentValues. This way, not only do you know that the entity is involved in that relationship, but also you can use the ordinal to retrieve the EntityKey of the related item. Notice in the example that it’s necessary to cast the item to an EntityKey.

Example 17-23. Finding relationships with a particular entity

VB
<Extension()> _
Public Function KeyIsinRelationship _
(ByVal relatedEnd As ObjectStateEntry, _
 ByVal eKey As EntityKey) As Nullable(Of Integer)
  'check currentvalues 0 and 1 for this entity key
  If CType(relatedEnd.CurrentValues(0), EntityKey) = eKey Then
    Return 0
  ElseIf CType(relatedEnd.CurrentValues(1), EntityKey) = eKey Then
    Return 1
  Else
    Return Nothing
  End If
End Function
C#
public static Nullable<int> KeyIsinRelationship
 (this ObjectStateEntry relatedEnd, EntityKey eKey)
{
  //check currentvalues 0 and 1 for this entity key
  if (((EntityKey)(relatedEnd.CurrentValues[0])) == eKey)
    return 0;
  else if (((EntityKey)(relatedEnd.CurrentValues[1])) == eKey)
    return 1;
  else
    return null;
}

If you had a Customer entity in the context, you could use the extension method to help you find all of the entities that are related to the Customer. Recall that the ApplyPropertyChanges method that you learned about in Chapter 9 only affects scalar values. If you were writing a method to apply changes throughout a graph and you needed the method to be generic, you could use this technique to discover additional entities in the graph that should have changes applied as well.

Example 17-24 iterates through the RelationshipEntries looking for a relationship that contains a particular entity. This example excludes Deleted entries. It then grabs the EntityKey of the other related end and gets the related entity from the context.

Remember that the purpose of this code is to be generic, which is why you see EntityObject being used rather than a particular entity type.

Example 17-24. Using the KeyIsInRelationship method to find the other end of a relationship

VB
Dim osm = context.ObjectStateManager
For Each relEntry In _
 (From entry In osm.GetObjectStateEntries _
  (EntityState.Unchanged Or EntityState.Added) _
  Where entry.IsRelationship)

  Dim otherKey As EntityKey
  Dim otherEntity As EntityObject
  Dim pmtOrdinal = relEntry.KeyIsinRelationship(entityKeyToFind)
  If pmtOrdinal.HasValue Then
    If pmtOrdinal = 0 Then
      'get entitykey of other end
      otherKey = CType(relEntry.CurrentValues(1), EntityKey)
    Else
      otherKey = CType(relEntry.CurrentValues(0), EntityKey)
    End If
      'get the entity on other end
    otherEntity = CType(context.GetObjectByKey(otherKey), EntityObject)
  End If
Next
C#
var osm = context.ObjectStateManager;
foreach (var relEntry in (
             from entry in osm.GetObjectStateEntries
              (EntityState.Unchanged | EntityState.Added)
             where entry.IsRelationship select entry))
{
  EntityKey otherKey = null;
  EntityObject otherEntity = null;
  var pmtOrdinal = relEntry.KeyIsinRelationship(PaymentKey);
  if (pmtOrdinal.HasValue)
  {
    if (pmtOrdinal == 0)
      //get entitykey of other end
      otherKey = (EntityKey)(relEntry.CurrentValues[1]);
    else
      otherKey = (EntityKey)(relEntry.CurrentValues[0]);
    otherEntity = (EntityObject)(context.GetObjectByKey(otherKey));
  }
}

Building graphs directly with the RelationshipManager

It is possible to get your hands on an instance of the RelationshipManager to build graphs on the fly, creating RelationshipEntries directly in your code.

The RelationshipManager’s entry point is through the IEntitywithRelationships interface. Every EntityObject implements this interface, and any custom objects that you build will need to implement it as well if you want to have relationships managed by Object Services.

The entity does not need to be attached to an ObjectContext to get the RelationshipManager.

To get the IEntityRelationship view of an existing entity, cast the entity to IEntityRelationship. From there, you can get a RelationshipManager associated specifically with your entity.

Example 17-25 gets a RelationshipManager for an existing instance of a Payment object, represented by the variable pmt.

Example 17-25. Getting the RelationshipManager for an instance of a Payment

VB
Dim pmtRelMgr = CType(pmt, IEntityWithRelationships).RelationshipManager
C#
var pmtRelMgr = (IEntityWithRelationships)pmt.RelationshipManager;

Once you have the RelationshipManager, the next step is to get a reference to the other end of the relationship that you want to add. To do this, you need to identify which association and which end of the association you want to work with. Unfortunately, you won’t be able to do this in a strongly typed way. You’ll have to use a string to specify the association’s name.

In Example 17-26, the goal is to add a Reservation to the Payment used in Example 17-25, so you’ll need to work with the FK_Payments_Reservations association and add it to the “Reservations” end.

Note

Some of the tricks that RelationshipManager performs do not require the ObjectContext. This is handy to know if you are building generic code without the aid of the ObjectContext. Check out the MSDN Entity Framework forum post titled “Remove Associations from Entity,” which shows how to use IRelatedEnd with reflection to strip related data from an entity. (When reading this forum thread, which I started, you’ll also see that I learned this lesson the hard way, too.)

RelatedEnd has an Add method, which is the final call you’ll need to make. Example 17-26 shows how you can add the existing Reservation entity to the RelatedEnd. This will create a new relationship between the Payment entity and the Reservation entity.

Example 17-26. Creating a relationship on the fly using the RelationshipManager created in Example 17-25

VB
Dim resRelEnd As IRelatedEnd =  _
  pmtRelMgr.GetRelatedEnd(FK_Payments_Reservations,Reservations)
resRelEnd.Add(myReservation)
C#
IRelatedEnd resRelEnd =
  pmtRelMgr.GetRelatedEnd(FK_Payments_Reservations,Reservations);
resRelEnd.Add(myReservation);

This method of building graphs works exactly the same as if you had called pmt.Reservation=myReservation. If neither object is attached to the context, you will still get a graph; however, no RelationshipEntry will be created in the context. If only one of the entities is attached to the context, the other one will be pulled in and given the appropriate EntityState (Attached or Added).

Note

RelatedEnd also has a Remove method, so you can deconstruct graphs as well.

ObjectStateManager and SavingChanges

One of the most useful places to take advantage of the ObjectStateManager is in the ObjectContext.SavingChanges event handler. You saw some examples of using the SavingChanges event in Chapter 10, where you used GetObjectStateEntries to find Modified and Added entries, and then to do some last-minute work on particular types.

Note

The other events, PropertyChanged/Changing and AssociationChanged, do not have access to the ObjectContext or its ObjectStateManager, so you won’t include this type of functionality in those event handlers.

Now that you have some additional tools at your disposal, you can create validators that will generically work with entities, without knowing their type. Example 17-27 locates any Added or Modified entries that have a ModifiedDate property and then updates that property with the current date and time.

This example handles two gotchas that you need to watch out for. The first is that if the entry is a RelationshipEntry, an exception will be thrown when you try to read the metadata. Although you could use IsRelationship to test this, another method will kill two birds with one stone: by testing to see whether the ObjectStateEntry has an Entity value, you not only filter out relationships, but also filter out the “stub” entries that exist only to provide an end for EntityReferences when the entity is not in the context. This filter is used in the first query that returns the entries variable.

The second gotcha is that it’s possible that a field named ModifiedDate is not a DateTime field. Never assume!

The LINQ query in the example drills into the CurrentValues of each entry. Then, using the Any method, it looks at the names of each FieldMetaData item for that entry, picking up only those whose name is ModifiedDate. You saw code similar to this when building the visualizer earlier in this chapter. Next, the If statement verifies that the ModifiedDate property is a DateTime field; then it updates the field using CurrentValues.SetDateTime.

Example 17-27. Updating ModifiedDate fields during SavingChanges

VB
Dim entries = _
  From e In osm.GetObjectStateEntries _
  (EntityState.Added Or EntityState.Modified) _
  Where Not e.Entity Is Nothing

For Each entry In entries.Where _
(Function(entry) entry.CurrentValues.DataRecordInfo.FieldMetadata _
.Any(Function(meta) meta.FieldType.Name = "ModifiedDate"))

  Dim ordinal = _
      ent.CurrentValues.DataRecordInfo.FieldMetadata _
      .Where(Function(meta) meta.FieldType.Name = "ModifiedDate") _
      .Select(Function(meta) meta.Ordinal).FirstOrDefault

  If ent.CurrentValues.DataRecordInfo.FieldMetadata(ordinal) _
    .FieldType.TypeUsage.EdmType.Name =  _
    PrimitiveTypeKind.DateTime.ToString Then
        ent.CurrentValues.SetDateTime(ordinal, Now)
   End If
Next
C#
var entries =
    from ose in osm.GetObjectStateEntries
                   (EntityState.Added | EntityState.Modified)
    where ose.Entity != null
    select ose;

foreach (var entry in entries.Where((entry) =>
          entry.CurrentValues.DataRecordInfo.FieldMetadata
               .Any((meta) => meta.FieldType.Name == "ModifiedDate")))
{
  var ordinal = entry.CurrentValues.DataRecordInfo.FieldMetadata
               .Where((meta) =>  meta.FieldType.Name == "ModifiedDate")
               .Select((meta) => meta.Ordinal)
               .FirstOrDefault();
  if (entry.CurrentValues.DataRecordInfo.FieldMetadata[ordinal]
      .FieldType.TypeUsage.EdmType.Name
        == PrimitiveTypeKind.DateTime.ToString())
    entry.CurrentValues.SetDateTime(ordinal, System.DateTime.Now);
}

Note

Importing the System.Data.Metadata.Edm namespace gives you access to the PrimitiveTypeKind class.

This code takes advantage of a lot of the details exposed in the metadata. The For Each has filtered down to any entity that has a ModifiedDate property, but you still need to know which property that is for the SetValue/SetDateTime method. This is why you see the line of code that finds the exact property and returns the ordinal that can be found in the metadata.

The FieldMetadata Hierarchy

The type name in the previous example is buried deep in the class and is not very discoverable. But as you have seen, it can definitely be a worthwhile effort to uncover that data. Everything that’s described in the EDM is accessible through metadata. But knowing where the information is and how to access it is definitely a challenge. In the MSDN documentation, a topic called “Metadata Type Hierarchy Overview” contains a diagram displaying the hierarchy of the EDM metadata.

To help you get started, here are some of the critical parts of the hierarchy:

CurrentValues.DataRecordInfo.FieldMetadata

This is an array of FieldMetadata objects for each scalar property (this includes complex types) in the entity. Each item in the Metadata array is a Metadata.Edm.MetadataProperty.

CurrentValues.DataRecordInfo.RecordType.EdmType

This contains the property settings of the entity; for example, Name, Abstract, and NamespaceName.

CurrentValues.DataRecordInfo.RecordType.EdmType.EntityType

In addition to the same properties that are exposed directly from EdmType, in here you can find the full metadata for each of the entity’s “members,” which means not only the scalar properties, but also the navigation properties.

Each member is detailed either as an EdmProperty or as a navigation property. Opening these will display the details of each property—the property’s name, its facets, and its TypeUsage, which contains information regarding its type (String, DateTime, etc.).

The KeyMembers property shows only those members that comprise the EntityKey. The Members property lists all of the members.

As you begin to investigate the EntityType, it starts to become clear that everything you did to define the entity, its properties, and its relationships is available here.

Additionally, the DataRecordInfo provides a variety of views. For example, FieldMetaData is a subset of RecordType.EdmType.EntityType.Members.

So, you really can get at the metadata you are seeking in a variety of ways.

Note

Now that you have read this, look again at Example 17-27. Hopefully the code will make a lot more sense than it did the first time you looked at it. Don’t hesitate to poke around in the debugger to see how all of these puzzle pieces fit together now that it isn’t quite as mysterious.

The MetadataWorkspace API

At this point, you have seen a lot of interaction with the metadata through the ObjectStateManager. You can also work directly with the metadata through the MetadataWorkspace, which is in the System.Data.Metadata.Edm namespace.

The MetadataWorkspace API is used internally throughout the Entity Framework. It is the mechanism that reads the EDM. It can also read the generated classes, as you’ve seen in this chapter’s examples. In addition to being able to get metadata about the entity types and other model objects, the EntityClient uses the metadata during query processing. After the LINQ to Entities or Entity SQL queries are turned into command trees using the conceptual model, these command trees are then transformed into a command tree using the store schema. The conceptual, store, and mapping layers of the model are read in order to perform this task.

You can use the MetadataWorkspace API to read and dissect each of the three layers, as well as the entity classes that are based on the model. The power of the MetadataWorkspace lies in its ability to let you not only write generic code, but also write code that can create objects on the fly. You could write utilities or entire applications that have no knowledge in advance of the conceptual model.

Loading the MetadataWorkspace

In Chapter 16, you learned that an EntityConnection loads the metadata when it is opened. Typically, it does this by loading the actual files (CSDL, Mapping Schema Layer [MSL], and Store Schema Definition Layer [SSDL]) that the metadata attribute of the connection string points to. It is also possible to load these files into a memory stream and pass that memory stream in when you are instantiating an EntityConnection.

The MetadataWorkspace can work only with metadata that has already been loaded, which happens when an EntityConnection is created directly or as a result of an ObjectContext being instantiated.

Example 17-28 demonstrates loading the MetadataWorkspace from an EntityConnection and then from an ObjectContext.

Example 17-28. Accessing the MetadataWorkspace from an EntityConnection and an ObjectContext

VB
Dim econn As New EntityConnection("name=BAEntities")
Dim mdw = econn.GetMetadataWorkspace()

Dim context As New BAEntities
Dim mdw = context.MetadataWorkspace
C#
var econn = new EntityConnection("name=BAEntities");
var emdw = econn.GetMetadataWorkspace();

BAEntities context = new BAEntities();
var mdw = context.MetadataWorkspace;

Creating a MetadataWorkspace without an EntityConnection

You can also instantiate a MetadataWorkspace if you don’t need to make a connection, by using the overload of the MetadataWorkspace constructor, which takes file paths and assemblies.

You can point directly to the files or instantiate a System.Reflection.Assembly to use this constructor. One enumerable that contains file paths and another enumerable that contains assemblies are required; however, you can leave the enumerables empty.

Example 17-29 loads the conceptual and store metadata directly from files using syntax to create an array on the fly. Because no assembly is needed in this example, an empty array is created for the second parameter.

Example 17-29. Creating a MetdataWorkspace using EDM files

VB
Dim mdw = New MetadataWorkspace _
    (New String(){"C:EFModelsBAModel.csdl", _
                  "C:EFModelsBAModel.ssdl"}, _
     New Assembly(){})
C#
var mdw = new MetadataWorkspace
   (new string[] { "C:\EFModels\BAModel.csdl",
                   "C:\EFModels\BAModel.ssdl" },
    new Assembly[]{});

If the model is embedded in an assembly, you can use syntax similar to the metadata property of an EntityConnection string to point to the files and then provide an assembly that is a type loaded in through System.Reflection. This enables the Entity Framework to inspect the assembly and find the embedded files. Example 17-30 shows one of many ways to load an assembly.

Example 17-30. Creating a MetadataWorkspace from EDM files embedded in an assembly file

VB
Dim assem As Assembly = Assembly.LoadFile("C:myappBreakAwayModel.dll")
Dim mdw = New MetadataWorkspace _
    (New String() {"res://*/BAModel.csdl", "res://*/BAModel.ssdl"}, _
     New Assembly() {assem})
C#
Assembly assem = Assembly.LoadFile("C:\myapp\BreakAwayModel.dll");
var mdw = new MetadataWorkspace
   (new string[] { "res://*/ BAModel.csdl", "res://*/ BAModel.ssdl" },
    new Assembly[] { assem });

If you need to use the MetadataWorkspace only to read the models, this is a nice option to leverage.

Clearing the MetadataWorkspace from Memory

Remember that loading the metadata into memory is expensive, so you should leave it in an application cache or in the lifetime of the application. It is possible, however, to clear it out and force it to be reloaded if you require it again, by calling MetadataWorkspace.ClearCache.

The MetadataWorkspace ItemCollections

The metadata has a number of sources: the model’s compiled assembly, the CSDL, the MSL, and the SSDL. The MetadataWorkspace contains five separate item collections, one for each of these different resources. Once you have the MetadataWorkspace, you can start to drill into the metadata, but you always need to specify which item collection to access by using a DataSpace enum: CSpace for the conceptual model, SSpace for the storage model, OSpace for the object model, CSSpace for the mapping layer, and finally, OCSpace for a mapping between the conceptual layer and the object model.

Note

When you’re reading about the Entity Framework, you will find that developers who have been working with the Entity Framework for a while sometimes use the words C-Space and O-Space, among other similar terms, to refer to the DataSpace. This is how they differentiate between the classes and the various parts of the model, since so many of the terms cross over into all of these areas. Saying “the contact in the O-Space” makes it clear that you are talking about the class. “The contact in the C-Space“ means the contact specified in the conceptual model, as opposed to “the contact in the S-Space,” which refers to the Contact table from the database as it is described in the model’s store layer.

These terms also appear in messages when the model can’t be validated because of an error somewhere in the schema.

Warning

Although the compiler will allow you to combine the DataSpace enums using operators such as And and Or, the enums are integers, not expressions, and they are not meant to be combined in this way. You won’t get an exception, but the returned values will be incorrect. Instead, perform the methods on one DataSpace at a time and then combine the results. You can use LINQ’s Union operator for this purpose.

ItemCollections Are Loaded as Needed with EntityCollection

When the MetadataWorkspace is created as the result of an EntityCollection being instantiated, not all of the item collections are loaded right away. For example, the metadata from the store layer isn’t loaded until the first time something is done that requires the store layer—a query execution, for instance, or a call to the ToTraceString method.

If you attempt to extract metadata from an item collection that has not yet been loaded, an exception will be thrown. Therefore, most of the methods for extracting metadata (e.g., GetItem, GetFunctions) come paired with a method using the Try pattern (TryGetItem, TryGetFunctions), which returns Booleans rather than risking an exception being thrown if no data is returned.

When you use the MetadataWorkspace constructor with the file paths overload as shown earlier, all of the designated models are loaded immediately.

GetItemCollection/TryGetItemCollection

You can also test to see whether an ItemCollection has been loaded prior to attempting to get information from it, by using GetItemCollection and TryGetItemCollection. It makes more sense to use the latter so that you don’t get an exception.

The code in Example 17-31 tests to see whether the SSpace is loaded.

Example 17-31. Testing to see whether a DataSpace, specifically the SSpace, is loaded

VB
Dim SSpaceColl As ItemCollection
Dim SSpaceLoaded as Boolean= _
  mdw.TryGetItemCollection(DataSpace.SSpace, SSpaceColl) Then
C#
ItemCollection SSpaceColl = null;
if (mdw.TryGetItemCollection(DataSpace.CSpace, out SSpaceColl))

Other than triggering the model to load through query generation, as explained earlier, there’s no other way to force a model to load to an existing MetadataWorkspace.

Reading Metadata from the MetadataWorkspace

You can pull information from these collections using a variety of methods.

GetItems/TryGetItems

You can use GetItems or TryGetItems to find all items or items of a specific type in a model. Example 17-32 will return an array of every item defined in the model.

Example 17-32. Requesting an array of every item defined in the CSDL

VB
mdw.GetItems(Metadata.Edm.DataSpace.CSpace)
C#
mdw.GetItems(Metadata.Edm.DataSpace.CSpace)

You’ll find in here not only the EntityType and AssociationType items that are defined in your model, but also all of the PrimitiveTypes and FunctionTypes that the model needs to be aware of. PrimitiveTypes are .NET, EDM, and store types. FunctionTypes are built-in functions from the provider as well as the functions that are created from stored procedures in the database. Most likely you will not need access to all of these items; therefore, the GetItems overload shown in Example 17-33, which lets you specify which types to return, might be more useful and efficient.

Example 17-33. Requesting an array of all EntityTypes in the CSDL

VB
mdw.GetItems(Of EntityType)(DataSpace.CSpace)
C#
mdw.GetItems<EntityType>(DataSpace.CSpace)

The array that is shown in Figure 17-8 should look familiar to you. CurrentValues.DataRecordInfo.RecordType.EdmType.EntityType returns the same Edm.EntityTypes. In there, you can find out anything you might want or need to know about the structure of an entity.

As with the other Try alternatives you have seen already, TryGetItems follows the .NET Try pattern to avoid an exception if no matching items are returned.

The results of mdw.GetItems(Of EntityType) (DataSpace.CSpace)

Figure 17-8. The results of mdw.GetItems(Of EntityType) (DataSpace.CSpace)

Notice again how the entity’s properties are grouped in a few different ways to make it easier to access what you are looking for:

  • KeyMembers returns only the properties that are used to build the EntityKey.

  • Members returns all of the properties.

  • NavigationProperties returns a subset of members.

  • Properties returns only the scalar properties (including ComplexTypes).

The biggest benefit of being able to get at this information is the ability to write dynamic functionality in your application. Not only can you instantiate objects, as you’ll see in a bit, but also this metadata is an optimal source for report design tools, just as other schemas, such as the DataSet XSD files, are used for report design.

GetItem/TryGetItem

GetItem and TryGetItem allow you to pass in a string to specify the name of the item you would like to get from the model, rather than returning an array. The name must be fully qualified, not just the string used for the entity’s name property. Example 17-34 shows how to call GetItem.

Example 17-34. Getting EntityTypes that are Contacts from the CSDL

VB
mdw.GetItem(Of EntityType)("BAModel.Contact",DataSpace.CSpace)
C#
mdw.GetItem<EntityType>("BAModel.Contact", DataSpace.CSpace)

Warning

If you pass multiple DataSpace enums into this method and one or more of them do not contain this particular item, an exception will be thrown. Use TryGetItem as a precaution. In Example 17-34, you can assume that the model to search is obvious, but if you are building generic methods where you always have a number of models in the parameter, it is possible to run into this problem.

GetFunctions/TryGetFunctions

GetFunctions and TryGetFunctions will return only functions, but they are different from just calling GetItems<EdmFunction>. Instead, you need to specify the name and the namespace of the function separately (as opposed to the fully qualified name requirement in GetItem), as well as the DataSpace.

The code in Example 17-35 returns the EdmFunction displayed in Figure 17-9.

Example 17-35. Getting a list of functions from the SSDL

VB
mdw.GetFunctions("UpdateContact", "BAModel.Store", DataSpace.SSpace)
C#
mdw.GetFunctions("UpdateContact", "BAModel.Store", DataSpace.SSpace);
Debugging a function’s metadata

Figure 17-9. Debugging a function’s metadata

Compare this to the function’s description in the SSDL, shown in Example 17-36.

Example 17-36. The UpdateContact function listing in the SSDL

<Function Name="UpdateContact" Aggregate="false" BuiltIn="false"
          NiladicFunction="false" IsComposable="false"
         ParameterTypeSemantics="AllowImplicitConversion" Schema="dbo">
   <Parameter Name="ContactID" Type="int" Mode="In" />
   <Parameter Name="FirstName" Type="nchar" Mode="In" />
   <Parameter Name="LastName" Type="nchar" Mode="In" />
   <Parameter Name="Title" Type="nchar" Mode="In" />
</Function>

Everything you see in the schema is available through the MetadataWorkspace.

Querying the Items

It’s also possible to perform LINQ queries against item collections. As an example, the method query in Example 17-37 searches the store model’s ItemCollection to find any item that has Contact in the name.

Example 17-37. Querying the items of the model with LINQ

VB
mdw.GetItems(Of EdmType)(DataSpace.SSpace) _
  .Where(Function(i) i.Name.Contains("Contact"))
C#
mdw.GetItems<EdmType>(DataSpace.SSpace)
  .Where(i => i.Name.Contains("Contact"));

From the BreakAway model, this query returns 10 items that represent the database schema:

  • Contact and ContactPersonalInfo EntityTypes (tables)

  • FK_Address_Contact, FK_Lodging_Contact, and FK_Customers_Contact AssociationTypes

  • Five different functions (stored procedures) with Contact in the title

If you want to search across models, you can use LINQ’s Union query method, which follows the pattern query1.Union(query2).otherMethods, as shown in Example 17-38.

Example 17-38. Combining items from the CSDL and SSDL in one request

VB
mdw.GetItems(Of EdmType)(DataSpace.CSpace) _
.Union(mdw.GetItems(Of EdmType)(DataSpace.SSpace)) _
.Where(Function(i) i.Name.Contains("Contact"))
C#
mdw.GetItems<EdmType>(DataSpace.CSpace)
 .Union(mdw.GetItems<EdmType>(DataSpace.SSpace))
 .Where(i => i.Name.Contains("Contact"));

This returns all of the items with the word Contact in both the conceptual and store models.

Building Entity SQL Queries Dynamically Using Metadata

You can also use the metadata to build Entity SQL queries thanks to the fact that an Entity SQL expression is simply a string.

Imagine that you have a query that returns data from every navigation property (EntityCollections or EntityReferences):

Select con, con.Addresses,con.Orders from MyEntities.Contacts ....

When the application is first written, Addresses and Orders comprise the only child data for the contact. But perhaps another navigation property is introduced with additional child data, such as PhoneNumbers. By writing the query dynamically using the metadata, you’ll never have to modify the query manually. The application will always be able to follow the rule of returning a contact with all of its child data without having to know what the definition of “all of its child data” is.

Building the query dynamically will also mean you won’t know exactly what is contained in the results, and therefore you may also need to use the metadata to handle the results.

Using the BreakAway model, here’s how to do that.

First, you need to make some presumptions. The method will need to know what name is used for an entity when it is a navigation property. It needs both the EntityReference version and the EntityCollection version. For example, if the entity is Reservation, in the Payment entity the navigation property is Payment.Reservation, whereas in Customer it’s Customer.Reservations. For this sample, assume that the model was designed so that Entity.Name and EntityReference are always the same and that EntityCollection is the same in every entity so that you don’t have to worry about “what if” scenarios. Therefore, the Reservation and Reservations strings need to be provided to the method.

Note

It’s a good idea to establish patterns in your model naming for many reasons. Being able to construct generic code in this way is one of those reasons.

You’ll also have the query perform a filter to find the reservation whose ID is 100.

The signature of the method shown in Example 17-39 takes the EntityReference, the EntityCollection, and the entity’s key value as well as a reference to the MetadataWorkspace to read.

Example 17-39. The BuildSingle signature

VB
Private Function BuildSingleEntQuery _
    (ByVal EntityRef As String, ByVal EntityColl As String, _
     ByVal EntityID As Int32, ByVal mdw As MetadataWorkspace) As String
C#
private static string BuildSingleEntQuery
  (string EntityRef, string EntityColl,
   Int32 EntityID, MetadataWorkspace mdw)

Next, you’re going to need the EntityContainer name for the query and the NamespaceName so that you can use the GetItem method. You can find the NamespaceName within various types, but not directly from the MetadataWorkspace, so the code in Example 17-40 grabs a random EntityType from the CSpace and gets the NamespaceName from there.

Example 17-40. Finding the Container and Namespace names using the MetadataWorkspace

VB
Dim mdw = context.MetadataWorkspace
Dim containername = mdw.GetItems(Of EntityContainer) _
   (DataSpace.CSpace).First.Name
Dim namespacename = mdw.GetItems(Of EntityType) _
   (DataSpace.CSpace).First.NamespaceName
C#
Var mdw=context.MetadataWorkspace;
var containername = mdw.GetItems<EntityContainer>
   (DataSpace.CSpace).First().Name;
var namespacename =  mdw.GetItems<EntityType>
   (DataSpace.CSpace).First().NamespaceName;

Next, get the Reservation EntityType to find its navigation properties. Then iterate through the navigation properties, adding each name to a List (see Example 17-41).

Example 17-41. Creating a list of navigation property names

VB
'instantiate a List to contain the navigation properties
 Dim listOfNavs As New List(Of String)
'get the Reservation type
 Dim EntityInfo = mdw.GetItem(Of EntityType) _
    (namespacename & "." & EntityRef, DataSpace.CSpace)
'get its nav properties
 Dim navs = EntityInfo.NavigationProperties
 For Each nav In navs
   listOfNavs.Add(nav.Name)
 Next
C#
//instantiate a List to contain the navigation properties
 List<string> listOfNavs = new List<string>();
 var EntityInfo = mdw.GetItem<EntityType>
     (namespacename + "." + EntityRef, DataSpace.CSpace);
//get nav properties
 var navs = EntityInfo.NavigationProperties;
 foreach (var nav in navs)
   listOfNavs.Add(nav.Name);

You can use the KeyMembers property we discussed in The FieldMetadata Hierarchy to get the name of the property or properties that contain the key field. The code in Example 17-42 assumes that only one property is used for the EntityKey and only one key value has been passed in (EntityID).

Example 17-42. Getting the name of an entity’s key property

VB
Dim singlekeyName = EntityInfo.KeyMembers(0).Name
C#
var singlekeyName = EntityInfo.KeyMembers[0].Name;

For the Reservation EntityType, this will return the string ReservationID.

Now you can finally build the string using the names in listofNavs and the various other variables you have collected along the way (see Example 17-43).

Example 17-43. Building the Entity SQL expression and closing the method

VB
  Dim esqlSB As New Text.StringBuilder
  esqlSB.Append("SELECT ent")
 'add each navigation property to the projection
  For Each name In listOfNavs
    esqlSB.Append(",ent." & name)
  Next
 'add the FROM...AS clause
  esqlSB.Append _
    (" FROM " & containername.Trim & "." & EntityColl & " AS ent")
  esqlSB.Append(" WHERE ent." & singlekeyName & " = " & IDtoFind)
  Return esqlSB.ToString

End Function 'end of the function
C#
  StringBuilder esqlSB = new StringBuilder();
  esqlSB.Append("SELECT ent");
 //add each navigation property to the projection
  foreach (var name in listOfNavs)
    esqlSB.Append(",ent." + name);
 //add the FROM...AS clause
  esqlSB.Append
   (" FROM " + containername.Trim() + "." + EntityColl + " AS ent");
 //Add the filter
  esqlSB.Append
   (" WHERE ent." + singlekeyName + " = " + EntityID);
  return esqlSB.ToString();

} //end of the method

Now you can create a routine to call the function (see Example 17-44). Note that the query is explicitly executed so that you can work with the results, not the actual query, and that you don’t have to worry about inadvertently executing the query again.

Example 17-44. Testing the method

VB
Private Sub DynamicESQLTest()
  Using context As New BAEntities
    Dim esql = BuildSingleEntQuery _
      ("Reservation", "Reservations", 90, context.MetadataWorkspace)
    Dim query = context.CreateQuery(Of DbDataRecord)(esql)
    Dim queryResults = query.Execute(MergeOption.AppendOnly)
    For Each entity In queryResults
      'PLACEHOLDER: iterate through the data
      Next
    End Using
  End Sub
C#
private static void DynamicESQLTest()
{
  using (BAEntities context = new BAEntities())
  {
    var esql = BuildSingleEntQuery
      ("Reservation", "Reservations", 90, context.MetadataWorkspace);
    var query = context.CreateQuery<DbDataRecord>(esql);
    var queryResults = query.Execute(MergeOption.AppendOnly);
    foreach (var entity in queryResults)
      //PLACEHOLDER: iterate through the data;
  }
}

Example 17-45 displays the Entity SQL expression that results.

Example 17-45. The Entity SQL expression that results

SELECT ent,ent.Customer,ent.Trip,ent.Payments,ent.UnpaidReservation
FROM BAEntities.Reservations AS ent
WHERE ent.ReservationID = 90

Reading the Results of a Dynamically Created Query

Iterating through the data without knowing what is in there might be a little tricky. However, since this was an ObjectQuery, all of the data is in the ObjectContext. So, as you iterate through the data, you can access ObjectStateEntries and use the tricks you learned earlier in this chapter to find out about the entities using generic code.

You can replace the empty iteration used earlier with a new bit of code that will do the following for each DbDataRecord returned by the query:

  1. Cast the row to an IExtendedDataRecord, which will turn the DbDataRecord into a record containing a DataRecordInfo object, a FieldCount property, and a list of items representing the fields of the row.

    Based on the preceding query, you should expect the row to contain the following:

    • Field(0): A Reservation entity

    • Field(1): A Customer entity

    • Field(2): A Trip entity

    • Field(3): EntityCollection<Payment>

    • Field(4): An UnpaidReservation entity

  2. Iterate through each field in the row.

  3. Identify whether the field is an EntityType.

  4. Pass the EntityKey of the entity to another method that will find the ObjectStateEntry in the ObjectStateManager and list the name and value of each field.

  5. Identify whether the field is an EntityCollectionType.

  6. Cast the field to an IEnumerable, and then iterate through the entities in that collection and perform the same method on the EntityKey of each entity.

Note

Casting to the IEnumerable is not a random act of coding. You need to cast the field to something in order to access it, but the EntityCollection surfaces as a generic List<Payment>, which causes a problem. Because this code is dynamic, you can’t specify the type. It would be nice to cast it to a List<EntityObject>, but you cannot cast a generic List<T> to another generic List<T>. Therefore, casting to a standard list type, such as ICollection or IEnumerable, does the trick. Internally, there are advantages to using IEnumerable, so this was the winning target of the cast.

In Example 17-44, a line of code indicated a placeholder for iterating through the data. Replace that placeholder with the code in Example 17-46, which drills deep into the FieldMetadata hierarchy. The routine calls out to another method, DisplayFields, shown in Example 17-46.

Example 17-46. Iterating through the results and determining whether the navigation properties are entities or EntityCollections

VB
For Each ent As Data.IExtendedDataRecord In query
  Dim fieldCount = ent.FieldCount
  For i As Integer = 0 To fieldCount - 1

   'If the navigation property is an Entity, list its fields.
    If ent.DataRecordInfo.FieldMetadata(i).FieldType.TypeUsage. _
          EdmType.BuiltInTypeKind = BuiltInTypeKind.EntityType Then

      DisplayFields CType(ent(i), EntityObject).EntityKey, context)

   'otherwise, if it's a collection, iterate through the collection
   'and list the fields for each entity in the collection
    ElseIf ent.DataRecordInfo.FieldMetadata(i).FieldType.TypeUsage. _
          EdmType.BuiltInTypeKind = BuiltInTypeKind.CollectionType Then

      Dim listofEntities = CType(ent(i), Collections.IEnumerable)
      For Each collEnt As EntityObject In listofEntities
        DisplayFields(collEnt.EntityKey, context)
      Next

    End If
  Next
Next
C#
foreach (IExtendedDataRecord ent in queryresults)
{
  var fields = ent.FieldCount;
  for (int i = 0; i < fields; i++)
  {
    //If the navigation property is an Entity, list its fields.
     if (ent.DataRecordInfo.FieldMetadata[i].FieldType.TypeUsage.
          EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)

       DisplayFields(((EntityObject)(ent[i])).EntityKey, context);

    //otherwise, if it's a collection, iterate through the collection
    //and list the fields for each entity in the collection
     else if (ent.DataRecordInfo.FieldMetadata[i].FieldType.TypeUsage.
              EdmType.BuiltInTypeKind == BuiltInTypeKind.CollectionType)
     {
       var listofEntities = (System.Collections.ICollection)(ent[i]);
       foreach (EntityObject collEnt in listofEntities)
         DisplayFields(collEnt.EntityKey, context);
      }
    }
  }
}

The DisplayFields method takes the EntityKey and the context, and then digs into the ObjectStateManager to get the information it needs regarding the entity. Like the visualizer, this method takes into account the possibility of complex types, as shown in Example 17-47.

Example 17-47. The DisplayFields method getting the field names and values from metadata

VB
Private Sub DisplayFields(ByVal ekey As EntityKey, _
                          ByVal context As ObjectContext)
  Dim entEntry = context.ObjectStateManager.GetObjectStateEntry(ekey)
  Dim fieldcount = entEntry.CurrentValues.FieldCount
  Dim metadata = entEntry.CurrentValues.DataRecordInfo.FieldMetadata()

  Console.WriteLine(entEntry.CurrentValues.DataRecordInfo _
                            .RecordType.EdmType.Name)
  For i = 0 To fieldcount - 1
    Select Case metadata(i).FieldType.TypeUsage.EdmType.BuiltInTypeKind
      Case BuiltInTypeKind.PrimitiveType
        Console.WriteLine(" " & metadata(i).FieldType.Name & ": " & _
                          entEntry.CurrentValues(i).ToString)
      Case BuiltInTypeKind.ComplexType
        Dim ct = entEntry.CurrentValues.GetDataRecord(i)
        For j = 0 To ct.FieldCount
          Console.WriteLine(" " & ct.GetName(i) & ": " & ct(j).ToString)
        Next
    End Select
  Next
  Console.WriteLine()
End Sub
C#
private static void DisplayFields(EntityKey ekey, ObjectContext context)
{
  var entEntry = context.ObjectStateManager.GetObjectStateEntry(ekey);
  var fieldcount = entEntry.CurrentValues.FieldCount;
  var metadata = entEntry.CurrentValues.DataRecordInfo.FieldMetadata;
  Console.WriteLine(entEntry.CurrentValues.DataRecordInfo
                            .RecordType.EdmType.Name);
  for (var i = 0; i < fieldcount; i++)
  {
    switch (metadata[i].FieldType.TypeUsage.EdmType.BuiltInTypeKind)
    {
      case BuiltInTypeKind.PrimitiveType:
        Console.WriteLine(" " + metadata[i].FieldType.Name + ": " +
                          entEntry.CurrentValues[i].ToString());
        break;
      case BuiltInTypeKind.ComplexType:
        var ct = entEntry.CurrentValues.GetDataRecord(i);
        for (var cti = 0; cti <= ct.FieldCount; cti++)
          Console.WriteLine("  " + ct.GetName(i) + ": " +
                            ct[cti].ToString());
        break;
    }
  }
  Console.WriteLine();
}

Example 17-48 shows the final output of the sample, using the dynamically built Entity SQL query from Example 17-44 and the dynamically evaluated results.

Example 17-48. The results of the generic query displayed using generic code

Reservation
 ReservationID: 90
 ReservationDate: 12/4/2005 12:00:00 AM
 TimeStamp: System.Byte[]

Customer
 ContactID: 569
 FirstName: Cecil
 LastName: Allison
 Title: Mr.
 AddDate: 1/10/2004 5:46:14 PM
 ModifiedDate: 8/7/2008 8:27:07 AM
 TimeStamp: System.Byte[]
 InitialDate: 10/21/2003 6:19:27 AM
 Notes:
 BirthDate: 4/13/1988 12:00:00 AM
 HeightInches: 63
 WeightPounds: 152
 DietaryRestrictions:

 CustTimeStamp: System.Byte[]

Trip
 TripID: 32
 StartDate: 3/4/2006 12:00:00 AM
 EndDate: 3/11/2006 12:00:00 AM
 TripCostUSD: 1500

Payment
 PaymentID: 8
 PaymentDate: 2/1/2006 12:00:00 AM
 Amount: 300.0000
 ModifiedDate: 1/1/1900 12:00:16 AM
 TimeStamp: System.Byte[]
 ContactID: 569

This demonstrates the power you can access by combining the MetadataWorkspace and the ObjectStateManager.

Note

Zlatko Michailov’s blog post titled “How to Parse an EntityDataReader” will give you the tools you need to iterate through the shaped results of an EntityClient query, identifying whether the items contain scalar values, a single entity object, an EntityCollection, and more. The post is at http://blogs.msdn.com/esql/ in the November 2007 archive.

Dynamic Entity SQL and Generics for Reference Lists

Here’s a very handy use for creating Entity SQL dynamically. Many applications use reference lists which are contained in separate tables. In the BreakAway app, Locations (aka Destinations), CustomerTypes, and Activities can be considered reference types. The entities follow a pattern as well. Each has an ID that is composed of the entity name and “ID,” e.g., ActivityID, as well as a description that is composed of the entity name and the word “Name,” e.g., ActivityName.

Rather than write individual queries to generate these lists, you could have a single, generic method that dynamically queries for these lists.

Look at the similarities between the Entity SQL expressions for querying these lists. I’ll use refData as the control variable for the expression:

SELECT refData FROM BAEntities.Activities AS refData
SELECT refData FROM BAEntities.CustomerTypes AS refData
SELECT refData FROM BAEntities.Destinations AS refData

If you were to create separate methods in your business layer to return each of these lists, you would have a lot of redundant code. Yet all that really differentiates the queries are the EntitySet name and the actual type returned in the resulting list.

Using a dynamic query and .NET Generics, you can create a single method that would be able to query for and return any of these lists.

The trickiest thing you’ll need to do in this method is to get the strongly typed name of the entity that you want to query. But you can pull this off with the MetadataWorkspace, which means that the method will need access to that. Therefore, the method will take a MetadataWorkspace as a parameter. If the calling code has an available context, then it can easily pass in the MetadataWorkspace property from the context. Otherwise, you can create a MetadataWorkspace object on the fly and pass that in.

Here is what the signature of such a method would look like. Note that I am using TEntity to refer to an entity type rather than the more common T, used in other generic methods:

VB
Public IList(Of TEntity) GetReferenceList(Of TEntity)(mdw As MetadataWorkspace)
C#
public IList<TEntity> GetReferenceList<TEntity>(MetadataWorkspace mdw)

The first bit of code in the method will use the MetadataWorkspace to get the strongly typed EntitySet name based on the name of the entity defined by TEntity. Rather than have this reusable function buried in the GetReferenceList method, I am encapsulating it in a separate extension method for the MetadataWorkspace class, which I am calling GetEntitySetName. This is shown in Example 17-49.

Example 17-49. The GetEntitySetFullName extension method for the MetadataWorkspace class

VB
<Extension()> _
Public Function GetEntitySetFullName(ByVal mdw As MetadataWorkspace, _
 ByVal entityName As String) As String
  Dim entContainer = mdw.GetItems(Of EntityContainer)(DataSpace.CSpace).First
  Dim entsets = From eset In entContainer.BaseEntitySets _
                Where eset.ElementType.Name = entityName
  Dim containerName = entsets.First.EntityContainer.Name
  Return containerName & "." & entsets.First.Name
End Function
C#
public static string GetEntitySetFullName(this MetadataWorkspace mdw, 
 string entityName)
{
  var entContainer = mdw.GetItems<EntityContainer>(DataSpace.CSpace).First();
  var entsets =
        from eset in entContainer.BaseEntitySets
        where eset.ElementType.Name == entityName
        select eset;
  var containerName = entsets.First().EntityContainer.Name;
  return containerName + "." + entsets.First().Name;
}

Now you can build the GetReferenceList method, as shown in Example 17-50, which will use GetEntitySetFullName to supply the strongly typed EntitySet name.

Example 17-50. The GetReferenceList extension method for the ObjectContext class

VB
Public Function GetReferenceList(Of TEntity)(ByVal context As ObjectContext) _
 As IList(Of TEntity)
  Dim entityName = GetType(TEntity).Name
  Dim eSetName = context.MetadataWorkspace.GetEntitySetFullName(entityName)
  Dim esql = "SELECT VALUE refData FROM " & eSetName & " AS refData"
  Dim objQuery = context.CreateQuery(Of TEntity)(esql)
  return objQuery.ToList
End 
C#
public IList<TEntity> GetReferenceList<TEntity>(ObjectContext context)
{
  var entityName = typeof(TEntity).Name;
  var eSetName = context.MetadataWorkspace.GetEntitySetFullName(entityName);
  var esql = "SELECT VALUE refData FROM " + eSetName + " AS refData";
  var objQuery = context.CreateQuery<TEntity>(esql);
  return objQuery.ToList();
}

Note that the queries are executed using the default MergeOption, which means they will be managed by the context. Although you won’t likely need to change-track these objects, if you want them to engage in relationships to other entities, they need to be managed by the context. You could create an overload that allows you to pass in a MergeOption and thereby have more control over each list.

This is a great example of using the MetadataWorkspace to construct code that is not only reusable but easier to maintain. It also highlights very useful functionality that you can’t easily achieve with LINQ to Entities.

Creating EntityObjects Without Entity Classes

Much of the metadata work done in the previous examples used metadata to inspect existing classes that were created through an ObjectContext. What if you want to create classes without the benefit of the generated entity classes? For example, you could have an application that works with any Entity Framework EDM passed to it that has no previous knowledge of the classes.

You can do this by combining the MetadataWorkspace API with System.Reflection.

Although many people are familiar with using System.Reflection for inspecting .NET objects, you also can use it to instantiate objects using type information. You can generate that type information through the MetadataWorkspace API and then let reflection create object instances for you.

Creating a New Entity with CreateInstance

You can instantiate a new class using the CreateInstance method of either System.Activator or System.Reflection.Assembly. Using Assembly.CreateInstance requires that you have a type instance of an assembly. Internally, Assembly.CreateInstance will eventually call Activator.CreateInstance.

There are reasons for choosing one over the other, but Assembly.CreateInstance suits the needs of these examples since the sample will do some other things with the assembly.

Getting a reference to an assembly

You can load an assembly into an Assembly type in a number of ways. One way is to use an existing object that comes from that assembly.

For example, if you have created a Customer object from the BreakAway model’s assembly, you can use that object to get a handle to the assembly using the static method Assembly.GetAssembly, as shown in Example 17-51.

Example 17-51. Loading an assembly programmatically

VB
Dim cust=context.Customers.First
Dim BAAssembly=Assembly.GetAssembly(context.GetType)
C#
var cust=context.Customers.First();
var BAAssembly=Assembly.GetAssembly(context.GetType);

Creating an entity from the assembly

Now you can use this assembly object to instantiate any class within the assembly. You can do this by passing in the strongly typed name of the class, as shown in Example 17-52.

Example 17-52. Instantiating a class in the assembly

VB
Dim newObj=BAAssembly.CreateInstance("BAGA.Payment")
C#
var newObj=BAAssembly.CreateInstance("BAGA.Payment");

This will create a new Payment object instance.

You can see how using reflection can let you build dynamic code. You can create objects just by passing in a string.

Note

Notice that the strong name of the type does not use the model name, as you are required to do when working with the metadata. Instead, it needs the strongly typed name of the class as it is known to the assembly, and in this example, the assembly’s namespace is BAGA.

Using System.Type to Inspect the EntityType

The object in the preceding section is a Payment entity, which has only the methods and properties of a Payment entity. That hasn’t gotten you very far with dynamic programming.

However, you can additionally create a Type object, either directly from the assembly or from the Payment instance. A Type object allows you to do the same type of detective work on the Payment type that you did on the metadata. (See Example 17-53.)

Example 17-53. Creating a Type object to be used with reflection

VB
Dim objectType = currAssembly.GetType("BAGA.Payment")
C#
var objectType=currAssembly.GetType("BAGA.Payment");

System.Type also lets you set values. Although setting scalar values using an Entity Framework CurrentValueRecord provides better performance, you cannot modify any other properties. System.Type will allow you to access and modify all of the properties of the object, including navigation properties. You’ll do this in the next example.

Creating Entities and Graphs Dynamically

Now it’s time to leverage what you just learned. The following example is a culmination of many of the techniques explained in this chapter, from using the MetadataWorkspace to dynamically creating relationships on the fly using the RelationshipManager. The code enables you to create entities, build a graph, and save the graph data—all with generic code that has no knowledge of the entity classes that it will work with. The example in this section will do the following:

  • Receive information about an existing parent entity and the child to be created dynamically. An array of KeyValuePairs will be used to provide field names and values for populating the new object.

  • Query the model to retrieve the parent entity.

  • Create a new instance of the child.

  • Populate the child with the data.

  • Attach the child to the parent using the RelationshipManager shown earlier in this chapter.

  • Save the new child to the database.

You will do all of this without any references to the actual entity types so that you can use any parent and child with the method.

The method shown in Example 17-54 takes advantage of a few custom functions and extension methods that are included in the downloads on the book’s website. You’ll see a frequently used custom method called GetEntitySetName. This method is a trimmed down version of GetEntitySetFullName, which does not bother with EntityContainer name. The extension methods are handy for a lot of MetadataWorkspace and ObjectStateManager scenarios.

Comments throughout the code explain what’s happening in detail.

Note

Because of the length of this code sample, only the Visual Basic version will be displayed. The C# version will be available for download from the book’s website.

Example 17-54. Building an entity graph dynamically with database interaction

VB
Private Function AddChildtoParentObject _
              (ByRef ParentTypeName As String, _
               ByVal ParentID As KeyValuePair(Of String, Integer),_
               ByVal ChildTypeName As String, _
               ByVal ParamArray FieldValues() As _
               KeyValuePair(Of String, Object)) As Boolean

  'using a default connection string, but metadata and store connection 
  'string can be passed in as well
   Using context As New ObjectContext( _
     New EntityClient.EntityConnection("name=BAEntities"))
     Dim mdw = context.MetadataWorkspace

     'will need namespace name further on
      Dim EDMNamespace = _
       mdw.GetItems(Of EntityType)(DataSpace.CSpace).First.NamespaceName
      

     'need assembly and its Namespace to create objects.
      Dim currAssembly = Assembly.GetAssembly(parentObject.GetType)

     'Assembly namespace is available in any of its types, 
     'get one randomly
      Dim assemblyNS = _
         currAssembly.GetTypes.Where _
         (Function(t) t.BaseType.Name = "EntityObject").First.Namespace

      'query the EDM/db for the parent object
      Dim esql = _
      "SELECT VALUE ent FROM " & _
      mdw.GetEntityFullSetName(ParentTypeName) & " AS ent " & _
      "WHERE ent. " & ParentID.Key & "=" & ParentID.Value.ToString


      Dim parentObject = context.CreateQuery _
       (Of EntityObject)(esql).FirstOrDefault
      If parentObject Is Nothing Then
        Return False  'need logic to deal with this in calling method
      End If

     'create a new System.Type of the child entity
     'this will be used for type inspection
      Dim childAsType As Type = _
       currAssembly.GetType(assemblyNS & "." & ChildTypeName)  

     'instantiate an actual entity
      Dim childAsEntity =  _
       CType(Activator.CreateInstance(childAsType), EntityObject)

     'get the relationshipmanager so you can add a relationship 
      Dim childRelationshipManager = _
       CType(childAsEntity, IEntityWithRelationships) _
       .RelationshipManager

     'need association name in order to get the related end
     'GetEntitySetName and GetAssociationName are custom methods     
      Dim associationName = _
       mdw.GetAssociationName _
       (mdw.GetEntitySetName(ChildTypeName), _
        mdw.GetEntitySetName(ParentTypeName))

     'now use AssociationName and the EntitySet of the parent
     'to get the related end
      Dim ParentRelatedEnd = _
       childRelationshipManager.GetRelatedEnd _
       (associationName, mdw.GetEntitySetName(ParentTypeName))

     'finally, add the parent object to the relationship of the child
      ParentRelatedEnd.Add(parentObject)

      'modify properties through ObjectStateEntry, _
      'provides better performance in this case than with reflection
      Dim childEntry = context.ObjectStateManager.GetObjectStateEntry _
       (childAsEntity.EntityKey)

      'iterate through FieldValues passed in to assign the properties
      For Each item In FieldValues
        Try
          childEntry.CurrentValues.SetValue _
           (childEntry.GetOrdinalforProperty(item.Key), item.Value)
        Catch ex as exception
          'if value types are incorrect, setvalue will fail
        End try
      Next

      context.SaveChanges()
    End Using
    Return True
     'wrap the entire method in a Try/Catch and add exception handling
  End Function

A lot is going on in this example, but you learned most of it earlier in the chapter. The example serves two purposes. First, it demonstrates how to use reflection to create entities dynamically, as well as how to build graphs dynamically. Second, it demonstrates the combined power of the ObjectStateManager, the MetadataWorkspace, and reflection to build dynamic code, whether it is an entire application that is purely dynamic or part of an application that needs to be dynamic.

Calling the AddChildtoParentObject Method

The AddChildtoParentObject method requires a number of values to be passed in as parameters, as listed here and as shown in the following code:

  • The name of the parent entity.

  • The key field name and value for the parent, which are bundled in a KeyValuePair. This allows the method to query the database for the parent record. The method assumes that only a single value is required for the key, which will suffice for most cases.

  • The name of the child entity that will be added.

  • An array of KeyValuePairs, which take a string and an object. Each key/value pair represents the field name and value of the fields that will be populated for the new child entity.

VB
AddChildtoParentObject _
  ("Reservation", _
   New KeyValuePair(Of String, Integer)("ReservationID", 10), _
   "Payment", _
   New KeyValuePair(Of String, Object)("PaymentDate", Now), _
   New KeyValuePair(Of String, Object)("Amount", CType(400, Decimal)))
C#
AddChildtoParentObject
 ("Reservation", 
  new KeyValuePair<string, int>("ReservationID", 10), "Payment",
  new KeyValuePair<string, object>("PaymentDate", System.DateTime.Now),
  new KeyValuePair<string, object>("Amount", System.Convert.ToDecimal(400)));

The method has some checks and balances built in to protect you from invalid property names and incorrect data types.

Note

Although an Entity SQL query is built into the method, the only provided parameter is the parent entity name, which your application, not the user, should supply, so you shouldn’t have to worry about an injection attack here.

Out of context, this looks like a lot of work—even more work than just using classic ADO.NET. But if you need to create dynamic functionality in your applications that can handle any entity types you throw at it, this is definitely the way to go.

Summary

In this chapter, you investigated ObjectStateManager and MetadataWorkspace—the two APIs that provide most of the internal functionality of the Entity Framework. Using the same classes and features that Object Services and EntityClient use to parse queries and materialize objects, you learned how to create a variety of dynamic functionality and explored some useful scenarios for taking advantage of these features.

Although you might not be able to overcome some of the more sophisticated challenges in your applications with a simple method that is already available in the Entity Framework, access to these low-level tools enables you to build your own tools and functionality.

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

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