Share via


Teil 5: Razor-Seiten mit EF Core in ASP.NET Core – Datenmodell

Von Tom Dykstra, Jeremy Likness und Jon P. Smith

Die Web-App Contoso University veranschaulicht, wie Razor-Seiten-Web-Apps mit EF Core und Visual Studio erstellt werden können. Informationen zu den Tutorials finden Sie im ersten Tutorial.

Wenn Probleme auftreten, die Sie nicht beheben können, laden Sie die vollständige App herunter, und vergleichen Sie diesen Code mit dem Code, den Sie anhand des Tutorials erstellt haben.

In den vorherigen Tutorials wurde mit einem einfachen Datenmodell gearbeitet, das aus drei Entitäten bestand. In diesem Tutorial wird Folgendes durchgeführt:

  • Weitere Entitäten und Beziehungen werden hinzugefügt.
  • Das Datenmodell wird angepasst, indem Regeln zur Formatierung, Validierung und Datenbankzuordnung angegeben werden.

Das vollständige Datenmodell wird in der folgenden Abbildung dargestellt:

Entity diagram

Das folgende Datenbankdiagramm wurde mit Dataedoerstellt:

Dataedo diagram

So erstellen Sie ein Datenbankdiagramm mit Dataedo:

Im Dataedo-Diagramm oben ist CourseInstructor eine Jointabelle, die von Entity Framework erstellt wurde. Weitere Informationen finden Sie unter m:n-Beziehung.

Die Entität „Student“

Ersetzen Sie den Code in Models/Student.cs durch folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Der Code oben fügt eine FullName-Eigenschaft hinzu und fügt den vorhandenen Eigenschaften die folgenden Attribute hinzu:

Die berechnete FullName-Eigenschaft

Bei FullName handelt es sich um eine berechnete Eigenschaft, die einen Wert zurückgibt, der durch das Verketten von zwei weiteren Eigenschaften erstellt wird. FullName kann nicht festgelegt werden und verfügt daher lediglich über einen get-Accessor. In der Datenbank wird keine FullName-Spalte erstellt.

Das DataType-Attribut

[DataType(DataType.Date)]

Für die Anmeldedaten von Studenten zeigen alle Webseiten derzeit die Zeit und das Datum an, obwohl nur das Datum relevant ist. Indem Sie Attribute für die Datenanmerkung verwenden, können Sie eine Codeänderungen vornehmen, durch die das Anzeigeformat auf jeder Seite korrigiert wird, in der die Daten angezeigt werden.

Das DataType-Attribut gibt einen Datentyp an, der spezifischer als der datenbankinterne Typ ist. In diesem Fall sollte nur das Datum angezeigt werden, nicht das Datum und die Uhrzeit. Die DataType-Enumeration stellt viele Datentypen bereit, wie z.B. „Date“, „Time“, „PhoneNumber“, „Currency“, „EmailAddress“. Das DataType-Attribut kann der App auch das Bereitstellen typspezifischer Features ermöglichen. Zum Beispiel:

  • Der Link mailto: wird automatisch für DataType.EmailAddress erstellt.
  • Die Datumsauswahl für DataType.Date wird in den meisten Browsern bereitgestellt.

Das DataType-Attribut gibt data--HTML5-Attribute (ausgesprochen „Datadash“) aus. Die DataType-Attribute bieten keine Validierung.

Das DisplayFormat-Attribut

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date gibt nicht das Format des Datums an, das angezeigt wird. Standardmäßig wird das Datumsfeld gemäß den Standardformaten basierend auf der CultureInfo-Klasse des Servers angezeigt.

Das DisplayFormat-Attribut dient zum expliziten Angeben des Datumsformats. Die ApplyFormatInEditMode-Einstellung gibt an, dass die Formatierung ebenfalls auf die Benutzeroberfläche für die Bearbeitung angewendet wird. Einige Felder sollten ApplyFormatInEditMode nicht verwenden. Das Währungssymbol sollte beispielsweise nicht im Textfeld für die Bearbeitung angezeigt werden.

Das DisplayFormat-Attribut kann eigenständig verwendet werden. Meist empfiehlt es sich, das DataType-Attribut mit dem DisplayFormat-Attribut zu verwenden. Das DataType-Attribut übermittelt die Semantik der Daten im Gegensatz zu deren Rendern auf einem Bildschirm. Das DataType-Attribut bietet folgende Vorteile, die in DisplayFormat nicht verfügbar sind:

  • Der Browser kann HTML5-Features aktivieren. Dazu zählen beispielsweise das Anzeigen eines Kalendersteuerelements, des dem Gebietsschema entsprechenden Währungssymbols, von E-Mail-Links und einigen clientseitigen Eingabevalidierungen.
  • Standardmäßig rendert der Browser Daten mit dem auf Ihrem Gebietsschema basierenden richtigen Format.

Weitere Informationen finden Sie in der Dokumentation zum <input>-Taghilfsprogramm.

Das StringLength-Attribut

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Die Regeln für die Datenvalidierung und Meldungen für Validierungsfehler können mithilfe von Attributen angegeben werden. Das StringLength-Attribut gibt den mindestens erforderlichen und maximal zulässigen Wert für die Zeichenlänge eines Datenfelds an. Der gezeigte Code beschränkt Namen auf maximal 50 Zeichen. Ein Beispiel, das die minimale Zeichenfolgenlänge festlegt, wird später gezeigt.

Das StringLength-Attribut stellt ebenfalls die clientseitige und serverseitige Validierung bereit. Der mindestens erforderliche Wert hat keine Auswirkungen auf das Datenbankschema.

Das StringLength-Attribut verhindert nicht, dass ein Benutzer einen Leerraum als Namen eingibt. Das Attribut RegularExpression kann verwendet werden, um Einschränkungen auf die Eingabe anzuwenden. Folgender Code erfordert beispielsweise, dass das erste Zeichen ein Großbuchstabe sein muss und die restlichen Zeichen alphabetisch sein müssen:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

Öffnen Sie im SQL Server-Objekt-Explorer (SSOX) den Tabellen-Designer „Student“, indem Sie auf die Tabelle Student doppelklicken.

Students table in SSOX before migrations

In der vorangehenden Abbildung wird das Schema der Student-Tabelle dargestellt. Die Namensfelder weisen den Typ nvarchar(MAX) auf. Wenn eine Migration später in diesem Tutorial erstellt und angewendet wird, werden die Namensfelder nvarchar(50) als Ergebnis der Attribute für die Zeichenfolgenlänge.

Das Column-Attribut

[Column("FirstName")]
public string FirstMidName { get; set; }

Durch Attribute kann gesteuert werden, wie Ihre Klassen und Eigenschaften der Datenbank zugeordnet werden. Im Student-Modell wird das Column-Attribut verwendet, um den Namen der FirstMidName-Eigenschaft in der Datenbank auf „FirstName“ festzulegen.

Wenn die Datenbank erstellt wird, werden die Eigenschaftsnamen des Moduls für Spaltennamen verwendet (außer bei Verwendung des Column-Attributs). Das Student-Modell verwendet FirstMidName für das Feld „first-name“, da dieses ebenfalls einen Zweitnamen enthalten kann.

Mit dem [Column]-Attribut wird Student.FirstMidName im Datenmodell der Spalte FirstName in der Tabelle Student zugeordnet. Durch das Hinzufügen des Column-Attributs wird das Modell geändert, das SchoolContext unterstützt. Das Modell, das SchoolContext unterstützt, stimmt nicht mehr mit der Datenbank überein. Diese Diskrepanz wird durch Hinzufügen einer Migration zu einem späteren Zeitpunkt in diesem Tutorial aufgelöst.

Das Attribut „Required“

[Required]

Durch das Required-Attribut werden die Name-Eigenschaften zu Pflichtfeldern. Das Required-Attribut ist für Typen, die keine NULL-Werte annehmen können (etwas Werttypen, z.B. DateTime, int und double) nicht erforderlich. Typen, die nicht auf NULL festgelegt werden können, werden automatisch als Pflichtfelder behandelt.

Das Required-Attribut muss mit MinimumLength verwendet werden, damit die MinimumLength erzwungen wird.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLength und Required lassen Leerzeichen zu, um die Überprüfung zu bestehen. Verwenden Sie das RegularExpression-Attribut, um vollständige Kontrolle über die Zeichenfolge zu erhalten.

Das Display-Attribut

[Display(Name = "Last Name")]

Das Display-Attribut gibt an, dass die Beschriftung der Textfelder „First Name“ (Vorname), „Last Name“ (Nachname), „Full Name“ (Vollständiger Name) und „Enrollment Date“ (Registrierungsdatum) lauten soll. Die Standardbeschriftungen hatten kein Leerzeichen zwischen den Wörtern, wie zum Beispiel „Lastname“.

Erstellen einer Migration

Führen Sie die Anwendung aus. Wechseln Sie zur Studentenseite. Es wird eine Ausnahme ausgelöst. Das [Column]-Attribut bewirkt, dass EF eine Spalte mit dem Namen FirstName erwartet, aber der Spaltenname in der Datenbank ist weiterhin FirstMidName.

Die Fehlermeldung lautet ungefähr wie im folgenden Beispiel:

SqlException: Invalid column name 'FirstName'.
There are pending model changes
Pending model changes are detected in the following:

SchoolContext
  • Geben Sie in der PMC folgende Befehle ein, um eine neue Migration zu erstellen und die Datenbank zu aktualisieren:

    Add-Migration ColumnFirstName
    Update-Database
    
    

    Mit dem ersten dieser Befehle wird die folgende Warnmeldung generiert:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    Die Warnung wird generiert, weil die Namensfelder nun auf 50 Zeichen beschränkt sind. Wenn der Name der Datenbank aus mehr als 50 Zeichen besteht, gehen alle Zeichen ab dem 51. verloren.

  • Öffnen Sie die Tabelle „Student“ im SSOX:

    Students table in SSOX after migrations

    Bevor die Migration angewendet wurde, wiesen die Namensspalten den Typ nvarchar(MAX) auf. Die Namensspalten weisen nun den Typ nvarchar(50) auf. Der Spaltenname wurde von FirstMidName in FirstName geändert.

  • Führen Sie die Anwendung aus. Wechseln Sie zur Studentenseite.
  • Beachten Sie, dass die Uhrzeiten nicht mit Datumsangaben eingegeben oder angezeigt werden.
  • Klicken Sie auf Neu erstellen, und versuchen Sie, einen Namen einzugeben, der länger als 50 Zeichen ist.

Hinweis

In den folgenden Abschnitten werden beim Erstellen der App in einigen Phasen Compilerfehler generiert. Diese Anweisungen präzisieren, wann die App erstellt werden soll.

Die Instructor-Entität

Erstellen Sie Models/Instructor.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<Course> Courses { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

In einer Zeile können mehrere Attribute enthalten sein. Das HireDate-Attribut kann folgendermaßen geschrieben werden:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Bei den Eigenschaften Courses und OfficeAssignment handelt es sich um Navigationseigenschaften.

Da ein Dozent eine beliebige Anzahl von Kursen geben kann, wird Courses als Auflistung definiert.

public ICollection<Course> Courses { get; set; }

Ein Dozent kann höchstens ein Büro haben, sodass die OfficeAssignment-Eigenschaft eine einzelne OfficeAssignment-Entität enthält. OfficeAssignment ist NULL, wenn kein Büro zugewiesen ist.

public OfficeAssignment OfficeAssignment { get; set; }

Die Entität „OfficeAssignment“

OfficeAssignment entity

Erstellen Sie Models/OfficeAssignment.cs mit dem folgenden Code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Das Key-Attribut

Das [Key]-Attribut wird verwendet, um eine Eigenschaft als Primärschlüssel zu identifizieren, wenn der Eigenschaftsname nicht classnameID oder ID entspricht.

Es besteht eine 1:0..1-Beziehung zwischen der Instructor- und der OfficeAssignment-Entität. Eine Bürozuweisung ist nur in Beziehung zu dem Dozenten vorhanden, dem sie zugewiesen ist. Der Primärschlüssel OfficeAssignment ist ebenfalls der Fremdschlüssel der Instructor-Entität. Eine 1:0..1-Beziehung liegt vor, wenn ein Primärschlüssel in einer Tabelle sowohl ein Primärschlüssel als auch ein Fremdschlüssel in einer anderen Tabelle ist.

EF Core kann InstructorID nicht automatisch als Primärschlüssel von OfficeAssignment erkennen, da InstructorID nicht den ID- oder classnameID-Benennungskonventionen folgt. Deshalb wird das Key-Attribut verwendet, um InstructorID als Primärschlüssel zu erkennen:

[Key]
public int InstructorID { get; set; }

Standardmäßig behandelt EF Core den Schlüssel nicht als datenbankgeneriert, da die Spalte für eine identifizierende Beziehung vorgesehen ist. Weitere Informationen finden Sie unter EF-Schlüssel.

Die Navigationseigenschaft „Instructor“

Die Instructor.OfficeAssignment-Navigationseigenschaft kann NULL sein, da möglicherweise keine OfficeAssignment-Zeile für einen bestimmten Dozenten vorhanden ist. Einem Dozenten möglicherweise kein Büro zugewiesen ist.

Die OfficeAssignment.Instructor-Navigationseigenschaft hat immer eine Instructor-Entität, da der Typ InstructorID des Fremdschlüssels int ist, ein Typ, der keine NULL-Werte annehmen kann. Eine Bürozuweisung nicht ohne einen Dozenten vorhanden sein kann.

Wenn die Entität Instructor mit der Entität OfficeAssignment verknüpft ist, besitzt jede Entität in den Navigationseigenschaften einen Verweis auf die andere.

Die Entität „Course“

Aktualisieren Sie Models/Course.cs mit folgendem Code:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<Instructor> Instructors { get; set; }
    }
}

