Julho de 2015

Número 7 do Volume 30

Pontos de dados - Explorando o Comportamento do Entity Framework na Linha de Comando com Scriptcs

Por Julie Lerman

Julie LermanPassei muito tempo tentando ensinar as pessoas sobre o comportamento do Entity Framework em que pesem as relações e os dados desconectados. O EF nem sempre segue as regras da maneira esperada, por isso meu objetivo é que você tenha uma ideia do que pode acontecer, explicando por que algumas coisas funcionam de uma determinada maneira e como lidar com (ou evitar) um comportamento específico.

Quando vou demonstrar algo para desenvolvedores, levo várias opções de fluxos de trabalho. Por exemplo, posso criar testes de integração e executá-los, um por vez, discutindo cada comportamento. O lado ruim de fazer isso é que muitas vezes decido realizar uma depuração para detalhar objetos e mostrar o que está acontecendo nos bastidores. Mas a depuração de testes automatizados pode demorar muito por causa de toda a fonte que o Visual Studio precisa carregar.

Outro fluxo de trabalho que já usei envolve criar um aplicativo de console e realizar a depuração através dele, parando em pontos de interrupção e inspecionando objetos. Mas esse método também não é interativo como eu gostaria, já que o código precisa estar inteiramente disponível com antecedência.

O que eu realmente queria fazer é digitar um código, executá-lo para minha plateia ver seu efeito, e, em seguida, modificar o código e executá-lo novamente para destacar as diferenças nas respostas. Por exemplo, já usei o método como no seguinte código, para demonstrar a diferença entre este:

using (var context = new AddressContext()) {
  aRegionFromDatabase=context.Regions.FirstOrDefault();
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

este:

using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

e este:

newAddress.Region=aRegionObjectFromDatabase;
using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
}

A propósito, a diferença é que, no primeiro exemplo, em que o EF recupera a Região (Region) na mesma instância de contexto, o EF saberá que a região já existe e não tentará adicioná-la novamente no banco de dados. Nos casos segundo e terceiro, as regras EF irão inferir que a região (assim como seu endereço "pai") é nova, e ela será adicionada ao banco de dados. A longo prazo, é recomendável usar um valor de chave estrangeiro e não uma instância. Poder demonstrar causa e efeito ajuda muito.

É claro que eu poderia demonstrar o efeito com testes, mas ficaria um pouco confuso. E há aquele velho argumento de que testes não servem para comprovar teorias, apenas para validar códigos. Também não gosto de expôr as diferenças usando um aplicativo de console, uma vez que você precisa executar o aplicativo após cada alteração no código. Uma alternativa melhor é usar o fantástico aplicativo LinqPad, e já fiz isso algumas vezes. Mas agora, sou tentada a usar uma quarta solução para acessar meu alegre local de demonstração: o Scriptcs.

O Scriptcs (scriptcs.net) já existe desde 2013 e é totalmente livre. Ele foi escrito sobre Roslyn para fornecer um tempo de execução de linha de comando que permite executar código C# fora do Visual Studio. O Scriptcs também fornece um mecanismo muito simples para criar scripts com C# em qualquer editor de texto que você prefira, e então executar esses scripts da linha de comando.

Eu já tinha ouvido falar muito do Scriptcs, porque uma das principais pessoas por trás dele é Glenn Block, por quem tenho um respeito enorme. Mas nunca tinha examinado ele muito de perto até pouco tempo atrás, depois de escutar Block falar sobre o futuro brilhante do Scriptcs em um episódio de DotNetRocks (bit.ly/1AA1m4z). Minha primeira impressão ao ver essa ferramenta foi que ela poderia permitir que eu realizasse demonstrações interativas diretamente da linha de comando. E, combinando ele com o EF, é possível racionalizar o compartilhamento do Scriptcs com os leitores desta coluna focada em dados.

Uma pequena introdução ao Scriptcs

