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.