Datos locales

La ejecución de una consulta LINQ directamente en un DbSet siempre enviará una consulta a la base de datos, pero puede acceder a los datos que están actualmente en memoria mediante la propiedad DbSet.Local. También puede acceder a la información adicional de la que EF realiza el seguimiento de las entidades mediante los métodos DbContext.Entry y DbContext.ChangeTracker.Entries. Las técnicas que se muestran en este tema se aplican igualmente a los modelos creados con Code First y EF Designer.

Uso de Local para examinar los datos locales

La propiedad Local de DbSet proporciona acceso sencillo a las entidades del conjunto sobre las que el contexto realiza el seguimiento y que no se han marcado como Eliminadas. El acceso a la propiedad Local nunca hace que se envíe una consulta a la base de datos. Esto significa que normalmente se usa después de que ya se haya realizado una consulta. El método de extensión Load se puede usar para ejecutar una consulta para que el contexto realice un seguimiento de los resultados. Por ejemplo:

using (var context = new BloggingContext())
{
    // Load all blogs from the database into the context
    context.Blogs.Load();

    // Add a new blog to the context
    context.Blogs.Add(new Blog { Name = "My New Blog" });

    // Mark one of the existing blogs as Deleted
    context.Blogs.Remove(context.Blogs.Find(1));

    // Loop over the blogs in the context.
    Console.WriteLine("In Local: ");
    foreach (var blog in context.Blogs.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            blog.BlogId,  
            blog.Name,
            context.Entry(blog).State);
    }

    // Perform a query against the database.
    Console.WriteLine("\nIn DbSet query: ");
    foreach (var blog in context.Blogs)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            blog.BlogId,  
            blog.Name,
            context.Entry(blog).State);
    }
}

Si teníamos dos blogs en la base de datos : "ADO.NET Blog" con un BlogId de 1 y "The Visual Studio Blog" con un BlogId de 2, podríamos esperar la siguiente salida:

In Local:
Found 0: My New Blog with state Added
Found 2: The Visual Studio Blog with state Unchanged

In DbSet query:
Found 1: ADO.NET Blog with state Deleted
Found 2: The Visual Studio Blog with state Unchanged

Esto ilustra tres puntos:

  • El nuevo blog "Mi nuevo blog" se incluye en la colección Local aunque aún no se haya guardado en la base de datos. Este blog tiene una clave principal de cero porque la base de datos aún no ha generado una clave real para la entidad.
  • El blog “ADO.NET Blog” no se incluye en la colección local aunque el contexto sigue realizando el seguimiento. Esto se debe a que lo quitamos del DbSet, lo que lo marca como eliminado.
  • Cuando DbSet se usa para realizar una consulta, el blog marcado para su eliminación (ADO.NET Blog) se incluye en los resultados y el nuevo blog (Mi nuevo blog) que aún no se ha guardado en la base de datos no se incluye en los resultados. Esto se debe a que DbSet está realizando una consulta en la base de datos y los resultados devueltos siempre reflejan lo que hay en la base de datos.

Uso de Local para agregar y quitar entidades del contexto

La propiedad Local de DbSet devuelve una ObservableCollection con eventos conectados de forma que permanece sincronizado con el contenido del contexto. Esto significa que las entidades se pueden agregar o quitar de la colección Local o el DbSet. También significa que las consultas que aportan nuevas entidades al contexto darán lugar a que la colección Local se actualice con esas entidades. Por ejemplo:

using (var context = new BloggingContext())
{
    // Load some posts from the database into the context
    context.Posts.Where(p => p.Tags.Contains("entity-framework")).Load();  

    // Get the local collection and make some changes to it
    var localPosts = context.Posts.Local;
    localPosts.Add(new Post { Name = "What's New in EF" });
    localPosts.Remove(context.Posts.Find(1));  

    // Loop over the posts in the context.
    Console.WriteLine("In Local after entity-framework query: ");
    foreach (var post in context.Posts.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            post.Id,  
            post.Title,
            context.Entry(post).State);
    }

    var post1 = context.Posts.Find(1);
    Console.WriteLine(
        "State of post 1: {0} is {1}",
        post1.Name,  
        context.Entry(post1).State);  

    // Query some more posts from the database
    context.Posts.Where(p => p.Tags.Contains("asp.net")).Load();  

    // Loop over the posts in the context again.
    Console.WriteLine("\nIn Local after asp.net query: ");
    foreach (var post in context.Posts.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            post.Id,  
            post.Title,
            context.Entry(post).State);
    }
}

