Esercizio - Interagire con i dati

Completato

Nell'esercizio precedente, sono state create classi di entità e un contesto di database. Si sono usate le migrazioni di EF Core per creare lo schema del database.

In questo esercizio viene completata l'implementazione PizzaService. Il servizio usa EF Core per eseguire operazioni CRUD nel database.

Codice delle operazioni CRUD

Per terminare l'implementazione PizzaService, completare i seguenti passaggi in Services\PizzaService.cs:

  1. Apportare le seguenti modifiche come mostrato nell'esempio:

    1. Aggiungere una direttiva using ContosoPizza.Data;.
    2. Aggiungere una direttiva using Microsoft.EntityFrameworkCore;.
    3. Aggiungere un campo a livello di classe per PizzaContext prima del costruttore.
    4. Modificare la firma del metodo del costruttore in modo che accetti un parametro PizzaContext.
    5. Modificare il codice del metodo del costruttore in modo da assegnare il parametro al campo.
    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
        /// ...
    }
    

    La chiamata al metodo AddSqlite aggiunta in precedenza a Program.cs ha registrato PizzaContext per l'inserimento delle dipendenza. Quando l'istanza PizzaService viene creata, PizzaContext viene inserito nel costruttore.

  2. Sostituire il metodo GetAll con il codice seguente:

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

    Nel codice precedente:

    • La raccolta Pizzas contiene tutte le righe della tabella pizzas.
    • Il metodo di estensione AsNoTracking indica a EF Core di disabilitare il rilevamento delle modifiche. Poiché questa operazione è di sola lettura, AsNoTracking può ottimizzare le prestazioni.
    • Tutte le pizze vengono restituite con ToList.
  3. Sostituire il metodo GetById con il codice seguente:

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

    Nel codice precedente:

    • Il metodo di estensione Include accetta un'espressione lambda per specificare che le proprietà di navigazione Toppings e Sauce devono essere incluse nel risultato usando il caricamento eager. Senza questa espressione, EF Core restituisce null per queste proprietà.
    • Il metodo SingleOrDefault restituisce una pizza corrispondente all'espressione lambda.
      • Se non viene trovato alcun record corrispondente, viene restituito null.
      • Se vengono trovati più record corrispondenti, viene generata un'eccezione.
      • L'espressione lambda descrive i record in cui la proprietà Id è uguale al parametro id.
  4. Sostituire il metodo Create con il codice seguente:

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

    Nel codice precedente:

    • Si presuppone che newPizza sia un oggetto valido. EF Core non esegue la convalida dei dati, quindi l'eventuale convalida deve essere gestita dal codice utente o dal runtime di ASP.NET Core.
    • Il metodo Add aggiunge l'entità newPizza al grafico di oggetti di EF Core.
    • Il metodo SaveChanges indica a EF Core di rendere permanenti le modifiche apportate all'oggetto nel database.
  5. Sostituire il metodo UpdateSauce con il codice seguente:

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

    Nel codice precedente:

    • I riferimenti agli oggetti Pizza e Sauce esistenti vengono creati utilizzando Find. Find è un metodo ottimizzato per eseguire query sui record in base alla chiave primaria. Find esegue una ricerca nel grafico delle entità locale prima di eseguire query sul database.
    • La proprietà Pizza.Sauce è impostata sull'oggetto Sauce.
    • La chiamata al metodo Update non è necessaria perché EF Core rileva che la proprietà Sauce è stata impostata su Pizza.
    • Il metodo SaveChanges indica a EF Core di rendere permanenti le modifiche apportate all'oggetto nel database.
  6. Sostituire il metodo AddTopping con il codice seguente:

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

    Nel codice precedente:

    • I riferimenti agli oggetti Pizza e Topping esistenti vengono creati utilizzando Find.
    • L'oggetto Topping viene aggiunto all'insieme Pizza.Toppings con il metodo .Add. Se non esiste, viene creata una nuova raccolta.
    • Il metodo SaveChanges indica a EF Core di rendere permanenti le modifiche apportate all'oggetto nel database.
  7. Sostituire il metodo DeleteById con il codice seguente:

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

    Nel codice precedente:

    • Il metodo Find recupera una pizza in base alla chiave primaria, che in questo caso è Id.
    • Il metodo Remove rimuove l'entità pizzaToDelete nel grafico di oggetti di EF Core.
    • Il metodo SaveChanges indica a EF Core di rendere permanenti le modifiche apportate all'oggetto nel database.
  8. Salvare tutte le modifiche ed eseguire dotnet build. Correggere eventuali errori che si verificano.

Specificare il valore di inizializzazione del database

Sono state codificate le operazioni CRUD per PizzaService, ma è più facile testare le operazioni di lettura se il database contiene dati validi. A questo punto l'app verrà modificata per effettuare il seeding del database all'avvio.

Avviso

Il codice di seeding del database non tiene conto delle race condition, quindi è necessario prestare attenzione quando viene usato in un ambiente distribuito senza mitigare le modifiche.

  1. Nella cartella Data aggiungere un nuovo file denominato DbInitializer.cs.

  2. Aggiungere il codice seguente a Data\DbInitializer.cs:

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

    Nel codice precedente:

    • La classe DbInitializer e il metodo Initialize sono entrambi definiti come static.
    • Initialize accetta un oggetto PizzaContext come parametro.
    • Se non sono presenti record in una delle tre tabelle, vengono creati gli oggetti Pizza, Sauce e Topping.
    • Gli oggetti Pizza (e le relative proprietà di esplorazione Sauce e Topping) vengono aggiunti al grafico di oggetti usando AddRange.
    • Le modifiche apportate al grafico di oggetti vengono sottoposte a commit nel database usando SaveChanges.

La classe DbInitializer è pronta per eseguire il seeding del database, ma deve essere chiamata da Program.cs. I passaggi seguenti creano un metodo di estensione per IHost che chiama DbInitializer.Initialize:

  1. Nella cartella Data aggiungere un nuovo file denominato Extensions.cs.

  2. Aggiungere il codice seguente a Data\Extensions.cs:

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

    Nel codice precedente:

    • Il metodo CreateDbIfNotExists viene definito come estensione di IHost.

    • Viene creato un riferimento al servizio PizzaContext.

    • EnsureCreated assicura l’esistenza del database.

      Importante

      Se un database non esiste, EnsureCreated crea un nuovo database. Il nuovo database non è configurato per le migrazioni, quindi è consigliabile usare questo metodo con cautela.

    • Viene chiamato il metodo DbIntializer.Initialize . L'oggetto PizzaContext viene passato come parametro.

  3. Infine, in Program.cs, sostituire il commento // Add the CreateDbIfNotExists method call con il seguente codice per chiamare il nuovo metodo di estensione:

    app.CreateDbIfNotExists();
    

    Questo codice chiama il metodo di estensione definito in precedenza ogni volta che l'app viene eseguita.

  4. Salvare tutte le modifiche ed eseguire dotnet build.

È stato scritto tutto il codice necessario per eseguire operazioni CRUD di base ed effettuare il seeding del database all'avvio. Nell'esercizio successivo, queste operazioni verranno testate nell’app.

Verifica le tue conoscenze

1.

Si supponga di voler scrivere una query di sola lettura. Come si specifica a EF Core che il rilevamento delle modifiche negli oggetti grafici non è necessario?