Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Generic Entity Framework AddOrUpdate Method with Composite Key Support

0.00/5 (No votes)
10 Aug 2016 1  
Generic AddOrUpdate for EF with composite key support

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.

/// <summary>
/// Provide an individual identifier for the class
/// The return format should mirror the column order, if applicable.
/// </summary>
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
{
    ...    // set definitions, other convenience methods, etc

    //Adding the DbSets from this example for consistency
    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[];

                // null resolution operator casts to object, so use ternary
                var tracked = (id != null)
                    ? Set<T>().Find(id)
                    : Set<T>().Find(entity.Id);

                if (tracked != null)
                {
                    // perform shallow copy
                    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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here