Pontos de dados

Dicas para atualizar e fazer a refatoração de seu código do Entity Framework, Parte 2

Julie Lerman

Julie LermanO Entity Framework (EF) já existe por mais de oito anos e tem passado por várias alterações. Isto significa que os desenvolvedores estão procurando por aplicativos que usam versões anteriores do EF para ver como eles podem se beneficiar de suas mais novas melhorias. Trabalhei com várias equipes de software que estavam passando por este processo e estou compartilhando algumas lições aprendidas e orientações em uma série de duas partes. No mês passado discuti a atualização para o EF6, usando um aplicativo EF4 antigo como um exemplo (bit.ly/1jqu5yQ). Eu também forneci dicas para dividir os Modelos de Dados de Entidade menores de modelos maiores (EDMX ou Code First), porque eu recomendo trabalhar com modelos focalizados múltiplos ao invés de usar um modelo grande—cercado de complicações, geralmente desnecessárias de relações—por todo seu aplicativo.

Este mês, eu usarei um exemplo específico para compartilhar mais detalhes sobre dividir um modelo pequeno, e depois, usando aquele modelo pequeno, trabalhar através de alguns dos problemas que você encontra ao alternar da API ObjectContext para a API DbContext mais recente. 

Neste artigo, eu uso um aplicativo existente que é um pouco mais do que o aplicativo de demonstração típico para apresentar os tipos de problemas reais que você talvez enfrente ao fazer a refatoração de suas próprias soluções. É um aplicativo de exemplo pequeno do meu livro, “Programming Entity Framework, 2nd Edition” (O’Reilly Media, 2010), escrito usando o EF4 e a API ObjectContext. Seu modelo EDMX foi gerado usando o Database First, depois personalizado no EF Designer. Nesta solução, eu mudei para o modelo de geração de código T4 padrão que cria classes entidade que todos herdam da classe EntityObject. Ao invés, eu usei um modelo T4 que criou objetos CLR antigos simples (POCOs), que foram suportados começando com o EF4, e eu personalizei um pouco o modelo. Devo observar que este é um aplicativo do lado do cliente e, no entanto, não possui alguns dos desafios de um aplicativo desconectado, onde o rastreamento do estado é mais desafiador. Independentemente, muitos dos problemas que você verá aqui aplicam-se a ambos os cenários.

Na coluna do mês passado, eu atualizei esta solução para usar o EF6 e o Microsoft .NET Framework 4.5, mas não fiz nenhuma alteração a minha base de código. Continua usando o modelo T4 original para gerar as classes POCO e uma classe ObjectContext que gerencia a persistência.

A interface inicial do aplicativo usar o Windows Presentation Foundation (WPF) para sua interface de usuário. Enquanto a arquitetura é em camadas, minhas ideias sobre projetar aplicativos têm definitivamente evoluído desde então. No entanto, vou me abster da refatoração completa que faria meu coração movimentar-se quando eu olhasse para o código atualizado.

Meus objetivos para esta coluna são:

  • Extrair um modelo menor que esteja focalizado em uma tarefa.
  • Alterar a geração de código do modelo menor para a produção de classes POCO com estilo mais novo e uma classe DbContext para gerenciar a persistência.
  • Corrigir um código existente que usa o ObjectContext e quebrou por causa da mudança para o DbContext. Em muitos casos, isto significa criar métodos duplicados e depois modificá-los para a lógica DbContext. Desta forma, eu não quebrarei o código restante que ainda está sendo usado pelo modelo original e seu ObjectContext.
  • Procure por oportunidades para substituir a lógica com funcionalidades mais simples e eficientes no EF5 e EF6.

Como criar o modelo de Manutenção de Viagem

Na coluna do mês passado, forneci ponteiros para identificar e extrair um modelo menor a partir de um modelo grande. Seguindo estas orientações, examinei o modelo deste aplicativo, que não é muito grande, mas contém entidades para gerenciar uma variedade de tarefas envolvidas na execução de uma viagem de negócios de aventura. Ele tem entidades para manter as viagens definidas por destinos, atividades, acomodações, datas e outros detalhes, bem como clientes, suas reservas, pagamentos e informações de contatos diversos. Você pode ver o modelo completo no lado esquerdo da Figura 1.

