Entitätstypen mit Konstruktoren

Es ist möglich, einen Konstruktor mit Parametern zu definieren und EF Core anzuweisen, diesen beim Erstellen einer Instanz der Entität aufzurufen. Die Konstruktorparameter können an zugeordnete Eigenschaften oder verschiedenartige Dienste gebunden werden, um Verhalten wie das verzögerte Laden zu erleichtern.

Hinweis

Derzeit erfolgen alle Konstruktorbindungen per Konvention. Die Konfiguration bestimmter zu verwendenden Konstruktoren ist für ein künftiges Release geplant.

Bindung an zugeordnete Eigenschaften

Betrachten Sie ein typisches Blog- bzw. Postingmodell:

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

Wenn der EF Core-Dienst Instanzen dieser Typen erstellt, z. B. für die Ergebnisse einer Abfrage, ruft er zuerst den parameterlosen Standardkonstruktor auf und legt dann jede Eigenschaft auf den Wert aus der Datenbank fest. Wenn EF Core jedoch einen parametrisierten Konstruktor mit Parameternamen und -typen findet, die mit denen der zugeordneten Eigenschaften übereinstimmen, ruft er stattdessen den parametrisierten Konstruktor mit Werten für diese Eigenschaften auf und legt nicht explizit jede Eigenschaft fest. Beispiel:

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

Hinweise, die Sie beachten sollten:

  • Nicht alle Eigenschaften müssen über Konstruktorparameter verfügen. Die Post.Content-Eigenschaft wird beispielsweise nicht von einem Konstruktorparameter festgelegt, sodass EF Core sie nach dem Aufrufen des Konstruktors wie gewohnt festlegt.
  • Die Parametertypen und -namen müssen den Eigenschaftstypen und -namen entsprechen, außer dass Eigenschaften in der Pascal-Schreibweise und Parameter in der Camel-Case-Schreibweise angegeben sein können.
  • EF Core kann keine Navigationseigenschaften (wie das Blog- bzw. Postingmodell oben) mithilfe eines Konstruktors festlegen.
  • Der Konstruktor kann öffentlich oder privat sein oder über andere Sichtbarkeiten verfügen. Proxys mit verzögertem Laden erfordern jedoch, dass der Konstruktor über die erbende Proxyklasse zugänglich ist. In der Regel bedeutet dies, dass der Konstruktor entweder öffentlich oder geschützt ist.

Schreibgeschützte Eigenschaften

Sobald Eigenschaften über den Konstruktor festgelegt werden, kann es sinnvoll sein, einige davon als schreibgeschützt festzulegen. Das wird von EF Core unterstützt, jedoch sollten Sie dabei Folgendes beachten:

  • Eigenschaften ohne Setter werden nicht per Konvention zugeordnet. (Dadurch könnten Eigenschaften zugeordnet werden, die nicht zugeordnet werden sollten, z. B. berechnete Eigenschaften.)
  • Für die Verwendung automatisch generierter Schlüsselwerte ist eine Schlüsseleigenschaft mit Lese-/Schreibzugriff erforderlich, da der Schlüsselwert beim Einfügen neuer Entitäten vom Schlüssel-Generator festgelegt werden muss.

Durch das Verwenden privater Setter kann dies einfach vermieden werden. Beispiel:

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 erkennt eine Eigenschaft mit einem privaten Setter als Lese-/Schreibzugriff. Das bedeutet, dass alle Eigenschaften wie zuvor zugeordnet werden und der Schlüssel trotzdem vom Speicher generiert werden kann.

Eine Alternative zur Verwendung privater Setter besteht darin, Eigenschaften wirklich als schreibgeschützt festzulegen und ein expliziteres Mapping bei der OnModelCreating-Methode hinzuzufügen. Ebenso können einige Eigenschaften vollständig entfernt und nur durch Felder ersetzt werden. Betrachten Sie beispielsweise die folgenden Entitätstypen:

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

Betrachten Sie auch diese Konfiguration der OnModelCreating-Methode:

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

Zu berücksichtigende Punkte:

  • Der Schlüssel „Property“ ist jetzt ein Feld. Dabei handelt es sich nicht um ein readonly-Feld, sodass die vom Speicher generierten Schlüssel verwendet werden können.
  • Die anderen Eigenschaften sind schreibgeschützte und werden nur im Konstruktor festgelegt.
  • Wenn der Primärschlüsselwert nur von EF festgelegt oder aus der Datenbank gelesen wird, müssen Sie ihn nicht in den Konstruktor aufnehmen. Dadurch wird der Schlüssel „Property“ als einfaches Feld belassen und es wird deutlich, dass er beim Erstellen neuer Blogs oder Postings nicht explizit festgelegt werden sollte.

Hinweis

Dieser Code führt zu der Compilerwarnung „169“, die angibt, dass das Feld nie verwendet wird. Diese kann ignoriert werden, da EF Core das Feld in Wirklichkeit außersprachlich verwendet.

Einfügen von Diensten

EF Core kann auch „Services“ in den Konstruktor eines Entitätstyps einfügen. Beispielsweise kann Folgendes eingefügt werden:

  • DbContext: Hierbei handelt es sich um die aktuelle Kontextinstanz, die auch als abgeleiteter DbContext-Typ typisiert werden kann.
  • ILazyLoader: Hierbei handelt es sich um den Dienst mit verzögertem Laden. Weitere Informationen finden Sie in der Dokumentation zum verzögerten Laden.
  • Action<object, string>: Hierbei handelt es sich um einen Delegaten mit verzögertem Laden. Weitere Informationen finden Sie in der Dokumentation zum verzögerten Laden.
  • IEntityType: Hierbei handelt es sich um die EF Core-Metadaten, die diesem Entitätstyp zugeordnet sind.

Hinweis

Derzeit können nur Dienste eingefügt werden, die EF Core bekannt sind. Die Unterstützung für das Einfügen von Anwendungsdiensten wird für ein künftiges Release berücksichtigt.

Beispielsweise kann ein eingefügter DbContext-Typ verwendet werden, um selektiv auf die Datenbank zuzugreifen und dort Informationen zu verwandten Entitäten abzurufen, ohne alle zu laden. Im folgenden Beispiel wird dieser Ansatz verwendet, um die Anzahl der Beiträge in einem Blog zu erhalten, ohne die Postings zu laden:

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

Dabei sollten Sie Folgendes beachten:

  • Der Konstruktor ist privat, da er nur von EF Core aufgerufen wird. Ein weiterer öffentlicher Konstruktor steht für die allgemeine Verwendung zur Verfügung.
  • Der Code, der den eingefügten Dienst (den Kontext) verwendet, verhindert einen null-Wert, um mit Fällen umzugehen, in denen EF Core die Instanz nicht erstellt.
  • Da der Dienst in einer Lese-/Schreibeigenschaft gespeichert wird, wird er zurückgesetzt, wenn die Entität an eine neue Kontextinstanz angefügt wird.

Warnung

Das Einfügen des DbContext-Typs wird häufig als Negativbeispiel betrachtet, da er die Entitätstypen direkt mit EF Core verknüpft. Berücksichtigen Sie sorgfältig alle Optionen, bevor Sie das hier dargestellte Einfügen von Diensten verwenden.