Update: before trying the code pattern here, see if you can use this instead.
– Chris
I was reading two good blogs about Linq to SQL and ASP.Net applications here and here.
Based on the pattern set up by Rocky Moore (see first link) I came up with this code. Using generics and a little bit of reflection in a base class.
Here is the base class
public class detachableEntity<T> where T : detachableEntity<T>, new()
{
public void OnDataLoaded()
{
original = Clone();
}
public T original { get; set; }
public T Copy()
{
return Copy((T)this);
}
public static T Copy(T Old)
{
T newItem = Clone(Old);
newItem.OnDataLoaded(); // set the original state for the new object to the currect state
return newItem;
}
public T Clone()
{
return Clone((T)this);
}
public static T Clone(T item)
{
if (item == null)
return null;
T newItem = new T();
// copy all subclass properties.
foreach (PropertyInfo prop in item.GetType().GetProperties())
{
PropertyInfo prop2 = item.GetType().GetProperty(prop.Name);
prop2.SetValue(newItem, prop.GetValue(item, null), null);
}
//the two items now share the same orginal state object, fix this by
//cloning the original state object on the item that to create a new original state
//object for the new item
if (item.original != null)
newItem.original = item.original.Clone();
return newItem;
}
}
In the partial classes created by Linq Designer here is how to add the base class
As the pattern is fixed, it is a good candidate for replace all in the partial class file.
partial class Address : detachableEntity<Address>
{
partial void OnLoaded() { base.OnDataLoaded(); }
}
and a usage example
static void Main(string[] args)
{
//Create new Entity
Address home = new Address();
//Create new data context
AdventureWorksDataContext AW = new AdventureWorksDataContext();
//Get Data
home = AW.Addresses.First();
// Disconnect Data Context
AW = null;
//Modify Data
home.City = home.City + “AAA”;
// New Data context
AW = new AdventureWorksDataContext();
//Attach Entity
AW.Addresses.Attach(home, home.original);
//Review Changes
ChangeSet Changes = AW.GetChangeSet();
//Update Data Source
AW.SubmitChanges();
//Dispose Data Context
AW.Dispose();
AW = null;
//Dispose Entity
home = null;
//Create new Data Context
using (AW = new AdventureWorksDataContext())
{
//Create new Entity
Address NewHome = AW.Addresses.FirstOrDefault(addr => addr.City.Contains(“AAA”));
//Modify Data (remove earlier changes)
NewHome.City = NewHome.City.TrimEnd(new char[] { ‘A’ });
//Submit Changes in the same context
AW.SubmitChanges();
}
}
This should be quite useful for detached applications.
-Chris
Hello.
Thank you for useful class but there is one problem.
When I use the entity which is derived from detachableEntity class the code below doesn’t work:
using (EntitiesDataContext dx = new EntitiesDataContext(connectionString))
{
suser user = dx.susers.FirstOrDefault(s => s.Login == “Alex”); //only for test. This user is present in database
user.email = “test@xxxx.com”;
dx.SubmitChanges();
}
There is exception:
An exception of type ‘System.Data.SqlClient.SqlException’ occurred in System.Data.Linq.dll but was not handled in user code
Additional information: Cannot insert duplicate key row in object ‘dbo.suser’ with unique index ‘IX_suser’.
I think LINQ generate insert coomand isntead of update. If I don’t use detacheble class as derived all is ok.
Can you help with that problem.
Hi Alexander,
I tried to replicate your error, and I could not.
Here is the code I used to try to replicate it.
static void Main(string[] args)
{
//Create new Entity
Address home = new Address();
//Create new data context
AdventureWorksDataContext AW = new AdventureWorksDataContext();
//Get Data
home = AW.Addresses.First();
// Disconnect Data Context
AW = null;
//Modify Data
home.City = home.City + "AAA";
// New Data context
AW = new AdventureWorksDataContext();
//Attach Entity
AW.Addresses.Attach(home, home.original);
//Review Changes
ChangeSet Changes = AW.GetChangeSet();
//Update Data Source
AW.SubmitChanges();
//Dispose Data Context
AW.Dispose();
AW = null;
//Dispose Entity
home = null;
//Create new Data Context
using (AW = new AdventureWorksDataContext())
{
//Create new Entity
Address NewHome = AW.Addresses.FirstOrDefault(addr => addr.City.Contains("AAA"));
//Modify Data (remove earlier changes)
NewHome.City = NewHome.City.TrimEnd(new char[] { 'A' });
//Submit Changes
AW.SubmitChanges();
}
}
I am using vs2008 and sql server 2005
I updated the usage example with this.
Hi Chris,
How do you use this class to update a collection of updating objects like List?
tumay
List<Answer>
Hi,
I’ve used your DetachableEntity class as laid out here, and all works brilliantly during the update, however, there seems to be an issue when I try to delete an entity. I’m not sure if I’m doing something wrong, so I wanted to check if you had experienced any problems during deletes.
All I have done is inherit one of my entity classes from DetachableEntity in a partial class, and provided an implementation of the OnLoaded partial method as described in your article.
If I then call DeleteAllOnSubmit passing in an Expression<Func> predicate to match some criteria I don’t get any delete SQL hit my database. If I inspect the return value of the DataContext.GetChangeSet() I can see the same number of items in the Insert collection that are in the Delete section, and these items appear to be same. I guess the context is normalising this and determining that nothing needs to be deleted.
If I remove the call to base.OnDataLoaded() in the partial class (effectively removing the assignment of the cloned entity), I can run through the same code but this time the GetChangeSet result doesn’t have anything the Inserts collection, and has the correct number in the Deletes section, so the rows are correctly deleted. But of course, the update method no longer works.
My delete method is on a generic base class (and I realise that it will select the ones to delete before actually deleting them), but looks like this:
public virtual void Delete(Expression<Func> predicate)
{
if (predicate == null)
{
throw new ArgumentNullException(“predicate”);
}
/// TODO: See if there’s a way we can delete items without having to
/// perform a SELECT first.
///
var query = DataContext.GetTable().Where(predicate);
DataContext.GetTable().DeleteAllOnSubmit(query);
DataContext.SubmitChanges();
}
I then call the method with something like Delete(t => t.SomeProperty == “foo”);
Has anyone used this code and experienced anything similar?
Thanks
Ian
Hmmm. I’ll take a look.
-Chris
Hi Ian,
I have tried a few things and cannot reproduce your problem. Thinking about it I think I know what might be happening and how to check.
In the onLoad function we do three things
1) create a new instance of the type
2) iterate through every property and copy it to the new type
3) call this recursivly until this.original == null
So either,
Creating a copy of the item is causing something in your class structure to add the new item to insert list
or
Copying every property we find is adding a new item to the instert list
I don’t think it is the recursion because your are only getting one extra per call.
How to check:
1) make DataContext public static (just for the test)
2) set break points inside the clone function.
3) evaluate [your class].DataContext.GetChangeSet() after each step
This will tell you what step is creating the insert record.
You can then find out the side efect code that is being invoked to add the insert.
Let me know what you find.
Thanks
Chris
Hi, thx for code,
There is one problem. The reflection code doesnt work on child relations.
I have a customer and i have orders of that customer.
I load customer with deferredloadingEnabled = false AND dataloadOptions to load the orders of that customers
Dim options As DataLoadOptions = New DataLoadOptions
options.LoadWith(Of customer)(Function(x As customer) x.orders)
When i use linq2sql without detachableEntity, i load 1 customer and 3 orders of that customer.
With detachableEntity, cust1.orders.count = 0 (i expect 3) and the cust1.original.count = 3
The problem is on this line when the property is an entityset.
prop2.SetValue(newItem, prop.GetValue(item, null), null);
So in that situation my original orders are at cust1.original.orders.original and my current orders are at cust.original.orders.
A little strange, i think
How can i fix this issue?
Warning: This code won’t work when you have read only properties in your class. It will try to write the property and fail to do so because it has not setter for the property.
Add the following line to you code in the detachableEntity – public static T Clone(T item) method to avoid this:
// copy all subclass properties.
foreach (PropertyInfo prop in item.GetType().GetProperties())
{
PropertyInfo prop2 = item.GetType().GetProperty(prop.Name);
// Check if the property can be written
if(prop2.CanWrite)
prop2.SetValue(newItem, prop.GetValue(item, null), null);
}
</code
Chris,
I implemented your detachableEntity class and I am trying to use an ObjectDataSource in an ASP.NET webpage to bind and update the entity class. For some reason, the Original property that the inheritance is adding to the entity is not being serialized (or at least it is null when the postback fires the update method). I have verified that it is set when the form loads but for some reason it is being cleared out. So when I call attach method as follows…
context.Customers.Attach(instance, instance.Original);
…it throws an exception stating that “Value cannot be null. Parameter name: original.”
Any clue? Have you used your code in a similar fashion (i.e. GridView, DetailsView, etc.)?
I tried this code and it works fine most of the time.
only when I have two entities which share the same PK and have a one to one relationship.
The PK will be lose after the clone.
Does anyone encounter this problem? any workaround?