Suponiendo que teníamos algunas publicaciones etiquetadas con "entity-framework" y "asp.net", la salida puede tener un aspecto similar al siguiente:

In Local after entity-framework query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
State of post 1: EF Beginners Guide is Deleted

In Local after asp.net query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
Found 4: ASP.NET Beginners Guide with state Unchanged

Esto ilustra tres puntos:

  • El contexto hace un seguimiento de la nueva publicación "Novedades de EF" que se agregó a la colección Local en el estado Agregado. Por lo tanto, se insertará en la base de datos cuando se llame a SaveChanges.
  • La publicación que se quitó de la colección Local (Guía para principiantes de EF) ahora se marca como eliminada en el contexto. Por lo tanto, se eliminará de la base de datos cuando se llame a SaveChanges.
  • La publicación adicional (Guía para principiantes de ASP.NET) cargada en el contexto con la segunda consulta se agrega automáticamente a la colección Local.

Una última cosa que hay que tener en cuenta sobre Local es que, dado que es un rendimiento observableCollection, no es excelente para un gran número de entidades. Por lo tanto, si está tratando con miles de entidades en el contexto, puede que no sea aconsejable usar Local.

Uso de Local para el enlace de datos de WPF

La propiedad Local de DbSet se puede usar directamente para el enlace de datos en una aplicación WPF porque es una instancia de ObservableCollection. Como se describe en las secciones anteriores, esto significa que permanecerá automáticamente sincronizado con el contenido del contexto y el contenido del contexto permanecerá automáticamente sincronizado con él. Tenga en cuenta que debe rellenar previamente la colección Local con datos para que haya algo que enlazar, ya que Local nunca provoca una consulta de base de datos.

Este no es un lugar adecuado para un ejemplo completo de enlace de datos de WPF, pero los elementos clave son:

  • Configurar un origen de enlace
  • Enlazarlo a la propiedad Local del conjunto
  • Rellenar Local mediante una consulta en la base de datos.

Enlace WPF a propiedades de navegación

Si va a realizar el enlace de datos maestro/detallado, es posible que desee enlazar la vista de detalles a una propiedad de navegación de una de las entidades. Una manera fácil de hacer este trabajo es usar observableCollection para la propiedad de navegación. Por ejemplo:

public class Blog
{
    private readonly ObservableCollection<Post> _posts =
        new ObservableCollection<Post>();

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

    public virtual ObservableCollection<Post> Posts
    {
        get { return _posts; }
    }
}

Uso de Local para limpiar entidades en SaveChanges

En la mayoría de los casos, las entidades eliminadas de una propiedad de navegación no se marcarán automáticamente como eliminadas en el contexto. Por ejemplo, si quita un objeto Post de la colección Blog.Posts, esa publicación no se eliminará automáticamente cuando se llame a SaveChanges. Si necesita eliminarla, es posible que tenga que encontrar estas entidades pendientes y marcarlas como eliminadas antes de llamar a SaveChanges o como parte de un SaveChanges invalidado. Por ejemplo:

public override int SaveChanges()
{
    foreach (var post in this.Posts.Local.ToList())
    {
        if (post.Blog == null)
        {
            this.Posts.Remove(post);
        }
    }

    return base.SaveChanges();
}

El código anterior usa la colección Local para buscar todas las publicaciones y marcas que no tengan una referencia de blog como eliminada. La llamada a ToList es necesaria porque, de lo contrario, la llamada Remove modificará la colección mientras se enumera. En la mayoría de las otras situaciones, puede consultar directamente en la propiedad Local sin usar ToList primero.

Uso de Local y ToBindingList para el enlace de datos de Windows Forms

Windows Forms no admite el enlace de datos de fidelidad completa mediante ObservableCollection directamente. Sin embargo, todavía puede usar la propiedad Local de DbSet para el enlace de datos para obtener todas las ventajas descritas en las secciones anteriores. Esto se logra mediante el método de extensión ToBindingList que crea una implementación de IBindingList respaldada por ObservableCollection Local.

Este no es un lugar adecuado para un ejemplo completo de enlace de datos de Windows Forms, pero los elementos clave son:

  • Configurar un origen de enlace de objetos
  • Enlazarlo a la propiedad Local del conjunto mediante Local.ToBindingList()
  • Rellenar Local mediante una consulta a la base de datos