Há muitos recursos excelentes para Scriptcs. Estou longe de ser uma especialista, portanto falarei de alguns destaques e indicarei o site scriptcs.net para você obter mais informações. Também encontrei uma introdução perfeita ao Scriptcs no curto vídeo do Sengal Latish em bit.ly/1R6mF8s.

Primeiro, você precisará instalar o Scriptcs em seu computador de desenvolvimento usando o Chocolatey, o que significa que você também precisará ter o Chocolatey instalado em seu computador. Se você ainda não tem o Chocolatey, é uma ferramenta incrível para instalar ferramentas de software. O Chocolatey usa o NuGet para instalar ferramentas (e as ferramentas ou APIs das quais elas dependem) da mesma maneira que o NuGet usa pacotes para implantar assemblies.

O Scriptcs fornece um ambiente Read, Evaluate, Play, Loop (REPL), que você pode encarar como um ambiente de tempo de execução. Você pode usar Scriptcs para executar comandos C# individualmente na linha de comando, ou para executar arquivos de script do Scriptcs criados em um editor de textos. Há até mesmo extensões IntelliSense do Scriptcs para alguns editores. A melhor maneira de ver o Scriptcs em ação é iniciar uma execução de linha por linha. Vamos começar assim.

Com o Scriptcs instalado, navegue até a pasta em que você deseja que os códigos salvos sejam armazenados e execute o comando scriptcs. Isso iniciará o ambiente de REPL e informará a você que o Scriptcs está em execução. Ele também retornará um prompt >.

No prompt, você pode digitar qualquer comando C# encontrado nos namespaces .NET mais comuns. Eles já vêm disponíveis, e são dos mesmos assemblies que um projeto de console de aplicativo .NET tem por padrão. A Figura 1 mostra a inicialização do Scriptcs no prompt de comando, a execução de uma linha de código C# e os resultados exibidos.

executando código C# na linha de comando no ambiente de REPL do Scriptcs
Figura 1: executando código C# na linha de comando no ambiente de REPL do Scriptcs