Die Course-Entität besitzt die Fremdschlüsseleigenschaft DepartmentID. DepartmentID zeigt auf die verknüpfte Department-Entität. Die Course-Entität besitzt eine Department-Navigationseigenschaft.

EF Core erfordert keine Fremdschlüsseleigenschaft für ein Datenmodell, wenn das Modell über eine Navigationseigenschaft für eine verknüpfte Entität verfügt. EF Core erstellt an den erforderlichen Stellen automatisch Fremdschlüssel in der Datenbank. EF Core erstellt Schatteneigenschaften für automatisch erstellte Fremdschlüssel. Durch explizites Einschließen des Fremdschlüssels in das Datenmodell können Updates jedoch einfacher und effizienter durchgeführt werden. Betrachten Sie beispielsweise ein Modell, bei dem die Fremdschlüsseleigenschaft DepartmentIDnicht enthalten ist. Folgendes wird durchgeführt, wenn eine Course-Entität zum Bearbeiten abgerufen wird:

  • Die Department-Eigenschaft ist null, wenn sie nicht explizit geladen wird.
  • Die Department-Entität muss abgerufen werden, um die Course-Entität zu aktualisieren.

Wenn die Fremdschlüsseleigenschaft DepartmentID im Datenmodell enthalten ist, muss die Department-Entität vor einem Update nicht abgerufen werden.

Das Attribut „DatabaseGenerated“

Das [DatabaseGenerated(DatabaseGeneratedOption.None)]-Attribut gibt an, dass der Primärschlüssel von der Anwendung bereitgestellt wird, statt von der Datenbank generiert zu werden.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Standardmäßig geht EF Core davon aus, dass Primärschlüsselwerte von der Datenbank generiert werden. Bei von der Datenbank generierten Primärschlüsselwerten handelt es sich in der Regel um die beste Herangehensweise. Bei Course-Entitäten wird der Primärschlüssel vom Benutzer angegeben, z.B. eine Kursnummer wie eine 1000er-Reihe für den Fachbereich Mathematik, eine 2000er-Reihe für den Fachbereich Englisch usw.

Das DatabaseGenerated-Attribut kann ebenfalls zum Generieren von Standardwerten verwendet werden. Die Datenbank kann beispielsweise automatisch ein Datenfeld generieren, um das Datum aufzuzeichnen, an dem eine Zeile erstellt oder aktualisiert wurde. Weitere Informationen finden Sie unter Generated Properties (Generierte Eigenschaften).

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften in der Entität Course stellen folgende Beziehungen dar:

Ein Kurs ist einem Fachbereich zugeordnet, es gibt also eine DepartmentID-Fremdschlüsseleigenschaft und eine Department-Navigationseigenschaft.

public int DepartmentID { get; set; }
public Department Department { get; set; }

In einem Kurs können beliebig viele Studenten angemeldet sein, darum stellt die Enrollments-Navigationseigenschaft eine Auflistung dar:

public ICollection<Enrollment> Enrollments { get; set; }

Ein Kurs kann von mehreren Dozenten unterrichtet werden, darum stellt die Instructors-Navigationseigenschaft eine Auflistung dar:

public ICollection<Instructor> Instructors { get; set; }

Entität „Department“

Erstellen Sie Models/Department.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",
                       ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Das Column-Attribut

Zuvor wurde das Column-Attribut verwendet, um die Zuordnung des Spaltennamens zu ändern. Im Code für die Department-Entität wird das Column-Attribut verwendet, um die Zuordnung des SQL-Datentyps zu ändern. Die Budget-Spalte wird mithilfe des SQL Server-Typs „money“ in der Datenbank definiert:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Die Spaltenzuordnung ist im Allgemeinen nicht erforderlich. EF Core wählt den geeigneten SQL Server-Datentyp basierend auf dem CLR-Typ der Eigenschaft aus. Der CLR-Typ decimal wird dem SQL Server-Typ decimal zugeordnet. Bei Budget wird eine Währung verwendet, und der Datentyp „money“ ist für Währungen besser geeignet.

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

  • Ein Fachbereich kann einen Administrator besitzen oder nicht.
  • Bei einem Administrator handelt es sich immer um einen Dozenten. Aus diesem Grund wird die InstructorID-Eigenschaft als Fremdschlüssel zur Instructor-Entität hinzugefügt.

Die Navigationseigenschaft heißt Administrator, enthält jedoch eine Instructor-Entität:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Das ? im Code oben gibt an, dass die Eigenschaft Nullwerte zulassend ist.

Eine Abteilung kann viele Kurse aufweisen, darum gibt es die Navigationseigenschaft „Courses“:

public ICollection<Course> Courses { get; set; }

Gemäß der Konvention aktiviert EF Core das kaskadierende Delete für nicht auf NULL festlegbare Fremdschlüssel und für m:n-Beziehungen. Das Standardverhalten kann zu Zirkelregeln für kaskadierende Löschvorgänge führen. Durch Zirkelregeln für kaskadierende Löschvorgänge wird eine Ausnahme ausgelöst, wenn eine Migration hinzugefügt wird.

Wenn die Department.InstructorID-Eigenschaft beispielsweise als nicht auf NULL festlegbare Eigenschaft definiert wurde, konfiguriert EF Core eine Löschregel für kaskadierende Löschvorgänge. In diesem Fall würde der Fachbereich gelöscht, wenn der als Administrator zugewiesene Dozent gelöscht wird. In diesem Szenario wäre eine Einschränkungsregel sinnvoller. Die folgende Fluent-API würde eine Einschränkungsregel festlegen und das kaskadierende Delete deaktivieren.

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Enrollment-Fremdschlüssel und -Navigationseigenschaften

Ein Enrollment-Datensatz gilt für einen Kurs, der von einem Studenten besucht wird.

Enrollment entity

Aktualisieren Sie Models/Enrollment.cs mit folgendem Code:

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

Ein Anmeldungsdatensatz gilt für einen einzelnen Kurs, sodass es eine CourseID-Fremdschlüsseleigenschaft und eine Course-Navigationseigenschaft gibt:

public int CourseID { get; set; }
public Course Course { get; set; }

Ein Anmeldungsdatensatz gilt für einen einzelnen Studenten, sodass es eine StudentID-Fremdschlüsseleigenschaft und eine Student-Navigationseigenschaft gibt:

public int StudentID { get; set; }
public Student Student { get; set; }

n:n-Beziehungen

Es besteht eine m:n-Beziehung zwischen der Student- und der Course-Entität. Die Enrollment-Entität fungiert in der Datenbank als m:n-Jointabelle mit Nutzdaten. Mit Nutzdaten bedeutet, dass die Enrollment-Tabelle außer den Fremdschlüsseln für die verknüpften Tabellen zusätzliche Daten enthält. In der Enrollment-Entität sind die zusätzlichen Daten neben Fremdschlüsseln der Primärschlüssel und Grade.

Die folgende Abbildung stellt dar, wie diese Beziehungen in einem Entitätsdiagramm aussehen. (Dieses Diagramm wurde mit EF Power Tools für EF 6.x erstellt. Die Erstellung des Diagramms ist nicht Teil des Tutorials.)

Student-Course many to many relationship

Jede Beziehung weist an einem Ende „1“ und am anderen Ende „*“ auf, wodurch eine 1:n-Beziehung dargestellt wird.

Wenn in der Tabelle Enrollment nicht die Information „Grade“ enthalten wäre, müsste diese nur die beiden Fremdschlüssel (CourseID und StudentID) enthalten. Eine m:n-Jointabelle ohne Nutzlast wird manchmal als „reine Jointabelle“ bezeichnet.

Zwischen den Entitäten Instructor und Course besteht eine m:n-Beziehung mithilfe einer PJT (reine Jointabelle).

Aktualisieren des Datenbankkontexts

Aktualisieren Sie Data/SchoolContext.cs mit folgendem Code:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable(nameof(Course))
                .HasMany(c => c.Instructors)
                .WithMany(i => i.Courses);
            modelBuilder.Entity<Student>().ToTable(nameof(Student));
            modelBuilder.Entity<Instructor>().ToTable(nameof(Instructor));
        }
    }
}

Der vorstehende Code fügt die neuen Entitäten hinzu und konfiguriert die M:N-Beziehung zwischen den Entitäten Instructor und Course.

Fluent-API-Alternativen für Attribute

Die OnModelCreating-Methode im vorangehenden Code verwendet die Fluent-API zum Konfigurieren des Verhaltens von EF Core. Die API wird als „Fluent“ bezeichnet, da sie häufig durch das Verketten mehrerer Methodenaufrufe zu einer einzigen Anweisung verwendet wird. Der folgende Code veranschaulicht die Fluent-API beispielhaft:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

In diesem Tutorial wird die Fluent-API nur für die Datenbankzuordnung verwendet, die nicht mit Attributen erfolgen kann. Die Fluent-API kann jedoch den Großteil der Regeln für Formatierung, Validierung und Zuordnung angeben, die mithilfe von Attributen festgelegt werden können.

Manche Attribute, z.B. MinimumLength, können nicht mit der Fluent-API angewendet werden. Durch MinimumLength wird das Schema nicht geändert, sondern lediglich eine Validierungsregel für die mindestens erforderliche Länge angewendet.

Einige Entwickler bevorzugen die exklusive Verwendung der Fluent-API, um ihre Entitätsklassen sauber zu halten. Attribute und die Fluent-API können gemeinsam verwendet werden. Es gibt einige Konfigurationen, die nur mit der Fluent-API vorgenommen werden können (z. B. das Angeben eines zusammengesetzten Primärschlüssels). Es gibt einige Konfigurationen, die nur mit Attributen (MinimumLength) vorgenommen werden können. Folgende Vorgehensweise wird für die Verwendung der Fluent-API oder von Attributen empfohlen:

  • Wählen Sie einen der beiden Ansätze aus.
  • Verwenden Sie den ausgewählten Ansatz so konsistent wie möglich.

Einige der in diesem Tutorial verwendeten Attribute werden für Folgendes verwendet:

  • Nur für die Validierung (z.B. MinimumLength)
  • Nur für die Konfiguration von EF Core (z. B. HasKey)
  • Für die Validierung und die Konfiguration von EF Core (z. B. [StringLength(50)])

Weitere Informationen zu Attributen und Fluent-API im Vergleich finden Sie unter Methods of configuration (Konfigurationsmethoden).

Ausführen eines Seedings für die Datenbank

Aktualisieren Sie den Code in Data/DbInitializer.cs:

