CHAPTER 8

image

Plain Old CLR Objects

Objects should not know how to save themselves, load themselves, or filter themselves. That’s a familiar mantra in software development, and especially in Domain Driven Development. There is a good bit of wisdom in this mantra. Having persistence knowledge bound too tightly to domain objects complicates testing, refactoring, and reuse. In ObjectContext, the classes generated by Entity Framework for model entities are heavily dependent on the plumbing of Entity Framework. For some developers, these classes know too much about the persistence mechanism, and they are too closely tied to the concerns of models and mapping. There is another option, however.

Entity Framework also supports using your own classes for the entities in the model. The term Plain Old CLR Object, often simply referred to as POCO, isn’t meant to imply that your classes are either plain or old. It merely means that they don’t contain any reference at all to specialized frameworks, they don’t need to derive from third-party code, they don’t need to implement any special interface, and they don’t need to live in any special assembly or namespace. You may implement your domain objects however you see fit and tie them to the model with a custom object context. With that being said, you are ready to leverage all of the power of Entity Framework and follow just about any architectural pattern you choose. You can also use DbContext to generate the POCO classes for you.

This chapter covers a wide variety of recipes specific to POCO. The first recipe shows you the basics of using POCO. The remaining recipes focus on loading entities and keeping Entity Framework in sync with the state of your objects.

In this chapter, we’ve intentionally focused on writing most of the POCO-related code by hand to demonstrate how things work. All of the work involved in building the POCO plumbing goes away if you use the POCO T4 template available from the ADO.NET development team at Microsoft.

8-1. Using POCO

Problem

You want to use Plain Old CLR Objects (POCO) in your application.

Solution

Let’s say that you have a data model like the one shown in Figure 8-1.

9781430257882_Fig08-01.jpg

Figure 8-1. A database model for customers and their orders

To create an Entity Framework model based on the database tables in Figure 8-1, and using the POCO classes generated by Entity Framework representing an Order, OrderDetail, Customer, and Product, follow the steps below:

  1. Right-click your project, and select Add arrow.jpg New Item.
  2. From the Visual C# Items Data templates, select ADO.NET Entity Data Model.
  3. Select Generate from database to create the model from our existing tables.
  4. Select the Order, OrderDetail, Customer, and Product tables, and click Next. In the generated model, the Product entity has an OrderDetails navigation property for all of the order details associated with this product. This is unnecessary here, so delete this navigation property. The completed model is shown in Figure 8-2.

    9781430257882_Fig08-02.jpg

    Figure 8-2. The model for our customers’ orders

  5. We will be using generated classes for our entities. By default, Entity Framework 6 generates entity classes in the form of POCO. Thus all of the database access code is in a separate class, and entities are generated in separate plain classes. It will yield the same implementation result as would have been created manually in previous Entity Framework versions by turning off code generation for the model. In this version, Code Generation Strategy is already set to None. The code in Listing 8-1 shows the classes for our model.

    Listing 8-1.  The Plain Old CLR classes for Our Model

    public partial class Customer
        {
            public Customer()
            {
                this.Orders = new HashSet<Order>();
            }

            public int CustomerId { get; set; }
            public string ContactName { get; set; }

            public virtual ICollection<Order> Orders { get; set; }
        }
    public partial class Order
        {
            public Order()
            {
                this.OrderDetails = new HashSet<OrderDetail>();
            }

            public int OrderId { get; set; }
            public int CustomerId { get; set; }
            public System.DateTime OrderDate { get; set; }

            public virtual Customer Customer { get; set; }
            public virtual ICollection<OrderDetail> OrderDetails { get; set; }
        }
    public partial class OrderDetail
        {
            public int OrderId { get; set; }
            public int ProductId { get; set; }
            public decimal UnitPrice { get; set; }
            public int Quantity { get; set; }

            public virtual Order Order { get; set; }
            public virtual Product Product { get; set; }
        }
    public partial class Product
        {
            public Product()
            {
                this.OrderDetails = new HashSet<OrderDetail>();
            }

            public int ProductId { get; set; }
            public string ProductName { get; set; }
            public decimal UnitPrice { get; set; }

            public virtual ICollection<OrderDetail> OrderDetails { get; set; }
        }

    Notice that there is no association from Product to OrderDetail, because we removed that navigation property in the designer.

  6. To use POCO classes, Entity Framework also generated the class that is derived from DbContext. This class will expose an ObjectSet<T> for each of the entities in our model. The code in Listing 8-2 illustrates how we might define this class.

    Listing 8-2.  DbContext for Our Model Created While Generating an Entity Data Model

    public partial class EFRecipesEntities : DbContext
        {
            public EFRecipesEntities()
                : base("name=EFRecipesEntities")
            {
            }

            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                throw new UnintentionalCodeFirstException();
            }

            public DbSet<Customer> Customers { get; set; }
            public DbSet<Order> Orders { get; set; }
            public DbSet<OrderDetail> OrderDetails { get; set; }
            public DbSet<Product> Products { get; set; }
        }

This completes the model with the generated POCO classes. The code in Listing 8-3 demonstrates inserting into and querying our model.

Listing 8-3.  Using Our POCO Classes

using (var context = new EFRecipesEntities())
         {
            var tea = new Product { ProductName = "Green Tea", UnitPrice = 1.09M };
            var coffee = new Product
            {
               ProductName = "Colombian Coffee",
               UnitPrice = 2.15M
            };
            var customer = new Customer { ContactName = "Karen Marlowe" };
            var order1 = new Order { OrderDate = DateTime.Parse("10/06/13") };
            order1.OrderDetails.Add(new OrderDetail
            {
               Product = tea,
               Quantity = 4,
               UnitPrice = 1.00M
            });
            order1.OrderDetails.Add(new OrderDetail
            {
               Product = coffee,
               Quantity = 3,
               UnitPrice = 2.15M
            });
            customer.Orders.Add(order1);
            context.Customers.Add(customer);
            context.SaveChanges();
         }
 
         using (var context = new EFRecipesEntities())
         {
            var query = context.Customers.Include("Orders.OrderDetails.Product");
            foreach (var customer in query)
            {
               Console.WriteLine("Orders for {0}", customer.ContactName);
               foreach (var order in customer.Orders)
               {
                  Console.WriteLine("--Order Date: {0}--",
                            order.OrderDate.ToShortDateString());
                  foreach (var detail in order.OrderDetails)
                  {
                     Console.WriteLine(
                        " {0}, {1} units at {2} each, unit discount: {3}",
                        detail.Product.ProductName,
                        detail.Quantity.ToString(),
                        detail.UnitPrice.ToString("C"),
                        (detail.Product.UnitPrice - detail.UnitPrice).ToString("C"));
                  }
               }
            }
         }

