Saturday, February 9, 2008

LINQ to SQL Updating in ASP.NET - The Right Way

Update (6/7/2008): I have posted my thoughts on how this can be done in a more n-tier based way here.

Click here to download the source for these examples. This is a link to a Google Code repository.  Use subversion to check it out.

Like many folks who have been creating web applications on the ASP.NET platform for a while, the glitz and glamour surrounding Language Integrated Query (LINQ) intrigued and frightened me at the same time.  All the demos seemed to imply that life was easier and clearer in the new world.  Don't believe the hype. 

Don't get me wrong, LINQ is great if you understand the right way to use it and our teams at Pariveda are really getting some great traction out of LINQ to SQL, but you definitely have to approach it differently than most of the demos seem to imply.  The QUERY part of LINQ to SQL is straightforward and extremely powerful; the create, update and delete support in the language is what takes some getting used to.

First off, it's good to understand the model that is used for LINQ to SQL Updates.  The entities that are passed around with LINQ to SQL are lightweight and do not store changed state information. The only things the entities do is store data and raise events when their properties change to other watching objects such as the DataContext-- and that is where folks get confused when working with LINQ in ASP.NET.

Almost ALL applications these days work in a disconnected state.  Meaning there are process/memory differences between when and where data is initially RETRIEVED and when and where it is eventually UPDATED.  Herein lies the problem with most of the demos that show LINQ in action.

ASP.NET web applications are and should remain stateless from an architectural implementation.  We have ways of "fooling" developers into believing that their pages are somehow living in memory using things like ViewState, but anyone who has worked in the tool long enough knows that ViewState is serialization/deserialization trickery to lull the developer into a stateful sense of well being.  I love me some ViewState, but you have to understand it to use it wisely.

So, what's the problem with ASP.NET and LINQ.  When you perform an update with LINQ, the entity you are updating must be watched when the properties are updated to their new values. 

Say, I have a page that displays a customer record:

Data Access Screen

On the SelectedIndexChanged of the ListBox, I would simply write some code that filled in the values in the TextBoxes like so:

protected void CustomerListBox_SelectedIndexChanged(object sender, EventArgs e)

{

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())

    {

        Customer customer = db.Customers.Single(c => c.CustomerID ==

            int.Parse(CustomerListBox.SelectedValue));

        NameTextBox.Text = customer.CompanyName;

        EmailTextBox.Text = customer.EmailAddress;

        FirstNameTextBox.Text = customer.FirstName;

        LastNameTextBox.Text = customer.LastName;

        IsActiveCheckBox.Checked = customer.IsActive;

    }

}

We think all is well with the world.  Then we proceed to Update the Customer object by hooking into the Save Button's click event like so:

protected void SaveButton_Click(object sender, EventArgs e)

{

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())

    {

        Customer customer = new Customer

        {

            CustomerID = int.Parse(CustomerListBox.SelectedValue)

        };

        db.Customers.Attach(customer);

        customer.CompanyName = NameTextBox.Text;

        customer.EmailAddress = EmailTextBox.Text;

        customer.FirstName = FirstNameTextBox.Text;

        customer.LastName = LastNameTextBox.Text;

        customer.IsActive = IsActiveCheckBox.Checked;

        db.SubmitChanges();

    }

}

And we get a "row not found or changed" error?  Why is this?  Because the datacontext uses concurrency checking to ensure that the data we originally retrieved has not changed between the time we retrieved it and the time we are attempting to update it.  Let's take a look at the SQL that was generated by the DataContext that caused the error:

UPDATE [SalesLT].[Customer]
SET [FirstName] = @p0, [LastName] = @p1, [CompanyName] = @p2, [EmailAddress] = @p3WHERE 0 = 1
-- @p0: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Orlando]
-- @p1: Input NVarChar (Size = 3; Prec = 0; Scale = 0) [Gee]
-- @p2: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [A Bike StoreX]
-- @p3: Input NVarChar (Size = 28; Prec = 0; Scale = 0) [orlando0@adventure-works.com]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

Notice, the WHERE 0=1 part?  What's up with that?

The best way to start off solving this is to create a timestamp column in your table and hold on to this value in ViewState so that when you perform your Attach and Update, you can vouch for the fact that the data hasn't changed since the last time you retrieved it.  We'll call it the column "Version".

Customer Table Definition

Next, we'll create a ViewState-backed property that will hold onto the value of the currently selected version:

Binary SelectedCustomerVersion