using ContosoUniversity.Models;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var alexander = new Student
            {
                FirstMidName = "Carson",
                LastName = "Alexander",
                EnrollmentDate = DateTime.Parse("2016-09-01")
            };

            var alonso = new Student
            {
                FirstMidName = "Meredith",
                LastName = "Alonso",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var anand = new Student
            {
                FirstMidName = "Arturo",
                LastName = "Anand",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var barzdukas = new Student
            {
                FirstMidName = "Gytis",
                LastName = "Barzdukas",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var li = new Student
            {
                FirstMidName = "Yan",
                LastName = "Li",
                EnrollmentDate = DateTime.Parse("2018-09-01")
            };

            var justice = new Student
            {
                FirstMidName = "Peggy",
                LastName = "Justice",
                EnrollmentDate = DateTime.Parse("2017-09-01")
            };

            var norman = new Student
            {
                FirstMidName = "Laura",
                LastName = "Norman",
                EnrollmentDate = DateTime.Parse("2019-09-01")
            };

            var olivetto = new Student
            {
                FirstMidName = "Nino",
                LastName = "Olivetto",
                EnrollmentDate = DateTime.Parse("2011-09-01")
            };

            var students = new Student[]
            {
                alexander,
                alonso,
                anand,
                barzdukas,
                li,
                justice,
                norman,
                olivetto
            };

            context.AddRange(students);

            var abercrombie = new Instructor
            {
                FirstMidName = "Kim",
                LastName = "Abercrombie",
                HireDate = DateTime.Parse("1995-03-11")
            };

            var fakhouri = new Instructor
            {
                FirstMidName = "Fadi",
                LastName = "Fakhouri",
                HireDate = DateTime.Parse("2002-07-06")
            };

            var harui = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Harui",
                HireDate = DateTime.Parse("1998-07-01")
            };

            var kapoor = new Instructor
            {
                FirstMidName = "Candace",
                LastName = "Kapoor",
                HireDate = DateTime.Parse("2001-01-15")
            };

            var zheng = new Instructor
            {
                FirstMidName = "Roger",
                LastName = "Zheng",
                HireDate = DateTime.Parse("2004-02-12")
            };

            var instructors = new Instructor[]
            {
                abercrombie,
                fakhouri,
                harui,
                kapoor,
                zheng
            };

            context.AddRange(instructors);

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    Instructor = fakhouri,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    Instructor = harui,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    Instructor = kapoor,
                    Location = "Thompson 304" }
            };

            context.AddRange(officeAssignments);

            var english = new Department
            {
                Name = "English",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = abercrombie
            };

            var mathematics = new Department
            {
                Name = "Mathematics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = fakhouri
            };

            var engineering = new Department
            {
                Name = "Engineering",
                Budget = 350000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = harui
            };

            var economics = new Department
            {
                Name = "Economics",
                Budget = 100000,
                StartDate = DateTime.Parse("2007-09-01"),
                Administrator = kapoor
            };

            var departments = new Department[]
            {
                english,
                mathematics,
                engineering,
                economics
            };

            context.AddRange(departments);

            var chemistry = new Course
            {
                CourseID = 1050,
                Title = "Chemistry",
                Credits = 3,
                Department = engineering,
                Instructors = new List<Instructor> { kapoor, harui }
            };

            var microeconomics = new Course
            {
                CourseID = 4022,
                Title = "Microeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var macroeconmics = new Course
            {
                CourseID = 4041,
                Title = "Macroeconomics",
                Credits = 3,
                Department = economics,
                Instructors = new List<Instructor> { zheng }
            };

            var calculus = new Course
            {
                CourseID = 1045,
                Title = "Calculus",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { fakhouri }
            };

            var trigonometry = new Course
            {
                CourseID = 3141,
                Title = "Trigonometry",
                Credits = 4,
                Department = mathematics,
                Instructors = new List<Instructor> { harui }
            };

            var composition = new Course
            {
                CourseID = 2021,
                Title = "Composition",
                Credits = 3,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var literature = new Course
            {
                CourseID = 2042,
                Title = "Literature",
                Credits = 4,
                Department = english,
                Instructors = new List<Instructor> { abercrombie }
            };

            var courses = new Course[]
            {
                chemistry,
                microeconomics,
                macroeconmics,
                calculus,
                trigonometry,
                composition,
                literature
            };

            context.AddRange(courses);

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    Student = alexander,
                    Course = chemistry,
                    Grade = Grade.A
                },
                new Enrollment {
                    Student = alexander,
                    Course = microeconomics,
                    Grade = Grade.C
                },
                new Enrollment {
                    Student = alexander,
                    Course = macroeconmics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = calculus,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = trigonometry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = alonso,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = anand,
                    Course = chemistry
                },
                new Enrollment {
                    Student = anand,
                    Course = microeconomics,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = barzdukas,
                    Course = chemistry,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = li,
                    Course = composition,
                    Grade = Grade.B
                },
                new Enrollment {
                    Student = justice,
                    Course = literature,
                    Grade = Grade.B
                }
            };

            context.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

Der vorangehende Code stellt Startwertdaten für die neuen Entitäten bereit. Durch diesen Code werden überwiegend neue Entitätsobjekte erstellt und Beispieldaten geladen. Die Beispieldaten werden für Tests verwendet.

Anwenden der Migration oder Löschen und erneutes Erstellen

Bei der vorhandenen Datenbank gibt es zwei Ansätze zum Ändern der Datenbank:

Beide Optionen funktionieren für SQL Server. Obwohl die apply-migration-Methode komplexer und zeitaufwendiger ist, ist dies in der Praxis die bevorzugte Methode für Produktionsumgebungen.

Löschen und Neuerstellen der Datenbank

Löschen und aktualisieren Sie die Datenbank, um EF Core zum Erstellen einer neuen Datenbank zu zwingen:

  • Löschen Sie den Ordner Migrations.
  • Führen Sie die folgenden Befehle in der Paket-Manager-Konsole (PMC) aus:
Drop-Database
Add-Migration InitialCreate
Update-Database

Führen Sie die App aus. Durch das Ausführen der App wird die DbInitializer.Initialize-Methode ausgeführt. Die DbInitializer.Initialize-Methode füllt die neue Datenbank mit Daten auf.

Öffnen Sie die Datenbank in SSOX:

  • Wenn der SSOX zuvor schon geöffnet war, klicken Sie auf die Schaltfläche Aktualisieren.
  • Erweitern Sie den Knoten Tabellen. Die erstellten Tabellen werden angezeigt.

Nächste Schritte

In den nächsten beiden Tutorials wird gezeigt, wie verwandte Daten gelesen und aktualisiert werden.

In den vorherigen Tutorials wurde mit einem einfachen Datenmodell gearbeitet, das aus drei Entitäten bestand. In diesem Tutorial wird Folgendes durchgeführt:

  • Weitere Entitäten und Beziehungen werden hinzugefügt.
  • Das Datenmodell wird angepasst, indem Regeln zur Formatierung, Validierung und Datenbankzuordnung angegeben werden.

Das vollständige Datenmodell wird in der folgenden Abbildung dargestellt:

Entity diagram

Die Entität „Student“

Student entity

Ersetzen Sie den Code in Models/Student.cs durch folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Der Code oben fügt eine FullName-Eigenschaft hinzu und fügt den vorhandenen Eigenschaften die folgenden Attribute hinzu:

  • [DataType]
  • [DisplayFormat]
  • [StringLength]
  • [Column]
  • [Required]
  • [Display]

Die berechnete FullName-Eigenschaft

Bei FullName handelt es sich um eine berechnete Eigenschaft, die einen Wert zurückgibt, der durch das Verketten von zwei weiteren Eigenschaften erstellt wird. FullName kann nicht festgelegt werden und verfügt daher lediglich über einen get-Accessor. In der Datenbank wird keine FullName-Spalte erstellt.

Das DataType-Attribut

[DataType(DataType.Date)]

Für die Anmeldedaten von Studenten zeigen alle Webseiten derzeit die Zeit und das Datum an, obwohl nur das Datum relevant ist. Indem Sie Attribute für die Datenanmerkung verwenden, können Sie eine Codeänderungen vornehmen, durch die das Anzeigeformat auf jeder Seite korrigiert wird, in der die Daten angezeigt werden.

Das DataType-Attribut gibt einen Datentyp an, der spezifischer als der datenbankinterne Typ ist. In diesem Fall sollte nur das Datum angezeigt werden, nicht das Datum und die Uhrzeit. Die DataType-Enumeration stellt viele Datentypen bereit, wie z.B. „Date“, „Time“, „PhoneNumber“, „Currency“, „EmailAddress“. Das DataType-Attribut kann der App auch das Bereitstellen typspezifischer Features ermöglichen. Zum Beispiel:

  • Der Link mailto: wird automatisch für DataType.EmailAddress erstellt.
  • Die Datumsauswahl für DataType.Date wird in den meisten Browsern bereitgestellt.

Das DataType-Attribut gibt data--HTML5-Attribute (ausgesprochen „Datadash“) aus. Die DataType-Attribute bieten keine Validierung.

Das DisplayFormat-Attribut

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

DataType.Date gibt nicht das Format des Datums an, das angezeigt wird. Standardmäßig wird das Datumsfeld gemäß den Standardformaten basierend auf der CultureInfo-Klasse des Servers angezeigt.

Das DisplayFormat-Attribut dient zum expliziten Angeben des Datumsformats. Die ApplyFormatInEditMode-Einstellung gibt an, dass die Formatierung ebenfalls auf die Benutzeroberfläche für die Bearbeitung angewendet wird. Einige Felder sollten ApplyFormatInEditMode nicht verwenden. Das Währungssymbol sollte beispielsweise nicht im Textfeld für die Bearbeitung angezeigt werden.

Das DisplayFormat-Attribut kann eigenständig verwendet werden. Meist empfiehlt es sich, das DataType-Attribut mit dem DisplayFormat-Attribut zu verwenden. Das DataType-Attribut übermittelt die Semantik der Daten im Gegensatz zu deren Rendern auf einem Bildschirm. Das DataType-Attribut bietet folgende Vorteile, die in DisplayFormat nicht verfügbar sind:

  • Der Browser kann HTML5-Features aktivieren. Dazu zählen beispielsweise das Anzeigen eines Kalendersteuerelements, des dem Gebietsschema entsprechenden Währungssymbols, von E-Mail-Links und einigen clientseitigen Eingabevalidierungen.
  • Standardmäßig rendert der Browser Daten mit dem auf Ihrem Gebietsschema basierenden richtigen Format.

Weitere Informationen finden Sie in der Dokumentation zum <input>-Taghilfsprogramm.

Das StringLength-Attribut

[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]

Die Regeln für die Datenvalidierung und Meldungen für Validierungsfehler können mithilfe von Attributen angegeben werden. Das StringLength-Attribut gibt den mindestens erforderlichen und maximal zulässigen Wert für die Zeichenlänge eines Datenfelds an. Der gezeigte Code beschränkt Namen auf maximal 50 Zeichen. Ein Beispiel, das die minimale Zeichenfolgenlänge festlegt, wird später gezeigt.

Das StringLength-Attribut stellt ebenfalls die clientseitige und serverseitige Validierung bereit. Der mindestens erforderliche Wert hat keine Auswirkungen auf das Datenbankschema.

Das StringLength-Attribut verhindert nicht, dass ein Benutzer einen Leerraum als Namen eingibt. Das Attribut RegularExpression kann verwendet werden, um Einschränkungen auf die Eingabe anzuwenden. Folgender Code erfordert beispielsweise, dass das erste Zeichen ein Großbuchstabe sein muss und die restlichen Zeichen alphabetisch sein müssen:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

Öffnen Sie im SQL Server-Objekt-Explorer (SSOX) den Tabellen-Designer „Student“, indem Sie auf die Tabelle Student doppelklicken.

Students table in SSOX before migrations

In der vorangehenden Abbildung wird das Schema der Student-Tabelle dargestellt. Die Namensfelder weisen den Typ nvarchar(MAX) auf. Wenn eine Migration später in diesem Tutorial erstellt und angewendet wird, werden die Namensfelder nvarchar(50) als Ergebnis der Attribute für die Zeichenfolgenlänge.

Das Column-Attribut

[Column("FirstName")]
public string FirstMidName { get; set; }

Durch Attribute kann gesteuert werden, wie Ihre Klassen und Eigenschaften der Datenbank zugeordnet werden. Im Student-Modell wird das Column-Attribut verwendet, um den Namen der FirstMidName-Eigenschaft in der Datenbank auf „FirstName“ festzulegen.

Wenn die Datenbank erstellt wird, werden die Eigenschaftsnamen des Moduls für Spaltennamen verwendet (außer bei Verwendung des Column-Attributs). Das Student-Modell verwendet FirstMidName für das Feld „first-name“, da dieses ebenfalls einen Zweitnamen enthalten kann.