The following is the output of the code in Listing 8-3:

Orders for Karen Marlowe
--Order Date: 4/19/2010--
        Green Tea, 4 units at $1.00 each, unit discount: $0.09
        Colombian Coffee, 3 units at $2.15 each, unit discount: $0.00

How It Works

The POCO class generation is the default feature of current version of Entity Framework. Code generation property value is already set to None. The DbContext class is also generated separately, so no data access code is plugged into the POCO classes.

All of the classes corresponding to each of the entities in our model are created. They are pretty simple and clean. Of course, without code generation, no DbContext is generated. To implement a DbContext that is specific to our model and our entities, a new class derived from DbContext is created while generating the Entity Data Model, and this class provides properties of type DbSet<T> corresponding to each of the Db sets in our context. By default, our EFRecipesEntities DbContext has the constructor code that enables it to be connected to the underlying database.

8-2. Loading Related Entities with POCO

Problem

Using POCO, you want to eagerly load related entities.

Solution

Suppose that you have a model like the one in Figure 8-3.

9781430257882_Fig08-03.jpg

Figure 8-3. A model representing venues, their events, and the competitors in the events

We’re using POCO for our entities, and we want to eagerly load the related entities (navigation properties). To do this, we use the Include() method available on the object context. The code in Listing 8-4 illustrates using the Include() method to do this.

Listing 8-4.  Using the Include() Method Explicitly to Load Navigation Properties

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }
 
    static void RunExample()
    {
using (var context = new EFRecipesEntities())
         {
            var venue = new Venue { Name = "Sports and Recreational Grounds" };
            var event1 = new Event { Name = "Inter-school Soccer" };
            event1.Competitors.Add(new Competitor { Name = "St. Mary's School" });
            event1.Competitors.Add(new Competitor { Name = "City School" });
            venue.Events.Add(event1);
            context.Venues.Add(venue);
            context.SaveChanges();
         }
         using (var context = new EFRecipesEntities())
         {
            foreach (var venue in context.Venues.Include("Events").Include("Events.Competitors"))
            {
               Console.WriteLine("Venue: {0}", venue.Name);
               foreach (var evt in venue.Events)
               {
                  Console.WriteLine(" Event: {0}", evt.Name);
                  Console.WriteLine(" --- Competitors ---");
                  foreach (var competitor in evt.Competitors)
                  {
                     Console.WriteLine(" {0}", competitor.Name);
                  }
               }
            }
         }
    }
}
 
public partial class Venue
    {
        public Venue()
        {
            this.Events = new HashSet<Event>();
        }

        public int VenueId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Event> Events { get; set; }
    }
public partial class Event
    {
        public Event()
        {
            this.Competitors = new HashSet<Competitor>();
        }

        public int EventId { get; set; }
        public string Name { get; set; }
        public int VenueId { get; set; }

        public virtual ICollection<Competitor> Competitors { get; set; }
        public virtual Venue Venue { get; set; }
    }
public partial class Competitor
    {
        public int CompetitorId { get; set; }
        public string Name { get; set; }
        public int EventId { get; set; }

        public virtual Event Event { get; set; }
    }
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
            this.Configuration.LazyLoadingEnabled = false;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public DbSet<Competitor> Competitors { get; set; }
        public DbSet<Event> Events { get; set; }
        public DbSet<Venue> Venues { get; set; }
    }

The following is the output of the code in Listing 8-4:

Venue: City Center Hall
        Event: All Star Boxing
        --- Competitors ---
        Big Joe Green
        Terminator Tim
Venue: Sports and Recreational Grounds
        Event: Inter-school Soccer
        --- Competitors ---
        St. Mary's School
        City School

How It Works

When we’re using code generated by Entity Framework for our model, we use the Include() method on the context query objects to load the related entities, and these related entities can be lists of entities or single objects. There are three methods of loading or querying related entities in Entity Framework: Eager Loading, Lazy Loading, and Explicit Loading. We have used the Include() method to demonstrate eagerly loading related entities. By default, Lazy Loading is enabled in Entity Framework, but we have disabled that here. To load a navigation property explicitly when using POCO, you need to use the Include() method exposed on the DbContext.

8-3. Lazy Loading with POCO

Problem

You are using Plain Old CLR Objects, and you want to lazy load related entities.

Solution

Let’s say that you have a model like the one in Figure 8-4.

9781430257882_Fig08-04.jpg

Figure 8-4. A simple model for traffic tickets, the offending vehicles, and the details of the violation

To enable lazy loading, you don’t need to do anything. Lazy loading is enabled by default when an Entity Data Model is added into a Visual Studio project. The code in Listing 8-5 illustrates this approach.

Listing 8-5.  Entity Classes Generation and Properties Set to Virtual: A Default Behavior of Entity Framework

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }
 
    static void RunExample()
    {
        using (var context = new EFRecipesEntities())
         {
            var vh1 = new Vehicle { LicenseNo = "BR-549" };
            var t1 = new Ticket { IssueDate = DateTime.Parse("06/10/13") };
            var v1 = new Violation
            {
               Description = "20 MPH over the speed limit",
               Amount = 125M
            };
            var v2 = new Violation
            {
               Description = "Broken tail light",
               Amount = 50M
            };
            t1.Violations.Add(v1);
            t1.Violations.Add(v2);
            t1.Vehicle = vh1;
            context.Tickets.Add(t1);
            var vh2 = new Vehicle { LicenseNo = "XJY-902" };
            var t2 = new Ticket { IssueDate = DateTime.Parse("06/12/13") };
            var v3 = new Violation
            {
               Description = "Parking in a no parking zone",
               Amount = 35M
            };
            t2.Violations.Add(v3);
            t2.Vehicle = vh2;
            context.Tickets.Add(t2);
            context.SaveChanges();
         }
         using (var context = new EFRecipesEntities())
         {
            foreach (var ticket in context.Tickets)
            {
               Console.WriteLine(" Ticket: {0}, Total Cost: {1}",
                 ticket.TicketId.ToString(),
                 ticket.Violations.Sum(v => v.Amount).ToString("C"));
               foreach (var violation in ticket.Violations)
               {
                  Console.WriteLine(" {0}", violation.Description);
               }
            }
         }
 
    }
}
 
