JC Grubbs
JC Grubbs

Reputation: 40341

How would you write an Upsert for LINQ to SQL?

So I'd like to write a generic Upsert function for LINQ to SQL and I'm having some trouble conceptualizing how to do it. I'd like it to work something like this:

var db = new DataContext();
db.Customers.UpsertOnSubmit(customer);

So it would have to be generic in some way and I guess and extension method on Table. I've been able to get this far in determining the primary key of the underlying table:

var context = source.Context;
var table = context.Mapping.GetTable(source.GetType());
var primaryMember = table.RowType.DataMembers.SingleOrDefault(m => m.IsPrimaryKey);

I'm assuming it will be necessary to have this to compose a query to tell if the item is in the DB already or not but I don't really know what to do with it at this point.

Upvotes: 21

Views: 10536

Answers (2)

orad
orad

Reputation: 16074

Short Answer:

Grab this: EntityExtensionMethods.cs

Explanation

To do UPSERT in LINQ-to-SQL without querying the records first, you can do the following. It will still hit the db once to check if record exists but will not pull the record:

var blob = new Blob { Id = "some id", Value = "some value" }; // Id is primary key (PK)

if (dbContext.Blobs.Contains(blob)) // if blob exists by PK then update
{
    // This will update all columns that are not set in 'original' object. For
    // this to work, Blob has to have UpdateCheck=Never for all properties except
    // for primary keys. This will update the record without querying it first.
    dbContext.Blobs.Attach(blob, original: new Blob { Id = blob.Id });
}
else // insert
{
    dbContext.Blobs.InsertOnSubmit(blob);
}
dbContext.Blobs.SubmitChanges();

Extension Method

I came up with the following extension method for it.

public static class EntityExtensionMethods
{
    public static void InsertOrUpdateOnSubmit<TEntity>(this Table<TEntity> table, TEntity entity, TEntity original = null)
        where TEntity : class, new()
    {
        if (table.Contains(entity)) // if entity exists by PK then update
        {
            if (original == null)
            {
                // Create original object with only primary keys set
                original = new TEntity();
                var entityType = typeof(TEntity);
                var dataMembers = table.Context.Mapping.GetMetaType(entityType).DataMembers;
                foreach (var member in dataMembers.Where(m => m.IsPrimaryKey))
                {
                    var propValue = entityType.GetProperty(member.Name).GetValue(entity, null);
                    entityType.InvokeMember(member.Name, BindingFlags.SetProperty, Type.DefaultBinder,
                        original, new[] {propValue});
                }
            }

            // This will update all columns that are not set in 'original' object. For
            // this to work, entity has to have UpdateCheck=Never for all properties except
            // for primary keys. This will update the record without querying it first.
            table.Attach(entity, original);
        }
        else // insert
        {
            table.InsertOnSubmit(entity);
        }
    }
}

Use it like below:

var blob = new Blob { Id = "some id", Value = "some value" }; // Id is primary key (PK)
dbContext.Blobs.InsertOrUpdateOnSubmit(blob);
dbContext.Blobs.SubmitChanges();

I added above extension method with more stuff to this gist: EntityExtensionMethods.cs

Upvotes: 3

ericvg
ericvg

Reputation: 3957

I do something similar, but with a different approach. Every entity implements IEntity. One of the properties of IEntity is a state if the object is new or existing. I then implement that for each entity, like:

public EntityState EntityState
{
    get
    {
        if (_Id > 0)
            return EntityState.Exisiting;
        else
            return EntityState.New;
    }
}

Then, a generic Upsert could be (on a generic repository type class):

public virtual void Upsert<Ta>(Ta entity)
    where Ta: class
{
    if (!(entity is IEntity))
        throw new Exception("T must be of type IEntity");

    if (((IEntity)entity).EntityState == EntityState.Exisiting)
        GetTable<Ta>().Attach(entity, true);
    else
        GetTable<Ta>().InsertOnSubmit(entity);
}

private System.Data.Linq.Table<Ta> GetTable<Ta>()
    where Ta: class
{
    return _dataContext.Context.GetTable<Ta>();
}

If your attaching from another datacontext, also make sure you have a timestamp on your objects.

Upvotes: 8

Related Questions