Tipos de entidad con constructores

Es posible definir un constructor con parámetros y que EF Core llame a este constructor al crear una instancia de la entidad. Los parámetros de constructor se pueden enlazar a propiedades asignadas o a varios tipos de servicios para facilitar comportamientos como la carga diferida.

Nota:

Actualmente, todos los enlaces de constructores son por convención. La configuración de constructores específicos está prevista en una versión futura.

Enlace a propiedades asignadas

Considere un modelo típico de blog/publicación:

public class Blog
{
    public int Id { get; set; }

    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Cuando EF Core crea instancias de estos tipos, por ejemplo, para los resultados de una consulta, primero llama al constructor sin parámetros predeterminado y, luego, establece cada propiedad en el valor de la base de datos. Sin embargo, si EF Core encuentra un constructor parametrizado con nombres y tipos de parámetros que coinciden con los de propiedades asignadas, en su lugar llamará al constructor parametrizado con valores para esas propiedades y no establecerá cada propiedad explícitamente. Por ejemplo:

public class Blog
{
    public Blog(int id, string name, string author)
    {
        Id = id;
        Name = name;
        Author = author;
    }

    public int Id { get; set; }

    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public Post(int id, string title, DateTime postedOn)
    {
        Id = id;
        Title = title;
        PostedOn = postedOn;
    }

    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Cosas que tener en cuenta:

  • No todas las propiedades tienen que tener parámetros de constructor. Por ejemplo, la propiedad Post.Content no está establecida por ningún parámetro de constructor, por lo que EF Core la establecerá después de llamar al constructor de la manera normal.
  • Los tipos y nombres de parámetros deben coincidir con los tipos y nombres de propiedades, salvo que las propiedades puedan tener la notación Pascal Case mientras que los parámetros tienen Camel Case.
  • EF Core no puede establecer propiedades de navegación (como blog o publicaciones anteriormente) mediante un constructor.
  • El constructor puede ser público, privado o tener cualquier otro nivel de acceso. Sin embargo, los servidores proxy de carga diferida requieren que el constructor sea accesible desde la clase de proxy heredada. Normalmente esto significa hacer que sea público o protegido.

Propiedades de solo lectura

Una vez que las propiedades se establecen a través del constructor, puede tener sentido que algunas de ellas sean de solo lectura. EF Core lo admite, pero hay algunas cosas que hay que mirar:

  • Las propiedades sin establecedores no se asignan por convención. (Al hacerlo, se tiende a asignar propiedades que no se deben asignar, como las propiedades calculadas).
  • El uso de valores de clave generados automáticamente requiere una propiedad de clave que sea de lectura y escritura, ya que el generador de claves debe establecer el valor de clave al insertar nuevas entidades.

Una manera fácil de evitar estos problemas es usar establecedores privados. Por ejemplo:

public class Blog
{
    public Blog(int id, string name, string author)
    {
        Id = id;
        Name = name;
        Author = author;
    }

    public int Id { get; private set; }

    public string Name { get; private set; }
    public string Author { get; private set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public Post(int id, string title, DateTime postedOn)
    {
        Id = id;
        Title = title;
        PostedOn = postedOn;
    }

    public int Id { get; private set; }

    public string Title { get; private set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; private set; }

    public Blog Blog { get; set; }
}

EF Core considera una propiedad con un establecedor privado de lectura y escritura, lo que significa que todas las propiedades se asignan como antes y la clave todavía puede generarse por el almacén.

Una alternativa al uso de establecedores privados es hacer que las propiedades sean verdaderamente de solo lectura y agregar una asignación más explícita en OnModelCreating. Del mismo modo, algunas propiedades se pueden quitar completamente y reemplazar por solo campos. Por ejemplo, considere estos tipos de entidad:

public class Blog
{
    private int _id;

    public Blog(string name, string author)
    {
        Name = name;
        Author = author;
    }

    public string Name { get; }
    public string Author { get; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    private int _id;

    public Post(string title, DateTime postedOn)
    {
        Title = title;
        PostedOn = postedOn;
    }

    public string Title { get; }
    public string Content { get; set; }
    public DateTime PostedOn { get; }

    public Blog Blog { get; set; }
}

Y esta configuración en OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        b =>
        {
            b.HasKey("_id");
            b.Property(e => e.Author);
            b.Property(e => e.Name);
        });

    modelBuilder.Entity<Post>(
        b =>
        {
            b.HasKey("_id");
            b.Property(e => e.Title);
            b.Property(e => e.PostedOn);
        });
}

Aspectos a considerar:

  • La "propiedad" de clave es ahora un campo. No es un campo readonly para que se puedan usar claves generadas por el almacén.
  • Las otras propiedades son propiedades de solo lectura establecidas solo en el constructor.
  • Si EF solo establece el valor de la clave principal o lee la base de datos, no es necesario incluirlo en el constructor. En este contexto, la "propiedad" de clave se deja como un campo simple y queda claro que no debe establecerse explícitamente al crear nuevos blogs o publicaciones.

Nota:

Este código dará lugar a la advertencia del compilador "169" que indica que el campo nunca se usa. Se puede omitir porque, en realidad, EF Core usa el campo de forma extralingüística.

Inserción de servicios

EF Core también puede insertar "servicios" en el constructor de un tipo de entidad. Por ejemplo, se puede insertar lo siguiente:

  • DbContext: la instancia de contexto actual, que también se puede escribir como el tipo DbContext derivado.
  • ILazyLoader: el servicio de carga diferida; consulte la documentación de carga diferida para más información.
  • Action<object, string>: un delegado de carga diferida; consulte la documentación de carga diferida para más información.
  • IEntityType: los metadatos de EF Core asociados a este tipo de entidad.

Nota:

Actualmente, solo se pueden insertar servicios conocidos por EF Core. Se está considerando la compatibilidad con la inserción de servicios de aplicaciones en una versión futura.

Por ejemplo, se puede usar un objeto DbContext insertado para acceder de forma selectiva a la base de datos a fin de obtener información sobre las entidades relacionadas sin cargarlas todas. En el ejemplo siguiente, esto se usa para obtener el número de publicaciones de un blog sin cargar las publicaciones:

public class Blog
{
    public Blog()
    {
    }

    private Blog(BloggingContext context)
    {
        Context = context;
    }

    private BloggingContext Context { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; set; }

    public int PostsCount
        => Posts?.Count
           ?? Context?.Set<Post>().Count(p => Id == EF.Property<int?>(p, "BlogId"))
           ?? 0;
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Algunas cosas que se deben tener en cuenta sobre esto:

  • El constructor es privado, ya que solo lo llama EF Core y hay otro constructor público para uso general.
  • El código que utiliza el servicio insertado (es decir, el contexto) se defiende de que sea null para controlar los casos en los que EF Core no crea la instancia.
  • Dado que el servicio se almacena en una propiedad de lectura y escritura, se restablecerá cuando la entidad se asocie a una nueva instancia de contexto.

Advertencia

La inserción de objetos DbContext como este suele considerarse un antipatrón, ya que acopla los tipos de entidad directamente a EF Core. Antes de usar una inserción de servicios como esta, tenga en cuenta cuidadosamente todas las opciones.