public partial class Ticket
    {
        public Ticket()
        {
            this.Violations = new HashSet<Violation>();
        }

        public int TicketId { get; set; }
        public int VehicleId { get; set; }
        public System.DateTime IssueDate { get; set; }

        public virtual Vehicle Vehicle { get; set; }
        public virtual ICollection<Violation> Violations { get; set; }
    }
public partial class Vehicle
    {
        public Vehicle()
        {
            this.Tickets = new HashSet<Ticket>();
        }

        public int VehicleId { get; set; }
        public string LicenseNo { get; set; }

        public virtual ICollection<Ticket> Tickets { get; set; }
    }
public partial class Violation
    {
        public int ViolationId { get; set; }
        public string Description { get; set; }
        public decimal Amount { get; set; }
        public int TicketId { get; set; }

        public virtual Ticket Ticket { get; set; }
    }
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public DbSet<Ticket> Tickets { get; set; }
        public DbSet<Vehicle> Vehicles { get; set; }
        public DbSet<Violation> Violations { get; set; }

The following is the output of the code in Listing 8-5:

Ticket: 1, Total Cost: $175.00
        20 MPH over the speed limit
        Broken tail light
Ticket: 2, Total Cost: $35.00
        Parking in a no parking zone

How It Works

Lazy loading is the default setting when generating an Entity Data Model. The navigation entity properties are also marked as virtual by default. You don’t need to do anything explicitly to get this to work.

We have not done anything in the console program code to load the Violation object, which is related to the Ticket object when the ticket context is fetched. Lazy loading enables the access of related entity properties at the moment you access them in your code. It does not require you to query those properties at the time of the first loading of the context object of the main entity, as we did using Include() method in the previous recipe.

8-4. POCO with Complex Type Properties

Problem

You want to use a complex type in your POCO entity.

Solution

Suppose that your model looks like the one in Figure 8-5. In this model, the Name property is a complex type.

9781430257882_Fig08-05.jpg

Figure 8-5. A model for an employee. The Name property is a complex type, composed of FirstName and LastName

Complex types are supported with POCO. When we refactor two or more entity properties to a new complex type, a new class is generated by default for that complex type. A property of the complex type class is also added into the main entity POCO class. Only classes are supported, as Entity Framework generates these while saving new complex types. The code in Listing 8-6 illustrates using the Name class for the complex type representing the employee’s FirstName and LastName.

Listing 8-6.  Using a Complex Type with POCO

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }
 
static void RunExample()
      {
         using (var context = new EFRecipesEntities())
         {
            context.Employees.Add(new Employee
            {
               Name = new Name
               {
                  FirstName = "Annie",
                  LastName = "Oakley"
               },
               Email = "[email protected]"
            });
            context.Employees.Add(new Employee
            {
               Name = new Name
               {
                  FirstName = "Bill",
                  LastName = "Jordan"
               },
               Email = "[email protected]"
            });
            context.SaveChanges();
         }
 
         using (var context = new EFRecipesEntities())
         {
            foreach (var employee in
                   context.Employees.OrderBy(e => e.Name.LastName))
            {
               Console.WriteLine("{0}, {1} email: {2}",
                              employee.Name.LastName,
                              employee.Name.FirstName,
                              employee.Email);
            }
         }
      }}
 
public partial class Employee
    {
        public Employee()
        {
            this.Name = new Name();
        }

        public int EmployeeId { get; set; }
        public string Email { get; set; }

        public Name Name { get; set; }
    }
public partial class Name
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public DbSet<Employee> Employees { get; set; }
    }

The following is the output of the code in Listing 8-6:

Jordan, Bill email:[email protected]
Oakley, Annie email:[email protected]

How It Works

When you use complex types with POCO, keep in mind the following two rules:

  • The complex type must be a class.
  • Inheritance cannot be used with complex type classes.

In Entity Framework, complex types do not leverage change tracking. Changes to complex types will not be reflected in change tracking. This means that if you mark the properties on a complex type as virtual, there is no change-tracking proxy support. All change tracking is snapshot-based.

When you delete or update a POCO entity with a complex type without first loading it from the database, you need to be careful to create an instance of the complex type. In Entity Framework, instances of complex types are structurally part of the entity, and null values are not supported. The code in Listing 8-7 illustrates one way to handle deletes.

Listing 8-7.  Deleting a POCO Entity with a Complex Type

int id = 0;
         using (var context = new EFRecipesEntities())
         {
            var emp = context.Employees.Where(e =>
                     e.Name.FirstName.StartsWith("Bill")).FirstOrDefault();
            id = emp.EmployeeId;
         }
 
         using (var context = new EFRecipesEntities())
         {
            var empDelete = new Employee
            {
               EmployeeId = id,

            };
            context.Employees.Attach(empDelete);
            context.Employees.Remove(empDelete);
            context.SaveChanges();
         }

In Listing 8-7, we first have to find the EmployeeId of Bill Jordan. Because we are trying to show how we would delete Bill without first loading the entity into the context, we create a new context to illustrate deleting Bill given just his EmployeeId. We need to create an instance of the Employee entity complete with the Name type. Because we are deleting, it doesn’t matter much what values we put in for FirstName and LastName. The key is that the Name property is not null. We satisfy this requirement by assigning a new (dummy) instance of Name. We then Attach() the entity and call Remove() and SaveChanges(). This deletes the entity.

8-5. Notifying Entity Framework About Object Changes

Problem

You are using POCO, and you want to have Entity Framework and the object state manager notified of changes to your objects.

Solution

Let’s say that you have a model like the one in Figure 8-6.

9781430257882_Fig08-06.jpg

Figure 8-6. A model for donors and their donations

This model represents donations and donors. Because some donations are anonymous, the relationship between donor and donation is 0..1 to *.

We want to make changes to our entities, such as moving a donation from one donor to another, and have Entity Framework and the object state manager notified of these changes. In addition, we want Entity Framework to leverage this notification to fix up any relationships that are affected by such changes. In our case, if we change the Donor on a Donation, we want Entity Framework to fix up both sides of the relationship. The code in Listing 8-8 demonstrates how to do this.

The key part of Listing 8-8 is that we marked each property as virtual and each collection a type of ICollection<T>. This allows Entity Framework to create proxies for our POCO entities that enable change tracking. When creating instances of POCO entity types, Entity Framework often creates instances of a dynamically generated derived type that acts as a proxy for the entity. This proxy overrides some virtual properties of the entity that inserts hooks for performing actions automatically when the property is accessed. This mechanism is used to support lazy loading of relationships and change tracking of objects. Note that Entity Framework will not create proxies for types where there is nothing for the proxy to do. This means that you can also avoid proxies by having types that are sealed and/or have no virtual properties.