Mit dem [Column]-Attribut wird Student.FirstMidName im Datenmodell der Spalte FirstName in der Tabelle Student zugeordnet. Durch das Hinzufügen des Column-Attributs wird das Modell geändert, das SchoolContext unterstützt. Das Modell, das SchoolContext unterstützt, stimmt nicht mehr mit der Datenbank überein. Diese Diskrepanz wird durch Hinzufügen einer Migration zu einem späteren Zeitpunkt in diesem Tutorial aufgelöst.

Das Attribut „Required“

[Required]

Durch das Required-Attribut werden die Name-Eigenschaften zu Pflichtfeldern. Das Required-Attribut ist für Typen, die keine NULL-Werte annehmen können (etwas Werttypen, z.B. DateTime, int und double) nicht erforderlich. Typen, die nicht auf NULL festgelegt werden können, werden automatisch als Pflichtfelder behandelt.

Das Required-Attribut muss mit MinimumLength verwendet werden, damit die MinimumLength erzwungen wird.

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLength und Required lassen Leerzeichen zu, um die Überprüfung zu bestehen. Verwenden Sie das RegularExpression-Attribut, um vollständige Kontrolle über die Zeichenfolge zu erhalten.

Das Display-Attribut

[Display(Name = "Last Name")]

Das Display-Attribut gibt an, dass die Beschriftung der Textfelder „First Name“ (Vorname), „Last Name“ (Nachname), „Full Name“ (Vollständiger Name) und „Enrollment Date“ (Registrierungsdatum) lauten soll. Die Standardbeschriftungen hatten kein Leerzeichen zwischen den Wörtern, wie zum Beispiel „Lastname“.

Erstellen einer Migration

Führen Sie die Anwendung aus. Wechseln Sie zur Studentenseite. Es wird eine Ausnahme ausgelöst. Das [Column]-Attribut bewirkt, dass EF eine Spalte mit dem Namen FirstName erwartet, aber der Spaltenname in der Datenbank ist weiterhin FirstMidName.

Die Fehlermeldung lautet ungefähr wie im folgenden Beispiel:

SqlException: Invalid column name 'FirstName'.
  • Geben Sie in der PMC folgende Befehle ein, um eine neue Migration zu erstellen und die Datenbank zu aktualisieren:

    Add-Migration ColumnFirstName
    Update-Database
    

    Mit dem ersten dieser Befehle wird die folgende Warnmeldung generiert:

    An operation was scaffolded that may result in the loss of data.
    Please review the migration for accuracy.
    

    Die Warnung wird generiert, weil die Namensfelder nun auf 50 Zeichen beschränkt sind. Wenn der Name der Datenbank aus mehr als 50 Zeichen besteht, gehen alle Zeichen ab dem 51. verloren.

  • Öffnen Sie die Tabelle „Student“ im SSOX:

    Students table in SSOX after migrations

    Bevor die Migration angewendet wurde, wiesen die Namensspalten den Typ nvarchar(MAX) auf. Die Namensspalten weisen nun den Typ nvarchar(50) auf. Der Spaltenname wurde von FirstMidName in FirstName geändert.

  • Führen Sie die Anwendung aus. Wechseln Sie zur Studentenseite.
  • Beachten Sie, dass die Uhrzeiten nicht mit Datumsangaben eingegeben oder angezeigt werden.
  • Klicken Sie auf Neu erstellen, und versuchen Sie, einen Namen einzugeben, der länger als 50 Zeichen ist.

Hinweis

In den folgenden Abschnitten werden beim Erstellen der App in einigen Phasen Compilerfehler generiert. Diese Anweisungen präzisieren, wann die App erstellt werden soll.

Die Instructor-Entität

Instructor entity

Erstellen Sie Models/Instructor.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

In einer Zeile können mehrere Attribute enthalten sein. Das HireDate-Attribut kann folgendermaßen geschrieben werden:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Bei den Eigenschaften CourseAssignments und OfficeAssignment handelt es sich um Navigationseigenschaften.

Da ein Dozent eine beliebige Anzahl von Kursen geben kann, wird CourseAssignments als Auflistung definiert.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Ein Dozent kann höchstens ein Büro haben, sodass die OfficeAssignment-Eigenschaft eine einzelne OfficeAssignment-Entität enthält. OfficeAssignment ist NULL, wenn kein Büro zugewiesen ist.

public OfficeAssignment OfficeAssignment { get; set; }

Die Entität „OfficeAssignment“

OfficeAssignment entity

Erstellen Sie Models/OfficeAssignment.cs mit dem folgenden Code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Das Key-Attribut

Das [Key]-Attribut wird verwendet, um eine Eigenschaft als Primärschlüssel zu identifizieren, wenn der Eigenschaftsname nicht „classnameID“ oder „ID“ entspricht.

Es besteht eine 1:0..1-Beziehung zwischen der Instructor- und der OfficeAssignment-Entität. Eine Bürozuweisung ist nur in Beziehung zu dem Dozenten vorhanden, dem sie zugewiesen ist. Der Primärschlüssel OfficeAssignment ist ebenfalls der Fremdschlüssel der Instructor-Entität.

EF Core kann InstructorID nicht automatisch als Primärschlüssel von OfficeAssignment erkennen, da InstructorID nicht den ID- oder classnameID-Benennungskonventionen folgt. Deshalb wird das Key-Attribut verwendet, um InstructorID als Primärschlüssel zu erkennen:

[Key]
public int InstructorID { get; set; }

Standardmäßig behandelt EF Core den Schlüssel nicht als datenbankgeneriert, da die Spalte für eine identifizierende Beziehung vorgesehen ist.

Die Navigationseigenschaft „Instructor“

Die Instructor.OfficeAssignment-Navigationseigenschaft kann NULL sein, da möglicherweise keine OfficeAssignment-Zeile für einen bestimmten Dozenten vorhanden ist. Einem Dozenten möglicherweise kein Büro zugewiesen ist.

Die OfficeAssignment.Instructor-Navigationseigenschaft hat immer eine Instructor-Entität, da der Typ InstructorID des Fremdschlüssels int ist, ein Typ, der keine NULL-Werte annehmen kann. Eine Bürozuweisung nicht ohne einen Dozenten vorhanden sein kann.

Wenn die Entität Instructor mit der Entität OfficeAssignment verknüpft ist, besitzt jede Entität in den Navigationseigenschaften einen Verweis auf die andere.

Die Entität „Course“

Course entity

Aktualisieren Sie Models/Course.cs mit folgendem Code:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Die Course-Entität besitzt die Fremdschlüsseleigenschaft DepartmentID. DepartmentID zeigt auf die verknüpfte Department-Entität. Die Course-Entität besitzt eine Department-Navigationseigenschaft.

EF Core erfordert keine Fremdschlüsseleigenschaft für ein Datenmodell, wenn das Modell über eine Navigationseigenschaft für eine verknüpfte Entität verfügt. EF Core erstellt an den erforderlichen Stellen automatisch Fremdschlüssel in der Datenbank. EF Core erstellt Schatteneigenschaften für automatisch erstellte Fremdschlüssel. Durch explizites Einschließen des Fremdschlüssels in das Datenmodell können Updates jedoch einfacher und effizienter durchgeführt werden. Betrachten Sie beispielsweise ein Modell, bei dem die Fremdschlüsseleigenschaft DepartmentIDnicht enthalten ist. Folgendes wird durchgeführt, wenn eine Course-Entität zum Bearbeiten abgerufen wird:

  • Die Department-Eigenschaft ist NULL, wenn diese nicht explizit geladen wird.
  • Die Department-Entität muss abgerufen werden, um die Course-Entität zu aktualisieren.

Wenn die Fremdschlüsseleigenschaft DepartmentID im Datenmodell enthalten ist, muss die Department-Entität vor einem Update nicht abgerufen werden.

Das Attribut „DatabaseGenerated“

Das [DatabaseGenerated(DatabaseGeneratedOption.None)]-Attribut gibt an, dass der Primärschlüssel von der Anwendung bereitgestellt wird, statt von der Datenbank generiert zu werden.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Standardmäßig geht EF Core davon aus, dass Primärschlüsselwerte von der Datenbank generiert werden. Bei von der Datenbank generierten Primärschlüsselwerten handelt es sich in der Regel um die beste Herangehensweise. Bei Course-Entitäten wird der Primärschlüssel vom Benutzer angegeben, z.B. eine Kursnummer wie eine 1000er-Reihe für den Fachbereich Mathematik, eine 2000er-Reihe für den Fachbereich Englisch usw.

Das DatabaseGenerated-Attribut kann ebenfalls zum Generieren von Standardwerten verwendet werden. Die Datenbank kann beispielsweise automatisch ein Datenfeld generieren, um das Datum aufzuzeichnen, an dem eine Zeile erstellt oder aktualisiert wurde. Weitere Informationen finden Sie unter Generated Properties (Generierte Eigenschaften).

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften in der Entität Course stellen folgende Beziehungen dar:

Ein Kurs ist einem Fachbereich zugeordnet, es gibt also eine DepartmentID-Fremdschlüsseleigenschaft und eine Department-Navigationseigenschaft.

public int DepartmentID { get; set; }
public Department Department { get; set; }

In einem Kurs können beliebig viele Studenten angemeldet sein, darum stellt die Enrollments-Navigationseigenschaft eine Auflistung dar:

public ICollection<Enrollment> Enrollments { get; set; }

Ein Kurs kann von mehreren Dozenten unterrichtet werden, darum stellt die CourseAssignments-Navigationseigenschaft eine Auflistung dar:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment wird später erläutert.

Entität „Department“

Department entity

Erstellen Sie Models/Department.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Das Column-Attribut

Zuvor wurde das Column-Attribut verwendet, um die Zuordnung des Spaltennamens zu ändern. Im Code für die Department-Entität wird das Column-Attribut verwendet, um die Zuordnung des SQL-Datentyps zu ändern. Die Budget-Spalte wird mithilfe des SQL Server-Typs „money“ in der Datenbank definiert:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Die Spaltenzuordnung ist im Allgemeinen nicht erforderlich. EF Core wählt den geeigneten SQL Server-Datentyp basierend auf dem CLR-Typ der Eigenschaft aus. Der CLR-Typ decimal wird dem SQL Server-Typ decimal zugeordnet. Bei Budget wird eine Währung verwendet, und der Datentyp „money“ ist für Währungen besser geeignet.

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

  • Ein Fachbereich kann einen Administrator besitzen oder nicht.
  • Bei einem Administrator handelt es sich immer um einen Dozenten. Aus diesem Grund wird die InstructorID-Eigenschaft als Fremdschlüssel zur Instructor-Entität hinzugefügt.

Die Navigationseigenschaft heißt Administrator, enthält jedoch eine Instructor-Entität:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Das Fragezeichen (?) im vorangehenden Code gibt an, dass die Eigenschaft auf NULL festlegbar ist.

Eine Abteilung kann viele Kurse aufweisen, darum gibt es die Navigationseigenschaft „Courses“:

public ICollection<Course> Courses { get; set; }

Gemäß der Konvention aktiviert EF Core das kaskadierende Delete für nicht auf NULL festlegbare Fremdschlüssel und für m:n-Beziehungen. Das Standardverhalten kann zu Zirkelregeln für kaskadierende Löschvorgänge führen. Durch Zirkelregeln für kaskadierende Löschvorgänge wird eine Ausnahme ausgelöst, wenn eine Migration hinzugefügt wird.

Wenn die Department.InstructorID-Eigenschaft beispielsweise als nicht auf NULL festlegbare Eigenschaft definiert wurde, konfiguriert EF Core eine Löschregel für kaskadierende Löschvorgänge. In diesem Fall würde der Fachbereich gelöscht, wenn der als Administrator zugewiesene Dozent gelöscht wird. In diesem Szenario wäre eine Einschränkungsregel sinnvoller. Die folgende Fluent-API würde eine Einschränkungsregel festlegen und das kaskadierende Delete deaktivieren.

modelBuilder.Entity<Department>()
   .HasOne(d => d.Administrator)
   .WithMany()
   .OnDelete(DeleteBehavior.Restrict)

Die Entität „Enrollment“

Ein Enrollment-Datensatz gilt für einen Kurs, der von einem Studenten besucht wird.

Enrollment entity

Aktualisieren Sie Models/Enrollment.cs mit folgendem Code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

Ein Anmeldungsdatensatz gilt für einen einzelnen Kurs, sodass es eine CourseID-Fremdschlüsseleigenschaft und eine Course-Navigationseigenschaft gibt:

public int CourseID { get; set; }
public Course Course { get; set; }

