Any time a new technology comes on the market, most developers go through the same thought process: Will this technology improve my coding productivity
and the applications I develop? We're all used to examining new technologies to determine whether products like Microsoft's Entity Framework provide
tangible benefits for developers so that they enhance applications while requiring as little additional work as possible. The challenges come when a
developer has to incorporate a new technology into an application, developing an architecture to achieve the following key features:
-
Easy to consume: Although the architecture may be more complicated because of the new technology, using it shouldn't be.
-
Reduced coding: The architecture should not require developers to write a lot more code than normal.
-
Abstraction: Consumers of the architecture shouldn't know it's working with Entity Framework-specific components. Loosely coupling components
together as much as possible is also a plus.
-
Dependency injection: The process of creating a data-access component should use advanced techniques such as the Service Locator pattern and
dependency injection.
-
Test support: As agile development processes and test-driven development with coded unit tests become more commonplace, embedding the
ObjectContext directly in an application has less value for this methodology than for others. It should be easy to write test fakes and test within the
application.
In this article, I'll discuss points to guide you in using Microsoft's ADO.NET Entity Framework to develop an architecture for an application, starting
off with a few preliminaries about working with Entity Framework objects.
Entity Framework Overview
To understand how you can use Entity Framework to develop your architecture, we first must understand some of the objects that Entity Framework uses
and how we can extract these objects out of the framework. To begin, Entity Framework employs the ObjectContext as the workhorse for the application.
It handles the tasks of state tracking, database reading/manipulation, and much more. Within an ObjectContext is the ObjectSet, a component that
focuses on the management of a single logical entity. LINQ queries executed against an ObjectSet are represented by the ObjectQuery class, and the
ObjectResult class represents the results from a stored procedure execution.
When setting up an Entity Framework model, the designer generates a new class that inherits from ObjectContext. For each entity defined in the model,
this custom ObjectContext contains a property of type ObjectSet<T> for each of the logical entities as well as a custom class that inherits from
EntityObject. Any stored procedures are mapped to an equivalent function made available through the ObjectContext. This function returns an
ObjectResult with the result of the stored procedure, either a scalar value, one of the existing entities, or a new complex type representing the
results of the stored procedure.
The IQueryable interface represents the result of an actual query against the database, but this interface is misleading. It makes you think that the
actual query has been executed against the database, when it may not have been. Behind the scenes, the ObjectQuery class represents a query created
against the ObjectContext. This class does not execute against the database and become a collection of objects until it is iterated through, or the
ToList method is called. This means the IQueryable resulting query may or may not be executed yet.
Although this seems a minor point, it has important consequences. If the query hasn't been executed against the database, any LINQ extension method or
secondary LINQ query will modify the original query result. This can have beneficial or undesirable consequences. It can be beneficial by allowing you
to only add the query parameters that you actually need, instead of inner conditional statements to flesh out the parameters. However, difficulties can
arise during application maintenance when you're trying to figure out why that additional query condition is causing the query to perform poorly or
return incorrect results. There is no direct way to determine the difference between the two issues at design time, except for examining the code and
checking for any foreach statements or ToList() calls.
Entity Framework Building Blocks
When working with an ObjectContext, there are a few techniques that we'll use to make it easier to get data from the API. I refer to these techniques
as "building blocks" because they let us refer to objects in a generic fashion.
We'll start with the technique shown in Figure 1, which illustrates how we can create an ObjectSet on the fly. The CreateObjectSet method generates a
new ObjectSet class for a given entity, which is actually what is used in the designer-generated context created for you.
Figure 1: Creating an ObjectSet
var ctx = new SamplesObjectContext();
var sets = ctx.CreateObjectSet<User>();
Unfortunately, a non-generic implementation of this method does not yet exist. However, with some additional work and a whole lot of reflection, the
ObjectContext can be made to dynamically refer to an object by type, without explicitly specifying the type as a generic, as shown in Figure 2.
Figure 2: Using reflection to create the ObjectSet
var ctx = new SamplesObjectContext();
var creationMethod = ctx.GetType().GetMethod("CreateObjectSet", new Type[] { });
var os = creationMethod.MakeGenericMethod(typeof(User)).Invoke(ctx, new object[] { });
This is important to understand because application frameworks, at times, need to use reflection to simplify the code and avoid having to write
repetitive code, per the Don't Repeat Yourself (DRY) principle. You may not understand why now, but as you navigate the sample code, you'll see several
examples that show why reflection is needed. Although I hard-code the User type that's in Figure 2, you can easily replace this value with a Type
variable in your own code (you can also do so for the hard-coded value in Figure 3).
Figure 3: Querying an ObjectSet using expressions
var ctx = ObjectContextCreator.Create();
var os = ctx.Users;
var param = Expression.Parameter(typeof(User), "u");
var right = Expression.Constant(3);
var left = Expression.Property(param, typeof(User).GetProperty("UserID"));
var query = Expression.Lambda(BinaryExpression.Equal(left, right), param);
var call = Expression.Call(typeof(Queryable), "Where", new Type[] { typeof(User) }, os.AsQueryable().Expression, query);
return View(new { Data = os.AsQueryable().Provider.CreateQuery(call) });