Listing 8-8.  By Marking Each Property as virtual and Each Collection a Type of ICollection<T>, We Get Proxies That Enable Change Tracking

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }
 

static void RunExample()
      {
         using (var context = new EFRecipesEntities())
         {
            var donation = context.Donations.Create();
            donation.Amount = 5000M;
 
            var donor1 = context.Donors.Create();
            donor1.Name = "Jill Rosenberg";
            var donor2 = context.Donors.Create();
            donor2.Name = "Robert Hewitt";
 
            // give Jill the credit for the donation and save
            donor1.Donations.Add(donation);
            context.Donors.Add(donor1);
            context.Donors.Add(donor2);
            context.SaveChanges();
 
            // now give Robert the credit
            donation.Donor = donor2;
 
            // report
            foreach (var donor in context.Donors)
            {
               Console.WriteLine("{0} has given {1} donation(s)", donor.Name,
                              donor.Donations.Count().ToString());
            }
            Console.WriteLine("Original Donor Id: {0}",
               context.Entry(donation).OriginalValues["DonorId"]);
            Console.WriteLine("Current Donor Id: {0}",
                           context.Entry(donation).CurrentValues["DonorId"]);
         }
      }}
 
public partial class Donor
    {
        public Donor()
        {
            this.Donations = new HashSet<Donation>();
        }

        public virtual int DonorId { get; set; }
        public virtual string Name { get; set; }

        public virtual ICollection<Donation> Donations { get; set; }
    }
 
public partial class Donation
    {
        public virtual int DonationId { get; set; }
        public virtual Nullable<int> DonorId { get; set; }
        public virtual decimal Amount { get; set; }

        public virtual Donor Donor { get; set; }
    }
 
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public DbSet<Donation> Donations { get; set; }
        public DbSet<Donor> Donors { get; set; }
    }

The following is the output of the code in Listing 8-8:

Jill Rosenberg has given 0 donation(s)
Robert Hewitt has given 1 donation(s)
Original Donor Id: 1
Current Donor Id: 2

How It Works

By default, Entity Framework uses a snapshot-based approach for detecting changes made to POCO entities. If you make some minor code changes to your POCO entities, Entity Framework can create change-tracking proxies that keep the DbContext synchronized with the runtime changes in your POCO entities.

There are two important benefits that come with change-tracking proxies. First, the DbContext stays informed of the changes, and it can keep the entity object graph state information synchronized with your POCO entities. This means that no time need be spent detecting changes using the snapshot-based approach.

Additionally, when the DbContext is notified of changes on one side of a relationship, it can mirror the change on the other side of the relationship if necessary. In Listing 8-8, when we moved a Donation from one Donor to another, Entity Framework also fixed up the Donations collections of both Donors.

For the Entity Framework to create the change-tracking proxies for your POCO classes, the following conditions must be met.

  • The class must be public, nonabstract, and nonsealed.
  • The class must implement virtual getters and setters for all properties that are persisted.
  • You must declare collection-based relationships navigation properties as ICollection<T>. They cannot be a concrete implementation or another interface that derives from ICollection<T>.

Once your POCO classes have met these requirements, Entity Framework will return instances of the proxies for your POCO classes. If you need to create instances, as we have in Listing 8-8, you will need to use the Create() method on the DbContext. This method creates the instance of the proxy for your POCO entity, and it initializes all of the collections as instances of EntityCollection. It is this initialization of your POCO class’s collections as instances of EntityCollection that enables fixing up relationships.

8-6. Retrieving the Original (POCO) Object

Problem

You are using POCO, and you want to retrieve the original object from a database.

Solution

Let’s say that you are using a model like the one in Figure 8-7, and you are working in a disconnected scenario. You want to use a Where clause with FirstOrDefault() to retrieve the original object from the database before you apply changes received from a client.

9781430257882_Fig08-07.jpg

Figure 8-7. A model with a single Item entity

To update the entity with new values after retrieving the entity and then to apply changes to save in database, follow the pattern in Listing 8-9.

Listing 8-9.  Retrieving the Newly Added Entity and Replacing Its Values Using the Entry() Method

class Program
   {
      static void Main(string[] args)
      {
         RunExample();
      }
 
      static void RunExample()
      {
         int itemId = 0;
         using (var context = new EFRecipesEntities())
         {
            var item = new Item
            {
               Name = "Xcel Camping Tent",
               UnitPrice = 99.95M
            };
            context.Items.Add(item);
            context.SaveChanges();
 
            // keep the item id for the next step
            itemId = item.ItemId;
            Console.WriteLine("Item: {0}, UnitPrice: {1}",
                   item.Name, item.UnitPrice.ToString("C"));
         }
 
         using (var context = new EFRecipesEntities())
         {
            // pretend this is the updated
            // item we received with the new price
            var item = new Item
            {
               ItemId = itemId,
               Name = "Xcel Camping Tent",
               UnitPrice = 129.95M
            };
            var originalItem = context.Items.Where(x => x.ItemId ==  itemId).FirstOrDefault<Item>();
            context.Entry(originalItem).CurrentValues.SetValues(item);
            context.SaveChanges();
         }
         using (var context = new EFRecipesEntities())
         {
            var item = context.Items.Single();
            Console.WriteLine("Item: {0}, UnitPrice: {1}", item.Name,
                           item.UnitPrice.ToString("C"));
         }
      }
   }
public partial class Item
    {
        public int ItemId { get; set; }
        public string Name { get; set; }
        public decimal UnitPrice { get; set; }
    }
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }
        public DbSet<Item> Items { get; set; }
    }

The following is the output of the code in Listing 8-9:

Item: Xcel Camping Tent, UnitPrice: $99.95
Item: Xcel Camping Tent, UnitPrice: $129.95

How It Works

In Listing 8-9, we inserted an item into the model and saved it to the database. Then we pretended to receive an updated item, perhaps from a Silverlight client.

Next we need to update the item in the database. To do this, we need to get the entity from the database into the context. To get the entity, we used a Where clause with FirstorDefault and checked with the ID of the item. After that, we used the Entry() method of the context, which enables access to entire entity to apply any methods on that entity. Thus we used CurrentValues.SetValues to replace the original values with new values that come through the client. Finally, SaveChanges is called on the DbContext.

8-7. Manually Synchronizing the Object Graph and the Change Tracker

Problem

You want to control manually the synchronization between your POCO classes and the Change Tracker.