Ein Anmeldungsdatensatz gilt für einen einzelnen Studenten, sodass es eine StudentID-Fremdschlüsseleigenschaft und eine Student-Navigationseigenschaft gibt:

public int StudentID { get; set; }
public Student Student { get; set; }

n:n-Beziehungen

Es besteht eine m:n-Beziehung zwischen der Student- und der Course-Entität. Die Enrollment-Entität fungiert in der Datenbank als m:n-Jointabelle mit Nutzlast. „Mit Nutzlast“ bedeutet, dass die Tabelle Enrollment außer den Fremdschlüsseln für die verknüpften Tabellen (in diesem Fall der Primärschlüssel und Grade) zusätzliche Daten enthält.

Die folgende Abbildung stellt dar, wie diese Beziehungen in einem Entitätsdiagramm aussehen. (Dieses Diagramm wurde mit EF Power Tools für EF 6.x erstellt. Die Erstellung des Diagramms ist nicht Teil des Tutorials.)

Student-Course many to many relationship

Jede Beziehung weist an einem Ende „1“ und am anderen Ende „*“ auf, wodurch eine 1:n-Beziehung dargestellt wird.

Wenn in der Tabelle Enrollment nicht die Information „Grade“ enthalten wäre, müsste diese nur die beiden Fremdschlüssel (CourseID und StudentID) enthalten. Eine m:n-Jointabelle ohne Nutzlast wird manchmal als „reine Jointabelle“ bezeichnet.

Zwischen den Entitäten Instructor und Course besteht eine m:n-Beziehung mithilfe einer reinen Jointabelle.

Hinweis: Entity Framework 6.x unterstützt implizite Jointabellen für m:n-Beziehungen, EF Core unterstützt diese dagegen nicht. Weitere Informationen finden Sie unter m:n-Beziehungen in EF Core 2.0.

Die Entität „CourseAssignment“

CourseAssignment entity

Erstellen Sie Models/CourseAssignment.cs mit dem folgenden Code:

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Die m:n-Beziehung zwischen „Instructor“ und „Courses“ erfordert eine Jointabelle, und die Entität für diese Jointabelle ist „CourseAssignment“.

Instructor-to-Courses m:M

Es ist üblich, eine Joinentität EntityName1EntityName2 zu benennen. Der Name der Jointabelle für „Instructor“ und „Courses“ würde mithilfe dieses Musters beispielsweise CourseInstructor lauten. Es wird jedoch empfohlen, einen Namen zu verwenden, der die Beziehung beschreibt.

Datenmodelle fangen einfach an und werden dann größer. Jointabellen ohne Nutzlast (PJTs) entwickeln sich häufig so, dass sie eine Nutzlast beinhalten. Wenn Sie mit einem aussagekräftigen Entitätsnamen beginnen, muss dieser nicht geändert werden, wenn die Jointabelle sich verändert. Im Idealfall verfügt die Joinentität über einen eigenen, nachvollziehbaren Namen (der möglicherweise aus nur einem Wort besteht) in der Geschäftsdomäne. „Books“ und „Customers“ könnten beispielsweise über eine Joinentität namens „Ratings“ verknüpft werden. Bei der m:n-Beziehung zwischen „Instructor“ und „Course“ ist CourseAssignmentCourseInstructor vorzuziehen.

Zusammengesetzte Schlüssel

Die beiden Fremdschlüssel in CourseAssignment (InstructorID und CourseID) identifizieren zusammen jede Zeile der CourseAssignment-Tabelle eindeutig. CourseAssignment erfordert keinen dedizierten Fremdschlüssel. Die Eigenschaften InstructorID und CourseID fungieren als zusammengesetzter Fremdschlüssel. Zusammengesetzte Fremdschlüssel können für EF Core nur über die Fluent-API angegeben werden. Im folgenden Abschnitt wird das Konfigurieren des zusammengesetzten Fremdschlüssels erläutert.

Durch den zusammengesetzten Schlüssel wird sichergestellt, dass:

  • Mehrere Zeilen für einen Kurs zulässig sind.
  • Mehrere Zeilen für einen Dozenten zulässig sind.
  • Mehrere Zeilen für denselben Dozenten und Kurs unzulässig sind.

Die Joinentität Enrollment definiert ihren eigenen Primärschlüssel, wodurch Duplikate dieser Art möglich sind. So verhindern Sie solche Duplikate:

  • Fügen Sie einen eindeutigen Index zu den Feldern für Fremdschlüssel hinzu, oder
  • konfigurieren Sie Enrollment mit einem zusammengesetzten Primärschlüssel, der CourseAssignment ähnelt. Weitere Informationen finden Sie unter Indizes.

Aktualisieren des Datenbankkontexts

Aktualisieren Sie Data/SchoolContext.cs mit folgendem Code:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Durch den vorangehenden Code werden neue Entitäten hinzugefügt, und der zusammengesetzte Primärschlüssel der Entität CourseAssignment wird konfiguriert.

Fluent-API-Alternativen für Attribute

Die OnModelCreating-Methode im vorangehenden Code verwendet die Fluent-API zum Konfigurieren des Verhaltens von EF Core. Die API wird als „Fluent“ bezeichnet, da sie häufig durch das Verketten mehrerer Methodenaufrufe zu einer einzigen Anweisung verwendet wird. Der folgende Code veranschaulicht die Fluent-API beispielhaft:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

In diesem Tutorial wird die Fluent-API nur für die Datenbankzuordnung verwendet, die nicht mit Attributen erfolgen kann. Die Fluent-API kann jedoch den Großteil der Regeln für Formatierung, Validierung und Zuordnung angeben, die mithilfe von Attributen festgelegt werden können.

Manche Attribute, z.B. MinimumLength, können nicht mit der Fluent-API angewendet werden. Durch MinimumLength wird das Schema nicht geändert, sondern lediglich eine Validierungsregel für die mindestens erforderliche Länge angewendet.

Einige Entwickler*inn verwenden lieber die Fluent-API, damit sie ihre Entitätsklassen „sauber“ halten können. Attribute und die Fluent-API können gemeinsam verwendet werden. Es gibt einige Konfigurationen, die nur mit der Fluent-API vorgenommen werden können, z.B. das Angeben eines zusammengesetzten Primärschlüssels. Es gibt einige Konfigurationen, die nur mit Attributen (MinimumLength) vorgenommen werden können. Folgende Vorgehensweise wird für die Verwendung der Fluent-API oder von Attributen empfohlen:

  • Wählen Sie einen der beiden Ansätze aus.
  • Verwenden Sie den ausgewählten Ansatz so konsistent wie möglich.

Einige der in diesem Tutorial verwendeten Attribute werden für Folgendes verwendet:

  • Nur für die Validierung (z.B. MinimumLength)
  • Nur für die Konfiguration von EF Core (z. B. HasKey)
  • Für die Validierung und die Konfiguration von EF Core (z. B. [StringLength(50)])

Weitere Informationen zu Attributen und Fluent-API im Vergleich finden Sie unter Methods of configuration (Konfigurationsmethoden).

Entitätsdiagramm

Die folgende Abbildung stellt das Diagramm dar, das von Entity Framework Power Tools für das vollständige Modell „School“ erstellt wird.

Entity diagram

Das vorherige Diagramm stellt Folgendes dar:

  • Mehrere Linien für 1:n-Beziehungen (1:*)
  • Die Linie für die 1:0..1-Beziehung (1:0..1) zwischen den Entitäten Instructor und OfficeAssignment.
  • Die Linie für die 0..1:n-Beziehung (0..1:*) zwischen den Entitäten Instructor und Department.

Ausführen eines Seedings für die Datenbank

Aktualisieren Sie den Code in Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2016-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2018-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2017-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2019-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2011-09-01") }
            };

            context.Students.AddRange(students);
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            context.Instructors.AddRange(instructors);
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            context.Departments.AddRange(departments);
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            context.Courses.AddRange(courses);
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            context.OfficeAssignments.AddRange(officeAssignments);
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            context.CourseAssignments.AddRange(courseInstructors);
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

Der vorangehende Code stellt Startwertdaten für die neuen Entitäten bereit. Durch diesen Code werden überwiegend neue Entitätsobjekte erstellt und Beispieldaten geladen. Die Beispieldaten werden für Tests verwendet. Beispiele dazu, wie viele m:n-Jointabellen eingerichtet werden können, finden Sie unter Enrollments und CourseAssignments.

Hinzufügen einer Migration

Erstellen Sie das Projekt.

Führen Sie in der PMC den folgenden Befehl aus.

Add-Migration ComplexDataModel

Der zuvor verwendete Befehl zeigt eine Warnung über möglichen Datenverlust an.

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
To undo this action, use 'ef migrations remove'

Wenn der Befehl database update ausgeführt wird, wird folgende Fehlermeldung erzeugt:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Im nächsten Abschnitt erfahren Sie, wie dieser Fehler zu behandeln ist.

Anwenden der Migration oder Löschen und erneutes Erstellen

Da Sie nun über eine Datenbank verfügen, müssen Sie überlegen, wie Änderungen an dieser vorgenommen werden sollen. In diesem Tutorial werden zwei Alternativen veranschaulicht:

Beide Optionen funktionieren für SQL Server. Obwohl die apply-migration-Methode komplexer und zeitaufwendiger ist, ist dies in der Praxis die bevorzugte Methode für Produktionsumgebungen.

Löschen und Neuerstellen der Datenbank

Überspringen Sie diesen Abschnitt, wenn Sie SQL Server verwenden und den apply-migration-Ansatz im folgenden Abschnitt ausführen möchten.

Löschen und aktualisieren Sie die Datenbank, um EF Core zum Erstellen einer neuen Datenbank zu zwingen:

  • Führen Sie folgenden Befehl in der Paket-Manager-Konsole aus:

    Drop-Database
    
  • Löschen Sie den Ordner Migrations, und führen Sie dann den folgenden Befehl aus:

    Add-Migration InitialCreate
    Update-Database
    

Führen Sie die App aus. Durch das Ausführen der App wird die DbInitializer.Initialize-Methode ausgeführt. Die DbInitializer.Initialize-Methode füllt die neue Datenbank mit Daten auf.

Öffnen Sie die Datenbank in SSOX:

  • Wenn der SSOX zuvor schon geöffnet war, klicken Sie auf die Schaltfläche Aktualisieren.

  • Erweitern Sie den Knoten Tabellen. Die erstellten Tabellen werden angezeigt.

    Tables in SSOX

  • Überprüfen Sie die Tabelle CourseAssignment:

    • Klicken Sie mit der rechten Maustaste auf die Tabelle CourseAssignment, und klicken Sie auf Daten anzeigen.
    • Überprüfen Sie, ob die Tabelle CourseAssignment-Daten enthält.

    CourseAssignment data in SSOX

Anwenden der Migration

Dieser Abschnitt ist optional. Diese Schritte funktionieren nur für SQL Server LocalDB und nur dann, wenn Sie den vorherigen Abschnitt Löschen und Neuerstellen der Datenbank übersprungen haben.

Wenn Migrationen mit vorhandenen Daten ausgeführt werden, gibt es möglicherweise Fremdschlüsseleinschränkungen, die durch die vorhandenen Daten nicht erfüllt werden. Bei Produktionsdaten müssen Schritte ausgeführt werden, um die vorhandenen Daten zu migrieren. In diesem Abschnitt ist ein Beispiel zum Beheben von Verstößen gegen die Fremdschlüsseleinschränkungen enthalten. Nehmen Sie diese Codeänderungen nicht vor, ohne zuvor eine Sicherung durchzuführen. Nehmen Sie diese Codeänderungen nicht vor, wenn Sie den vorherigen Abschnitt Löschen und Neuerstellen der Datenbank abgeschlossen haben.

Die Datei {timestamp}_ComplexDataModel.cs enthält den folgenden Code:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Der vorangehende Code fügt den nicht auf NULL festlegbaren Fremdschlüssel DepartmentID zur Tabelle Course hinzu. Die Datenbank aus dem vorherigen Tutorial enthält Zeilen in Course und kann daher nicht durch Migrationen aktualisiert werden.

So ermöglichen Sie die ComplexDataModel-Migration mit vorhandenen Daten:

  • Ändern Sie den Code, um der neuen Spalte (DepartmentID) einen Standardnamen zuzuweisen.
  • Erstellen Sie einen Dummy-Fachbereich namens „Temp“, die als Standardfachbereich fungiert.

Aufheben der Fremdschlüsseleinschränkungen

