8

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?

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Not quite sure I understand the question (probably not). Can't you use: partial void OnLoaded() { this.UpdatedBy = currentUser; } so it always sets the UpdatedBy? – Razzie Oct 13 '09 at 14:27
  • If the UpdatedBy isn't seen as a genuine change (because it is the same value as last time), then no `UPDATE` for that column happens. – Marc Gravell Oct 13 '09 at 14:35
  • hmm couldn't you do it by attaching the entity manually? never done that before, but I believe it updates all the columns by default. See http://blogs.rev-net.com/ddewinter/2009/04/07/linq-to-sql-updating-entities/ especially the second chapter. – Razzie Oct 13 '09 at 15:05
  • The entity is already attached, since it was given to us *by* the data-context. – Marc Gravell Oct 13 '09 at 15:28
  • Wow, Marc, this is a tough one indeed! Gonna see if I can hack something together :) – leppie Oct 13 '09 at 16:57
  • Why don't you just add a column update date which will be updated every time you save and this way you guarantee that changes are committed and that your stored procedures will execute? – Martin Hanna Oct 31 '13 at 21:46

5 Answers5

8

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);
                }
            }
        }
    }
}
Community
  • 1
  • 1
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

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

Josh Stodola
  • 81,538
  • 47
  • 180
  • 227
  • 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 Oct 13 '09 at 14:36
1

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 Beckham
  • 18,254
  • 3
  • 35
  • 58
  • Yes, this was already my backup - for this scenario I was *hoping* to leave it using the inbuilt TSQL generation... – Marc Gravell Oct 13 '09 at 17:54
1

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 What's the cleanest way to make a Linq object "dirty"? )
4) Make the column dirty (there's a dirtyMemberCache field in the StandardTrackedObject class)

Community
  • 1
  • 1
KristoferA
  • 12,287
  • 1
  • 40
  • 62
  • 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 Oct 14 '09 at 06:34
0

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
  • 30,834
  • 11
  • 63
  • 78