Change Tracker has access to the information that Entity Framework is storing about the entities that it is tracking. This information goes beyond the values stored in the properties of your entities and includes the current state of the entity, the original values from the database, which properties have been modified, and other data. The Change Tracker also gives access to additional operations that can be performed on an entity, such as reloading its values from the database to ensure that you have the latest data.

There are two different ways that Entity Framework can track changes to your objects: snapshot change tracking and change-tracking proxies.

Snapshot Change Tracking

POCO classes don’t contain any logic to notify Entity Framework when a property value is changed. Because there is no way to be notified when a property value changes, Entity Framework will take a snapshot of the values in each property when it first sees an object and store the values in memory. This snapshot occurs when the object is returned from a query or when we add it to a DbSet. When Entity Framework needs to know what changes have been made, it will scan each object and compare its current values to the snapshot. This process of scanning each object is triggered through a method of Change Tracker called DetectChanges.

Change-Tracking Proxies

The other mechanism for tracking changes is through change-tracking proxies, which allow Entity Framework to be notified of changes as they are made. Change-tracking proxies are created using the mechanism of dynamic proxies that are created for lazy loading, but in addition to providing for lazy loading, they also have the ability to communicate changes to the context. To use change-tracking proxies, you need to structure your classes in such a way that Entity Framework can create a dynamic type at runtime that derives from your POCO class and overrides every property. This dynamic type, known as a dynamic proxy, includes logic in the overridden properties to notify Entity Framework when those properties are changed.

Snapshot change tracking depends on Entity Framework being able to detect when changes occur. The default behavior of the DbContext API is to perform this detection automatically as the result of many events on the DbContext. DetectChanges not only updates the context’s state management information so that changes can be persisted to the database, but it also performs relationship fix-up when you have a combination of reference navigation properties, collection navigation properties, and foreign keys. It’s important to have a clear understanding of how and when changes are detected, what to expect from them, and how to control them.

The most obvious time that Entity Framework needs to know about changes is during SaveChanges, but there are many others. For example, if we ask the Change Tracker for the current state of an object, it will need to scan and check if anything has changed. Scanning isn’t just restricted to the object in question either—many of the operations you perform on the DbContext API will cause DetectChanges to be run. In most cases, DetectChanges is fast enough that it doesn’t cause performance issues. However, if you have a very large number of objects in memory or you are performing a lot of operations on DbContext in quick succession, the automatic DetectChanges behavior may be a performance concern. Fortunately, you have the option of switching off the automatic DetectChanges behavior and calling it manually when you know that it needs to be called. Failure to do this can result in unexpected side effects. DbContext takes care of this requirement for you, provided that you leave automatic DetectChanges enabled. If you switch it off, you are responsible for calling DetectChanges for poorly performing sections of code and to reenable it once the section in question has finished executing. Automatic DetectChanges can be toggled on and off via the DbContext.Configuration.AutoDetectChangesEnabled Boolean flag.

Solution

Suppose that we have a model for speakers and the talks prepared for various conferences. The model might look something like the one in Figure 8-8.

9781430257882_Fig08-08.jpg

Figure 8-8. A model with a many-to-many association between speakers and the talks they prepare

The first thing to note in our model is that Speaker and Talk are in a many-to-many association. We have, through an independent association (and in an intermediate SpeakerTalk table in the database), a model that supports many speakers for any given talk and many talks for any given speaker.

We want to control manually the synchronization between our object graph and the Change Tracker. We will do this by calling the DetectChanges() method. Along the way, we’ll illustrate how the synchronization is progressing.

Follow the pattern in Listing 8-10 to synchronize manually your POCO object graph with the Change Tracker.

Listing 8-10.  Using DetectChanges() Explicitly When Required to Synchronize the Change Tracker Manually

class Program
{
    static void Main(string[] args)
    {
        RunExample();
    }
 

static void RunExample()
      {
         using (var context = new EFRecipesEntities())
         {
            context.Configuration.AutoDetectChangesEnabled = false;
            var speaker1 = new Speaker { Name = "Karen Stanfield" };
            var talk1 = new Talk { Title = "Simulated Annealing in C#" };
            speaker1.Talks = new List<Talk> { talk1 };
 
            // associations not yet complete
            Console.WriteLine("talk1.Speaker is null: {0}",
                           talk1.Speakers == null);
 
            context.Speakers.Add(speaker1);
 
            // now it's fixed up
            Console.WriteLine("talk1.Speaker is null: {0}",
                           talk1.Speakers == null);
            Console.WriteLine("Number of added entries tracked: {0}",
                           context.ChangeTracker.Entries().Where(e => e.State == System.Data.Entity.EntityState.Added).Count());
            context.SaveChanges();
            // change the talk's title
            talk1.Title = "AI with C# in 3 Easy Steps";
            Console.WriteLine("talk1's state is: {0}",
                           context.Entry(talk1).State);
            context.ChangeTracker.DetectChanges();
            Console.WriteLine("talk1's state is: {0}",
                           context.Entry(talk1).State);
            context.SaveChanges();
         }
 
         using (var context = new EFRecipesEntities())
         {
            foreach (var speaker in context.Speakers.Include("Talks"))
            {
               Console.WriteLine("Speaker: {0}", speaker.Name);
               foreach (var talk in speaker.Talks)
               {
                  Console.WriteLine(" Talk Title: {0}", talk.Title);
               }
            }
         }
      }
}
 
public partial class Speaker
    {
        public int SpeakerId { get; set; }
        public string Name { get; set; }
        public ICollection<Talk> Talks { get; set; }
    }
 
public partial class Talk
    {
        public int TalkId { get; set; }
        public string Title { get; set; }
        public System.DateTime CreateDate { get; set; }
        public System.DateTime RevisedDate { get; set; }
        public ICollection<Speaker> Speakers { get; set; }
    }
 
public partial class EFRecipesEntities : DbContext
    {
        public EFRecipesEntities()
            : base("name=EFRecipesEntities")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            throw new UnintentionalCodeFirstException();
        }

        public DbSet<Speaker> Speakers { get; set; }
        public DbSet<Talk> Talks { get; set; }
 
      public override int SaveChanges()
      {
         var changeSet = this.ChangeTracker.Entries().Where(e => e.Entity is Talk);
         if (changeSet != null)
         {
            foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Added).Select(a => a.Entity as Talk))
            {
               entry.CreateDate = DateTime.UtcNow;
               entry.RevisedDate = DateTime.UtcNow;
            }
            foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Modified).Select(a => a.Entity as Talk))
            {
               entry.RevisedDate = DateTime.UtcNow;
            }
         }
         return base.SaveChanges();
      }
   }