{

    get { return (Binary)ViewState["SelectedCustomerVersion"]; }

    set { ViewState["SelectedCustomerVersion"] = value; }

}

We'll set the Version property in our SelectedIndexChanged event of our ListBox like so:

protected void CustomerListBox_SelectedIndexChanged(object sender, EventArgs e)

{

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())

    {

        Customer customer = db.Customers.Single(c => c.CustomerID ==

            int.Parse(CustomerListBox.SelectedValue));

        NameTextBox.Text = customer.CompanyName;

        EmailTextBox.Text = customer.EmailAddress;

        FirstNameTextBox.Text = customer.FirstName;

        LastNameTextBox.Text = customer.LastName;

        IsActiveCheckBox.Checked = customer.IsActive;

        SelectedCustomerVersion = customer.Version;

    }

}

Finally, in our update code, we will be sure and set the Version value of our Customer entity before we attach to the data context.

protected void SaveButton_Click(object sender, EventArgs e)

{

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())

    {

        Customer customer = new Customer

        {

            CustomerID = int.Parse(CustomerListBox.SelectedValue),

            Version = SelectedCustomerVersion

        };

        db.Customers.Attach(customer);

        customer.CompanyName = NameTextBox.Text;

        customer.EmailAddress = EmailTextBox.Text;

        customer.FirstName = FirstNameTextBox.Text;

        customer.LastName = LastNameTextBox.Text;

        customer.IsActive = IsActiveCheckBox.Checked;

        db.SubmitChanges();

    }

}

And this works... MOST of the time.  You can be deceived into thinking you have things working until you understand what is actually occurring.  When you attach your customer entity to your datacontext, you are basically promising that the values you plan to change in your entity while your data context watches are ALREADY SET to the original values they were in the database.  The reason why this is important for LINQ is that your Customer entity will only alert the data context that something has changed if the value the property is changing FROM is different.  So how would you see this problem manifest itself? It will occur when you are setting a property back to the default value that it would already have when a new entity is created-- boolean values are the easiest ways to see the issue.

If we we set the IsActive property from true to false, our current code won't work because the default value of a boolean is already false.  Let's look at what the SQL would look like if the ONLY thing we changed in the entity was the IsActive checkbox going from checked to unchecked (true to false).

UPDATE [SalesLT].[Customer]
SET [FirstName] = @p2, [LastName] = @p3, [CompanyName] = @p4, [EmailAddress] = @p5
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)

SELECT [t1].[Version]
FROM [SalesLT].[Customer] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p6)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [Brian]
-- @p3: Input NVarChar (Size = 3; Prec = 0; Scale = 0) [Gee]
-- @p4: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [A Bike Store]
-- @p5: Input NVarChar (Size = 28; Prec = 0; Scale = 0) [orlando0@adventure-works.com]
-- @p6: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

Notice, that even though the ONLY column we changed is the IsActive value from true to false, it was the only column that wasn't attempted to be updated by the database.  Let's examine why. 

When you attach an entity to the datacontext, it is basically hooking up to be alerted everytime a property changes.   Since we just created an empty customer from scratch and only set the CustomerID and Version properties on the entity, all of the other property values are null.  When we set CompanyName, it alerts that there was a change because we changed the property from null to a value, and the same for the other columns... except for IsActive.  The CURRENT value of IsActive is false and then we attempt to set the value of the property to false.  No change event is raised because the property isn't being changed and therefore the database is not updated during the SubmitChanges call.

So, the last piece of the puzzle is coming up with a way to hold on to the original values of the customer entity so that before we attach the customer entity we set all the properties to their new values, the old values are already set.  This is the part that had my head spinning for a while.  At first, I tried putting the Customer entity directly into ViewState but it complained about a host of serialization problems-- you can try making it happen and see for yourself the problems that occur.

The way I finally was able to get serialization of a LINQ entity to work was to set the dbml file to the Serialization Mode of Unidirectional.  You can do this by clicking in a blank area of the designer and then opening up the properties window.

DBML Serialization Settings

Next, I created an OriginalCustomer property to store the object.

Customer OriginalCustomer { get; set; }

Finally, I overrode the LoadViewState and SaveViewState methods of the page so that I could persist the deserialized string of  the OriginalCustomer property to ViewState.

protected override void LoadViewState(object savedState)

{

    object[] allStates = (object[])savedState;

    base.LoadViewState(allStates[0]);

    OriginalCustomer = LinqEntityUtil.Deserialize<Customer>((string)allStates[1]);

}

protected override object SaveViewState()

