Todo mundo sabe que eu gosto do Entity Framework (EF), mas não morro de amores por ele.

Pois bem, estou preparando uma demonstração para o pessoal do curso de ASP.Net MVC que estou ministrando (amanhã é o segundo dia). Eles me pediram para demonstrar como usar EF com o ASP.Net MVC. E legal, funciona bem. Até você precisar atualizar uma entidade: aí você está com problemas.

O cenário do problema é o seguinte: você recebe uma entidade via Request.Form no ASP.Net, e quer reconstituir a entidade e salvá-la no banco. Se você estivesse usando NHibernate isso seria muito simples, o NH é persistence ignorant (ou seja, as entidades não conhecem o banco de dados, e não sabem se são novas entidades, se já vieram do banco, etc, etc), então uma entidade é sempre uma entidade, não importa se você a obteve via sessão (análogo ao contexto do EF) ou acabou de reconstituí-la via serialização HTTP. É uma entidade e isso é o que importa. No EF não é assim. Para atualizar uma entidade, ele assume que ela tem que ter vindo do banco. Não só isso, ela tem que ter vindo do banco pelo mesmo contexto que você está utilizando para atualizar. Isso causa um problemão. Veja esse código, que não funciona:

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Models.Categories cat)
        {
            try
            {
                _context.Attach(cat);
                _context.SaveChanges(); 
            }
            catch (Exception ex)
            {
                return View(cat);
            }
        } 

Nesse código estou usando o default model binder do ASP.Net MVC para montar a entidade para mim. Tudo maravilhoso, tudo lindo. Só que a entidade, que é uma entidade do EF, está independente do contexto. Se eu estivesse criando uma entidade nova e adicionando ela no banco, tudo seria perfeito. Aliás, é o que eu faço na ação create:

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(FormCollection collection)
        {
            try
            {
                var cat = new Models.Categories();
                UpdateModel(cat);
                _context.AddToCategories(cat);
                _context.SaveChanges();
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

Nesse caso não tem default model binder, estou usando outra abordagem, mas a idéia é a mesma, e se tivesse usando o model binder funcionaria. Não é a melhor abordagem mas funciona…

Então o problema é o seguinte: a entidade não conhece o contexto, e vice-versa. Ao chamar Attach, que é o método usado para anexar uma entidade externa ao contexto de volta para ele, você ganha de presente de páscoa antecipado uma bela exceção, que diz:

"An object with a null EntityKey value cannot be attached to an object context" 

Legal, não é? Então o problema é que eu não tenho uma EntityKey. Então vou criar uma. Você faz assim:

cat.EntityKey = _context.CreateEntityKey("Categories", cat);

Depois chama o Attach. Aí funciona. Mas não funciona tudo ainda. O contexto do EF é um cara muito controlador, e ele quer saber tudo que está acontecendo com suas entidades, então antes de salvar uma entidade ele pergunta a ela qual das suas propriedades mudaram, e adivinhem? A entidade neste caso acha que não mudou nada, fica com o status "unchanged" (sem modificações), então você precisa avisar a lesada que ela é uma entidade modificada. Aí entra um artigo bem legal do John Papa na MSDN Magazine que mostra um exemplo de como fazer isso.

Eu peguei o exemplo do Papa e dei uma melhorada. Criei um método de extensão que faz o trabalho pra mim que ficou assim:

        public static void Update(this ObjectContext context, string entitySetName, IEntityWithKey entity) 
        {
            entity.EntityKey = context.CreateEntityKey(entitySetName, entity);
            context.Attach(entity);
            var stateEntry = context.ObjectStateManager.GetObjectStateEntry(entity.EntityKey);
            var propertyNameList = stateEntry.CurrentValues.DataRecordInfo.FieldMetadata.Select(pn => pn.FieldType.Name);
            foreach (var propName in propertyNameList)
            {
                stateEntry.SetModifiedProperty(propName);
            }
        }

Para usar, nada mais fácil. Quase parece NHibernate de tão fácil. Chamo Update (meu método de extensão), depois sigo o padrão chamando SaveChanges:

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Models.Categories cat)
        {
            try
            {
                _context.Update("Categories", cat);
                _context.SaveChanges(); 
                return RedirectToAction("Index");
            }
            catch (Exception ex)
            {
                return View(cat);
            }
        }

Eu até podia chamar SaveChanges no método de extensão, mas quis manter o padrão de deixar o cliente do EF fazer isso. E com o Update dei uma boa idéia de atualização com entidade remota. Ficou legal.

Ah, existe uma outra opção, bem mais simples, mas que te custa uma viagem extra ao banco de dados:

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(int id, FormCollection collection)
        {
            try
            {
                var cat = (from cats in _context.Categories
                           where cats.CategoryID == id
                           select cats).FirstOrDefault();
                UpdateModel(cat);
                _context.SaveChanges(); 
                return RedirectToAction("Index");
            }
            catch (Exception ex)
            {
                return View(cat);
            }
        } 

Nessa caso você faz uma consulta ao BD, atualiza a entidade e salva. Duas viagens ao BD para fazer uma coisa. Caro! Essa é a maneira padrão que o ASP.Net MVC sugere. Apenas para constar… fuja desta última implementação!

Ah, estou postando isso porque sei que muita gente vai precisar, e isso inclui eu mesmo. Tenho certeza que cedo ou tarde vou precisar fazer isso de novo. No .Net Architects já chegamos a comentar um pouco sobre as possibilidades do EF, o André Dias até levantou a idéia do método de extensão, então fica aqui para todos.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.