The following is the output of the code in Listing 8-10:

talk1.Speaker is null: True
talk1.Speaker is null: False
Number of added entries tracked: 2
talk1's state is: Unchanged
talk1's state is: Modified
Speaker: Karen Stanfield
        Talk Title: AI with C# in 3 Easy Steps

How It Works

The code in Listing 8-10 is a little involved, so let’s take it one step at a time. First off, we create a speaker and a talk. Then we add the talk to the speaker’s collection. At this point, the talk is part of the speaker’s collection, but the speaker is not part of the talk’s collection. The other side of the association has not been fixed up just yet.

Next we add the speaker to the DbContext with Add(speaker1). The second line of the output shows now that the talk’s speaker collection is correct. Entity Framework has fixed up the other side of the association. Here Entity Framework did two things. It notified the object state manager that there are three entries to be created, although it is not shown in the result of number of entities added by Entity Framework, as it considers many-to-many relationships as independent relationships and not as separate entities. Thus it is only showing the entries as two: one of these entries is for the speaker and the other is for the talk. No entry is made for the many-to-many association entry, because Change Tracker does not return the state of independent relationships. The second thing that Entity Framework did was to fix up the talk’s speaker collection.

When we call SaveChanges(), Entity Framework raises the overridden SaveChanges event. Inside this event, we update the CreateDate and RevisedDate properties. Before the SaveChanges() method is called, Entity Framework calls DetectChanges() to find any changes that occurred before. In Listing 8-10, we override the SaveChanges() method.

The DetectChanges() method relies on a snapshot base comparison of the original and current values for each property on each entity. This process determines what has changed in the object graph. For large object graphs, this comparison process may be time consuming.

8-8. Testing Domain Objects

Problem

You want to create unit tests for the business rules you have defined for your entities.

This type of recipe is often used when unit testing of specific data access functionality has to be performed.

Solution

For this solution, you’ll use the POCO template to generate the classes for your entities. Using the POCO template will reduce the amount of code you need to write, and it will make the solution a more clear. Of course, you will use the remaining steps in this solution with your handcrafted POCO classes.

Suppose you have a model like the one shown in Figure 8-9.

9781430257882_Fig08-09.jpg

Figure 8-9. A model of reservations, schedules, and trains

