Übung: Interagieren mit Daten

Abgeschlossen

In der vorherigen Übung haben Sie Entitätsklassen und einen Datenbankkontext erstellt. Anschließend haben Sie EF Core-Migrationen verwendet, um das Datenbankschema zu erstellen.

In dieser Übung schließen Sie die Implementierung von PizzaService ab. Der Dienst verwendet EF Core, um CRUD-Vorgänge für die Datenbank auszuführen.

Programmieren der CRUD-Vorgänge

Führen Sie die folgenden Schritte in Services\PizzaService.cs aus, um die Implementierung von PizzaService abzuschließen:

  1. Nehmen Sie wie im folgenden Beispiel gezeigt die folgenden Änderungen vor:

    1. Fügen Sie eine using ContosoPizza.Data;-Anweisung hinzu.
    2. Fügen Sie eine using Microsoft.EntityFrameworkCore;-Anweisung hinzu.
    3. Fügen Sie vor dem Konstruktor ein Feld auf Klassenebene für PizzaContext hinzu.
    4. Ändern Sie die Konstruktormethodensignatur so, dass sie einen Parameter PizzaContext akzeptiert.
    5. Ändern Sie den Konstruktormethodencode, damit der Parameter dem Feld zugewiesen wird.
    using ContosoPizza.Models;
    using ContosoPizza.Data;
    using Microsoft.EntityFrameworkCore;
    
    namespace ContosoPizza.Services;
    
    public class PizzaService
    {
        private readonly PizzaContext _context;
    
        public PizzaService(PizzaContext context)
        {
            _context = context;
        }
    
        /// ...
        /// CRUD operations removed for brevity
        /// ...
    }
    

    Der AddSqlite-Methodenaufruf, den Sie Program.cs zuvor hinzugefügt haben, hat PizzaContext für die Abhängigkeitsinjektion registriert. Nach dem Erstellen der PizzaService-Instanz wird PizzaContext in den Konstruktor eingefügt.

  2. Ersetzen Sie die GetAll-Methode durch folgenden Code:

    public IEnumerable<Pizza> GetAll()
    {
        return _context.Pizzas
            .AsNoTracking()
            .ToList();
    }
    

    Für den Code oben gilt:

    • Die Auflistung Pizzas enthält alle Zeilen in der Pizzatabelle.
    • Die AsNoTracking-Erweiterungsmethode weist EF Core an, die Änderungsnachverfolgung zu deaktivieren. Da dieser Vorgang schreibgeschützt ist, kann AsNoTracking die Leistung optimieren.
    • Alle Pizzas werden mit ToList zurückgegeben.
  3. Ersetzen Sie die GetById-Methode durch folgenden Code:

    public Pizza? GetById(int id)
    {
        return _context.Pizzas
            .Include(p => p.Toppings)
            .Include(p => p.Sauce)
            .AsNoTracking()
            .SingleOrDefault(p => p.Id == id);
    }
    

    Für den Code oben gilt:

    • Die Include-Erweiterungsmethode verwendet einen Lambdaausdruck, um anzugeben, dass die Navigationseigenschaften Toppings und Sauce mittels Eager Loading in das Ergebnis eingeschlossen werden sollen. Ohne diesen Ausdruck gibt EF Core für diese Eigenschaften NULL zurück.
    • Die SingleOrDefault-Methode gibt eine Pizza zurück, die dem Lambdaausdruck entspricht.
      • Wenn keine Datensätze übereinstimmen, wird null zurückgegeben.
      • Wenn mehrere Datensätze übereinstimmen, wird eine Ausnahme ausgelöst.
      • Der Lambdaausdruck beschreibt Datensätze, bei denen die Id-Eigenschaft dem id-Parameter entspricht.
  4. Ersetzen Sie die Create-Methode durch folgenden Code:

    public Pizza Create(Pizza newPizza)
    {
        _context.Pizzas.Add(newPizza);
        _context.SaveChanges();
    
        return newPizza;
    }
    

    Für den Code oben gilt:

    • Von newPizza wird angenommen, dass es sich um ein gültiges Objekt handelt. EF Core nimmt keine Datenüberprüfung vor, daher müssen eventuelle Überprüfungen von der ASP.NET Core-Runtime oder dem Benutzercode durchgeführt werden.
    • Durch die Add-Methode wird dem EF Core-Objektgraphen die newPizza-Entität hinzugefügt.
    • Die SaveChanges-Methode weist EF Core an, die Objektänderungen in der Datenbank zu speichern.
  5. Ersetzen Sie die UpdateSauce-Methode durch folgenden Code:

    public void UpdateSauce(int pizzaId, int sauceId)
    {
        var pizzaToUpdate = _context.Pizzas.Find(pizzaId);
        var sauceToUpdate = _context.Sauces.Find(sauceId);
    
        if (pizzaToUpdate is null || sauceToUpdate is null)
        {
            throw new InvalidOperationException("Pizza or sauce does not exist");
        }
    
        pizzaToUpdate.Sauce = sauceToUpdate;
    
        _context.SaveChanges();
    }
    

    Für den Code oben gilt:

    • Verweise auf vorhandene Pizza- und Sauce-Objekte werden mithilfe von Find erstellt. Find ist eine optimierte Methode zum Abfragen von Datensätzen nach ihrem Primärschlüssel. Find durchsucht zunächst den lokalen Entitätsgraphen, bevor die Datenbank abfragt wird.
    • Die Pizza.Sauce-Eigenschaft ist auf das Sauce-Objekt festgelegt.
    • Ein Update-Methodenaufruf ist nicht erforderlich, weil EF Core erkennt, dass Sie die Sauce-Eigenschaft auf Pizza festgelegt haben.
    • Die SaveChanges-Methode weist EF Core an, die Objektänderungen in der Datenbank zu speichern.
  6. Ersetzen Sie die AddTopping-Methode durch folgenden Code:

    public void AddTopping(int pizzaId, int toppingId)
    {
        var pizzaToUpdate = _context.Pizzas.Find(pizzaId);
        var toppingToAdd = _context.Toppings.Find(toppingId);
    
        if (pizzaToUpdate is null || toppingToAdd is null)
        {
            throw new InvalidOperationException("Pizza or topping does not exist");
        }
    
        if(pizzaToUpdate.Toppings is null)
        {
            pizzaToUpdate.Toppings = new List<Topping>();
        }
    
        pizzaToUpdate.Toppings.Add(toppingToAdd);
    
        _context.SaveChanges();
    }
    

    Für den Code oben gilt:

    • Verweise auf vorhandene Pizza- und Topping-Objekte werden mithilfe von Find erstellt.
    • Das Topping-Objekt wird der Pizza.Toppings-Sammlung mit der .Add-Methode hinzugefügt. Die Auflistung wird neu erstellt, falls sie nicht vorhanden ist.
    • Die SaveChanges-Methode weist EF Core an, die Objektänderungen in der Datenbank zu speichern.
  7. Ersetzen Sie die DeleteById-Methode durch folgenden Code:

    public void DeleteById(int id)
    {
        var pizzaToDelete = _context.Pizzas.Find(id);
        if (pizzaToDelete is not null)
        {
            _context.Pizzas.Remove(pizzaToDelete);
            _context.SaveChanges();
        }        
    }
    

    Für den Code oben gilt:

    • Die Find-Methode ruft eine Pizza nach dem Primärschlüssel ab (in diesem Fall Id).
    • Durch die Remove-Methode wird die Entität pizzaToDelete aus dem EF Core-Objektgraphen entfernt.
    • Die SaveChanges-Methode weist EF Core an, die Objektänderungen in der Datenbank zu speichern.
  8. Speichern Sie alle Änderungen, und führen Sie dotnet build aus. Beheben Sie alle auftretenden Fehler.