{

    object baseState = base.SaveViewState();

    object[] allStates = new object[2];

    allStates[0] = baseState;

    allStates[1] = LinqEntityUtil.Serialize<Customer>(OriginalCustomer);

    return allStates;

}

Here's the utility class that handles the deserialization of LINQ Entities:

public static class LinqEntityUtil

{

    /// <summary>

    /// Serializes a LINQ to SQL Entity to a WCF-Serialized

    /// string using the DataContractSerializer

    /// </summary>

    /// <typeparam name="T">Type of the LINQ Entity to be serialized</typeparam>

    /// <param name="linqEntity">LINQ Entity object to be serialized</param>

    /// <returns>Serialized string</returns>

    public static string Serialize<T>(T linqEntity)

    {

        DataContractSerializer serializer = new DataContractSerializer(typeof(T));

        StringWriter writer = new StringWriter();

        XmlTextWriter xmlWriter = new XmlTextWriter(writer);

        serializer.WriteObject(xmlWriter, linqEntity);

        writer.Flush();

        return writer.ToString();

    }

 

    /// <summary>

    /// Deserializes from a string back to a LINQ to SQL Entity

    /// object using the DataContractSerializer

    /// </summary>

    /// <typeparam name="T">Type of the LINQ Entity to be serialized</typeparam>

    /// <param name="xml">Serialized string to deserialize</param>

    /// <returns>LINQ Entity object</returns>

    public static T Deserialize<T>(string xml)

    {

        DataContractSerializer serializer = new DataContractSerializer(typeof(T));

        StringReader reader = new StringReader(xml);

        XmlTextReader xmlReader = new XmlTextReader(reader);

        T linqEntity = (T)serializer.ReadObject(xmlReader);

        return linqEntity;

    }

}

Now, with my OriginalCustomer serialization in place, I could hold on to the original customer entity and attach it back to the datacontext when I was ready to save.

protected void SaveButton_Click(object sender, EventArgs e)

{

    using (AdventureWorksDataContext db = new AdventureWorksDataContext())

    {

        db.Customers.Attach(OriginalCustomer);

        OriginalCustomer.CompanyName = NameTextBox.Text;

        OriginalCustomer.EmailAddress = EmailTextBox.Text;

        OriginalCustomer.FirstName = FirstNameTextBox.Text;

        OriginalCustomer.LastName = LastNameTextBox.Text;

        OriginalCustomer.IsActive = IsActiveCheckBox.Checked;

        db.SubmitChanges();

    }

}

With this in place, I can make changes to IsActive where I set it from true to false and the data context "sees" the changes appropriately.  Here is the much more optimized SQL that occurs, when I change just the IsActive field.

UPDATE [SalesLT].[Customer]
SET [IsActive] = @p2
WHERE ([CustomerID] = @p0) AND ([Version] = @p1)

