Version 4.1 of the Entity Framework contains both the Code First approach and the new DbContext API. This API provides a more productive surface for working with the Entity Framework and can be used with the Code First, Database First, and Model First approaches. This is the seventh post of a twelve part series containing collections of patterns and code fragments showing how features of the new API can be used.
The posts in this series do not contain complete walkthroughs. If you haven’t used EF 4.1 before then you should read Part 1 of this series and also Code First Walkthrough or Model and Database First with DbContext before tackling this post.
The Local property of DbSet provides simple access to the entities of the set that are currently being tracked by the context and have not been marked as Deleted. Accessing the Local property never causes a query to be sent to the database. This means that it is usually used after a query has already been performed. The Load extension method can be used to execute a query so that the context tracks the results. For example:
using (var context = new UnicornsContext()) { // Load all unicorns from the database into the context context.Unicorns.Load(); // Add a new unicorn to the context context.Unicorns.Add(new Unicorn { Name = "Linqy" }); // Mark one of the existing unicorns as Deleted context.Unicorns.Remove(context.Unicorns.Find(1)); // Loop over the unicorns in the context. Console.WriteLine("In Local: "); foreach (var unicorn in context.Unicorns.Local) { Console.WriteLine("Found {0}: {1} with state {2}", unicorn.Id, unicorn.Name, context.Entry(unicorn).State); } // Perform a query against the database. Console.WriteLine("\nIn DbSet query: "); foreach (var unicorn in context.Unicorns) { Console.WriteLine("Found {0}: {1} with state {2}", unicorn.Id, unicorn.Name, context.Entry(unicorn).State); } }
Using the data set by the initializer defined in Part 1 of this series, running the code above will print out:
In Local: Found 0: Linqy with state Added Found 2: Silly with state Unchanged Found 3: Beepy with state Unchanged Found 4: Creepy with state Unchanged
In DbSet query: Found 1: Binky with state Deleted Found 2: Silly with state Unchanged Found 3: Beepy with state Unchanged Found 4: Creepy with state Unchanged
This illustrates three points:
The Local property on DbSet returns an ObservableCollection with events hooked up such that it stays in sync with the contents of the context. This means that entities can be added or removed from either the Local collection or the DbSet. It also means that queries that bring new entities into the context will result in the Local collection being updated with those entities. For example:
using (var context = new UnicornsContext()) { // Load some unicorns from the database into the context context.Unicorns.Where(u => u.Name.StartsWith("B")).Load(); // Get the local collection and make some changes to it var localUnicorns = context.Unicorns.Local; localUnicorns.Add(new Unicorn { Name = "Linqy" }); localUnicorns.Remove(context.Unicorns.Find(1)); // Loop over the unicorns in the context. Console.WriteLine("In Local: "); foreach (var unicorn in context.Unicorns.Local) { Console.WriteLine("Found {0}: {1} with state {2}", unicorn.Id, unicorn.Name, context.Entry(unicorn).State); } var unicorn1 = context.Unicorns.Find(1); Console.WriteLine("State of unicorn 1: {0} is {1}", unicorn1.Name, context.Entry(unicorn1).State); // Query some more unicorns from the database context.Unicorns.Where(u => u.Name.EndsWith("py")).Load(); // Loop over the unicorns in the context again. Console.WriteLine("\nIn Local after query: "); foreach (var unicorn in context.Unicorns.Local) { Console.WriteLine("Found {0}: {1} with state {2}", unicorn.Id, unicorn.Name, context.Entry(unicorn).State); } }
In Local: Found 3: Beepy with state Unchanged Found 0: Linqy with state Added State of unicorn 1: Binky is Deleted
In Local after query: Found 3: Beepy with state Unchanged Found 0: Linqy with state Added Found 4: Creepy with state Unchanged
The Local property on DbSet can be used directly for data binding in a WPF application because it is an instance of ObservableCollection. As described in the previous sections this means that it will automatically stay in sync with the contents of the context and the contents of the context will automatically stay in sync with it. Note that you do need to pre-populate the Local collection with data for there to be anything to bind to since Local never causes a database query.
This is not an appropriate post for a full WPF data binding sample but the key elements are:
We will put up a separate post on the ADO.NET team blog describing how to do this in detail.
If you are doing master/detail data binding you may want to bind the detail view to a navigation property of one of your entities. An easy way to make this work is to use an ObservableCollection for the navigation property. For example:
public class Princess { private readonly ObservableCollection<Unicorn> _unicorns = new ObservableCollection<Unicorn>(); public int Id { get; set; } public string Name { get; set; } public virtual ObservableCollection<Unicorn> Unicorns { get { return _unicorns; } } }
We will put up a separate post on the ADO.NET team blog describing how you would then use this class for WPF binding.
In most cases entities removed from a navigation property will not be automatically marked as deleted in the context. For example, if you remove a Unicorn object from the Princess.Unicorns collection then that unicorn will not be automatically deleted when SaveChanges is called. If you need it to be deleted then you may need to find these dangling entities and mark them as deleted before calling SaveChanges or as part of an overridden SaveChanges. For example:
public override int SaveChanges() { foreach (var unicorn in this.Unicorns.Local.ToList()) { if (unicorn.Princess == null) { this.Unicorns.Remove(unicorn); } } return base.SaveChanges(); }
The code above uses LINQ to Objects against the Local collection to find all unicorns and marks any that do not have a Princess reference as deleted. The ToList call is required because otherwise the collection will be modified by the Remove call while it is being enumerated. In most other situations you can do LINQ to Objects directly against the Local property without using ToList first.
Windows Forms does not support full fidelity data binding using ObservableCollection directly. However, you can still use the DbSet Local property for data binding to get all the benefits described in the previous sections. This is achieved through the ToBindingList extension method which creates an IBindingList implementation backed by the Local ObservableCollection.
This is not an appropriate post for a full Windows Forms data binding sample but the key elements are:
Many of the examples in this series use the Entry method to return a DbEntityEntry instance for an entity. This entry object then acts as the starting point for gathering information about the entity such as its current state, as well as for performing operations on the entity such as explicitly loading a related entity.
The Entries methods return DbEntityEntry objects for many or all entities being tracked by the context. This allows you to gather information or perform operations on many entities rather than just a single entry. For example:
using (var context = new UnicornsContext()) { // Load some entities into the context context.Unicorns.Include(u => u.Princess.LadiesInWaiting).Load(); // Make some changes context.Unicorns.Add(new Unicorn { Name = "Linqy" }); context.Unicorns.Remove(context.Unicorns.Find(1)); context.Princesses.Local.First().Name = "Belle"; context.LadiesInWaiting.Local.First().Title = "Special"; // Look at the state of all entities in the context Console.WriteLine("All tracked entities: "); foreach (var entry in context.ChangeTracker.Entries()) { Console.WriteLine("Found entity of type {0} with state {1}", ObjectContext.GetObjectType(entry.Entity.GetType()).Name, entry.State); } // Find modified entities of any type Console.WriteLine("\nAll modified entities: "); foreach (var entry in context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified)) { Console.WriteLine("Found entity of type {0} with state {1}", ObjectContext.GetObjectType(entry.Entity.GetType()).Name, entry.State); } // Get some information about just the tracked princesses Console.WriteLine("\nTracked princesses: "); foreach (var entry in context.ChangeTracker.Entries<Princess>()) { Console.WriteLine("Found Princess {0}: {1} with original Name {2}", entry.Entity.Id, entry.Entity.Name, entry.Property(p => p.Name).OriginalValue); } // Find any person (lady or princess) whose name starts with 'S' Console.WriteLine("\nPeople starting with 'S': "); foreach (var entry in context.ChangeTracker.Entries<IPerson>() .Where(p => p.Entity.Name.StartsWith("S"))) { Console.WriteLine("Found Person {0}", entry.Entity.Name); } }
All tracked entities: Found entity of type Unicorn with state Added Found entity of type Princess with state Modified Found entity of type LadyInWaiting with state Modified Found entity of type Unicorn with state Deleted Found entity of type Unicorn with state Unchanged Found entity of type Unicorn with state Unchanged Found entity of type Princess with state Unchanged Found entity of type LadyInWaiting with state Unchanged Found entity of type Unicorn with state Unchanged Found entity of type Princess with state Unchanged Found entity of type LadyInWaiting with state Unchanged
All modified entities: Found entity of type Princess with state Modified Found entity of type LadyInWaiting with state Modified
Tracked princesses: Found Princess 1: Belle with original Name Cinderella Found Princess 2: Sleeping Beauty with original Name Sleeping Beauty Found Princess 3: Snow White with original Name Snow White
People starting with 'S': Found Person Special Lettice Found Person Sleeping Beauty Found Person Snow White
These examples illustrate several points:
Note that DbEntityEntry instances always contain a non-null Entity. Relationship entries and stub entries are not represented as DbEntityEntry instances so there is no need to filter for these.
In this part of the series we looked at how the Local property of a DbSet can be used to look at the local collection of entities in that set and how it this local collection stays in sync with the context. We also touched on how Local can be used for data binding. Finally, we looked at the Entities methods which provide detailed information about tracked entities.
As always we would love to hear any feedback you have by commenting on this blog post.
For support please use the Entity Framework Forum.
Arthur Vickers Developer ADO.NET Entity Framework