Using DbContext in EF 4.1 Part 7: Local Data

 


The information in this post is out of date.

Visit msdn.com/data/ef for the latest information on current and past releases of EF.

For Local Data see https://msdn.com/data/jj592872


 

Introduction

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.

Using Local to look at local data

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 new unicorn Linqy is included in the Local collection even though it has not yet been saved to the database. Linqy has a primary key of zero because the database has not yet generated a real key for the entity.
  • The unicorn Binky is not included in the local collection even though it is still being tracked by the context. This is because we removed Binky from the DbSet thereby marking it as deleted.
  • When DbSet is used to perform a query the entity marked for deletion (Binky) is included in the results and the new entity (Linqy) that has not yet been saved to the database is not included in the results. This is because DbSet is performing a query against the database and the results returned always reflect what is in the database.

Using Local to add and remove entities from the context

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);
    }
}

Using the data set by the initializer defined in Part 1 of this series, running the code above will print out:

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

This illustrates three points:

  • The new unicorn Linqy that was added to the Local collection becomes tracked by the context in the Added state. It will therefore be inserted into the database when SaveChanges is called.

  • The unicorn that was removed from the Local collection (Binky) is now marked as deleted in the context. It will therefore be deleted from the database when SaveChanges is called.

  • The additional unicorn (Creepy) loaded into the context with the second query is automatically added to the Local collection.

    One final thing to note about Local is that because it is an ObservableCollection performance is not great for large numbers of entities. Therefore if you are dealing with thousands of entities in your context it may not be advisable to use Local.

Using Local for WPF data binding

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:

  • Setup a binding source
  • Bind it to the Local property of your set
  • Populate Local using a query to the database.

We will put up a separate post on the ADO.NET team blog describing how to do this in detail.

WPF binding to navigation properties

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.

Using Local to clean up entities in SaveChanges

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.

Using Local and ToBindingList for Windows Forms data binding

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:

  • Setup an object binding source
  • Bind it to the Local property of your set using Local.ToBindingList()
  • Populate Local using a query to the database

We will put up a separate post on the ADO.NET team blog describing how to do this in detail.

Getting detailed information about tracked entities

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);
    }
}

Using the data set by the initializer defined in Part 1 of this series, running the code above will print out:

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:

  • The Entries methods return entries for entities in all states, including Deleted. Compare this to Local which excludes Deleted entities.
  • Entries for all entity types are returned when the non-generic Entries method is used. When the generic entries method is used entries are only returned for entities that are instances of the generic type. This was used above to get entries for all princesses. It was also used to get entries for all entities that implement IPerson. This demonstrates that the generic type does not have to be an actual entity type.
  • LINQ to Objects can be used to filter the results returned. This was used above to find entities of any type as long as they are modified. It was also used to find IPeople with a Name property starting with ‘S’.

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.

Summary

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