views:

343

answers:

5

Is there a way to force LINQ-to-SQL to treat a column as dirty? Globally would suffice....

Basically, I've got a problem with some audit code on a legacy system that I'm talking to with L2S, imagine:

var ctx = new SomeDataContext(); // disposed etc - keeping it simple for illustration
var cust = ctx.Customers.First(); // just for illustration
cust.SomeRandomProperty = 17; // whatever
cust.LastUpdated = DateTime.UtcNowl;
cust.UpdatedBy = currentUser;
ctx.SubmitChanges(); // uses auto-generated TSQL

This is fine, but if the same user updates it twice in a row, the UpdatedBy is a NOP, and the TSQL will be (roughly):

UPDATE [dbo].[Customers]
SET SomeRandomColumn = @p0 , LastUpdated = @p1 -- note no UpdatedBy
WHERE Id = @p2 AND Version = @p3

In my case, the problem is that there is currently a belt-and-braces audit trigger on all tables, which checks to see if the audit column has been updated, and if not assumes the developer is at fault (substituting SUSER_SNAME(), although it could just as readily throw an error).

What I'd really like to be able to do is say "always update this column, even if it isn't dirty" - is this possible?

+1  A: 

Unfortunately, I think you will have to use a new DataContext

Josh Stodola
The `DataContext` isn't the issue; this would happen only with a single submit from a single `DataContext`; I mean that the new value hasn't changed from what was loaded from the database, but I still want it to issue an `UPDATE` statement including this column.
Marc Gravell
+1  A: 

Details at: http://blog.benhall.me.uk/2008/01/custom-insert-logic-with-linq-to-sql.html

You can override the default update behavior. There are 2 ways of doing this

The easiest is to create a stored procedure (if you can't do that on your database, the second method should work) which takes the parameters of your customer object and updates the table:

  1. Create the stored procedure that has a parameter for each property of Customers that needs to be updated.
  2. Import that stored procedure into your Linq To SQL DBML file.
  3. Now you can right click on your customers entity and select "Configure Behavior".
  4. Select your Customers class under the Class dropdown and "Update" on the behavior drop down.
  5. Select the "Customize" radio button and choose the stored procedure you just created.
  6. Now you can map class's properties to the stored procedure.

Now when Linq to SQL tries to update your Customers table, it'll use your stored procedure instead. Just be careful because this will override the update behavior for Customers everywhere.

The second method is to use partial methods. I haven't actually tried this, so hopefully this might just give you some general direction to pursue. In a partial class for your data context, make a partial method for the update (It'll be Update_____ with whatever your class is in the blank. I'd suggest searching in your data context's designer file to make sure you get the right one)

public partial SomeDataContext
{
    partial void UpdateCustomer(Customer instance)
    {
       // this is where you'd do the update, but I'm not sure exactly how it's suppose to work, though. :(
    }
}
Joel
Yes, this was already my backup - for this scenario I was *hoping* to leave it using the inbuilt TSQL generation...
Marc Gravell
+1  A: 

If you want to go down the [dirty] reflection route, you could try something along the lines of:

1) Override SubmitChanges
2) Go through the change set
3) Use reflection to get hold of the change tracker for each updated object (see http://stackoverflow.com/questions/1321255/whats-the-cleanest-way-to-make-a-linq-object-dirty )
4) Make the column dirty (there's a dirtyMemberCache field in the StandardTrackedObject class)

KristoferA - Huagati.com
Ooh, pure unadulterated evil. I like it. A lot; thanks.
Marc Gravell
true, strong in the dark side this one is... if only MSFT would have left some of the useful classes and members public, then it could be done in a more clean way... :)
KristoferA - Huagati.com
A: 

The following works for me. Note though that I'm using the linq2sql provider from DevArt, but that may not matter:

MyDataContext dc = new MyDataContext();

Message msg = dc.Messages.Single(m => m.Id == 1);
Message attachingMsg = new Message();
attachingMsg.Id = msg.Id;

dc.Messages.Attach(attachingMsg);

attachingMsg.MessageSubject = msg.MessageSubject + " is now changed"; // changed
attachingMsg.MessageBody = msg.MessageBody; // not changed
dc.SubmitChanges();

This produces the following sql:

UPDATE messages SET messageSubject = :p1, messageBody = :p2 WHERE Id = :key1

So, messageBody is updated even though its value is not changed. One other change necessary for this, is that for each property (column) of my entity Message, I have set UpdatedCheck = UpdateCheck.Never, except for its ID, which is the primary key.

Razzie
+5  A: 

Based on KristoferA's answer, I ended up with something like below; this is evil and brittle (reflection often is), but may have to suffice for now. The other side of the battle is to change the triggers to behave:

partial class MyDataContext // or a base-class
{
    public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
    {
        this.MakeUpdatesDirty("UpdatedBy", "Updated_By");
        base.SubmitChanges(failureMode);
    }
}
public static class DataContextExtensions
{
    public static void MakeUpdatesDirty(
        this DataContext dataContext,
        params string[] members)
    {
        if (dataContext == null) throw new ArgumentNullException("dataContext");
        if (members == null) throw new ArgumentNullException("members");
        if (members.Length == 0) return; // nothing to do
        foreach (object instance in dataContext.GetChangeSet().Updates)
        {
            MakeDirty(dataContext, instance, members);
        }
    }
    public static void MakeDirty(
        this DataContext dataContext, object instance ,
        params string[] members)
    {
        if (dataContext == null) throw new ArgumentNullException("dataContext");
        if (instance == null) throw new ArgumentNullException("instance");
        if (members == null) throw new ArgumentNullException("members");
        if (members.Length == 0) return; // nothing to do
        const BindingFlags AllInstance = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
        object commonDataServices = typeof(DataContext)
            .GetField("services", AllInstance)
            .GetValue(dataContext);
        object changeTracker = commonDataServices.GetType()
            .GetProperty("ChangeTracker", AllInstance)
            .GetValue(commonDataServices, null);
        object trackedObject = changeTracker.GetType()
            .GetMethod("GetTrackedObject", AllInstance)
            .Invoke(changeTracker, new object[] { instance });
        var memberCache = trackedObject.GetType()
            .GetField("dirtyMemberCache", AllInstance)
            .GetValue(trackedObject) as BitArray;

        var entityType = instance.GetType();
        var metaType = dataContext.Mapping.GetMetaType(entityType);
        for(int i = 0 ; i < members.Length ; i++) {
            var member = entityType.GetMember(members[i], AllInstance);
            if(member != null && member.Length == 1) {
                var metaMember = metaType.GetDataMember(member[0]);
                if (metaMember != null)
                {
                    memberCache.Set(metaMember.Ordinal, true);
                }
            }
        }
    }
}
Marc Gravell