The Full Model on the Left, the Constrained Model on the Right, Created Using the Entities Shown in Red in the Bigger Model
Figura 1 O modelo completo à esquerda, o modelo restrito à direita, criado usando as entidades mostradas em vermelho no modelo maior.

Uma das tarefas do aplicativo permite aos usuários definir novas viagens, bem como manter as viagens existentes usando um formulário WPF conforme exibido na Figura 2.

The Windows Presentation Foundation Form for Managing Trip Details
Figura 2 O formulário do Windows Presentation Foundation para gerenciar detalhes da viagem

O formulário WPF trata da definição das viagens: selecionar um destino e um hotel, as datas iniciais e finais, e uma variedade de atividades. No entanto, eu posso prever um modelo limitado para satisfazer apenas este conjunto de tarefas. Eu criei um novo modelo (também conhecido na Figura 1) usando o assistente de Modelo de Dados de Entidade e selecionei apenas as tabelas relevantes. O modelo também está ciente da tabela conjunta responsável pelas relações de muitos para muitos entre os eventos e as atividades.

Em seguida, apliquei as mesmas personalizações (nas quais meu código depende) para estas entidades no meu novo modelo que eu defini anteriormente no modelo original. A maioria foram alterações no nome para entidades e suas propriedades; por exemplo, Evento se tornou Viagem e Local se tornou Destino. 

Estou trabalhando com um EDMX, mas você pode definir um novo modelo para o Code First criando uma nova classe DbContext destinada somente as quatro entidades. Neste caso, no entanto, você precisa definir novos tipos que não incluem as relações supérfluas ou usar os mapeamentos da API Fluent para ignorar relações particulares. Minha coluna de Pontos de dados de janeiro de 2013, “Shrink Models with DDD Bounded Contexts” (bit.ly/1isIoGE), fornece orientações para o caminho do Code First. 

É legal se livrar dos desafios de manter relações sobre o que, neste contexto, eu simplesmente não me importo. Por exemplo, reservas, e todas as coisas ligadas às reservas (como pagamento e clientes), agora desapareceram.

Eu também cortei a Acomodação e Atividade, porque eu realmente preciso de sua identidade e nome somente para a manutenção da viagem. Infelizmente, algumas regras em torno do mapeamento para banco de dados não anulável significam que eu tive que reter CustomerID e DestinationID. Porém eu não preciso mais de propriedades de navegação do Destino ou Acomodação, back to Trip, então eu exclui elas do modelo.

Depois, eu tive que considerar o que fazer com as classes geradas por códigos. Eu já tinha classes geradas do outro modelo, mas estas classes relacionadas as relações, eu não preciso (ou tenho) neste modelo. Porque eu tenho me concentrado no design controlado por domínio (DDD) por vários anos, estou confortável em ter um conjunto de classes específicas para este novo modelo e usá-las separadamente das outras classes geradas. Estas classes estão em um projeto separado e em um espaço de nomes diferente. Porque ele mapeia novamente para um banco de dados comum, não tenho que me preocupar sobre alterações feitas em um local que não aparece em outro. Tendo classes que estão focadas somente na tarefa de manter viagens tornará o código mais simples, embora isto significará mais duplicações por toda a minha solução. A redundância é um compromisso que eu estou disposto a assumir e um que já tive que aceitar em projetos anteriores. O lado esquerdo da Figura 3 mostra o modelo (BreakAway.tt) e as classes geradas associadas ao modelo grande. Elas estão no seu próprio projeto, BreakAwayEntities. Explicarei o lado direito da Figura 3 logo em seguida.

The Original Classes Generated from the T4 Template for the Large Model, Compared to the New Model and Its Generated Classes
Figura 3 As classes originais geradas do modelo T4 para o modelo grande em comparação com o novo modelo e suas classes geradas

