views:

497

answers:

6

I have a Linq object, and I want to make changes to it and save it, like so:

public void DoSomething(MyClass obj) {
  obj.MyProperty = "Changed!";
  MyDataContext dc = new MyDataContext();
  dc.GetTable<MyClass>().Attach(dc, true); // throws exception
  dc.SubmitChanges();
}

The exception is:

System.InvalidOperationException: An entity can only be attached as modified without original state if it declares a version member or does not have an update check policy.

It looks like I have a few choices:

  1. put a version member on every one of my Linq classes & tables (100+) that I need to use in this way.
  2. find the data context that originally created the object and use that to submit changes.
  3. implement OnLoaded in every class and save a copy of this object that I can pass to Attach() as the baseline object.
  4. To hell with concurrency checking; load the DB version just before attaching and use that as the baseline object (NOT!!!)

Option (2) seems the most elegant method, particularly if I can find a way of storing a reference to the data context when the object is created. But - how?

Any other ideas?

EDIT

I tried to follow Jason Punyon's advice and create a concurrency field on on table as a test case. I set all the right properties (Time Stamp = true etc.) on the field in the dbml file, and I now have a concurrency field... and a different error:

System.NotSupportedException: An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext.  This is not supported.

So what the heck am I supposed to attach, then, if not an existing entity? If I wanted a new record, I would do an InsertOnSubmit()! So how are you supposed to use Attach()?

Edit - FULL DISCLOSURE

OK, I can see it's time for full disclosure of why all the standard patterns aren't working for me.

I have been trying to be clever and make my interfaces much cleaner by hiding the DataContext from the "consumer" developers. This I have done by creating a base class

public class LinqedTable<T> where T : LinqedTable<T> {
  ...
}

... and every single one of my tables has the "other half" of its generated version declared like so:

public partial class MyClass : LinqedTable<MyClass> {
}

Now LinqedTable has a bunch of utility methods, most particularly things like:

public static T Get(long ID) {
  // code to load the record with the given ID
  // so you can write things like:
  //   MyClass obj = MyClass.Get(myID);
  // instead of:
  //   MyClass obj = myDataContext.GetTable<MyClass>().Where(o => o.ID == myID).SingleOrDefault();
}
public static Table<T> GetTable() {
  // so you can write queries like:
  //   var q = MyClass.GetTable();
  // instead of:
  //   var q = myDataContext.GetTable<MyClass>();
}

Of course, as you can imagine, this means that LinqedTable must somehow be able to have access to a DataContext. Up until recently I was achieving this by caching the DataContext in a static context. Yes, "up until recently", because that "recently" is when I discovered that you're not really supposed to hang on to a DataContext for longer than a unit of work, otherwise all sorts of gremlins start coming out of the woodwork. Lesson learned.

So now I know that I can't hang on to that data context for too long... which is why I started experimenting with creating a DataContext on demand, cached only on the current LinqedTable instance. This then led to the problem where the newly created DataContext wants nothing to do with my object, because it "knows" that it's being unfaithful to the DataContext that created it.

Is there any way of pushing the DataContext info onto the LinqedTable at the time of creation or loading?

This really is a poser. I definitely do not want to compromise on all these convenience functions I've put into the LinqedTable base class, and I need to be able to let go of the DataContext when necessary and hang on to it while it's still needed.

Any other ideas?

+2  A: 

I think 2 is not the best option. It's sounding like you're going to create a single DataContext and keep it alive for the entire lifetime of your program which is a bad idea. DataContexts are lightweight objects meant to be spun up when you need them. Trying to keep the references around is also probably going to tightly couple areas of your program you'd rather keep separate.

Running a hundred ALTER TABLE statements one time, regenerating the context and keeping the architecture simple and decoupled is the elegant answer...

