43

How to combine Find() with AsNoTracking() when making queries to an EF context to prevent the returned object from being tracked. This is what I can't do

 _context.Set<Entity>().AsNoTracking().Find(id);

How can I do that? I am using EF version 6.

Note: I do not want to use SingleOrDefault(), or Where. I just can't because the parameter Id is generic and it's a struct and I can not apply operator == for generics in that case.

Nean Der Thal
  • 3,189
  • 3
  • 22
  • 40
  • 3
    "*I just can't*" is not an enforcable claim unless you can show us why you can't. Given the knowledge of the SO community, perhaps you can, you just don't know **how**. – Tommy Jan 23 '16 at 18:42
  • 2
    @Tommy thanks, added more details. – Nean Der Thal Jan 23 '16 at 18:44
  • 1
    You should be able to use `Equals` rather than using `==` if that's your only problem, but I suspect there will also be some other problems that you're not sharing in your question yet: there's no standard way of extracting an entity's key properties, for instance. –  Jan 23 '16 at 19:46
  • @hvd Equals does not translate as well, the error I got was: "Unable to create a constant value of type 'System.Object'. Only primitive types or enumeration types are supported in this context" – Nean Der Thal Jan 23 '16 at 19:57
  • @HeidelBerGensis Some overloads of `Equals` aren't supported, others are. –  Jan 23 '16 at 20:02
  • Did you ever find a solution for this? – Derked Sep 13 '17 at 01:26
  • @Derked nope, I ended up overriding every get method in my repositories.. – Nean Der Thal Sep 13 '17 at 07:29
  • Internally EF Core uses a support class `IEntityFinder` (see https://github.com/dotnet/efcore/blob/0103866b680b626813d0e2a258b62d0551e6fea6/src/EFCore/Internal/InternalDbSet.cs#L429) to build the where expression. This interface also has a `Query` method. Pity it's internal, or I'd suggest using that. – Jeremy Lakeman Feb 03 '22 at 01:31

6 Answers6

53

So instead of using AsNoTracking() what you can do is Find() and then detach it from the context. I believe that this gives you the same result as AsNoTracking() besides the additional overhead of getting the entity tracked. See EntityState for more information.

var entity = Context.Set<T>().Find(id);
Context.Entry(entity).State = EntityState.Detached;
return entity;

Edit: This has some potential issues, if the context hasn't loaded some relationships, then those navigation properties will not work and you will be confused and frustrated why everything is returning null! See https://stackoverflow.com/a/10343174/2558743 for more info. For now on those repositories I'm overriding the FindNoTracking() methods in my repositories that I need that in.

Derked
  • 934
  • 10
  • 17
  • Rosedi Kasim should be the correct answer. Read the documentation and it just doesn't make sense to use Find with AsNoTracking https://learn.microsoft.com/en-us/ef/ef6/querying/ You may as well find an entity using a query. – Tom McDonough Jun 26 '19 at 15:05
  • 2
    If you read the note on the question, Rosedi Kasim's answer is not sufficient when using generics since you can't use the == operator so it won't even compile. – Derked Jun 27 '19 at 17:06
  • 1
    You can eager load navigation properties before detaching the entity. Pass inn a params array of Expression>[] of navigationproperties and then just use the Include method to include the navigation property in a foreach. And starting with IQueryable query = dbContext.Set(); Then you will not have null values in the navigation properties. Do this before you detach. Great answer by the way. – Tore Aurstad Jun 05 '22 at 23:34
27
<context>.<Entity>.AsNoTracking().Where(s => s.Id == id);

Find() does not make sense with AsNoTracking() because Find is supposed to be able to return tracked entities without going to database.. your only option with AsNoTracking is either Where or First or Single...

Rosdi Kasim
  • 24,267
  • 23
  • 130
  • 154
  • 4
    Can't use that, because the `Id` is generic and I can not use the operator `==` with generics :) – Nean Der Thal Jan 23 '16 at 18:34
  • @HeidelBerGensis Can you show your full code snippet why generic can't work in this case? – Rosdi Kasim Jan 23 '16 at 18:35
  • 1
    check [this post](http://stackoverflow.com/questions/390900/cant-operator-be-applied-to-generic-types-in-c), for me the TKey is struct type. So, I jsut can't. – Nean Der Thal Jan 23 '16 at 18:37
  • 1
    I suspect there is a way to work around that.. but without a sample code I can't help much since I do not know your actual situation.. if you add some code I am sure someone could show you some workaround... – Rosdi Kasim Jan 23 '16 at 18:52
  • This should be the correct answer. Read the documentation and it just doesn't make sense to use Find with AsNoTracking https://learn.microsoft.com/en-us/ef/ef6/querying/ – Tom McDonough Jun 26 '19 at 15:04
  • This is the correct answer. Make use of the Base class for EF models and reflection. – Cubelaster Feb 01 '20 at 11:48
4

The accepted answer has the issue that if the item you are trying to find is already being tracked, it will return that item then mark it as untracked (which may mess up other parts of the code).

Akos is on the right track with his suggestion to build the expression yourself, but the example only works for entities that have a single primary key (which covers most cases).

This extension method works in EF Core and effectively matches the signature for the DbSet<T>.Find(object []). But it is an extension method for DbContext instead of DbSet because it needs access to the Entity's metadata from the DbContext.

public static T FindNoTracking<T>(this DbContext source, params object[] keyValues)
    where T : class
{
    DbSet<T> set = source.Set<T>();
    if (keyValues == null || !keyValues.Any())
    {
        throw new Exception("No Keys Provided.");
    }

    PropertyInfo[] keyProps = GetKeyProperties<T>(source);
    if (keyProps.Count() != keyValues.Count())
    {
        throw new Exception("Incorrect Number of Keys Provided.");
    }

    ParameterExpression prm = Expression.Parameter(typeof(T));
    Expression body = null;
    for (int i = 0; i < keyProps.Count(); i++)
    {
        PropertyInfo pi = keyProps[i];
        object value = keyValues[i];
        Expression propertyEx = Expression.Property(prm, pi);
        Expression valueEx = Expression.Constant(value);
        Expression condition = Expression.Equal(propertyEx, valueEx);
        body = body == null ? condition : Expression.AndAlso(body, condition);
    }

    var filter = Expression.Lambda<Func<T, bool>>(body, prm);
    return set.AsNoTracking().SingleOrDefault(filter);
}

public static PropertyInfo[] GetKeyProperties<T>(this DbContext source)
{
    return source.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(p => p.PropertyInfo).ToArray();
} 

you can then use the method directly on the DbContext. For example, if your entity has a composite key consisting of two strings:

context.FindNoTracking<MyEntity>("Key Value 1", "Key Value 2");

If you really want the Extension method to be on DbSet instead of the DbContext, you can do so but you'll need to get the context from the set in order to gain access to the metadata about the entity. Currently there isn't a good way to do this. There are some hacky ways to do this, but they involve using reflection to access private fields of framework classes, so I'd advise against it.


Alternatively...

If you have a way of figure out what the Key properties are without using the DbContext/Metadata, you can make it an extension for DbSet instead. For example, if all of your Key properties are marked with the [Key] attribute, you can use this code:

public static T FindNoTracking<T>(this DbSet<T> source, params object[] keyValues)
    where T : class
{
    //Pretty much the same...
}

public static PropertyInfo[] GetKeyProperties<T>()
{
    return typeof(T).GetProperties()
        .Where(pi => pi.GetCustomAttribute<KeyAttribute>() != null).ToArray();
}    

This would also work in both Entity Framework and EF Core.

NuclearProgrammer
  • 806
  • 1
  • 9
  • 19
4

Back in 2015, an official request was made to include the functionality, i.e. combine Find() and AsNoTracking(). The issue was immediately closed after giving this argument:

AsNoTracking doesn't really make sense for Find since one of the key features of find is that it will return the already tracked version of the entity without hitting the database if it is already in memory. If you want to load an entity by key without tracking it then use Single.

Hence, you could replace:

_context.Set<Entity>().AsNoTracking().Find(id); // Invalid

with something like this:

_context.Set<Entity>().AsNoTracking().Single(e => e.Id == id);
Jess Rod
  • 190
  • 11
1

Well, I guess if you really want to do this, you can try creating your expression yourself. I assume you have a base entity class that's generic and that's where the generic key property comes from. I named that class KeyedEntityBase<TKey>, TKey is the type of the key (if you don't have such a class, that's fine, the only thing that I used that for is the generic constraint). Then you can create an extension method like this to build the expression yourself:

public static class Extensions
{
   public static IQueryable<TEntity> WhereIdEquals<TEntity, TKey>(
            this IQueryable<TEntity> source,
            Expression<Func<TEntity, TKey>> keyExpression,
            TKey otherKeyValue)
            where TEntity : KeyedEntityBase<TKey>
  {
    var memberExpression = (MemberExpression)keyExpression.Body; 
    var parameter = Expression.Parameter(typeof(TEntity), "x"); 
    var property = Expression.Property(parameter, memberExpression.Member.Name); 
    var equal = Expression.Equal(property, Expression.Constant(otherKeyValue));  
    var lambda = Expression.Lambda<Func<TEntity, bool>>(equal, parameter);
    return source.Where(lambda);
  }
}

And then, you can use it like this (for an integer key type):

context.Set<MyEntity>.AsNoTracking().WhereIdEquals(m=>m.Id, 9).ToList();
Akos Nagy
  • 4,201
  • 1
  • 20
  • 37
0

In asp net core 7 you can disable tracking when you want to https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries:

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

var product = await context.Products.FindAsync(id);
Sergey Anisimov
  • 323
  • 1
  • 4
  • 10
  • is it possible to change the tracking behavior , select the data, and then enable it without affecting any of the objects already being tracked? – Niklas May 16 '23 at 13:48