Conforme a coluna do mês passado, quando criei um novo modelo, eu usei meu modelo de geração de código original para que meu único desafio durante esta etapa seria assegurar que meu aplicativo funcionasse usando o modelo pequeno, sem ter que simultaneamente se preocupar com as APIs do EF modificadas. Fui capaz de fazer isto ao substituir o código dentro dos arquivos de modelo padrão (TripMaintenance.Context.tt e TripMaintenance.tt) com o código dentro dos meus arquivos BreakAwayEntities.tt. Além disso, tive que modificar o caminho do arquivo no código do modelo para apontar para um novo arquivo EDMX.

Ao todo, levei aproximadamente uma hora para alterar referências e nomes de espaço até eu conseguir executar meu pequeno aplicativo e meus testes novamente usando o novo modelo, não apenas recuperar dados, mas editar e inserir novos gráficos de dados.

Finalizando o processo: Movendo para a API DbContext

Agora eu tenho um modelo menor que é muito menos complicado e tornará a tarefa de interagir com estes dados no meu código WPF mais simples. Estou pronto para combater os trabalhos mais difíceis: substituindo meu ObjectContext pelo DbContext para que eu possa excluir o código que escrevi para compensar a complexidade de trabalhar diretamente com o ObjectContext. Estou agradecido estarei enrolado apenas com a área de superfície menor do código que está relacionada ao meu novo modelo. É como quebrar novamente um braço quebrado para que ele fixe adequadamente, enquanto o resto dos ossos do meu aplicativo continuarão intactos e sem dor.

Atualizando o modelo de geração de código para meu EDMX Continuarei a usar o EDMX, então para alterar a Manutenção de Viagem para a API DbContext, devo selecionar um novo modelo de geração de código. Eu posso facilmente acessar o que desejo: O gerador DbContext do EF 6.x, porque ele foi instalado com o Visual Studio 2013. Primeiro, excluirei os dois arquivos TT que estão anexados ao arquivo TripMaintenance.EDMX e depois eu posso usar a ferramenta Adicionar Item de Geração de Código do designer para selecionar o novo modelo. (Se você não estiver familiarizado com o uso dos modelos de geração de código, consulte o documento MSDN, “EF Designer Code Generation Templates,” em bit.ly/1i7zU3Y).

Lembre-se que eu personalizei o modelo original. Uma personalização crítica que precisarei duplicar no novo modelo é remover o código que injeta a palavra-chave virtual nas propriedades de navegação. Isto assegurará que o carregamento lento não seja acionado, visto que meu código depende deste comportamento. Se você está usando um modelo T4 e personalizou alguma coisa para seu modelo original, esta é uma etapa muito importante para se lembrar. (Você pode exibir um vídeo antigo que eu criei para o MSDN que demonstra a edição do modelo EDMX T4 em bit.ly/1jKg4jB.)

Um último detalhe foi participar de algumas classes parciais que eu criei para algumas entidades e para o ObjectContext. Eu copiei as relevantes no novo projeto do modelo e verifiquei se estavam ligadas às classes geradas recentemente.

Corrigindo os métodos AddObject, AttachObject e DeleteObject Agora é hora de ver os danos. Enquanto muitos do meus códigos quebrados são particulares em como eu codifico em relação à API ObjectContext, há um conjunto de métodos facilmente direcionados que serão comuns a muitos dos aplicativos, então eu escolherei estes primeiro.

A API ObjectContext oferece várias maneiras de adicionar, anexar e remover objetos de um ObjectSet. Por exemplo, para adicionar um item, como uma instância de uma viagem nominada newTrip, você pode usar o ObjectContext diretamente:

