Ejercicio: Interacción con datos

Completado

En el ejercicio anterior, ha creado clases de entidad y un contexto de base de datos. Después, ha usado migraciones de EF Core para crear el esquema de la base de datos.

En este ejercicio, completará la implementación de PizzaService. El servicio usa EF Core para realizar operaciones CRUD en la base de datos.

Codificación de las operaciones CRUD

Para finalizar la implementación de PizzaService, complete los pasos siguientes en Services\PizzaService.cs:

  1. Realice los siguientes cambios como se muestra en el ejemplo:

    1. Agregue una directiva using ContosoPizza.Data;.
    2. Agregue una directiva using Microsoft.EntityFrameworkCore;.
    3. Agregue un campo de nivel de clase para PizzaContext antes del constructor.
    4. Cambie la firma del método del constructor para aceptar un parámetro PizzaContext.
    5. Cambie el código del método del constructor para asignar el parámetro 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 llamada de método AddSqlite que agregó anteriormente a Program.cs registró PizzaContext para la inserción de dependencias. Cuando se crea la instancia de PizzaService, se inserta un elemento PizzaContext en el constructor.

  2. Reemplace el método GetAll con el código siguiente:

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

    En el código anterior:

    • La colección Pizzas contiene todas las filas de la tabla de pizzas.
    • El método de extensión AsNoTracking indica a EF Core que deshabilite el seguimiento de cambios. Puesto que esta operación es de solo lectura, AsNoTracking puede optimizar el rendimiento.
    • Todas las pizzas se devuelven con ToList.
  3. Reemplace el método GetById con el código siguiente:

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

    En el código anterior:

    • El método de extensión Include adopta una expresión lambda para especificar que las propiedades de navegación Toppings y Sauce se van a incluir en el resultado mediante la carga diligente. Sin esta expresión, EF Core devuelve null para esas propiedades.
    • El método SingleOrDefault devuelve una pizza que coincide con la expresión lambda.
      • Si no hay registros coincidentes, se devuelve null.
      • Si coinciden varios registros, se produce una excepción.
      • La expresión lambda describe los registros en los que la propiedad Id es igual al parámetro id.
  4. Reemplace el método Create con el código siguiente:

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

    En el código anterior:

    • Se supone que newPizza es un objeto válido. EF Core no realiza la validación de datos, por lo que el código de usuario o el entorno de ejecución de ASP.NET Core debe controlar cualquier validación.
    • El método Add agrega la entidad newPizza al gráfico de objetos de EF Core.
    • El método SaveChanges indica a EF Core que conserve los cambios del objeto en la base de datos.
  5. Reemplace el método AddTopping con el código siguiente:

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

    En el código anterior:

    • Las referencias a los objetos Pizza y Topping existentes se crean mediante Find.
    • El objeto Topping se agrega a la colección Pizza.Toppings con el método .Add. Si no existe, se crea una colección.
    • El método SaveChanges indica a EF Core que conserve los cambios del objeto en la base de datos.
  6. Reemplace el método UpdateSauce con el código siguiente:

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

    En el código anterior:

    • Las referencias a los objetos Pizza y Sauce existentes se crean mediante Find. Find es un método optimizado para consultar registros por su clave principal. Find busca primero en el gráfico de entidades locales antes de consultar la base de datos.
    • La propiedad Pizza.Sauce se establece en el objeto Sauce.
    • No es necesaria una llamada de método Update porque EF Core detecta que se establece la propiedad Sauce en Pizza.
    • El método SaveChanges indica a EF Core que conserve los cambios del objeto en la base de datos.
  7. Reemplace el método DeleteById con el código siguiente:

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

    En el código anterior:

    • El método Find recupera una pizza por la clave principal (que, en este caso, es Id).
    • El método Remove elimina la entidad pizzaToDelete del gráfico de objetos de EF Core.
    • El método SaveChanges indica a EF Core que conserve los cambios del objeto en la base de datos.
  8. Guarde todos los cambios y ejecute dotnet build. Corrija los errores que se produzcan.

Inicializar la base de datos

Ha codificado las operaciones CRUD para PizzaService, pero es más fácil probar la operación de lectura si la base de datos contiene buenos datos. Decide modificar la aplicación para inicializar la base de datos en el momento en que arranca.

Advertencia

Este código de inicialización de la base de datos no tiene en cuenta las condiciones de carrera, por lo que debe tener cuidado al usarlo en un entorno distribuido sin mitigar los cambios.

  1. En la carpeta Data, agregue un archivo nuevo llamado DbInitializer.cs.

  2. Agregue el siguiente código 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();
            }
        }
    }
    

    En el código anterior:

    • La clase DbInitializer y el método Initialize se definen como static.
    • Initialize acepta un objeto PizzaContext como parámetro.
    • Si no hay registros en ninguna de las tres tablas, se crean los objetos Pizza, Sauce y Topping.
    • Los objetos Pizza y sus propiedades de navegación Sauce y Topping se agregan al gráfico de objetos mediante AddRange.
    • Los cambios del gráfico de objetos se confirman en la base de datos mediante SaveChanges.

La clase DbInitializer está lista para inicializar la base de datos, pero se le debe llamar desde Program.cs. En los pasos siguientes se crea un método de extensión para IHost que llame a DbInitializer.Initialize:

  1. En la carpeta Data, agregue un archivo nuevo llamado Extensions.cs.

  2. Agregue el siguiente código 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);
                }
            }
        }
    }
    

    En el código anterior:

    • El método CreateDbIfNotExists se define como una extensión de IHost.

    • Se crea una referencia al servicio PizzaContext.

    • EnsureCreated garantiza que la base de datos existe.

      Importante

      Si no existe ninguna base de datos, EnsureCreated crea una. La nueva base de datos no está configurada para migraciones, así que use este método con precaución.

    • Se llama al método DbIntializer.Initialize . El objeto PizzaContext se pasa como un parámetro.

  3. Por último, en Program.cs, reemplace el comentario // Add the CreateDbIfNotExists method call por el código siguiente para llamar al nuevo método de extensión:

    app.CreateDbIfNotExists();
    

    Este código llama al método de extensión que definió anteriormente cada vez que se ejecuta la aplicación.

  4. Guarde todos los cambios y ejecute dotnet build.

Escribió todo el código que necesita para realizar operaciones CRUD básicas y inicializar la base de datos al iniciarse. En el siguiente ejercicio, pruebe esas operaciones en la aplicación.

Comprobación de conocimientos

1.

Suponga que quiere escribir una consulta de solo lectura. ¿Cómo indica a EF Core que el seguimiento de los cambios del gráfico de objeto no es necesario?