Can you convince a DataContext to treat a column as always dirty?
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开发者_C百科 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?
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);
}
}
}
}
}
Unfortunately, I think you will have to use a new DataContext
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:
- Create the stored procedure that has a parameter for each property of Customers that needs to be updated.
- Import that stored procedure into your Linq To SQL DBML file.
- Now you can right click on your customers entity and select "Configure Behavior".
- Select your Customers class under the Class dropdown and "Update" on the behavior drop down.
- Select the "Customize" radio button and choose the stored procedure you just created.
- 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. :(
}
}
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)
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.
精彩评论