Introduction
This code snippet is a generic method to provide AddOrUpdate
functionality to a DbContext
. It was designed to update Code First entities without needing type-specific logic and without reflection.
This method is intended for atomic operations, so it is only suitable for updating single entities; it will not update their related entities in a single operation.
This code sample is suitable for EF 5+.
Background
After working with Entity Framework for the past couple of years, I've noticed that most AddOrUpdate
solutions lean towards type-specific implementations or are not robust enough to handle multiple scenarios; specifically composite keying. In a recent project, I needed (well, wanted) to get this functionality up and running in a lightweight manner. I also wanted to avoid costly operations if at all possible. I found several solutions, most leaning on reflection or relatively heavyweight operations, so I decided to use a different method.
The primary stumbling block was the differences in how mapping works between 1-X and M-X entities.
Using the Code
The first thing that we need to do is add an interface. This will decorate our Model
classes so that they can be properly processed when passed to our eventual AddOrUpdate
function.
public interface IModel
{
object Id { get; }
}
Now, we add a method to our DbContext
implementation that will leverage the IModel
interface to uniquely identify our entities, whether they are using simple or composite keying.
public class ExampleContext : DbContext
{
...
public DbSet<MyIntIndexModel> IntIndexs { get; set; }
public DbSet<MyStringIndexModel> StringsIndexs{ get; set; }
public DbSet<MyCompositeIndexModel> CompositeIndexs { get; set; }
public int AddOrUpdate<T>(T entity)
where T : class, IModel
{
return AddOrUpdateRange(new[] {entity});
}
public int AddOrUpdateRange<T>(IEnumerable<T> entities)
where T : class, IModel
{
foreach (var entity in entities)
{
var id = entity.Id as object[];
var tracked = (id != null)
? Set<T>().Find(id)
: Set<T>().Find(entity.Id);
if (tracked != null)
{
Entry(tracked).CurrentValues.SetValues(entity);
}
else
{
Entry(entity).State = EntityState.Added;
}
}
return SaveChanges();
}
}
The final piece of the puzzle is to provide a meaningful Id values on the persistence classes. I'm including a sample M-M mapping table for reference.
public class MyIntIndexModel : IModel
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[NotMapped]
object IModel.Id { get { return Id; }}
}
public class MyStringIndexModel : IModel
{
[Key]
public string UserName { get; set; }
[NotMapped]
object IModel.Id { get { return UserName; }}
}
public class MyCompositeIndexModel : IModel
{
[Key, Column(Order=0), ForeignKey("IntModel")]
public int IntModelId { get; set; }
[Key, Column(Order=1), ForeignKey("StringModel")]
public string StringModelId { get; set; }
[NotMapped]
object IModel.Id { get { return new object[] { IntModelId, StringModelId}}}
public virtual MyIntIndexModel IntModel { get; set; }
public virtual MyStringIndexModel StringModel { get; set; }
}
Points of Interest
The DbSet<>.Find
method is very robust, but care has to be taken in what is passed to it. Basically any operations performed as an argument parameter will downcast to object, which will prevent the proper usage of composite keys. The null
resolution operator sadly caused me a few headaches here, since that behavior should have been obvious.
The AddOrUpdateRange
can be safely made into an AddOrUpdate
overload if your IModel
classes will never implement IEnumerable
. If you can't ensure that, though, it should be left as is.
If you're using Entity Framework with a Web Application, you can send your composite keys down the pipe to a JavaScript client with a very small modification:
public class MyCompositeIndexModel : IModel
{
...
[NotMapped]
public object Id { get { return new object[] { IntModelId, StringModelId}}}
}
This will expose the property for JSON.NET serialization, as explicit interface implementations seem to be ignored by the serializer.
History
- 8th August, 2016: Initial submission