Aktualisieren Sie in der ComplexDataModel-Migrationsklasse die Up-Methode:

  • Öffnen Sie die Datei {timestamp}_ComplexDataModel.cs.
  • Kommentieren Sie die Codezeile aus, die die Spalte DepartmentID zur Tabelle Course hinzufügt.
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

Fügen Sie folgenden hervorgehobenen Code hinzu: Der neue Code folgt auf den Block .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Durch die zuvor durchgeführten Änderungen sind die vorhandenen Course-Zeilen mit dem Fachbereich „Temp“ verknüpft, nachdem die Methode ComplexDataModel.Up ausgeführt wurde.

Die Vorgehensweise zum Behandeln der hier gezeigten Situation wurde für dieses Tutorial vereinfacht. Ein Produktions-App würde:

  • Code oder Skripts einfügen, um Department-Zeilen und verknüpfte Course-Zeilen zu den neuen Department-Zeilen hinzuzufügen
  • Den Fachbereich „Temp“ nicht als Standardwert für Course.DepartmentID verwenden
  • Führen Sie folgenden Befehl in der Paket-Manager-Konsole aus:

    Update-Database
    

Da die DbInitializer.Initialize-Methode nur für die Verwendung mit einer leeren Datenbank konzipiert ist, löschen Sie mithilfe von SSOX alle Zeilen in den Tabellen „Student“ und „Course“. (Der kaskadierende Löschvorgang übernimmt die Tabelle „Enrollment“.)

Führen Sie die App aus. Durch das Ausführen der App wird die DbInitializer.Initialize-Methode ausgeführt. Die DbInitializer.Initialize-Methode füllt die neue Datenbank mit Daten auf.

Nächste Schritte

In den nächsten beiden Tutorials wird gezeigt, wie verwandte Daten gelesen und aktualisiert werden.

In den vorherigen Tutorials wurde mit einem einfachen Datenmodell gearbeitet, das aus drei Entitäten bestand. In diesem Tutorial wird Folgendes durchgeführt:

  • Weitere Entitäten und Beziehungen werden hinzugefügt.
  • Das Datenmodell wird angepasst, indem Regeln zur Formatierung, Validierung und Datenbankzuordnung angegeben werden.

Die Entitätsklassen des vollständigen Datenmodells werden in der folgenden Abbildung dargestellt:

Entity diagram

Wenn nicht zu lösende Probleme auftreten, laden Sie die fertige App herunter.

Anpassen des Datenmodells mithilfe von Attributen

In diesem Abschnitt wird das Datenmodell mithilfe von Attributen angepasst.

Das DataType-Attribut

Die Seite für Studenten zeigt derzeit die Uhrzeit des Anmeldedatums an. Üblicherweise zeigen Datumsfelder nur das Datum an, nicht die Uhrzeit.

Aktualisieren Sie Models/Student.cs mit dem folgenden hervorgehobenen Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Das DataType-Attribut gibt einen Datentyp an, der spezifischer als der datenbankinterne Typ ist. In diesem Fall sollte nur das Datum angezeigt werden, nicht das Datum und die Uhrzeit. Die DataType-Enumeration stellt viele Datentypen bereit, wie z.B. „Date“, „Time“, „PhoneNumber“, „Currency“, „EmailAddress“. Das DataType-Attribut kann der App auch das Bereitstellen typspezifischer Features ermöglichen. Zum Beispiel:

  • Der Link mailto: wird automatisch für DataType.EmailAddress erstellt.
  • Die Datumsauswahl für DataType.Date wird in den meisten Browsern bereitgestellt.

Das DataType-Attribut gibt data--HTML5-Attribute (ausgesprochen „Datadash“) aus, die von HTML5-Browsern genutzt werden. Die DataType-Attribute bieten keine Validierung.

DataType.Date gibt nicht das Format des Datums an, das angezeigt wird. Standardmäßig wird das Datumsfeld gemäß den Standardformaten basierend auf der CultureInfo-Klasse des Servers angezeigt.

Das DisplayFormat-Attribut dient zum expliziten Angeben des Datumsformats:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Die ApplyFormatInEditMode-Einstellung gibt an, dass die Formatierung ebenfalls auf die Benutzeroberfläche für die Bearbeitung angewendet wird. Einige Felder sollten ApplyFormatInEditMode nicht verwenden. Das Währungssymbol sollte beispielsweise nicht im Textfeld für die Bearbeitung angezeigt werden.

Das DisplayFormat-Attribut kann eigenständig verwendet werden. Meist empfiehlt es sich, das DataType-Attribut mit dem DisplayFormat-Attribut zu verwenden. Das DataType-Attribut übermittelt die Semantik der Daten im Gegensatz zu deren Rendern auf einem Bildschirm. Das DataType-Attribut bietet folgende Vorteile, die in DisplayFormat nicht verfügbar sind:

  • Der Browser kann HTML5-Features aktivieren. Dazu zählen beispielsweise das Anzeigen eines Kalendersteuerelements, des dem Gebietsschema entsprechenden Währungssymbols, von E-Mail-Links, einigen clientseitigen Eingabevalidierungen usw.
  • Standardmäßig rendert der Browser Daten mit dem auf Ihrem Gebietsschema basierenden richtigen Format.

Weitere Informationen finden Sie in der Dokumentation zum <input>-Taghilfsprogramm.

Führen Sie die App aus. Navigieren Sie zur Indexseite „Studenten“. Die Uhrzeiten werden nicht mehr angezeigt. Jede Ansicht, die das Student-Modell verwendet, zeigt das Datum ohne Uhrzeit an.

Students index page showing dates without times

Das StringLength-Attribut

Die Regeln für die Datenvalidierung und Meldungen für Validierungsfehler können mithilfe von Attributen angegeben werden. Das StringLength-Attribut gibt den mindestens erforderlichen und maximal zulässigen Wert für die Zeichenlänge eines Datenfelds an. Das StringLength-Attribut stellt ebenfalls die clientseitige und serverseitige Validierung bereit. Der mindestens erforderliche Wert hat keine Auswirkungen auf das Datenbankschema.

Aktualisieren Sie das Student-Modell mit folgendem Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Der vorangehende Code beschränkt Namen auf maximal 50 Zeichen. Das StringLength-Attribut verhindert nicht, dass ein Benutzer einen Leerraum als Namen eingibt. Das Attribut RegularExpression wird verwendet, um Einschränkungen auf die Eingabe anzuwenden. Folgender Code erfordert beispielsweise, dass das erste Zeichen ein Großbuchstabe sein muss und die restlichen Zeichen alphabetisch sein müssen:

[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]

Führen Sie die App aus:

  • Navigieren Sie zur Seite für Studenten.
  • Klicken Sie auf Neu erstellen, und geben Sie einen Namen ein, der länger als 50 Zeichen ist.
  • Wenn Sie auf Erstellen klicken, zeigt die clientseitige Validierung eine Fehlermeldung an.

Students index page showing string length errors

Öffnen Sie im SQL Server-Objekt-Explorer (SSOX) den Tabellen-Designer „Student“, indem Sie auf die Tabelle Student doppelklicken.

Students table in SSOX before migrations

In der vorangehenden Abbildung wird das Schema der Student-Tabelle dargestellt. Die Namensfelder weisen den Typ nvarchar(MAX) auf, da die Migrationen nicht auf der Datenbank ausgeführt wurden. Wenn die Migrationen im Verlauf dieses Tutorials ausgeführt werden, ändern sich die Namensfelder in nvarchar(50).

Das Column-Attribut

Durch Attribute kann gesteuert werden, wie Ihre Klassen und Eigenschaften der Datenbank zugeordnet werden. In diesem Abschnitt wird das Column-Attribut verwendet, um den Namen der FirstMidName-Eigenschaft in der Datenbank auf „FirstName“ festzulegen.

Wenn die Datenbank erstellt wird, werden die Eigenschaftsnamen des Moduls für Spaltennamen verwendet (außer bei Verwendung des Column-Attributs).

Das Student-Modell verwendet FirstMidName für das Feld „first-name“, da dieses ebenfalls einen Zweitnamen enthalten kann.

Aktualisieren Sie die Datei Student.cs mit dem folgenden hervorgehobenen Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Durch die zuvor vorgenommene Änderung wird Student.FirstMidName in der App der FirstName-Spalte der Student-Tabelle zugeordnet.

Durch das Hinzufügen des Column-Attributs wird das Modell geändert, das SchoolContext unterstützt. Das Modell, das SchoolContext unterstützt, stimmt nicht mehr mit der Datenbank überein. Wenn die App ausgeführt wird, bevor Migrationen angewendet werden, wird folgende Ausnahme generiert:

SqlException: Invalid column name 'FirstName'.

So aktualisieren Sie die Datenbank:

  • Erstellen Sie das Projekt.
  • Öffnen Sie ein Befehlsfenster im Projektordner. Geben Sie folgende Befehle ein, um eine neue Migration zu erstellen und die Datenbank zu aktualisieren:
Add-Migration ColumnFirstName
Update-Database

Der Befehl migrations add ColumnFirstName generiert folgende Warnmeldung:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

Die Warnung wird generiert, weil die Namensfelder nun auf 50 Zeichen beschränkt sind. Wenn der Name der Datenbank aus mehr als 50 Zeichen besteht, gehen alle Zeichen ab dem 51. verloren.

  • Testen Sie die App.

Öffnen Sie die Tabelle „Student“ im SSOX:

Students table in SSOX after migrations

Bevor die Migration angewendet wurde, wiesen die Namensspalten den Typ nvarchar(MAX) auf. Die Namensspalten weisen nun den Typ nvarchar(50) auf. Der Spaltenname wurde von FirstMidName in FirstName geändert.

Hinweis

Im folgenden Abschnitt werden beim Erstellen der App in einigen Phasen Compilerfehler generiert. Diese Anweisungen präzisieren, wann die App erstellt werden soll.

Aktualisieren der Entität „Student“

Student entity

Aktualisieren Sie Models/Student.cs mit folgendem Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }
        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Das Attribut „Required“

Durch das Required-Attribut werden die Name-Eigenschaften zu Pflichtfeldern. Das Required-Attribut ist für nicht auf NULL festlegbare Typen wie Werttypen (DateTime, int, double usw.) nicht erforderlich. Typen, die nicht auf NULL festgelegt werden können, werden automatisch als Pflichtfelder behandelt.

Das Required-Attribut kann durch einen Parameter mit der mindestens erforderlichen Länge im StringLength-Attribut ersetzt werden:

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

Das Display-Attribut

Das Display-Attribut gibt an, dass die Beschriftung der Textfelder „First Name“ (Vorname), „Last Name“ (Nachname), „Full Name“ (Vollständiger Name) und „Enrollment Date“ (Registrierungsdatum) lauten soll. Die Standardbeschriftungen hatten kein Leerzeichen zwischen den Wörtern, wie zum Beispiel „Lastname“.

Die berechnete FullName-Eigenschaft

Bei FullName handelt es sich um eine berechnete Eigenschaft, die einen Wert zurückgibt, der durch das Verketten von zwei weiteren Eigenschaften erstellt wird. FullName verfügt lediglich über einen get-Accessor und kann daher nicht festgelegt werden. In der Datenbank wird keine FullName-Spalte erstellt.

Erstellen der Instructor-Entität

Instructor entity

Erstellen Sie Models/Instructor.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

In einer Zeile können mehrere Attribute enthalten sein. Das HireDate-Attribut kann folgendermaßen geschrieben werden:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

Die Navigationseigenschaften „CourseAssignments“ und „OfficeAssignment“

Bei den Eigenschaften CourseAssignments und OfficeAssignment handelt es sich um Navigationseigenschaften.

Da ein Dozent eine beliebige Anzahl von Kursen geben kann, wird CourseAssignments als Auflistung definiert.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

Folgendes gilt, wenn eine Navigationseigenschaft mehrere Entitäten enthält:

  • Bei der Eigenschaft muss es sich um einen Listentyp handeln, bei dem Einträge hinzugefügt, gelöscht und aktualisiert werden können.

Folgendes gilt für die Typen von Navigationseigenschaften:

  • ICollection<T>
  • List<T>
  • HashSet<T>

Wenn ICollection<T> angegeben wird, erstellt EF Core standardmäßig eine HashSet<T>-Auflistung.

Die CourseAssignment-Entität wird im Abschnitt über m:n-Beziehungen erläutert.