context.AddObject(“Viagens",newTrip)

Ou você pode fazer isto a partir do ObjectSet:

_context.Trips.AddObject(newTrip)

O método ObjectContext é um pouco estranho, pois necessita de uma cadeia de caracteres para identificar qual conjunto a entidade pertence. O método ObjectSet é mais simples porque o conjunto já está definido, mas ainda usa termos estranhos: AddObject, AttachObject e DeleteObject. Com o DbContext, existe apenas um caminho—por meio do DbSet. Os nomes dos métodos foram simplificados para Add, Attach e Remove para melhor emular os métodos de coleção, por exemplo:

_context.Trips.Add(newTrip);

Observe que o DeleteObject tornou-se Remover. Isto pode ser confuso porque enquanto Remove se alinha melhor às coleções, na verdade não expressa a intenção do método, que é eventualmente excluir o registro do banco de dados. Tenho visto desenvolvedores supor equivocadamente que o Collection.Remove terá o mesmo resultado que o DbSet.Remove—indicando ao EF que a entidade de destino seja excluída do banco de dados. Mas ela não será. Então, tenha cuidado com isso.

O resto dos problemas que eu enfrento são específicos a como eu uso o ObjectContext no meu aplicativo original. Eles não são necessariamente os mesmos problemas que você encontrará, mas vendo estas interrupções em particular, e como corrigi-las, ajudará você a se preparar para qualquer coisa que você encontre ao fazer a mudança para o DbContext nas suas próprias soluções.

Corrigindo o código nas classes parciais personalizadas do contexto Comecei minha correção criando o projeto que contém meu novo modelo. Assim que este projeto for classificado, eu posso investir nos projetos que dependem dele. A classe parcial que criei originalmente para ampliar o ObjectContext foi a primeira a falhar.

Um de nossos métodos personalizados na classe parcial é o ManagedEntities, que me ajudou a ver quais entidades estavam sendo rastreadas pelo contexto. O ManagedEntities baseou-se em um método de extensão que eu criei: uma sobrecarga sem parâmetros do GetObjectStateEntries. O meu uso daquela sobrecarga foi a causa do erro do compilador:

public IEnumerable<T> ManagedEntities<T>() { var oses = ObjectStateManager.GetObjectStateEntries(); return oses.Where(entry => entry.Entity is T) .Select(entry => (T)entry.Entity); }

Ao invés de corrigir o método de extensão subjacente GetObjectStateEntries, eu posso apenas eliminá-lo e o método ManagedEntities porque a API DbContext tem um método na classe DbSet chamado Local que posso usar no lugar.

Há duas abordagens que eu poderia obter para esta refatoração. Um é encontrar todos os códigos que usam meu novo modelo que chama o ManagedEntities e substitui-lo pelo método DbSet.Local. Veja um exemplo do código que usa o ManagedEntities para iterar por todas da Viagens que o contexto está rastreamento:

foreach (var trip in _context.ManagedEntities<Trip>())

Eu posso substituir isto por:

foreach (var trip in _context.Trips.Local.ToList())

Observe que Local retorna um ObservableCollection, então eu adiciono ToList para extrair as entidades dele.

Como alternativa, se meu código tem várias chamadas para o ManagedEntities, eu posso apenas alterar a lógica por trás do ManagedEntities e evitar ter que editar todos os locais que estão sendo usados. Porque o método é genérico, não é tão direto quanto simplesmente usar as Viagens DbSet, mas a alteração continua simples o suficiente:

public IEnumerable<T> ManagedEntities<T>() where T : class  { return Set<T>().Local.ToList(); }

Mais importante, a minha lógica do Gerenciamento de viagem não depende mais do método de extensão sobrecarregado GetObjectStateEntries. Eu posso deixar esse método intacto para que o meu original ObjectContext possa continuar usando ele.

A longo prazo, muitos dos truques que tive que realizar com os métodos de extensão se tornaram desnecessários ao usar a API DbContext. Tinha tantos modelos comumente necessários usados que a API DbContext inclui maneiras simples, como o método Local de realizá-los.

O próximo método quebrado que encontrei na classe parcial foi um que usei para definir o estado rastreado da entidade para Modified. Novamente, fui forçado a usar alguns códigos do EF não óbvios para a ação. A linha do código que falha é:

ObjectStateManager.ChangeObjectState(entity, EntityState.Modified);

Eu posso substitui-la por uma propriedade mais direta DbContext.Entry().State. Porque este código está na classe parcial que estende meu DbContext, eu posso acessar o método Entry diretamente ao invés de uma instância do TripMaintenanceContext. A nova linha de código é:

Entry(entity).State = EntityState.Modified;

O método final na minha classe parcial—também quebrado—usa o método ObjectSet.ApplyCurrentValues para corrigir o estado de uma entidade rastreada usando os valores de outra instância (não rastreada) do mesmo tipo. O ApplyCurrentValues usa o valor de entidade da instância que você transmite, encontra a entidade correspondente no rastreador de alteração e depois atualiza ele usando os valores do objeto transmitido.

Não há equivalente no DbContext para ApplyCurrentValues. O Db­Context permite que você faça uma substituição de valor semelhante usando o Entry().CurrentValues().Set, mas isso exige que você já tenha acesso à entidade rastreada. Não há uma maneira fácil de criar um método genérico para encontrar aquele entidade rastreada para substituir a funcionalidade. No entanto, nem tudo está perdido. Você pode continuar usando o método especial ApplyCurrentValues aproveitando a capacidade para acessar a lógica ObjectContext de um DbContext. Lembre-se, o DbContext é um embrulho em volta do ObjectContext e a API fornece uma forma de pesquisar o ObjectContext subjacente em busca de casos especiais, como o IObjectContextAdapter. Eu adicionei uma propriedade simples, a Core, à minha classe parcial para torná-la mais fácil de usar novamente:

public ObjectContext Core { get { return (this as IObjectContextAdapter).ObjectContext; }}

Eu então modifiquei o método relevante da minha classe parcial para continuar a usar o ApplyCurrentValues, chamando o CreateObjectSet da propriedade Core. Isto me fornece um ObjectSet para que eu possa continuar a usar o ApplyCurrentValues:

public void UpdateEntity<T>(T modifiedEntity) where T : class { var set = Core.CreateObjectSet<T>(); set.ApplyCurrentValues(modifiedEntity); }

Com esta última alteração, meu projeto do modelo foi capaz de compilar. Agora é tempo de tratar do código quebrado nas camadas entre minha interface de usuário e o modelo.

MergeOption.NoTracking para AsNoTracking Queries e Lambdas, também especificando que o EF não deveria rastrear os resultados de uma forma importante para evitar o processamento desnecessário e melhorar o desempenho. Há vários locais em meu aplicativo onde eu consulto dados que serão usados somente como uma lista de referência. Veja um exemplo onde eu recuperei um lista de Viagens somente leitura ao longo com suas informações de Destino para serem exibidas na Caixa de Listagem na minha interface de usuário. Com a API antiga, você tinha que definir o MergeOption em uma consulta antes de executá-lo. Veja o código feio que eu escrevi:

var query = _context.Trips.Include("Destination"); query.MergeOption = MergeOption.NoTracking; _trips = query.OrderBy(t=>t.Destination.Name).ToList();

O DbSet tem uma forma mais simples de fazer isto usando seu método AsNoTracking. Enquanto estiver nele, posso me livrar da cadeia de caracteres no método Include, a medida que o DbContext finalmente adicionou a capacidade de usar uma expressão lambda lá. Veja aqui o código revisado:

 

_trips= _context.Trips.AsNoTracking().Include(t=>t.Destination) .OrderBy(t => t.Destination.Name) .ToList();

DbSet.Local para o Rescue Again Um número de locais no meu código exigem que eu descubra se um entidade tem sido rastreada quando tudo que eu tinha era seu valor de identidade. Eu escrevi um método auxiliar, exibido na Figura 4, para fazer isto, e você pode ver porque eu queria encapsular este código. Não se preocupe em decifrá-lo; foi encaminhado para a lixeira.

Figura 4 Meu método auxiliar IsTracked tornou-se desnecessário graças ao novo método New DbSet.Local

public static bool IsTracked<TEntity>(this ObjectContext context, Expression<Func<TEntity, object>> keyProperty, int keyId) where TEntity : class { var keyPropertyName = ((keyProperty.Body as UnaryExpression) .Operand as MemberExpression).Member.Name; var set = context.CreateObjectSet<TEntity>(); var entitySetName = set.EntitySet.EntityContainer.Name + "." + set.EntitySet.Name; var key = new EntityKey(entitySetName, keyPropertyName, keyId); ObjectStateEntry ose; if (context.ObjectStateManager.TryGetObjectStateEntry(key, out ose)) { return true; } return false; }

Veja um exemplo do código no meu aplicativo que chamou o IsTracked passando o valor de uma Viagem que quero encontrar:

_context.IsTracked<Trip>(t => t.TripID, tripId)

Graças ao mesmo método DbSet.Local que usei anteriormente, eu pude substituí-lo por:

_context.Trips.Local.Any(d => d.TripID == tripId))