SELECT [t1].[Version]
FROM [SalesLT].[Customer] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[CustomerID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input Bit (Size = 0; Prec = 0; Scale = 0) [False]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8

Hopefully this helps some of you struggling with similar issues.  The key thing when using attach is that you want all of the properties that you plan to change to be set to their original values before you attach it to the datacontext and ViewState serialization is the easiest way to do that in ASP.NET.

Click here to download the source for these examples.

Technorati Tags: ,,,

26 comments:

Anonymous said...

Newbie question here - why can't you just do this on the save? Why the need for attaching the entity in this case?

protected void SaveButton_Click(object sender, EventArgs e)
{
using (AdventureWorksDataContext db = new AdventureWorksDataContext())
{
Customer customer = db.Customers.Single(c => c.CustomerID ==
int.Parse(CustomerListBox.SelectedValue));

customer.CompanyName = NameTextBox.Text;
customer.EmailAddress = EmailTextBox.Text;
customer.FirstName = FirstNameTextBox.Text;
customer.LastName = LastNameTextBox.Text;
customer.IsActive = IsActiveCheckBox.Checked;
db.SubmitChanges();
}
}

Brian said...

Great question.

You can most certainly do that but it would result in a second database read against the database. Since you already retrieved the customer object on the page load, it would be twice as costly from a performance perspective on the database.

Anonymous said...

It looks good, but just too complicate. And If i don't want to expose the DataContext to web page?

Brinn Crowdus said...

This is about the most informative article about Linq to Sql and using the Attach method. I did read on msdn that the Attach method should only call the method with a new or deserialized entity.
http://msdn2.microsoft.com/en-us/library/bb546187.aspx

However, they forgot to mention the Serialization Mode on the dbml file.

Thanks a lot for the post. There are still many questions and issue I have about Linq to Sql but this helps me to make some advancement so that I can actually start using this in a real world application.

Thanks Brian

Anonymous said...

I still can't understand why the example without TimeStamp doesn't work.

Brian said...

The TimeStamp is needed to provide for optimistic concurrency checks. Without having some way of showing that the data hasn't changed since the last time you retrieved the data, the update method has no way of ensuring that the data hasn't changed out from underneath while the user was editing.

Keep in mind this isn't the only way to do this. You can use a datetime field. Or you could choose that you don't care about optimistic concurrency (not advised) and say "last in wins". To do that, you would change each column in your table to be "Update Check = Never"... and then it would work the way you might expect.

Александр said...

I used dataContext.Customers.Attach(customer, originalCustomer); but it didn't work too

Anonymous said...

I have timestamps on my table and I'm trying to do this with a detailsView control.

However, when I use an UPDATE I always get the cursed "Row not found or changed." error when I'm changing a field.

I have two datetime fields, but I'm not changing those, I'm changing an unrelated INTEGER field.

I set my timestamp field in the .dbml as a timestamp and all the fields are set to NEVER check in the .dbml but this still happens.

I'm just using the default / integrated EDIT/NEW buttons in the detailsView.

Any suggestions?

Brian said...

First off, Details view controls (and form view controls for that matter) are pure evil. I have seen nothing but trouble from them.

Be sure you are using a SQL Server 2005 Timestamp column, not a datetime field that you set as a timestamp. I couldn't tell from your post which you were using. Also, be sure you set your DataKeyNames property correctly in your data source control.

Anonymous said...


First off, Details view controls (and form view controls for that matter) are pure evil. I have seen nothing but trouble from them.

Be sure you are using a SQL Server 2005 Timestamp column, not a datetime field that you set as a timestamp. I couldn't tell from your post which you were using. Also, be sure you set your DataKeyNames property correctly in your data source control.


I am using a TimeStamp field on SQL Server 05 and I have my gridView's DataKeyName set to the PK of the table it is pulling from.

Now that I've played around with it more I realize my problem is that I'm using a custom SELECTING statement for my LinqDataSource's SELECTING event.

The reason for this is because for some reason the gridView won't display a field in a FK linked table (I have something like ProductID in my detailsView and I want to use the EVAL statment to display ProductName via Product.ProductName instead of just ProductID however, that always displays a blank field so I wrote a custom LINQ Selecting statement instead to just return the ProductName as part of the SELECT statement and display that in the detailsView).
So if I do NOT use the Selecting Event the update works fine now that I’ve added the TimeStamp field (thank you for that in your post).
But I’m still out of luck on how to do the update when I’m using the custom SELECTING LINQ statement (I really just wish my EVAL statement would work… but nothing is returned from that either…)
It is all most perplexing sadly…
Thank you again for your help (If you have any suggestions on how to solve this quandary I’d appreciate it greatly!)

Brian said...

This is exactly the reason I would use a standard form and not use the DetailsView or FormView. They are deceptively simple and then when you have a situation like you have with a foreign key lookup they take more time than if you used the method I outline in my post. It's really not hard at all to do on your own with custom code.

Ian Dykes said...

I've been searching for a simple yet functional example of updating a row using LINQ to SQL from ASP.Net for a few days now, but yours is the only one that really makes sense to me.

Thanks a lot for the info.

Anonymous said...

Responding to anonymous post on April 7:

You can use iqueryable to access a property such as Product.ProductName in an Eval.

http://forums.asp.net/p/1262668/2377732.aspx#2377732

-pugs421

rei said...

There must be a better way to do this.

It's not even anything to do with ASP.NET or Attach -- even if I do the same thing entirely in code by just updating a value, it'll refuse to update when the value is set to false.

Am I missing something?

This fails:

Invoice inv = Data.DC.Invoices.Single(x => x.Id == id);
inv.IsActive = false; //works only when setting to true
Data.DC.SubmitChanges();

Brian said...

It fails with an exception or it appears successful but doesn't update the value. Are you using a timestamp column?

What is Data.DC-- you should NEVER hold onto a datacontext. It should be used and then disposed-- similar to a database connection.

Do not cache your datacontext in a middle tier static or some such thing. That is not the right approach.

Have a look at my latest post on using a service tier if you want to have a separated business tier for updates.

rei said...

Oops scratch that... it was a totally unrelated problem :)

