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.

Verificar seu conhecimento

1.

Suponha que você deseja escrever uma consulta somente leitura. Como você indica para o EF Core que o controle de alterações do grafo de objetos é desnecessário?