2
respostas

C# - Atualizar uma linha do SQL Server está estourando uma Invalid Operation Exception

Estou na aula 2 parte 7 do Curso Entity Framework Core: Banco de dados de forma eficiente e estou tendo problemas com uma exceção quando tento atualizar uma linha da minha tabela de Dados.

Na aula nós criamos um ProductDAO (DataAssetObject) de um produto da nossa loja para guardar num banco de dados.

using System;
using System.Collections.Generic;
using System.Linq;

namespace DataBaseStudy
{/// <summary>
/// Product Data Asset Object. This is a class that will help to manage the DataBase from this Product.
/// </summary>
    class ProductDAO : IDAO<Product>, IDisposable
    {
        public StoreContext Context { get; }

        public ProductDAO()
        {
            Context = new StoreContext();
        }


        public void RefreshElement(Product obj)
        {

            Context.Products.Update(obj);
            Context.SaveChanges();

        }

        public void Dispose()
        {
            if (Context != null)
            {
                Context.Dispose();
            }
        }


    }
}

Quando eu quero atualizar algum campo que está no Database (Exemplo: Preço do produto mudou de R$100 para R$50) eu chamo minha função Refresh Element(Product obj) como argumento aquele mesmo objeto, só com uma propriedade com valor diferente. Quando ele passa pelo Update estoura uma exceção de Operação Inválida cuja a mensagem é essa:

System.InvalidOperationException: 'The instance of entity type 'Product' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.'

Se eu deletar o objeto anterior, criar um novo e salvar ele eu resolveria o problema, mas me cheira uma má pratica (AKA Gambiarra).

2 respostas

Eu notei que eu n posso passar uma instância de um objeto com os mesmos dados por conta que dessa Exception. Como campos do meu produto possuem PRIVATED SET eu fiz uma função para mudar os valores da instancia dentro do DB e aí o código funcionou. Existe alguma outra solução melhor que está?

Main

  class Program
    {
        static void Main(string[] args)
        {
            using (var ProductDAO = new ProductDAO())
            {
                //This database has 1 item called Livro1 with the price 80.99;
                ProductDAO.ListDatabase();
                Console.WriteLine("---------");
                var Item = new Product("Livro1", "Book", 55.80); //Now i want the same book to cost 55.80
                ProductDAO.RefreshElement(Item);
                ProductDAO.ListDatabase();

            }

            Console.ReadLine();
        }

ProductDAO Update function

 public void RefreshElement(Product obj)
        {

            var DesiredObject = GetRegistratedObjects().FirstOrDefault(Product => Product.Name == obj.Name);
            if(DesiredObject == null)
            {
                return;
            }

            DesiredObject.UpdateData(obj);
            Context.Products.Update(DesiredObject);
            Context.SaveChanges();

        }

Product Class

namespace DataBaseStudy
{
   public class Product
    {
        public string Category { get; private set; }
        public int ProductID { get; private set; }
        public string Name { get;  private set; }
        public double Price { get;  set; }

        public Product(string name, string category, double price)
        {
            UpdateData(name, category, price);
        }

        public Product()
        {

        }

        public void UpdateData(Product NewData)
        {
            Name = NewData.Name;
            Category = NewData.Category;
            Price = NewData.Price;
            ProductID = GetHashCode();
        }

        public void UpdateData(string name, string category, double price)
        {
            Name = name;
            Price = price;
            Category = category;
            ProductID = GetHashCode();
        }


        public override string ToString()
        {
            return $"[Category:{Category} - {Name} [R${Price}]]";
        }

        public override bool Equals(object obj)
        {
            var Other = obj as Product;

            if (Other == null)
            {
                return false;
            }

            return this.ProductID == Other.ProductID;
        }

        public override int GetHashCode()
        {

            return Math.Abs(Name.GetHashCode());
        }


   }
}

Murilo, como estou com preguiça de testar o que vou dizer, você faz e diz se deu certo.

O problema deve ser pelo fato que não fez uso de "using". Quando vc chama o context com "using" no exemplo do exercício, o final desse comando automaticamente implementa o IDisposable e libera os recursos do context. Como vc declarou o context em nivél de classe e não de método, talvez precise dizer manualmente ao Entity que o estado do tracking é "modified" para depois salvar...

Então acredito em duas possíveis soluções: ou implementa o uso do Context igual ao exercício e ele vai criar uma instância do tracking para cada operação; ou muda o estado do tracking antes de atualizar o objeto, através desse link terá a resposta:

https://docs.microsoft.com/pt-br/ef/ef6/saving/change-tracking/entity-state

Mas deve ser assim: Context.Entry(obj).State = EntityState.Modified;

Ou assim: Context.Products.Attach(obj);

SEM O UPDATE no seu caso.

Se consegui te ajudar então coloque a resposta como solução. Obrigado!