Jason Punyon
+1 When you put it that way, it makes sense. Let me just ask you another question based on your comment "...regenerating the context..." - is there a simple way of refreshing the table structure, or do I have to redo the whole dbml diagram? That's a loaded question; I've renamed a bunch of things (particularly association names) from the default values, and I do NOT want to have to go through that again!
Shaul
DBML Design Surface = Old And Busted. SqlMetal = New Hotness. SqlMetal allows you to run a single command that updates your DataContext from SQL Server. (1) Run Alter Table Statement, (2) Run SqlMetal (3) Profit!!. It's unfortunate that you've done some custom renaming because you'll probably lose that.
Jason Punyon
I updated the question with results of some testing... it doesn't look good... :p
Shaul
Aside from the concurrency problems I'm still having, SqlMetal is very bad news for me. Not only have I renamed relationships, but many of my attributes are tinyints on the DB that translate to enumerations in my code. I worked long and hard at setting those things up in the O/R Designer, and I'm not giving up on that! I'd rather go into the dbml file with a text editor to add the extra field!
Shaul
@Shaul - have you tried my add-in? http://www.huagati.com/dbmltools/ ? It adds refresh functionality, has 'exclusion lists' for things you don't want it to touch, allows you to define naming convention rules etc.
KristoferA - Huagati.com
+1 on Jason's reply - it is easy to just add a timestamp and it will save you a lot of trouble. Don't hang on to datacontexts.Here's a query that will output 'alter table' stmts for adding a rowversion/timestamp to tables that don't already have one...select 'alter table ' + ss.name + '.' + st.name + ' add row_version rowversion not null;' from sys.tables as st inner join sys.schemas as ss on ss.schema_id = st.schema_id left outer join sys.columns as sc on sc.object_id = st.object_id and sc.system_type_id=189where st.type='U' and sc.object_id is nullorder by ss.name, st.name
KristoferA - Huagati.com
@KristoferA - thanks for the link to your software, will give it a try! Thanks also for the "alter table" query.
Shaul
A: 

find the data context that originally created the object and use that to submit changes

Where did your datacontext go? Why is it so hard to find? You're only using one at any given time right?

So what the heck am I supposed to attach, then, if not an existing entity? If I wanted a new record, I would do an InsertOnSubmit()! So how are you supposed to use Attach()?

You're supposed to attach an instance that represents an existing record... but was not loaded by another datacontext - can't have two contexts tracking record state on the same instance. If you produce a new instance (ie. clone) you'll be good to go.

You might want to check out this article and its concurrency patterns for update and delete section.

David B
See my latest edit to the question for why I "lost" my data context...
Shaul
+4  A: 

Updating with LINQ to SQL is, um, interesting.

If the data context is gone (which in most situations, it should be), then you will need to get a new data context, and run a query to retrieve the object you want to update. It's an absolute rule in LINQ to SQL that you must retrieve an object to delete it, and it's just about as iron-clad that you should retrieve an object to update it as well. There are workarounds, but they are ugly and generally have lots more ways to get you in trouble. So just go get the record again and be done with it.

Once you have the re-fetched object, then update it with the content of your existing object that has the changes. Then do a SubmitChanges() on the new data context. That's it! LINQ to SQL will generate a fairly heavy-handed version of optimistic concurrency by comparing every value in the record to the original (in the re-fetched) record. If any value changed while you had the data, LINQ to SQL will throw a concurrency exception. (So you don't need to go altering all your tables for versioning or timestamps.)

If you have any questions about the generated update statements, you'll have to break out SQL Profiler and watch the updates go to the database. Which is actually a good idea, until you get confidence in the generated SQL.

One last note on transactions - the data context will generate a transaction for each SubmitChanges() call, if there is no ambient transaction. If you have several items to update and want to run them as one transaction, make sure you use the same data context for all of them, and wait to call SubmitChanges() until you've updated all the object contents.

If that approach to transactions isn't feasible, then look up the TransactionScope object. It will be your friend.

Cylon Cat
+1 Thanks, that contained a lot of good information!
Shaul
A: 

You can reattach to a new DataContext. The only thing that prevents you from doing so under normal circumstances is the property changed event registrations that occur within the EntitySet<T> and EntityRef<T> classes. To allow the entity to be transferred between contexts, you first have to detach the entity from the DataContext, by removing these event registrations, and then later on reattach to the new context by using the DataContext.Attach() method.

Here's a good example.

Andrew Matthews
Trouble is, this pattern doesn't appear to work if you're updating the related items in the same transaction. Or have I misunderstood something?
Shaul
Hi Shaul, Perhaps. :) I do precisely that in some cases in my current project. In most cases I don't place the whole get, mod, save cycle inside of a TransactionScope, but I have had to do so in one or two cases and it worked OK. Question: If you're detaching your entities, that implies that you're keeping hold of the objects for an extended period and you don't want to tie up the DB connection. Don't you want to take the same approach towards long lived transactions?
Andrew Matthews
A: 