Ausführen eines Seedings für die Datenbank

Sie haben die CRUD-Vorgänge für PizzaService programmiert, aber es ist einfacher, den Lesevorgang zu testen, wenn die Datenbank geeignete Daten enthält. Sie ändern die App so, dass beim Start ein Seeding für die Datenbank ausgeführt wird.

Warnung

Dieser Datenbank-Seedingcode berücksichtigt Racebedingung nicht. Achten Sie daher darauf, wenn Sie ihn in einer verteilten Umgebung verwenden, ohne Änderungen zu beschränken.

  1. Fügen Sie im Ordner Daten eine neue Datei mit dem Namen DbInitializer.cs hinzu.

  2. Fügen Sie den folgenden Code in Data/DbInitializer.cs ein:

    using ContosoPizza.Models;
    
    namespace ContosoPizza.Data
    {
        public static class DbInitializer
        {
            public static void Initialize(PizzaContext context)
            {
    
                if (context.Pizzas.Any()
                    && context.Toppings.Any()
                    && context.Sauces.Any())
                {
                    return;   // DB has been seeded
                }
    
                var pepperoniTopping = new Topping { Name = "Pepperoni", Calories = 130 };
                var sausageTopping = new Topping { Name = "Sausage", Calories = 100 };
                var hamTopping = new Topping { Name = "Ham", Calories = 70 };
                var chickenTopping = new Topping { Name = "Chicken", Calories = 50 };
                var pineappleTopping = new Topping { Name = "Pineapple", Calories = 75 };
    
                var tomatoSauce = new Sauce { Name = "Tomato", IsVegan = true };
                var alfredoSauce = new Sauce { Name = "Alfredo", IsVegan = false };
    
                var pizzas = new Pizza[]
                {
                    new Pizza
                        { 
                            Name = "Meat Lovers", 
                            Sauce = tomatoSauce, 
                            Toppings = new List<Topping>
                                {
                                    pepperoniTopping, 
                                    sausageTopping, 
                                    hamTopping, 
                                    chickenTopping
                                }
                        },
                    new Pizza
                        { 
                            Name = "Hawaiian", 
                            Sauce = tomatoSauce, 
                            Toppings = new List<Topping>
                                {
                                    pineappleTopping, 
                                    hamTopping
                                }
                        },
                    new Pizza
                        { 
                            Name="Alfredo Chicken", 
                            Sauce = alfredoSauce, 
                            Toppings = new List<Topping>
                                {
                                    chickenTopping
                                }
                            }
                };
    
                context.Pizzas.AddRange(pizzas);
                context.SaveChanges();
            }
        }
    }
    

    Für den Code oben gilt:

    • Die DbInitializer-Klasse und die Initialize-Methode sind beide als static definiert.
    • Initialize akzeptiert ein PizzaContext-Objekt als Parameter.
    • Wenn in keiner der drei Tabellen Datensätze enthalten sind, werden die Objekte Pizza, Sauce und Topping erstellt.
    • Die Pizza-Objekte (und die zugehörigen Navigationseigenschaften Sauce und Topping) werden dem Objektgraphen mithilfe von AddRange hinzugefügt.
    • Die Änderungen am Objektgraphen werden unter Verwendung von SaveChanges in der Datenbank committet.