This model represents reservations for train travel. Each reservation is for a particular scheduled train departure. To create the model and prepare the application for unit testing, do the following:

  1. Create an empty solution. Right-click the solution in the Solution Explorer, and select Add arrow.jpg New Project. Add a new Class Library project. Name this new project TrainReservation.
  2. Right-click the TrainReservation project, and select Add arrow.jpg New Item. Add a new ADO.NET Entity Data Model. Import the Train, Schedule, and Reservation tables. The resulting model should look like the one in Figure 8-9.
  3. Add the IValidate interface and ChangeAction enum in Listing 8-11 to the project.

    Listing 8-11.  The IValidate Interface

    public enum ChangeAction
    {
        Insert,
        Update,
        Delete
    }
     
    interface IValidate
    {
        void Validate(ChangeAction action);
    }
  4. Add the code in Listing 8-12 to the project. This code adds the validation code (the implementation of IValidate) to the Reservation and Schedule classes.

    Listing 8-12.  Implementation of the IValidate Interface for the Reservation and Schedule Classes

    public partial class Reservation : IValidate
    {
        public void Validate(ChangeAction action)
        {
            if (action == ChangeAction.Insert)
            {
                if (Schedule.Reservations.Count(r =>
                              r.ReservationId != ReservationId &&
                              r.Passenger == this.Passenger) > 0)
                    throw new InvalidOperationException(
                              "Reservation for the passenger already exists");
            }
        }
    }
     
    public partial class Schedule : IValidate
    {
        public void Validate(ChangeAction action)
        {
            if (action == ChangeAction.Insert)
            {
                if (ArrivalDate < DepartureDate)
                {
                    throw new InvalidOperationException(
                              "Arrival date cannot be before departure date");
                }
     
                if (LeavesFrom == ArrivesAt)
                {
                    throw new InvalidOperationException(
                              "Can't leave from and arrive at the same location");
                }
            }
        }
    }
  5. Override the SaveChanges() method in the DbContext with the code in Listing 8-13. This will allow you to validate the changes before they are saved to the database.

    Listing 8-13.  Overriding the SaveChanges() Method

    public partial class EFRecipesEntities
    {

    public override int SaveChanges()
          {
             this.ChangeTracker.DetectChanges();
             var entries = from e in this.ChangeTracker.Entries().Where(e => e.State == (System.Data.Entity.EntityState.Added | EntityState.Modified | EntityState.Deleted))
                        where (e.Entity != null) &&
                            (e.Entity is IValidate)
                        select e;
             foreach (var entry in entries)
             {
                switch (entry.State)
                {
                   case EntityState.Added:
                      ((IValidate)entry.Entity).Validate(ChangeAction.Insert);
                      break;
                   case EntityState.Modified:
                      ((IValidate)entry.Entity).Validate(ChangeAction.Update);
                      break;
                   case EntityState.Deleted:
                      ((IValidate)entry.Entity).Validate(ChangeAction.Delete);
                      break;
                }
             }
             return base.SaveChanges();
          }
    }
  6. Create the IReservationContext interface in Listing 8-14. We’ll use this interface to help us test against a fake DbContext so that changes are not saved to the real database.

    Listing 8-14.  Use this IReservationContext to Define the Methods You’ll Need from the DbContext

    public interface IReservationContext : IDisposable
    {
        IDbSet<Train> Trains { get; }
        IDbSet<Schedule> Schedules { get; }
        IDbSet<Reservation> Reservations { get; }

        int SaveChanges();
    }
  7. The POCO template generates both the POCO classes and the class that implements the object context. We’ll need this object context class to implement the IReservationContext interface. To do this, edit the Recipe8.Context.tt template file and add IReservationContext at the end of the line that generates the name of the object context class. The complete line should look like the following:
    <#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> :
     DbContext,IReservationContext
  8. Create the repository class in Listing 8-15. This class takes an IReservationContext in the constructor.

    Listing 8-15.  The ReservationRepository Class That Takes an IReservationContext in the Constructor

    public class ReservationRepository: IDisposable
       {
          private IReservationContext _context;
     
          public ReservationRepository(IReservationContext context)
          {
             if (context == null)
                throw new ArgumentNullException("context is null");
             _context = context;
          }
          public void AddTrain(Train train)
          {
             _context.Trains.Add(train);
          }
     
          public void AddSchedule(Schedule schedule)
          {
             _context.Schedules.Add(schedule);
          }
     
          public void AddReservation(Reservation reservation)
          {
             _context.Reservations.Add(reservation);
          }
     
          public void SaveChanegs()
          {
             _context.SaveChanges();
          }
     
          public List<Schedule> GetActiveSchedulesForTrain(int trainId)
          {
             var schedules = from r in _context.Schedules
                         where r.ArrivalDate.Date >= DateTime.Today &&
                              r.TrainId == trainId
                         select r;
             return schedules.ToList();
          }
       }
  9. Right-click the solution, and select Add arrow.jpg New Project. Add a Test Project to the solution. Name this new project Tests. Add a reference to System.Data.Entity.
  10. Create a fake object set and fake DbContext so that you can test your business rules in isolation without interacting with the database. Use the code in Listing 8-16.

    Listing 8-16.  The Implementation of the Fake Object Set and Fake Object Context

    public class FakeDbSet<T> : IDbSet<T>
        where T : class
    {
        HashSet<T> _data;
        IQueryable _query;
     
        public FakeDbSet()
        {
            _data = new HashSet<T>();
            _query = _data.AsQueryable();
        }
     
        public virtual T Find(params object[] keyValues)
        {
            throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
        }
     
        public void Add(T item)
        {
            _data.Add(item);
        }
     
        public void Remove(T item)
        {
            _data.Remove(item);
        }
     
        public void Attach(T item)
        {
            _data.Add(item);
        }
        public void Detach(T item)
        {
            _data.Remove(item);
        }
        Type IQueryable.ElementType
        {
            get { return _query.ElementType; }
        }
        System.Linq.Expressions.Expression IQueryable.Expression
        {
            get { return _query.Expression; }
        }
     
        IQueryProvider IQueryable.Provider
        {
            get { return _query.Provider; }
        }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return _data.GetEnumerator();
        }
    }
     
    public class FakeReservationContext : IReservationContext, IDisposable
       {
          private IDbSet<Train> trains;
          private IDbSet<Schedule> schedules;
          private IDbSet<Reservation> reservations;
          public FakeReservationContext()
          {
             trains = new FakeDbSet<Train>();
             schedules = new FakeDbSet<Schedule>();
             reservations = new FakeDbSet<Reservation>();
          }
     
          public IDbSet<Train> Trains
          {
             get { return trains; }
          }
     
          public IDbSet<Schedule> Schedules
          {
             get { return schedules; }
          }
     
          public IDbSet<Reservation> Reservations
          {
             get { return reservations; }
          }
     
          public int SaveChanges()
          {
             foreach (var schedule in Schedules.Cast<IValidate>())
             {
                schedule.Validate(ChangeAction.Insert);
             }
             foreach (var reservation in Reservations.Cast<IValidate>())
             {
                reservation.Validate(ChangeAction.Insert);
             }
             return 1;
          }
          public void Dispose()
          {
          }
       }
  11. We don’t want to test against our real database, so we need to create a fake DbContext that simulates the DbContext with in-memory collections acting as our data store. Add the unit test code in Listing 8-17 to the Tests project.

    Listing 8-17.  The Unit Tests for Our Tests Project

    [TestClass]
    public class ReservationTest: IDisposable
    {
        private IReservationContext _context;
     
        [TestInitialize]
        public void TestSetup()
        {
            var train = new Train { TrainId = 1, TrainName = "Polar Express" };
            var schedule = new Schedule { ScheduleId = 1, Train = train,
                                          ArrivalDate = DateTime.Now,
                                          DepartureDate = DateTime.Today,
                                          LeavesFrom = "Dallas",
                                          ArrivesAt = "New York" };
            var reservation = new Reservation { ReservationId = 1,
                                                Passenger = "Phil Marlowe",
                                                Schedule = schedule };
            _context = new FakeReservationContext();
            var repository = new ReservationRepository(_context);
            repository.AddTrain(train);
            repository.AddSchedule(schedule);
            repository.AddReservation(reservation);
            repository.SaveChanges();
        }
     
        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void TestForDuplicateReservation()
        {
            var repository = new ReservationRepository(_context);
            var schedule = repository.GetActiveSchedulesForTrain(1).First();
            var reservation = new Reservation { ReservationId = 2,
                                                Schedule = schedule,
                                                Passenger = "Phil Marlowe" };
            repository.AddReservation(reservation);
            repository.SaveChanges();
        }
     
        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void TestForArrivalDateGreaterThanDepartureDate()
        {
            var repository = new ReservationRepository(_context);
            var schedule = new Schedule { ScheduleId = 2, TrainId = 1,
                                          ArrivalDate = DateTime.Today,
                                          DepartureDate = DateTime.Now,
                                          ArrivesAt = "New York",
                                          LeavesFrom = "Chicago" };
            repository.AddSchedule(schedule);
            repository.SaveChanges();
        }
     
        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void TestForArrivesAndLeavesFromSameLocation()
        {
            var repository = new ReservationRepository(_context);
            var schedule = new Schedule { ScheduleId = 3, TrainId = 1,
                                          ArrivalDate = DateTime.Now,
                                          DepartureDate = DateTime.Today,
                                          ArrivesAt = "Dallas",
                                          LeavesFrom = "Dallas" };
            repository.AddSchedule(schedule);
            repository.SaveChanges();
        }
    }

The Test project now has three unit tests that exercise the following business rules:

  • A passenger cannot have more than one reservation for a scheduled departure.
  • The arrival date and time for a schedule must be after the departure date and time.
  • The departure location cannot be the same as the arrival location.

How It Works

With quite a lot of code, we’ve managed to build a complete solution that includes an interface (IReservationContext) that we can use to abstractly reference a DbContext, a fake DbSet (FakeDbSet<T>), a fake DbContext (FakeReservationContext), and a small set of unit tests. We use the fake DbContext so that our tests don’t interact with the database. The purpose of the tests is to validate our business rules, not to test the database interactions.

One key to the solution is that we created a simplified repository that managed the inserting and selecting of our objects. The constructor for this repository takes an IReservationContext. This subtle abstraction allows us to pass in an instance of any class that implements IReservationContext. To test our domain objects, we pass in an instance of FakeReservationContext. To allow our domain objects to be persisted to the database, we would pass in an instance of our real DbContext: EFRecipesEntities.