E depois eu consegui excluir o método IsTracked! Você está acompanhando quantos códigos eu consegui excluir até agora?

Outro método que eu escrevi para o aplicativo, o AddActivity, precisava fazer mais do que apenas verificar se uma entidade já estava sendo rastreada. Ele precisar obter aquele entidade—outra tarefa que Local pode me ajudar. O método AddActivity (Figura 5) adiciona uma Atividade a uma viagem em particular usando um código feio e não óbvio que tive que escrever para a API ObjectContext. Isto envolve relações de muitos para muitos entre a Viagem e a Atividade. Anexando uma instância da atividade para uma viagem que está sendo rastreada faz com que o EF comece a rastrear a atividade, então eu precisava proteger o contexto de uma duplicada e meu aplicativo da exceção resultante. No meu método, eu tentei recuperar o ObjectStateEntry da entidade. O TryGetObjectStateEntry realiza dois truques de uma vez. Primeiro, ele retorna um Booliano se a entrada for encontrada e, segundo, retorna a entrada ou nulo. Se a entrada não foi nula, eu usei sua entidade para anexar a viagem; caso contrário, eu anexei a que passou no meu método AddActivity. Apenas descrevendo que isto é cansativo.

Figura 5 O método original AddActivity usando a API ObjectContext

public void AddActivity(Activity activity) { if (_context.GetEntityState(activity) == EntityState.Detached) { ObjectStateEntry existingOse; if (_context.ObjectStateManager .TryGetObjectStateEntry(activity, out existingOse)) { activity = existingOse.Entity as Activity; } else     { _context.Activities.Attach(activity); } } _currentTrip.Activities.Add(activity); }