Die Geschäftsregeln der Contoso University besagen, dass ein Dozent maximal ein Büro besitzen kann. Die OfficeAssignment-Eigenschaft enthält also eine einzige OfficeAssignment-Entität. OfficeAssignment ist NULL, wenn kein Büro zugewiesen ist.

public OfficeAssignment OfficeAssignment { get; set; }

Erstellen der Entität „OfficeAssignment“

OfficeAssignment entity

Erstellen Sie Models/OfficeAssignment.cs mit dem folgenden Code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Das Key-Attribut

Das [Key]-Attribut wird verwendet, um eine Eigenschaft als Primärschlüssel zu identifizieren, wenn der Eigenschaftsname nicht „classnameID“ oder „ID“ entspricht.

Es besteht eine 1:0..1-Beziehung zwischen der Instructor- und der OfficeAssignment-Entität. Eine Bürozuweisung ist nur in Beziehung zu dem Dozenten vorhanden, dem sie zugewiesen ist. Der Primärschlüssel OfficeAssignment ist ebenfalls der Fremdschlüssel der Instructor-Entität. EF Core erkennt InstructorID nicht automatisch als Primärschlüssel von OfficeAssignment, weil:

  • InstructorID der Namenskonvention „ID“ oder „classnameID“ nicht folgt.

Deshalb wird das Key-Attribut verwendet, um InstructorID als Primärschlüssel zu erkennen:

[Key]
public int InstructorID { get; set; }

Standardmäßig behandelt EF Core den Schlüssel nicht als datenbankgeneriert, da die Spalte für eine identifizierende Beziehung vorgesehen ist.

Die Navigationseigenschaft „Instructor“

Die OfficeAssignment-Navigationseigenschaft für die Instructor-Entität ist auf NULL festlegbar, weil:

  • Referenztypen (z.B. Klassen) auf NULL festlegbar sind.
  • Einem Dozenten möglicherweise kein Büro zugewiesen ist.

Die OfficeAssignment-Entität besitzt eine nicht auf NULL festlegbare Instructor-Navigationseigenschaft, weil:

  • InstructorID nicht auf NULL festlegbar ist.
  • Eine Bürozuweisung nicht ohne einen Dozenten vorhanden sein kann.

Wenn die Entität Instructor mit der Entität OfficeAssignment verknüpft ist, besitzt jede Entität in den Navigationseigenschaften einen Verweis auf die andere.

Das [Required]-Attribut kann auf die Instructor-Navigationseigenschaft angewendet werden:

[Required]
public Instructor Instructor { get; set; }

Der vorangehende Code legt fest, dass ein zugehöriger Dozent vorhanden sein muss. Der vorangehende Code ist nicht erforderlich, da der Fremdschlüssel InstructorID (der ebenfalls der Primärschlüssel ist) nicht auf NULL festlegbar ist.

Ändern der Entität „Course“

Course entity

Aktualisieren Sie Models/Course.cs mit folgendem Code:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "Number")]
        public int CourseID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Title { get; set; }

        [Range(0, 5)]
        public int Credits { get; set; }

        public int DepartmentID { get; set; }

        public Department Department { get; set; }
        public ICollection<Enrollment> Enrollments { get; set; }
        public ICollection<CourseAssignment> CourseAssignments { get; set; }
    }
}

Die Course-Entität besitzt die Fremdschlüsseleigenschaft DepartmentID. DepartmentID zeigt auf die verknüpfte Department-Entität. Die Course-Entität besitzt eine Department-Navigationseigenschaft.

EF Core erfordert keine Fremdschlüsseleigenschaft für ein Datenmodell, wenn das Modell über eine Navigationseigenschaft für eine verknüpfte Entität verfügt.

EF Core erstellt an den erforderlichen Stellen automatisch Fremdschlüssel in der Datenbank. EF Core erstellt Schatteneigenschaften für automatisch erstellte Fremdschlüssel. Durch Fremdschlüssel im Datenmodell können Updates einfacher und effizienter durchgeführt werden. Betrachten Sie beispielsweise ein Modell, bei dem die Fremdschlüsseleigenschaft DepartmentIDnicht enthalten ist. Folgendes wird durchgeführt, wenn eine Course-Entität zum Bearbeiten abgerufen wird:

  • Die Department-Entität ist NULL, wenn diese nicht explizit geladen wird.
  • Die Department-Entität muss abgerufen werden, um die Course-Entität zu aktualisieren.

Wenn die Fremdschlüsseleigenschaft DepartmentID im Datenmodell enthalten ist, muss die Department-Entität vor einem Update nicht abgerufen werden.

Das Attribut „DatabaseGenerated“

Das [DatabaseGenerated(DatabaseGeneratedOption.None)]-Attribut gibt an, dass der Primärschlüssel von der Anwendung bereitgestellt wird, statt von der Datenbank generiert zu werden.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

Standardmäßig geht EF Core davon aus, dass Primärschlüsselwerte von der Datenbank generiert werden. Bei von der Datenbank generierten Primärschlüsselwerten handelt es sich in der Regel um die beste Herangehensweise. Bei Course-Entitäten wird der Primärschlüssel vom Benutzer angegeben, z.B. eine Kursnummer wie eine 1000er-Reihe für den Fachbereich Mathematik, eine 2000er-Reihe für den Fachbereich Englisch usw.

Das DatabaseGenerated-Attribut kann ebenfalls zum Generieren von Standardwerten verwendet werden. Die Datenbank kann beispielsweise automatisch ein Datenfeld generieren, um das Datum aufzuzeichnen, an dem eine Zeile erstellt oder aktualisiert wurde. Weitere Informationen finden Sie unter Generated Properties (Generierte Eigenschaften).

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften in der Entität Course stellen folgende Beziehungen dar:

Ein Kurs ist einem Fachbereich zugeordnet, es gibt also eine DepartmentID-Fremdschlüsseleigenschaft und eine Department-Navigationseigenschaft.

public int DepartmentID { get; set; }
public Department Department { get; set; }

In einem Kurs können beliebig viele Studenten angemeldet sein, darum stellt die Enrollments-Navigationseigenschaft eine Auflistung dar:

public ICollection<Enrollment> Enrollments { get; set; }

Ein Kurs kann von mehreren Dozenten unterrichtet werden, darum stellt die CourseAssignments-Navigationseigenschaft eine Auflistung dar:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment wird später erläutert.

Erstellen der Entität „Department“

Department entity

Erstellen Sie Models/Department.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Department
    {
        public int DepartmentID { get; set; }

        [StringLength(50, MinimumLength = 3)]
        public string Name { get; set; }

        [DataType(DataType.Currency)]
        [Column(TypeName = "money")]
        public decimal Budget { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Start Date")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public Instructor Administrator { get; set; }
        public ICollection<Course> Courses { get; set; }
    }
}

Das Column-Attribut

Zuvor wurde das Column-Attribut verwendet, um die Zuordnung des Spaltennamens zu ändern. Im Code für die Department-Entität wird das Column-Attribut verwendet, um die Zuordnung des SQL-Datentyps zu ändern. Die Budget-Spalte wird mithilfe des SQL Server-Typs „money“ in der Datenbank definiert:

[Column(TypeName="money")]
public decimal Budget { get; set; }

Die Spaltenzuordnung ist im Allgemeinen nicht erforderlich. EF Core wählt üblicherweise den geeigneten SQL Server-Datentyp basierend auf dem CLR-Typ der Eigenschaft aus. Der CLR-Typ decimal wird dem SQL Server-Typ decimal zugeordnet. Bei Budget wird eine Währung verwendet, und der Datentyp „money“ ist für Währungen besser geeignet.

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

  • Ein Fachbereich kann einen Administrator besitzen oder nicht.
  • Bei einem Administrator handelt es sich immer um einen Dozenten. Aus diesem Grund wird die InstructorID-Eigenschaft als Fremdschlüssel zur Instructor-Entität hinzugefügt.

Die Navigationseigenschaft heißt Administrator, enthält jedoch eine Instructor-Entität:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

Das Fragezeichen (?) im vorangehenden Code gibt an, dass die Eigenschaft auf NULL festlegbar ist.

Eine Abteilung kann viele Kurse aufweisen, darum gibt es die Navigationseigenschaft „Courses“:

public ICollection<Course> Courses { get; set; }

Hinweis: Gemäß der Konvention aktiviert EF Core das kaskadierende Delete für nicht auf NULL festlegbare Fremdschlüssel und für m:n-Beziehungen. Das kaskadierende Delete kann zu Zirkelregeln für kaskadierende Deletes führen. Durch Zirkelregeln für kaskadierende Deletes wird eine Ausnahme ausgelöst, wenn eine Migration hinzugefügt wird.

Beispielsweise dann, wenn die Department.InstructorID-Eigenschaft als nicht auf NULL festlegbar definiert wurde:

  • EF Core konfiguriert eine Regel für kaskadierende Deletes, um den Fachbereich (Department) zu löschen, wenn ein Dozent gelöscht wird.

  • Das Löschen eines Fachbereichs in Folge des Löschens eines Dozenten stellt nicht das beabsichtigte Verhalten dar.

  • Die folgende Fluent-API würde eine Einschränkungsregel anstelle eines kaskadierenden Deletes festlegen.

    modelBuilder.Entity<Department>()
        .HasOne(d => d.Administrator)
        .WithMany()
        .OnDelete(DeleteBehavior.Restrict)
    

Durch den vorangehenden Code werden kaskadierende Deletes für die Beziehung zwischen „Department“ und „Instructor“ deaktiviert.

Aktualisieren der Entität „Enrollment“

Ein Enrollment-Datensatz gilt für einen Kurs, der von einem Studenten besucht wird.

Enrollment entity

Aktualisieren Sie Models/Enrollment.cs mit folgendem Code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

Fremdschlüssel- und Navigationseigenschaften

Die Fremdschlüssel- und Navigationseigenschaften stellen folgende Beziehungen dar:

Ein Anmeldungsdatensatz gilt für einen einzelnen Kurs, sodass es eine CourseID-Fremdschlüsseleigenschaft und eine Course-Navigationseigenschaft gibt:

public int CourseID { get; set; }
public Course Course { get; set; }

Ein Anmeldungsdatensatz gilt für einen einzelnen Studenten, sodass es eine StudentID-Fremdschlüsseleigenschaft und eine Student-Navigationseigenschaft gibt:

public int StudentID { get; set; }
public Student Student { get; set; }

n:n-Beziehungen

Es besteht eine m:n-Beziehung zwischen der Student- und der Course-Entität. Die Enrollment-Entität fungiert in der Datenbank als m:n-Jointabelle mit Nutzlast. „Mit Nutzlast“ bedeutet, dass die Tabelle Enrollment außer den Fremdschlüsseln für die verknüpften Tabellen (in diesem Fall der Primärschlüssel und Grade) zusätzliche Daten enthält.

Die folgende Abbildung stellt dar, wie diese Beziehungen in einem Entitätsdiagramm aussehen. (Dieses Diagramm wurde mit EF Power Tools für EF 6.x erstellt. Die Erstellung des Diagramms ist nicht Teil des Tutorials.)

Student-Course many to many relationship

Jede Beziehung weist an einem Ende „1“ und am anderen Ende „*“ auf, wodurch eine 1:n-Beziehung dargestellt wird.

Wenn in der Tabelle Enrollment nicht die Information „Grade“ enthalten wäre, müsste diese nur die beiden Fremdschlüssel (CourseID und StudentID) enthalten. Eine m:n-Jointabelle ohne Nutzlast wird manchmal als „reine Jointabelle“ bezeichnet.

Zwischen den Entitäten Instructor und Course besteht eine m:n-Beziehung mithilfe einer reinen Jointabelle.

Hinweis: Entity Framework 6.x unterstützt implizite Jointabellen für m:n-Beziehungen, EF Core unterstützt diese dagegen nicht. Weitere Informationen finden Sie unter m:n-Beziehungen in EF Core 2.0.

Die Entität „CourseAssignment“

CourseAssignment entity

Erstellen Sie Models/CourseAssignment.cs mit dem folgenden Code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

Beziehung zwischen „Instructor“ und „Course“

Instructor-to-Courses m:M

Folgendes gilt für die m:n-Beziehung zwischen „Instructor“ und „Course“:

  • Eine Jointabelle ist erforderlich, die durch eine Entitätenmenge dargestellt werden muss.
  • Diese muss eine reine Jointabelle (eine Tabelle ohne Nutzlast) sein.

