Exercício: interagir com os dados

Concluído

No exercício anterior, você criou classes de entidade e um contexto de banco de dados. Em seguida, você usou as migrações do EF Core para criar o esquema do banco de dados.

Neste exercício, você concluirá a implementação de PizzaService. O serviço usa o EF Core para executar operações CRUD no banco de dados.

Codificar as operações CRUD

Para concluir a implementação de PizzaService, conclua as seguintes etapas em Services\PizzaService.cs:

  1. Faça as seguintes alterações, conforme mostrado no exemplo:

    1. Adicione uma diretiva using ContosoPizza.Data;.
    2. Adicione uma diretiva using Microsoft.EntityFrameworkCore;.
    3. Adicione um campo de nível de classe para PizzaContext antes do construtor.
    4. Altere a assinatura do método do construtor para aceitar um parâmetro PizzaContext.
    5. Altere o código do método do construtor para atribuir o parâmetro ao 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
        /// ...
    }
    

    A chamada de método AddSqlite adicionada anteriormente a Program.cs registrou PizzaContext para a injeção de dependência. Quando a instância PizzaService é criada, um PizzaContext é injetado como uma dependência.

  2. Substitua o método GetAll pelo seguinte código:

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

    No código anterior:

    • A coleção Pizzas contém todas as linhas na tabela de pizzas.
    • O método de extensão AsNoTracking instrui o EF Core a desabilitar o controle de alterações. Como essa operação é somente leitura, o AsNoTracking pode otimizar o desempenho.
    • Todas as pizzas são retornadas com ToList.
  3. Substitua o método GetById pelo seguinte código:

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

    No código anterior:

    • O método de extensão Include utiliza uma expressão lambda para especificar que as propriedades de navegação Toppings e Sauce devem ser incluídas no resultado por meio do carregamento adiantado. Sem essa expressão, o EF Core retorna null para essas propriedades.
    • O método SingleOrDefault retorna uma pizza que corresponde à expressão lambda.
      • Se nenhum registro corresponder, null será retornado.
      • Se vários registros corresponderem, uma exceção será lançada.
      • A expressão lambda descreve os registros em que a propriedade Id é igual ao parâmetro id.
  4. Substitua o método Create pelo seguinte código:

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

    No código anterior:

    • newPizza é considerado um objeto válido. O EF Core não faz validação de dados, então o runtime do ASP.NET Core ou o código do usuário deve lidar com qualquer validação.
    • O método Add adiciona a entidade newPizza ao grafo de objetos do EF Core.
    • O método SaveChanges instrui o EF Core para persistir as alterações de objeto no banco de dados.
  5. Substitua o método AddTopping pelo seguinte código:

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

    No código anterior:

    • Referências a objetos Pizza e Topping existentes são criadas usando Find.
    • O objeto Topping é adicionado à coleção Pizza.Toppings com o método .Add. Uma nova coleção será criada se ela não existir.
    • O método SaveChanges instrui o EF Core para persistir as alterações de objeto no banco de dados.
  6. Substitua o método UpdateSauce pelo seguinte código:

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

    No código anterior:

    • Referências a objetos Pizza e Sauce existentes são criadas usando Find. Find é um método otimizado para consultar registros por sua chave primária. Find pesquisa o grafo de entidade local primeiro antes de consultar o banco de dados.
    • A propriedade Pizza.Sauce é definida como o objeto Sauce.
    • Uma chamada de método Update é desnecessária porque o EF Core detecta que definimos a propriedade Sauce em Pizza.
    • O método SaveChanges instrui o EF Core para persistir as alterações de objeto no banco de dados.
  7. Substitua o método DeleteById pelo seguinte código:

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

    No código anterior:

    • O método Find recupera uma pizza pela chave primária (nesse caso, Id).
    • O método Remove remove a entidade pizzaToDelete no grafo de objeto do EF Core.
    • O método SaveChanges instrui o EF Core para persistir as alterações de objeto no banco de dados.
  8. Salve todas as alterações e execute dotnet build. Corrija todos os erros que ocorrerem.

Propagar o banco de dados

Você codificou as operações CRUD para PizzaService, mas é mais fácil testar a operação de Leitura se o banco de dados contiver bons dados. Você decide modificar o aplicativo para propagar o banco de dados na inicialização.

Aviso

Esse código de propagação de banco de dados não explica as condições de corrida, portanto, tenha cuidado ao usá-lo em um ambiente distribuído sem atenuar as alterações.

  1. Na pasta Dados, adicione um novo arquivo chamado DbInitializer.cs.

  2. Adicione o seguinte código em 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();
            }
        }
    }
    

    No código anterior:

    • A classe DbInitializer e o método Initialize são definidos como static.
    • Initialize aceita um objeto PizzaContext como um parâmetro.
    • Se não houver nenhum registro em nenhuma das três tabelas, os objetos Pizza, Sauce e Topping serão criados.
    • Os objetos Pizza (e as respectivas propriedades de navegação Sauce e Topping) são adicionados ao grafo do objeto usando AddRange.
    • As alterações do grafo de objeto são confirmadas no banco de dados usando SaveChanges.

A classe DbInitializer está pronta para propagar o banco de dados, mas precisa ser chamada por Program.cs. As seguintes etapas criam um método de extensão para IHost que chama DbInitializer.Initialize:

  1. Na pasta Dados, adicione um novo arquivo chamado Extensions.cs.

  2. Adicione o seguinte 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);
                }
            }
        }
    }
    

    No código anterior:

    • O método CreateDbIfNotExists é definido como uma extensão de IHost.

    • Uma referência ao serviço PizzaContext é criada.

    • EnsureCreated garante que o banco de dados exista.

      Importante

      EnsureCreated criará um banco de dados se ele ainda não existir. O novo banco de dados não está configurado para migrações, portanto, use-o com cuidado.

    • O método DbIntializer.Initialize é chamado. O objeto PizzaContext é passado como um parâmetro.

  3. Por fim, em Program.cs, substitua o comentário // Add the CreateDbIfNotExists method call pelo seguinte código a fim de chamar o novo método de extensão:

    app.CreateDbIfNotExists();
    

    Esse código chama, sempre que o aplicativo é executado, o método de extensão que você definiu anteriormente.

  4. Salve todas as alterações e execute dotnet build.

Você escreveu todo o código necessário para fazer operações CRUD básicas e semear o banco de dados na inicialização. No próximo exercício, você testará essas operações no aplicativo.