Die DbInitializer-Klasse ist bereit, ein Seeding für die Datenbank durchzuführen, muss jedoch in der Datei Program.cs aufgerufen werden. Mit den folgenden Schritten wird eine Erweiterungsmethode für IHost erstellt, die DbInitializer.Initialize aufruft:

  1. Fügen Sie im Ordner Daten eine neue Datei mit dem Namen Extensions.cs hinzu.

  2. Fügen Sie den folgenden Code in Data/Extensions.cs ein:

    namespace ContosoPizza.Data;
    
    public static class Extensions
    {
        public static void CreateDbIfNotExists(this IHost host)
        {
            {
                using (var scope = host.Services.CreateScope())
                {
                    var services = scope.ServiceProvider;
                    var context = services.GetRequiredService<PizzaContext>();
                    context.Database.EnsureCreated();
                    DbInitializer.Initialize(context);
                }
            }
        }
    }
    

    Für den Code oben gilt:

    • Die CreateDbIfNotExists-Methode wird als Erweiterung von IHost definiert.

    • Ein Verweis auf den PizzaContext-Dienst wird erstellt.

    • Mit EnsureCreated wird sichergestellt, dass die Datenbank vorhanden ist.

      Wichtig

      EnsureCreated erstellt eine neue Datenbank, wenn keine vorhanden ist. Die neue Datenbank ist nicht für Migrationen konfiguriert, verwenden Sie diese Methode daher mit Vorsicht.

    • Die DbIntializer.Initialize -Methode wird aufgerufen. Das PizzaContext-Objekt wird als Parameter übergeben.

  3. Ersetzen Sie abschließend in Program.cs den Kommentar // Add the CreateDbIfNotExists method call durch den folgenden Code, um die neue Erweiterungsmethode aufzurufen:

    app.CreateDbIfNotExists();
    

    Dieser Code ruft bei jeder Ausführung der App die Erweiterungsmethode auf, die Sie zuvor definiert haben.

  4. Speichern Sie alle Änderungen, und führen Sie dotnet build aus.

Sie haben den gesamten Code geschrieben, der zum Durchführen grundlegender CRUD-Vorgänge und zum Seeding der Datenbank beim Startvorgang erforderlich ist. In der nächsten Übung testen Sie diese Vorgänge in der App.

Überprüfen Sie Ihr Wissen

1.

Angenommen, Sie möchten eine schreibgeschützte Abfrage schreiben. Wie weisen Sie EF Core darauf hin, dass die Nachverfolgung von Objektdiagrammänderungen unnötig ist?