We need the DbSets returned by our fake DbContext to match the DbSets returned by the real EFRecipesEntities DbContext. To do this, we changed the T4 template that generates the context to return IDbSet<T> in place of DbSet<T>. We made sure our fake DbContext also returned DbSets of type IDbSet<T>. With this in place, we implemented our FakeDbSet<T> and derived it from IDbSet<T>.

In the Tests project, we set up the tests by creating a Reservation Repository based on an instance of the FakeReservationContext. The unit tests interact with the FakeReservationContext in place of the real DbContext.

Best Practice

There are two testing approaches that seem to work well for Entity Framework: Define a repository interface that both the real repository and one or more “testing” repositories implement. By hiding all of the interactions with the persistence framework behind the implementation of the repository interface, there is no need to create fake versions of any of the other infrastructure parts. This can simplify the implementation of the testing code, but it may leave parts of the repository itself untested.

Define an interface for the DbContext that exposes properties of type IDbSet<T> and a SaveChanges() method, as we have done in this recipe. The real DbContext and all of the fake DbContexts must implement this interface. Using this approach, you don’t need to fake the entire repository, which may be difficult in some cases. Your fake DbContexts don’t need to mimic the behavior of the entire DbContext class; that would be a real challenge. You do need to limit your code to just what is available on the interfaces.

8-9. Testing a Repository Against a Database

Problem

You want to test your repository against the database.

This type of recipe is often used when integration testing of whole-data access functionality has to be performed.

Solution

You have created a repository that manages all of the queries, inserts, updates, and deletes. You want to test this repository against a real instance of the underlying database. Suppose that you have a model like the one shown in Figure 8-10. Because we will create and drop the database during the tests, let’s start from the beginning in a test database.

9781430257882_Fig08-10.jpg

Figure 8-10. A model of books in categories

To test your repository, do the following:

  1. Create an empty solution. Right-click the solution in the Solution Explorer, and select Add arrow.jpg New Project. Add a new Class Library project. Name this new project BookRepository.
  2. Create a new database. Call the database Test. We’ll create and drop this database in the unit tests, so make sure you create a new empty database.
  3. Add the Book and Category tables along with the relation corresponding to the model in Figure 8-10. Import these tables into a new model. Alternatively, you can use Model First to create the model and then generate the database script to create the database.
  4. Add the code in Listing 8-18. This will create a BookRepository class that handles inserts and queries against the model.

    Listing 8-18.  The BookRepository Class That Handles Inserts and Queries Against the Model

    namespace BookRepository
    {
        public class BookRepository
       {
          private TestEntities _context;
     
          public BookRepository(TestEntities context)
          {
             _context = context;
          }
     
          public void InsertBook(Book book)
          {
             _context.Books.Add(book);
          }
     
          public void InsertCategory(Category category)
          {
             _context.Categories.Add(category);
          }
     
          public void SaveChanges()
          {
             _context.SaveChanges();
          }
     
          public IQueryable<Book> BooksByCategory(string name)
          {
             return _context.Books.Where(b => b.Category.Name == name);
          }
     
          public IQueryable<Book> BooksByYear(int year)
          {
             return _context.Books.Where(b => b.PublishDate.Year == year);
          }
       }
     
    }
  5. Right-click the solution, and select Add arrow.jpg New Project. Select Test Project from the installed templates. Add a reference to System.Data.Entity and a project reference to BookRepository.
  6. Right-click the Test project, and select Add arrow.jpg New Test. Add a Unit Test to the Test project. Add the code in Listing 8-19 to create the tests.

    Listing 8-19.  BookRepositoryTest Class with the Unit Tests

    [TestClass]
    public class BookRepositoryTest
    {
        private TestEntities _context;
     
        [ClassInitialize]
        public void TestSetup()
        {
            _context = new TestEntities();
            if (_context.DatabaseExists())
            {
                _context.DeleteDatabase();
            }
            _context.CreateDatabase();
        }
     
        [TestMethod]
        public void TestsBooksInCategory()
        {
            var repository = new BookRepository.BookRepository(_context);
            var construction = new Category { Name = "Construction" };
            var book = new Book { Title = "Building with Masonary",
                                  Author = "Dick Kreh",
                                  PublishDate = new DateTime(1998, 1, 1) };
            book.Category = construction;
            repository.InsertCategory(construction);
            repository.InsertBook(book);
            repository.SaveChanges();
     
            // test
            var books = repository.BooksByCategory("Construction");
            Assert.AreEqual(books.Count(), 1);
        }
     
        [TestMethod]
        public void TestBooksPublishedInTheYear()
        {
            var repository = new BookRepository.BookRepository(_context);
            var construction = new Category { Name = "Construction" };
            var book = new Book { Title = "Building with Masonary",
                                  Author = "Dick Kreh",
                                  PublishDate = new DateTime(1998, 1, 1) };
            book.Category = construction;
            repository.InsertCategory(construction);
            repository.InsertBook(book);
            repository.SaveChanges();
     
            // test
            var books = repository.BooksByYear(1998);
            Assert.AreEqual(books.Count(), 1);
        }
    }
  7. Right-click the Test project, and select Add arrow.jpg New Item. Select Application Configuration File from the General templates. Copy the <connectionStrings> section from the App.config file in the BookRepository project, and insert it into the new App.config file in the Test project.
  8. Right-click the Test project, and select Set as Startup Project. Select Debug arrow.jpg Start Debugging, or press F5 to execute the tests. Make sure that there are no active connections to the Test database. Active connections will cause the DropDatabase() method to fail.

How It Works

There are two common approaches to testing that are used with Entity Framework. The first approach is to test the business logic implemented in your objects. For this approach, you test against a “fake” database layer because your focus is on the business logic that governs the interactions of the objects and the rules that apply just before objects are persisted to the database. We illustrated this approach in Recipe 8-8.

A second approach is to test both the business logic and the persistence layer by interacting with the real database. This approach is more extensive, and also more costly in terms of time and resources. When it is implemented in an automated test harness, like the ones often used in a continuous integration environment, you need to automate the creation and dropping of the test database.

Each test iteration should start with a database in a known clean state. Subsequent test runs should not be affected by residue left in the database by previous tests. Dropping and creating databases together with the end-to-end code exercise requires more resources than the business logic only testing, as illustrated in Recipe 8-8.

In the unit tests in Listing 8-19, we checked to see whether the database exists in the Test Initialize phase. If the database exists, it is dropped with the DropDatabase() method. Next we create the database with the CreateDatabase() method. These methods use the connection string contained in the App.config file. This connection string would likely be different from the development database connection string. For simplicity, we used the same connection string for both.

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

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