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.
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).
Anytime an entity leaves the cache, its ObjectStateEntry
is automatically destroyed as
well as any RelationshipEntry
objects
that are bound to that entity.
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.
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.
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).
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.
.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);
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
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).
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.
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.
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
.
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 DbDataReader
s, 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.
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 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
.
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 EntityKey
s are serializable. In addition, it
takes an ObjectContext
as a parameter
so that it can search the context to find the ObjectStateEntry
.
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.
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
}
}
}
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.
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
{
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).
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];
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
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 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();
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.
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).
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.
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 EntityKey
s 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.
Although you will also find the EntityKey
s in the OriginalValues
(unless the relationship is
Added
), the OriginalValues
are not truly viable. The
property exists because it is there for all EntityStateObject
s, but you should not
rely on it for RelationshipEntries
. Stick with the
CurrentValues
.
Because the RelationshipEntry
describes a relationship between two entities, the EntityKey
s found within
the CurrentValues
will match up
with EntityKey
s of ObjectStateEntries
in
the context. Figure 17-6 shows a RelationshipEntry
that defines the
relationship between a Customer
and
a Reservation
.
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.
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
.
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.
Figure 17-7. An EntityKey that is the result of the RelationshipEntry’s CurrentValues property requested in Example 17-22
The EntityKey
s 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.
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));
}
}
It is possible to get your hands on an instance of the
RelationshipManager
to build
graphs on the fly, creating RelationshipEntr
ies 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.
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
).
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.
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 EntityReference
s
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);
}
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 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.
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.
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.
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;
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.
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 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.
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.
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.
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.
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
.
You can pull information from these collections using a variety of methods.
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 PrimitiveType
s and FunctionType
s that the model needs to be
aware of. PrimitiveType
s are
.NET, EDM, and store types. FunctionType
s 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.EntityType
s. 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.
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:
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
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)
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
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);
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
.
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.
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 (EntityCollection
s or EntityReference
s):
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.
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.
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:
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
Iterate through each field in the row.
Identify whether the field is an EntityType
.
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.
Identify whether the field is an EntityCollectionType
.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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 KeyValuePair
s 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.
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.
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 KeyValuePair
s, 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.
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.
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.
3.138.105.215