EntityFramework asNoTracking - Por que preciso saber disto ?

Fala pessoal,

Esta semana me deparei com um problema em um cliente que é bem comum, e causa muito transtorno, pois envolve muito o conceito de como o EF trabalha.

Quando você cria um contexto para o EF e indica as classes do mapeamento, basicamente diz a ele que todos os objetos deverão ser rastreados, ou seja, o simples fato de você criar um objeto ou ler a partir do contexto, coloca este objeto sobre o controle do EF.

Vamos considerar o seguinte contexto:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFAsNoTracking
{
    public class Contexto : DbContext
    {
        public DbSet<Categories> Categories { get; set; }
        public DbSet<Products> Products { get; set; }
    }
}

Agora vamos simular uma injeção de dependência, onde temos uma classe Dados que recebe o contexto:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFAsNoTracking
{
    public class Dados
    {
        private Contexto _contexto;
        public Dados(Contexto contexto)
        {
            _contexto = contexto;
        }

        public Categories GetCategory(int id) => _contexto.Categories.First(c => c.CategoryID == id);
    }
}

Vamos lembrar que o princípio básico da injeção de dependência é a propagação do objeto, neste caso temos apenas um Contexto, que é utilizado por todas classes através do mecanismo de injeção!

Vamos agora consultar o banco e trazer uma categoria:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFAsNoTracking
{
    class Program
    {
        static void Main(string[] args)
        {
            var db = new Contexto();

            var dados1 = new Dados(db);
            var cat = dados1.GetCategory(1);
            Console.WriteLine(cat.CategoryName);

        }
    }
}

A partir deste momento o objeto “cat” faz parte do mapeamento do EF, ou seja, ele irá fazer o tracker deste objeto, ou seja, controlar o status deste objeto perante o EF (novo, modificado, deletado, etc). Bom, até aí tudo bem, pois é exatamente isto que esperamos que ele faça.

O problema começa quando instanciamos outros objetos, lembrando que estamos em uma ambiente de injeção de dependência. Vamos então realizar uma nova consulta e também vamos listar os objetos que estão no tracker do EF:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFAsNoTracking
{
    class Program
    {
        static void Main(string[] args)
        {
            var db = new Contexto();

            var dados1 = new Dados(db);
            var cat = dados1.Get(1);
            Console.WriteLine(cat.CategoryName);

            var dados2 = new Dados(db);
            var cat2 = dados2.Get(2);
            cat2.CategoryName += " 2";

            var tracker = db.ChangeTracker.Entries();
            foreach(var t in tracker)
            {
                Console.WriteLine($"{t.Entity.ToString()}, {t.State}");
            }

        }
    }
}

Ao executar este código temos o seguinte resultado:

_Beverages_ 
_System.Data.Entity.DynamicProxies.
Categories_58C84246D9EE9DEB30950140620833728474B6132D2BC59BD4306359B33CE2A1, Modified_
_System.Data.Entity.DynamicProxies.
Categories_58C84246D9EE9DEB30950140620833728474B6132D2BC59BD4306359B33CE2A1, Unchanged_

Isto indica que o EF está mapeando os dois objetos, mesmo eles tendo sido consultados em diferentes instâncias da classe Dados, pois compartilham o mesmo contexto.

Sendo assim, se enviarmos um comando SaveChanges() e tivermos mudados os dois objetos, mesmo “sem querer”, este serão enviados para o banco!

E como resolvemos isto ???

Exitem várias maneiras de resolvermos, e talvez a primeira que vem a sua cabeça é “vamos instanciar outro contexto”, mas se eu fizer isto, qual o benefício da injeção de dependência neste caso ?

Para este tipo de situação temos o método “AsNoTracking()", que em termos bem simples diz ao contexto para não mapear o objeto!

Então vamos criar um outro método GetNoTracking() implementando esta chamada:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFAsNoTracking
{
    public class Dados
    {
        private Contexto _contexto;
        public Dados(Contexto contexto)
        {
            _contexto = contexto;
        }

        public Categories Get(int id) => _contexto.Categories.First(c => c.CategoryID == id);
        public Categories GetNoTracking(int id) => _contexto.Categories.AsNoTracking().First(c => c.CategoryID == id);
    }
}

Veja que somente adicionamos o AsNoTracking() após o objeto na consulta!

Agora vamos mudar a chamada do primeiro objecto, que neste exemplo é somente para leitura, ou seja, não iremos modificar nada nele:

var db = new Contexto();

var dados1 = new Dados(db);
var cat = dados1.GetNoTracking(1);
Console.WriteLine(cat.CategoryName);
}

Executando novamente o código teremos um resultado diferente, ou seja, somente um objeto está mapeado pelo EF:

_Beverages_  
_System.Data.Entity.DynamicProxies.Categories_58C84246D9EE9DEB30950140620833728474B6132D2BC59BD4306359B33CE2A1, Modified_

Resumindo:

Se você está apenas consultando um objeto no EF, ou seja, não vai modificar e gravar, use AsNoTracking() sempre que possível.

Isto não quer dizer que você não possa instanciar outro contexto, mas evitar isto pode lhe dar um ganho de performance!

O código fonte esta no meu GitHub: https://github.com/carloscds/CSharpSamples/tree/master/EFAsNoTracking

E isto aí pessoal e até a próxima!
Carlos dos Santos.