Es ist üblich, eine Joinentität EntityName1EntityName2 zu benennen. Der Name der Jointabelle für „Instructor“ und „Course“ lautet mithilfe dieses Musters beispielsweise CourseInstructor. Es wird jedoch empfohlen, einen Namen zu verwenden, der die Beziehung beschreibt.

Datenmodelle fangen einfach an und werden dann größer. Währenddessen erhalten Joins ohne Nutzlast häufig Nutzlasten. Wenn Sie mit einem aussagekräftigen Entitätsnamen beginnen, muss dieser nicht geändert werden, wenn die Jointabelle sich verändert. Im Idealfall verfügt die Joinentität über einen eigenen, nachvollziehbaren Namen (der möglicherweise aus nur einem Wort besteht) in der Geschäftsdomäne. „Books“ und „Customers“ könnten beispielsweise über eine Joinentität namens „Ratings“ verknüpft werden. Bei der m:n-Beziehung zwischen „Instructor“ und „Course“ ist CourseAssignmentCourseInstructor vorzuziehen.

Zusammengesetzte Schlüssel

Fremdschlüssel sind nicht auf NULL festlegbar. Die beiden Fremdschlüssel in CourseAssignment (InstructorID und CourseID) identifizieren zusammen jede Zeile der CourseAssignment-Tabelle eindeutig. CourseAssignment erfordert keinen dedizierten Fremdschlüssel. Die Eigenschaften InstructorID und CourseID fungieren als zusammengesetzter Fremdschlüssel. Zusammengesetzte Fremdschlüssel können für EF Core nur über die Fluent-API angegeben werden. Im folgenden Abschnitt wird das Konfigurieren des zusammengesetzten Fremdschlüssels erläutert.

Durch den zusammengesetzten Schlüssel wird sichergestellt, dass:

  • Mehrere Zeilen für einen Kurs zulässig sind.
  • Mehrere Zeilen für einen Dozenten zulässig sind.
  • Mehrere Zeilen für denselben Dozenten und Kurs nicht zulässig sind.

Die Joinentität Enrollment definiert ihren eigenen Primärschlüssel, wodurch Duplikate dieser Art möglich sind. So verhindern Sie solche Duplikate:

  • Fügen Sie einen eindeutigen Index zu den Feldern für Fremdschlüssel hinzu, oder
  • konfigurieren Sie Enrollment mit einem zusammengesetzten Primärschlüssel, der CourseAssignment ähnelt. Weitere Informationen finden Sie unter Indizes.

Aktualisieren des Datenbankkontexts

Fügen Sie folgenden hervorgehobenen Code zu Data/SchoolContext.cs hinzu:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

Durch den vorangehenden Code werden neue Entitäten hinzugefügt, und der zusammengesetzte Primärschlüssel der Entität CourseAssignment wird konfiguriert.

Fluent-API-Alternativen für Attribute

Die OnModelCreating-Methode im vorangehenden Code verwendet die Fluent-API zum Konfigurieren des Verhaltens von EF Core. Die API wird als „Fluent“ bezeichnet, da sie häufig durch das Verketten mehrerer Methodenaufrufe zu einer einzigen Anweisung verwendet wird. Der folgende Code veranschaulicht die Fluent-API beispielhaft:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

In diesem Tutorial wird die Fluent-API nur für die Datenbankzuordnung verwendet, die nicht mit Attributen erfolgen kann. Die Fluent-API kann jedoch den Großteil der Regeln für Formatierung, Validierung und Zuordnung angeben, die mithilfe von Attributen festgelegt werden können.

Manche Attribute, z.B. MinimumLength, können nicht mit der Fluent-API angewendet werden. Durch MinimumLength wird das Schema nicht geändert, sondern lediglich eine Validierungsregel für die mindestens erforderliche Länge angewendet.

Einige Entwickler*inn verwenden lieber die Fluent-API, damit sie ihre Entitätsklassen „sauber“ halten können. Attribute und die Fluent-API können gemeinsam verwendet werden. Es gibt einige Konfigurationen, die nur mit der Fluent-API vorgenommen werden können, z.B. das Angeben eines zusammengesetzten Primärschlüssels. Es gibt einige Konfigurationen, die nur mit Attributen (MinimumLength) vorgenommen werden können. Folgende Vorgehensweise wird für die Verwendung der Fluent-API oder von Attributen empfohlen:

  • Wählen Sie einen der beiden Ansätze aus.
  • Verwenden Sie den ausgewählten Ansatz so konsistent wie möglich.

Einige der in diesem Tutorial verwendeten Attribute werden für Folgendes verwendet:

  • Nur für die Validierung (z.B. MinimumLength)
  • Nur für die Konfiguration von EF Core (z. B. HasKey)
  • Für die Validierung und die Konfiguration von EF Core (z. B. [StringLength(50)])

Weitere Informationen zu Attributen und Fluent-API im Vergleich finden Sie unter Methods of configuration (Konfigurationsmethoden).

Entitätsdiagramm mit angezeigten Beziehungen

Die folgende Abbildung stellt das Diagramm dar, das von Entity Framework Power Tools für das vollständige Modell „School“ erstellt wird.

Entity diagram

Das vorherige Diagramm stellt Folgendes dar:

  • Mehrere Linien für 1:n-Beziehungen (1:*)
  • Die Linie für die 1:0..1-Beziehung (1:0..1) zwischen den Entitäten Instructor und OfficeAssignment.
  • Die Linie für die 0..1:n-Beziehung (0..1:*) zwischen den Entitäten Instructor und Department.

Ausführen eines Seedings mit Testdaten für die Datenbank

Aktualisieren Sie den Code in Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Student.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander",
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            foreach (Student s in students)
            {
                context.Student.Add(s);
            }
            context.SaveChanges();

            var instructors = new Instructor[]
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
                    HireDate = DateTime.Parse("2004-02-12") }
            };

            foreach (Instructor i in instructors)
            {
                context.Instructors.Add(i);
            }
            context.SaveChanges();

            var departments = new Department[]
            {
                new Department { Name = "English",     Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000,
                    StartDate = DateTime.Parse("2007-09-01"),
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };

            foreach (Department d in departments)
            {
                context.Departments.Add(d);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                    DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
                },
            };

            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var officeAssignments = new OfficeAssignment[]
            {
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
                    Location = "Smith 17" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
                    Location = "Gowan 27" },
                new OfficeAssignment {
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
                    Location = "Thompson 304" },
            };

            foreach (OfficeAssignment o in officeAssignments)
            {
                context.OfficeAssignments.Add(o);
            }
            context.SaveChanges();

            var courseInstructors = new CourseAssignment[]
            {
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Harui").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
                new CourseAssignment {
                    CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
                    InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
                    },
            };

            foreach (CourseAssignment ci in courseInstructors)
            {
                context.CourseAssignments.Add(ci);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
                    Grade = Grade.A
                },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
                    Grade = Grade.C
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                        StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B
                    },
                new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B
                    },
                    new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B
                    }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

Der vorangehende Code stellt Startwertdaten für die neuen Entitäten bereit. Durch diesen Code werden überwiegend neue Entitätsobjekte erstellt und Beispieldaten geladen. Die Beispieldaten werden für Tests verwendet. Beispiele dazu, wie viele m:n-Jointabellen eingerichtet werden können, finden Sie unter Enrollments und CourseAssignments.

Hinzufügen einer Migration

Erstellen Sie das Projekt.

Add-Migration ComplexDataModel

Der zuvor verwendete Befehl zeigt eine Warnung über möglichen Datenverlust an.

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

Wenn der Befehl database update ausgeführt wird, wird folgende Fehlermeldung erzeugt:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in
database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.

Anwenden der Migration

Da Sie nun über eine Datenbank verfügen, müssen Sie überlegen, wie zukünftig Änderungen an dieser vorgenommen werden sollen. In diesem Tutorial werden zwei Vorgehensweisen veranschaulicht:

  • Löschen und Neuerstellen der Datenbank
  • Anwenden der Migration auf die vorhandene Datenbank. Obwohl diese Methode komplexer und zeitaufwendiger ist, ist dies in der Praxis die bevorzugte Methode für Produktionsumgebungen. Hinweis: Dies ist ein optionaler Abschnitt des Tutorials. Sie können diesen Abschnitt überspringen und die Schritte zum Löschen und Neuerstellen durchführen. Wenn Sie stattdessen die Schritte in diesem Abschnitt ausführen möchten, führen Sie nicht die Schritte zum Löschen und Neuerstellen aus.

Löschen und Neuerstellen der Datenbank

Durch den Code in der aktualisierten DbInitializer-Klasse werden Startwertdaten für die neuen Entitäten hinzugefügt. Löschen und aktualisieren Sie die Datenbank, um EF Core zum Erstellen einer neuen Datenbank zu zwingen:

Führen Sie folgenden Befehl in der Paket-Manager-Konsole aus:

Drop-Database
Update-Database

Führen Sie Get-Help about_EntityFrameworkCore über die Paket-Manager-Konsole aus, um Hilfeinformationen zu erhalten.

Führen Sie die App aus. Durch das Ausführen der App wird die DbInitializer.Initialize-Methode ausgeführt. Die DbInitializer.Initialize-Methode füllt die neue Datenbank auf.

Öffnen Sie die Datenbank im SSOX:

  • Wenn der SSOX zuvor schon geöffnet war, klicken Sie auf die Schaltfläche Aktualisieren.
  • Erweitern Sie den Knoten Tabellen. Die erstellten Tabellen werden angezeigt.

Tables in SSOX

Überprüfen Sie die Tabelle CourseAssignment:

  • Klicken Sie mit der rechten Maustaste auf die Tabelle CourseAssignment, und klicken Sie auf Daten anzeigen.
  • Überprüfen Sie, ob die Tabelle CourseAssignment-Daten enthält.

CourseAssignment data in SSOX

Anwenden der Migration auf die vorhandene Datenbank

Dieser Abschnitt ist optional. Diese Schritte funktionieren nur, wenn Sie den vorherigen Abschnitt Löschen und Neuerstellen der Datenbank übersprungen haben.

Wenn Migrationen mit vorhandenen Daten ausgeführt werden, gibt es möglicherweise Fremdschlüsseleinschränkungen, die durch die vorhandenen Daten nicht erfüllt werden. Bei Produktionsdaten müssen Schritte ausgeführt werden, um die vorhandenen Daten zu migrieren. In diesem Abschnitt ist ein Beispiel zum Beheben von Verstößen gegen die Fremdschlüsseleinschränkungen enthalten. Nehmen Sie diese Codeänderungen nicht vor, ohne zuvor eine Sicherung durchzuführen. Nehmen Sie diese Codeänderungen nicht vor, wenn Sie den vorherigen Abschnitt abgeschlossen und die Datenbank aktualisiert haben.

Die Datei {timestamp}_ComplexDataModel.cs enthält den folgenden Code:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

Der vorangehende Code fügt den nicht auf NULL festlegbaren Fremdschlüssel DepartmentID zur Tabelle Course hinzu. Die Datenbank aus dem vorherigen Tutorial enthält Zeilen in Course und kann daher nicht durch Migrationen aktualisiert werden.

So ermöglichen Sie die ComplexDataModel-Migration mit vorhandenen Daten:

  • Ändern Sie den Code, um der neuen Spalte (DepartmentID) einen Standardnamen zuzuweisen.
  • Erstellen Sie einen Dummy-Fachbereich namens „Temp“, die als Standardfachbereich fungiert.

Aufheben der Fremdschlüsseleinschränkungen

Aktualisieren Sie die Up-Methode der ComplexDataModel-Klasse:

  • Öffnen Sie die Datei {timestamp}_ComplexDataModel.cs.
  • Kommentieren Sie die Codezeile aus, die die Spalte DepartmentID zur Tabelle Course hinzufügt.
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

Fügen Sie folgenden hervorgehobenen Code hinzu: Der neue Code folgt auf den Block .CreateTable( name: "Department":

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

Aufgrund der zuvor durchgeführten Änderungen beziehen sich die vorhandenen Course-Zeilen auf den Fachbereich „Temp“, nachdem die Methode ComplexDataModelUp ausgeführt wurde.

Ein Produktions-App würde:

  • Code oder Skripts einfügen, um Department-Zeilen und verknüpfte Course-Zeilen zu den neuen Department-Zeilen hinzuzufügen
  • Den Fachbereich „Temp“ nicht als Standardwert für Course.DepartmentID verwenden

Im folgenden Tutorial werden verknüpfte Daten behandelt.

Zusätzliche Ressourcen