Pensei muito e de forma aprofundada em uma maneira eficiente de realizar esta lógica, mas terminei com um código com quase o mesmo comprimento. Lembre-se, isto é uma relação de muitos para muitos e elas exigem mais cuidado. No entanto, o código é mais fácil de escrever e mais fácil de ler. Você não precisa se complicar com o ObjectStateManager de forma alguma. Veja aqui a parte do método que atualizei para usar um padrão semelhante como eu fiz antes com o Destino:

var trackedActivity=_context.Activities.Local .FirstOrDefault(a => a.ActivityID == activity.ActivityID); if (trackedActivity != null) { activity = trackedActivity; } else { _context.Activities.Attach(activity); }

Missão concluída

Com esta última correção, todos os meus testes passaram e eu consegui usar com êxito todos os recursos do formulário Manutenção de Viagem. Agora é hora de buscar oportunidades para aproveitar os novos recursos do EF6.

Mais importante, qualquer manutenção adicional nesta parte do meu aplicativo será simplificada porque muitas tarefas que foram difíceis com o ObjectContext são muito mais fáceis com a API DbContext. Focando neste modelo pequeno, aprendi muito que eu posso aproveitar qualquer outra transformação do ObjectContext-to-DbContext e estou melhor preparado para o futuro.

Igualmente importante é ser inteligente na escolha de qual código deve ser atualizado e qual não deve ser incomodado. Eu geralmente viso os recursos que eu sei que tenho que manter no futuro e não tenho que me preocupar em interagir com a API mais difícil. Se você tem um código que não será mais tocado e continua a funcionar a medida que o EF evolui, eu pensaria duas vezes antes de mergulhar neste desafio. Mesmo que seja para o melhor, um braço quebrado pode ser muito doloroso. 

Julie Lerman é MVP da Microsoft, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrar ela apresentando sobre o acesso a dados e outros tópicos do .NET em grupos de usuário e conferências pelo mundo todo. Seu blog está em thedatafarm.com/blog e é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga Julie no Twitter em twitter.com/julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Andrew Oakley