Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#
Tip/Trick

Generic Entity Framework AddOrUpdate Method with Composite Key Support

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
10 Aug 2016CPOL2 min read 31.9K   13   2
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.

C#
/// <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.

C#
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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
Specialized in Web Application back-end development using .NET(C#)

Comments and Discussions

 
Questioncode Pin
Joseph Franklin8-Aug-16 21:47
Joseph Franklin8-Aug-16 21:47 
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

GeneralRe: code Pin
Nathan Minier9-Aug-16 1:49
professionalNathan Minier9-Aug-16 1:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.