Obtener la información detallada sobre las entidades con seguimiento

Muchos de los ejemplos de esta serie usan el método Entry para devolver una instancia de DbEntityEntry para una entidad. A continuación, este objeto de entrada actúa como punto de partida para recopilar información sobre la entidad, como su estado actual, así como para realizar operaciones en la entidad, como cargar explícitamente una entidad relacionada.

Los métodos Entries devuelven objetos DbEntityEntry para muchas o todas las entidades sobre las que el contexto realiza el seguimiento. Esto le permite recopilar información o realizar operaciones en muchas entidades en lugar de una sola entrada. Por ejemplo:

using (var context = new BloggingContext())
{
    // Load some entities into the context
    context.Blogs.Load();
    context.Authors.Load();
    context.Readers.Load();

    // Make some changes
    context.Blogs.Find(1).Title = "The New ADO.NET Blog";
    context.Blogs.Remove(context.Blogs.Find(2));
    context.Authors.Add(new Author { Name = "Jane Doe" });
    context.Readers.Find(1).Username = "johndoe1987";

    // 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 blogs
    Console.WriteLine("\nTracked blogs: ");
    foreach (var entry in context.ChangeTracker.Entries<Blog>())
    {
        Console.WriteLine(
            "Found Blog {0}: {1} with original Name {2}",
            entry.Entity.BlogId,  
            entry.Entity.Name,
            entry.Property(p => p.Name).OriginalValue);
    }

    // Find all people (author or reader)
    Console.WriteLine("\nPeople: ");
    foreach (var entry in context.ChangeTracker.Entries<IPerson>())
    {
        Console.WriteLine("Found Person {0}", entry.Entity.Name);
    }
}

Observará que estamos introduciendo una clase Author y Reader en el ejemplo; ambas clases implementan la interfaz IPerson.

public class Author : IPerson
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public string Biography { get; set; }
}

public class Reader : IPerson
{
    public int ReaderId { get; set; }
    public string Name { get; set; }
    public string Username { get; set; }
}

public interface IPerson
{
    string Name { get; }
}

Supongamos que tenemos los siguientes datos en la base de datos:

Blog con BlogId = 1 y Nombre = "ADO.NET Blog"
Blog con BlogId = 2 y Nombre = "The Visual Studio Blog"
Blog con BlogId = 3 y Nombre = ".NET Framework Blog"
Author con AuthorId = 1 y Nombre = "Joe Bloggs"
Reader con ReaderId = 1 y Nombre = "John Doe"

La salida de la ejecución del código sería:

All tracked entities:
Found entity of type Blog with state Modified
Found entity of type Blog with state Deleted
Found entity of type Blog with state Unchanged
Found entity of type Author with state Unchanged
Found entity of type Author with state Added
Found entity of type Reader with state Modified

All modified entities:
Found entity of type Blog with state Modified
Found entity of type Reader with state Modified

Tracked blogs:
Found Blog 1: The New ADO.NET Blog with original Name ADO.NET Blog
Found Blog 2: The Visual Studio Blog with original Name The Visual Studio Blog
Found Blog 3: .NET Framework Blog with original Name .NET Framework Blog

People:
Found Person John Doe
Found Person Joe Bloggs
Found Person Jane Doe

En estos ejemplos se muestran varios puntos:

  • Los métodos Entries devuelven entradas para entidades en todos los estados, incluido Eliminado. Compare esto con Local, que excluye las entidades eliminadas.
  • Las entradas de todos los tipos de entidad se devuelven cuando se usa el método Entries no genérico. Cuando se usa el método de entradas genéricas, solo se devuelven entradas para entidades que son instancias del tipo genérico. Esto se usó anteriormente para obtener entradas para todos los blogs. También se usó para obtener entradas para todas las entidades que implementan IPerson. Esto demuestra que el tipo genérico no tiene que ser un tipo de entidad real.
  • LINQ to Objects se puede usar para filtrar los resultados devueltos. Esto se usó anteriormente para buscar entidades de cualquier tipo siempre que se modifiquen.

Tenga en cuenta que las instancias de DbEntityEntry siempre contienen una entidad que no sea NULL. Las entradas de relación y las subentradas no se representan como instancias de DbEntityEntry, por lo que no es necesario filtrarlas.