The "An entity can only be attached as modified without original state if it declares a version member" error when attaching an entitity that has a timestamp member will (should) only occur if the entity has not travelled 'over the wire' (read: been serialized and deserialized again). If you're testing with a local test app that is not using WCF or something else that will result in the entities being serialized and deserialized then they will still keep references to the original datacontext through entitysets/entityrefs (associations/nav. properties).

If this is the case, you can work around it by serializing and deserializing it locally before calling the datacontext's .Attach method. E.g.:

internal static T CloneEntity<T>(T originalEntity)
{
    Type entityType = typeof(T);

    DataContractSerializer ser =
        new DataContractSerializer(entityType);

    using (MemoryStream ms = new MemoryStream())
    {
        ser.WriteObject(ms, originalEntity);
        ms.Position = 0;
        return (T)ser.ReadObject(ms);
    }
}

Alternatively you can detach it by setting all entitysets/entityrefs to null, but that is more error prone so although a bit more expensive I just use the DataContractSerializer method above whenever I want to simulate n-tier behavior locally...

(related thread: http://social.msdn.microsoft.com/Forums/en-US/linqtosql/thread/eeeee9ae-fafb-4627-aa2e-e30570f637ba )

KristoferA - Huagati.com
Nice idea... but it's bringing up a new problem. I've marked my objects as Serializable, but when I try serialize them, I get another error: Type 'System.DelegateSerializationHolder+DelegateEntry' with data contract name 'DelegateSerializationHolder.DelegateEntry:http://schemas.datacontract.org/2004/07/System' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer. (contd...)
Shaul
I looked up this problem on the web, and found suggestions to add the "DataContract" attribute - but then you have to explicitly put the "DataMember" attribute on everything that you want serialized... and all my data attributes are defined in the generated file MyDataClasses.designer.cs - so I cannot put attributes on them! Any other ideas how to achieve the desired end?
Shaul
Doh - never mind previous 2 comments - I hadn't set up serialization on the dbml diagram. But I still have a problem that when the object deserialized, it throws a ForeignKeyReferenceAlreadyHasValueException when it tries to set the foreign key values. Now what?
Shaul
Are you changing the value of a foreign key scalar value field? If so, you need to null out the corresponding association (nav. property) to not have the association and the fk scalar value conflict each other...
KristoferA - Huagati.com
OK, you can ignore the previous comment as well - I was setting an object reference in my OnCreated() partial method, which was causing that exception. Now I just need to find a way to make that creation conditional on not being in the middle of deserializing... :-S
Shaul
Well, your solution works as far as what I've asked for, so you get answer credit - thanks! But I have another weird problem that is thwarting me, and I'll post it as another question... watch this space for a link...
Shaul
thanks :) and I'm happy to hear that it worked out
KristoferA - Huagati.com
You seriously went and serialized the object to get rid of the tracking info when all you had to do was a Refresh? Did you not bother reading new answers?
Jacob Proffitt
@Jacob Umm, refresh does a new db roundtrip. That is a tad more expensive that just serializing and deserializing...
KristoferA - Huagati.com
It does do a round trip. Since that wasn't a concern in the question, I didn't treat it as one. In most cases, it's well worth the expense for the sake of both the clarity of the code and the ability to be able to pre-screen concurrency issues.
Jacob Proffitt
A: 

When you retrieve the data in the first place, turn off object tracking on the context that does the retrieval. This will prevent the object state from being tracked on the original context. Then, when it's time to save the values, attach to the new context, refresh to set the original values on the object from the database, and then submit changes. The following worked for me when I tested it.

MyClass obj = null;
using (DataContext context = new DataContext())
{
    context.ObjectTrackingEnabled = false;
    obj = (from p in context.MyClasses
            where p.ID == someId
            select p).FirstOrDefault();
}

obj.Name += "test";

using (DataContext context2 = new ())
{
    context2.MyClasses.Attach(obj);
    context2.Refresh(System.Data.Linq.RefreshMode.KeepCurrentValues, obj);
    context2.SubmitChanges();
}
Jacob Proffitt
In further testing, it still works if I omit the context.ObjectTrackingEnabled = false; on the initial read. The key seems to be attach and refresh.
Jacob Proffitt
Isn't it a bit unnecessary to do a second db roundtrip?
KristoferA - Huagati.com
Depends. In most cases, the unnecessary round trip is well worth both the clarity of the code and the opportunity to pre-screen for concurrency problems.
Jacob Proffitt