tim said...

Your solution is over engineer. According to MSDN article on the Attach method "the entity is assumed to be in its original value state. After calling this method, you can then update its fields, for example with additional data sent from the client."

Therefore, a better way to update data from an ASP.NET web page is

DataContext db = new DataContext();

Customer c = new Customer();
c.CustId = paramCustId
c.Version = ViewStateVersion
db.Customers.Attach(c);
c.comment = "some comments";
db.SubmitChanges();

// Store the row version in view state.
ViewStateVersion = c.Version;

The trick is to assign the customer entity with the original rowversion before attached it to the dataContext

Brian said...

Tim, unfortunately that is not the case. Read the post and you'll see why. The rowversion property will not help you there. The easiest way to see the problem is with boolean values. If you are setting a value to false that was originally true, the datacontext will not register a change unless you set the value of the boolean value (to true) PRIOR to setting it to false after being attached.

Row versions are for optimistic concurrency checking ONLY. This is an issue I have confirmed with Microsoft, it's not solved with your solution.

Anonymous said...

So you would rather push the data via viewstate to the client browser, then have then client browser push it all the way back to you vs:

1) Hit the db w/the *same context* for reinitialize
2) Update multiple cols (DB pages/extents come into play for the update but the overhead at that point is the lookup)

Unless its a row with a ridiculous amount of cols I bet the serialize|deserialize is worse than the DB round trip for a single read (based on PK lookup). Kudos to the newbie on that one

Brian said...

Definitely agree on the ViewState vs. the re-lookup. For a basic update, it would make more sense to a do a re-select before updating rather than storing the original in viewstate.

However, this article also addresses the need to add multiple children (all at once) rather than requiring the parent be saved first before children can be added. You have to use some form of temporary storage (whether Viewstate, Session, etc...) to maintain state while you are adding multiple children to a parent.

Good points. We are all struggling with the "right" way to do this stuff and I'm sure we'll all be wrong in the long run. :)

Anonymous said...

Hi Brian,

Thanks for a very good article. I think I understand both your main points (about the boolean values that need to be set to their original values before the Attach and about your approach to store the original object and use it for the Attach).

However I'm not 100% sure about the timestamp. If I use your technique of storing the original object (and using it for the attach during the Save event), do I still need a timestamp? Doesn't seem that way to me but I may be overlooking something...

PS to the group: I don't think newbie's solution of re-reading the db during the Save event will apply in a scenario where you want to load the original data from the db in the OnLoad event of an "Edit" page and then "watch for changes" while the Edit page is displayed to the user (i.e. if you want to use LINQ's optimistic concurrrency to find out whether the underlying data has changed since the user started editing the data on the form/page up to the point where the user clicks Save). In such a case I think Brian's proposed approach of storing the original object would work like a charm.

Louis

Kurt said...

This was a very helpful article, as I encountered the same issue. It would be cool if there was a way to set the concurrency mode, as there are times when you really do want it to commit the data is it is presented, and yes, the source may have been changed by something else during the process.

In any event, I was able to resolve the issue by using stored procedures for the Insert and Update actions, then mapping the Insert and Update actions to those stored procedures in the LINQ to SQL dbml designer. Still not terribly pretty, but I do know what its doing now :) .

Anonymous said...

Thank you Brian. Could you update the link for source code.

Brian said...

Updated now. Thanks.

thzero said...

Why in the world are you using the ViewState? As one commenter mentioned, you are sending this information to the browser and back. Not only does that effectively send the information twice, but it could end up sending information that you do not want available. It also increases the size of the content stream that must be delivered to the client.

It does seem like storing the object and reattaching it is the proper mechanism and is definetly helpful for pointing that out.

But it would seem that using the Session as the storage mechanism would be much more appropriate. That way the object is solely on the server side.

This is even more true if you want to use Ajax (whether you use ASP Ajax, YUI, jQuery, etc.) or Silverlight, etc. to do form submissions.

Brian said...

ViewState does not have to be stored in the page. There are many great examples out there of storing viewstate server-side only.

The primary difference between ViewState and Session is the ability for state to be saved on a per-page basis which has its merits for the scenario described in the article.

Totally agree that ViewState can become unwieldy and overly heavy from a network load perspective (if used the way it is setup out of the box).

The point of the article is less about how to persist but rather how to rehyrdrate persisted objects for sending back to a services "tier". LINQ to SQL missed the mark on making this scenario as straightforward as it could be.