O Scriptcs tem várias diretivas que permitem que você realize ações como referenciar um assembly (#r) ou carregar um arquivo de script existente (#load).

Outro recurso interessante é o Scriptcs permitir que você instale pacotes do NuGet com facilidade. Você pode fazer isso no prompt de comando usando o parâmetro - install do comando Scriptcs. Por exemplo, a Figura 2 mostra o que acontece quando instalo o Entity Framework.

Instalando pacotes NuGet no Scriptcs
Figura 2: instalando pacotes NuGet no Scriptcs

Mas as instalações de pacote realizam muito mais, como você pode ver na captura de tela da minha pasta depois de instalar o Entity Framework (veja a Figura 3): o Scriptcs criou uma pasta scriptcs_packages junto com o conteúdo do pacote relevante.

o instalador de pacotes NuGet do Scriptcs cria pastas familiares para o pacote e arquivos de configuração
Figura 3: o instalador de pacotes NuGet do Scriptcs cria pastas familiares para o pacote e arquivos de configuração

Criando um modelo e códigos comuns de configuração

Farei minhas experiências em um modelo específico que já criei anteriormente no Visual Studio usando o código. Eu tenho um assembly com minhas classes Monkey (macaco), Banana e Country (país), e outro assembly que contém um MonkeysContext que herda do EF DbContext e expõe DbSets de Monkeys (macacos), Bananas e Countries (países). Verifiquei se meu modelo foi configurado corretamente usando o recurso exibir modelo de dados da entidade da extensão do Entity Framework Power Tools para Visual Studio (bit.ly/1K8qhkO). Dessa forma, eu sei que posso depender dos assemblies compilados que usarei em meus testes de linha de comando.

Para fazer essas experiências, há algumas configurações comuns que preciso executar. Preciso de referências a minhas assemblies personalizadas, bem como ao assembly do EF, e preciso de código que instancia os novos objetos monkey, country e MonkeysContext.

Em vez de repetir essa instalação continuamente no prompt de comando, vou criar um arquivo de script para Scriptcs. Os arquivos de script, que têm uma extensão. csx, são a maneira mais comum para tirar proveito do Scriptcs. No começo, eu usava o Notepad++ com o plug-in do Scriptcs, mas fui incentivada a experimentar o Sublime Text 3 (sublimetext.com/3). Ao fazer isso, pude usar o plug-in do Scriptcs e também o OmniSharp, um plug-in IDE do C# para Sublime Text 3, que fornece uma incrível experiência de codificação para um editor de texto que suporta OSX e Linux, além do Windows.

O OmniSharp permite que você crie para Scriptcs, além de criar para vários outros sistemas. Com o Sublime instalado, estou pronta para criar um arquivo .csx que encapsula o código de configuração comum para testar meu modelo.

No editor de texto, crio um novo arquivo, SetupForMonkeyTests.csx, adiciono alguns comandos do Scriptcs para referência dos assemblies necessários e adiciono o código C# para criar alguns objetos, como mostro aqui:

#r "..\DataModel Assemblies\DataModel.dll"
#r "..\DataModel Assemblies\DomainClasses.dll"
#r "..\scriptcs_packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll"
using DomainClasses;
using DataModel;
using System.Data.Entity;
var country=new Country{Id=1,Name="Indonesia"};
var monkey=Monkey.Create("scripting gal", 1);
Database.SetInitializer(new NullDatabaseInitializer<MonkeysContext>());
var ctx=new MonkeysContext();

Lembre-se de que os comandos #r são comandos do Scriptcs para adicionar referências aos assemblies necessários. Vários assemblies do sistema usados com frequência são referenciados por padrão para proporcionar uma experiência pré-configurada, como quando você cria um novo projeto de console no Visual Studio. Por isso você não precisa especificá-los, mas eu preciso referenciar meus assemblies personalizados, assim como o assembly EntityFramework instalado pelo NuGet. O restante é apenas codificação C#. Observe que não estou definindo classes aqui. Isto é um script, então o Scriptcs irá apenas ler e executar linha por linha tudo o que estiver no arquivo. Com a combinação de Sublime Text e OmniSharp, posso até mesmo compilar para me certificar que a minha sintaxe está correta.

Com esse arquivo em mãos, vou retornar para a linha de comando e verificar comportamentos do EF usando meu modelo e classes.

Seguindo meus experimentos de linha de comando com o EF

Voltando ao prompt de comando, iniciarei o REPL do Scriptcs apenas com o comando scriptcs, sem parâmetros.

Depois que o REPL estiver ativo, a primeira coisa a fazer é carregar o arquivo .csx. Em seguida, usarei os comandos :references e :vars para verificar se o REPL executou o arquivo .csx corretamente. (O Scriptcs tem vários comandos REPL que começam com dois-pontos. Você pode acessar uma lista digitando :help.) A Figura 4 mostra minha sessão até agora; você pode ver todas as APIs referenciadas, bem como os objetos que criei.

Figura 4: iniciando o Scriptcs; carregando um arquivo .csx; verificando referências e variáveis existentes

D:\ScriptCS Demo>scriptcs
scriptcs (ctrl-c to exit or :help for help)
> #load "SetupForMonkeyTests.csx"
> :references
[
  "System",
  "System.Core",
  "System.Data",
  "System.Data.DataSetExtensions",
  "System.Xml",
  "System.Xml.Linq",
  "System.Net.Http",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Core.dll",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Contracts.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\   
    net45\\EntityFramework.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\
    net45\\EntityFramework.SqlServer.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DataModel.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DomainClasses.dll"
]
> :vars
[
  "DomainClasses.Country country = DomainClasses.Country",
  "DomainClasses.Monkey monkey = DomainClasses.Monkey",
  "DataModel.MonkeysContext ctx = DataModel.MonkeysContext"
]
>

Eu também posso inspecionar os objetos simplesmente digitando o nome da variável no prompt. Esse, por exemplo, é meu objeto monkey:

> monkey
{
  "Id": 0,
  "Name": "scripting gal",
  "Bananas": [],
  "CountryOfOrigin": null,
  "CountryOfOriginId": 1,
  "State": 1
}
>

Agora estou pronta para começar a explorar alguns comportamento do EF. Voltando aos meus exemplos anteriores, vou verificar como o EF responde ao anexar o objeto country ao monkey quando o contexto está ou não rastreando o monkey e quando está ou não rastreando o country.

No primeiro teste, irei anexar o country já existente à propriedade de CountryOfOrigin do monkey, e adicionarei o monkey ao contexto. Finalmente, usarei a propriedade DbContext.Entry().State para examinar como o EF compreende o estado dos objetos. Faz sentido que monkey seja adicionado, mas observe que o EF considera que country foi adicionado também. É assim que o EF lida com um gráfico. Como usei o método Add na raiz do gráfico (monkey), o EF está marcando tudo no gráfico como adicionado. Quando eu chamar SaveChanges, o país será inserido na tabela de banco de dados e, como você pode ver na Figura 5, haverá duas linhas para Indonésia. O fato de que esse país já tinha um valor de chave (Id) será ignorado.

usando o Scriptcs para ver imediatamente como o EF responde ao método DbSet.Add com um gráfico
Figura 5: usando o Scriptcs para ver imediatamente como o EF responde ao método DbSet.Add com um gráfico

Em seguida, usarei o comando :reset para limpar o histórico do REPL, e então #load para carregar o .csx novamente. Depois disso, vou tentar um novo fluxo de trabalho: adicionar monkey ao contexto e anexar country ao monkey que já está sendo rastreado. Observe que não estou incluindo todas as respostas na seguinte listagem:

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Added

Novamente, o contexto atribui o estado adicionado (Added) ao objeto country porque o anexei a outra entidade adicionada (Added).

Se você realmente quiser usar a propriedade de navegação, o padrão que lhe permitirá sucesso é certificar-se de que o contexto já está ciente do país (country). Isso pode acontecer porque você recuperou o país (country) usando uma consulta na mesma instância de contexto, e, como resultado, o contexto atribuiu o estado inalterado (Unchanged) ao objeto, ou porque você mesmo atribuiu o estado inalterado (Unchanged) manualmente.

A Figura 6 mostra um exemplo desse último caso, permitindo que eu realize esse teste sem trabalhar com um banco de dados.

Figura 6: atribuindo manualmente o estado inalterado

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Entry(country).State=EntityState.Unchanged;
2
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Unchanged
>

Como o contexto já estava rastreando o país, o estado do objeto country não será alterado. O EF altera apenas o estado do objeto relacionado quando o seu estado é desconhecido.

O Scriptcs não serve apenas para ensino

Pessoalmente, prefiro evitar a confusão entre essas regras e simplesmente definir o valor de chave estrangeiro (CountryOfOriginId=country.ID), sem anexar uma referência usando uma propriedade de navegação. Na verdade, com esse padrão, posso até mesmo examinar bem a propriedade de navegação CountryOfOrigin para considerar se ainda quero que ela exista. Demonstrar todas as variações e como o EF responde a cada uma é uma verdadeira lição para muitos que acompanham isso.

Ao tentar demostrar esses comportamentos para desenvolvedores, gosto da resposta imediata e óbvia que consigo obter de uma janela interativa. O LINQPad também pode também me ajudar a realizar isso, mas gosto muito da interação pela linha de comando. E mais importante ainda é ter usado essas experiências como uma forma de conhecer o Scriptcs, porque agora sei o grande valor que ele pode agregar além de simplesmente executar testes de linha de comando em uma API.


Julie Lerman* é MVP da Microsoft, mentora e consultora do .NET, e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos .NET em grupos de usuários e conferências em todo o mundo. Ela escreve no blog em thedatafarm.com/blog e é a autora de “Programming Entity Framework” (2010), bem como uma edição do Code First (2011) e uma edição do 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: Justin Rusbatch