Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 7 de mayo de 2019
Entity Framework Core Para los desarrolladores que hemos utilizado Entity Framework 6.x y anteriores durante años, lo normal es que al dar el salto a Entity Framework Core continuemos utilizando más o menos las mismas cosas que ya conocíamos, y muchas veces no nos detenemos a ver qué novedades interesantes acompañan al nuevo marco de trabajo.

Ya llevamos varios posts dedicados a comentar algunas características novedosas o que cambian bastante respecto a las versiones anteriores (como la evaluación en cliente o las shadow properties, y vamos a continuar en esta línea presentando ahora otra interesante novedad: el soporte para campos de respaldo o backing fields.

¿Backing fields? ¿No me suena eso de algo?

Pues claro, el concepto de backing fields no es nuevo, ni propio de Entity Framework Core: lo usamos en .NET desde el principio de los tiempos. Como se define en la guía oficial de programación con C#:
Un campo de respaldo es un campo privado que almacena los datos expuestos por una propiedad pública.
Vaya, básicamente es lo que utilizábamos a diario antes de que se introdujeran en el lenguaje las propiedades automáticas:
public class Friend
{
    private string _name; // <-- backing field

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

Vale, ¿y por qué necesitamos backing fields en Entity Framework?

Si habéis trabajado ya con Entity Framework "clásico", probablemente que alguna vez habréis necesitado proteger las entidades de accesos no controlados a algunas de sus propiedades. Aunque algunos de esos escenarios eran salvables jugando con la visibilidad de las propiedades, hay otros donde esto no era técnicamente posible, suponiendo un peligro desde el punto de vista de la encapsulación de datos, el mantenimiento de invariantes o la consistencia de la información almacenada.

Por ejemplo, en la siguiente entidad tenemos una propiedad llamada LastUpdate para mantener la fecha en que los datos de la persona son actualizados. Para asegurar la consistencia, en el setter de Name actualizamos la fecha de actualización:
public class Friend
{
    private string _fullName;

    public string Name
    {
        get => _fullName;
        set
        {
            _fullName = value;
            LastUpdate = DateTime.Now;
        }
    }

    public int Id { get; set; }
    public DateTime LastUpdate { get; private set; }
}

De esta forma, cuando desde código asignemos la propiedad Name, se actualizará su fecha de modificación. Además, para evitar que se modifique desde fuera, hacemos el setter privado: aparentemente, todo correcto.

Sin embargo, este código tiene un problema: cada vez que una entidad Friend sea materializada desde la base de datos se establecerá la propiedad Name, y esto provocará que a su vez se actualice el contenido de LastUpdate con la fecha actual. Por tanto, estaremos machacándola continuamente y perderemos el valor original de la propiedad en cuanto salvemos cambios.

Otro caso frecuente lo podemos encontrar en el siguiente ejemplo. Echad un vistazo, ¿qué impide añadir una línea a la factura directamente, obviando todos los controles y lógica que el método AddLine() pudiera contener?
public class Invoice
{
    ...
    public HashSet<InvoiceLine> Lines { get; private set; }
    public void AddLine(InvoiceLine line)
    {
        if (!IsEditable())
        {
            throw new InvalidOperationException("You can only add lines to editable invoices");
        }
        if (line.Product.Stock <= 0)
        {
            throw new InvalidOperationException("You can't add products out of stock");
        }
        Amount += (line.Quantity * line.UnitPrice);
        Lines.Add(line);
    }
}
En este escenario, el ideal sería que la propiedad Lines fuera de algún tipo que no permitiera alterar el contenido de la colección, como un IEnumerable<InvoiceLine>.

Pues aquí es donde entran en escena los backing fields, cuya idea de es permitir que el framework sea capaz de leer y escribir directamente en campos privados de las entidades, sin que tengan que existir propiedades públicas o setters que abran la puerta a accesos arbitrarios a sus datos.

Entity Framework detectará los campos privados de las entidades, bien mediante convenciones o bien porque así lo hayamos indicado expresamente en la configuración, y los usará para almacenar los datos directamente durante el proceso de materialización.

Uso de backing fields

La configuración de este tipo de campos la debemos hacer desde el método OnModelCreating() del contexto de datos, donde asociaremos los backing fields con las propiedades de la entidad, como en el siguiente ejemplo:
// En el contexto de datos, indicamos el backing field para "Name":
protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<Friend>()
        .Property(f => f.Name)
        .HasField("_fullName");
...
}

// En la entidad:
public class Friend
{
    private string _fullName;
    public int Id { get; set; }
    public string Name
    {
        get => _fullName;
        set 
        { 
            _fullName = value;
            LastUpdate = DateTime.Now;
        }
    }
    public DateTime LastUpdate { get; private set; }
}
Hecho esto, cuando EF vaya a materializar un objeto Friend desde el almacén, escribirá directamente sobre el campo privado _fullName en lugar de hacerlo sobre la propiedad Name, por lo que el valor de LastUpdated no será actualizado, aglo que si ocurrirá cuando establezcamos manualmente el nombre.

¿Y cómo podemos aprovechar esta característica también para proteger las colecciones, como en el escenario que describíamos algo más arriba? Pues en este caso, lo que nos interesa es que el campo privado sea de tipo HashSet o similar y permita todas las operaciones de adición y eliminación de elementos, pero que hacia fuera nuestra entidad exponga un IEnumerable de sólo lectura, como en el siguiente código:
public class Invoice
{
    private HashSet<InvoiceLine> _invoiceLines;

    public IEnumerable<InvoiceLine> Lines => _invoiceLines;
    public void AddLine(InvoiceLine line)
    {
        // Hacer algo y añadir la línea a _lines
    }
    ...
}
En esta ocasión, al tratarse de una propiedad de navegación, la forma de configurarlo será ligeramente diferente:
// En el contexto de datos:
protected override void OnModelCreating(ModelBuilder builder)
{
    var navigation = builder.Entity<Invoice>().Metadata.FindNavigation(nameof(Invoice.Lines));
    navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
    navigation.SetField("_invoiceLines");
    ...
}

Convenciones de nombrado de backing fields

Por defecto, Entity Framework Core detectará backing fields de forma automática cuando en la entidad exista un campo privado cuyo nombre coincida con el de una propiedad incluida en el modelo según una convención de nombres predeterminada. Por ejemplo, a una propiedad llamada CustomerEmail le asociará automáticamente, sin necesidad de indicarlo expresamente en el OnModelCreating(), un backing field llamado (en orden de prioridad):
  • _customerEmail
  • _CustomerEmail
  • m_customerEmail
  • m_CustomerEmail
Y esto es todo, ¡espero que os sea útil!

Publicado en Variable not found.

2 Comentarios:

Albert Capdevila dijo...

¡Muy útil! Detalles que le faltaba a EF. Gracias por compartir :)

José María Aguilar dijo...

Hola!

Me alegra saber que lo ves útil, Albert.

Muchas gracias por comentar!