Types d’entités avec constructeurs

Il est possible de définir un constructeur avec des paramètres et qu’EF Core appele ce constructeur lors de la création d’une instance de l’entité. Les paramètres du constructeur peuvent être liés à des propriétés mappées ou à différents types de services pour faciliter les comportements tels que le chargement différé.

Remarque

Actuellement, toutes les liaisons de constructeur sont faites par convention. La configuration des constructeurs spécifiques à utiliser est prévue pour une version ultérieure.

Liaison aux propriétés mappées

Considérez un modèle de blog/billet classique :

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

Quand EF Core crée des instances de ces types, comme pour les résultats d’une requête, il appelle d’abord le constructeur sans paramètre par défaut, puis définit chaque propriété sur la valeur de la base de données. Toutefois, si EF Core trouve un constructeur paramétrable avec des noms et des types de paramètres qui correspondent à ceux des propriétés mappées, il appelle plutôt le constructeur paramétrable avec des valeurs pour ces propriétés et ne définit pas explicitement chaque propriété. Par exemple :

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

Quelques éléments à prendre en compte :

  • Toutes les propriétés ne nécessitent pas des paramètres de constructeur. Par exemple, la propriété Post.Content n’est définie par un paramètre de constructeur. EF Core la définit donc après l’appel du constructeur de la manière normale.
  • Les types et les noms de paramètres doivent correspondre aux types et aux noms de propriétés, sauf que les propriétés peuvent utilisées la casse Pascal pendant que les paramètres utilisent le camel case.
  • EF Core ne peut pas définir les propriétés de navigation (telles que Blog ou Billet ci-dessus) à l’aide d’un constructeur.
  • Le constructeur peut être public, privé ou avoir n’importe quelle autre accessibilité. Toutefois, les proxys de chargement différé nécessitent que le constructeur soit accessible à partir de la classe proxy héritée. Cela signifie généralement qu’il doit être public ou protégé.

Propriétés en lecture seule

Une fois que les propriétés sont définies via le constructeur, il peut être judicieux de limiter certaines d’entre elles en lecture seule. EF Core prend en charge cette fonctionnalité, mais il y a certains détails à surveiller :

  • Les propriétés sans setters ne sont pas mappées par convention. (Cela tend à mapper les propriétés qui ne devraient pas l’être, telles que les propriétés calculées.)
  • L’utilisation de valeurs de clé générées automatiquement nécessite une propriété de clé en lecture-écriture, car la valeur de clé doit être définie par le générateur de clés lors de l’insertion de nouvelles entités.

Un moyen simple d’éviter cela est d’utiliser des setters privés. Par exemple :

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 voit une propriété avec un setter privé en lecture-écriture, ce qui signifie que toutes les propriétés sont mappées comme avant et que la clé peut toujours être générée en magasin.

Une alternative à l’utilisation de setters privés consiste à vraiment limiter les propriétés en lecture seule et à ajouter un mappage plus explicite dans OnModelCreating. De même, certaines propriétés peuvent être complètement supprimées et remplacées par des champs uniquement. Par exemple, tenez compte de ces types d'entités :

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

Et cette configuration dans 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);
        });
}

Points à retenir :

  • La clé « property » est désormais un champ. Il ne s’agit pas d’un champ readonly afin que les clés générées par le magasin puissent être utilisées.
  • Les autres propriétés sont des propriétés en lecture seule définies uniquement dans le constructeur.
  • Si la valeur de clé primaire n’est toujours définie que par EF ou lue à partir de la base de données, il n’est alors pas nécessaire de l’inclure dans le constructeur. Cela laisse la clé « property » en tant que champ simple et indique clairement qu’elle ne doit pas être définie explicitement lors de la création de nouveaux blogs ou de billets.

Remarque

Ce code génère un avertissement du compilateur « 169 » indiquant que le champ n’est jamais utilisé. Cela peut être ignoré, car en réalité EF Core utilise le champ de manière extralinguistique.

Injection de services

EF Core peut également injecter des « services » dans le constructeur d’un type d’entité. Par exemple, vous pouvez injecter les éléments suivants :

  • DbContext : l’instance de contexte actuelle, qui peut également être typée en tant que type DbContext dérivé
  • ILazyLoader : le service de chargement différé. Consultez la documentation sur le chargement différé pour plus d’informations
  • Action<object, string> : un délégué de chargement différé. Consultez la documentation sur le chargement différé pour plus d’informations
  • IEntityType : les métadonnées EF Core associées à ce type d’entité

Remarque

Actuellement, seuls les services connus par EF Core peuvent être injectés. La prise en charge de l’injection de services d’application est envisagée pour une prochaine version.

Par exemple, dbContext injecté peut être utilisé pour accéder de manière sélective à la base de données afin d’obtenir des informations sur les entités associées sans toutes les charger. Dans l’exemple ci-dessous, cette méthode est utilisée pour obtenir le nombre de billets dans un blog sans charger les billets :

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

Quelques points à noter sur ce sujet :

  • Le constructeur est privé, car il n’est toujours appelé que par EF Core, et il existe un autre constructeur public pour une utilisation générale.
  • Le code utilisant le service injecté (autrement dit, le contexte) est défensif à l’idée qu’il soit null pour gérer les cas où EF Core ne crée pas l’instance.
  • Étant donné que le service est stocké dans une propriété en lecture/écriture, il sera réinitialisé lorsque l’entité sera attachée à une nouvelle instance de contexte.

Avertissement

L’injection de DbContext de cette manière est souvent considérée comme un anti-modèle, car elle couple vos types d’entités directement à EF Core. Examinez attentivement toutes les options avant d’utiliser l’injection de service comme celle-ci.