There seems to be a bug in the .NET Soap serialization stack when passing a DataSet with an expression column in it that uses a relation. Allow me to elaborate But don't run off just yet if you don't care about the serialization stack, there's some useful stuff in there as well
Imagine a typed DataSet "TestDataSet" that you're passing from your Service layer to your client UI layer through an ASP.NET Web Service. Say you have a table Employee and a table Company in it, and you want to show the employees in a DataGrid (e.g. WinForms but that doesn't really matter here) with the name of the company in a separate column. The service backend doesn't care about your needs to display the company name inline with the rest of the data, so all you have is the relation CompanyEmployee between the two tables.
For display purposes, you can add a column to the Employee table with its Expression set to "Parent(CompanyEmployee).Name" and it will display just fine: it calculates the new column by navigating to the parent row through the CompanyEmployee relationship and then takes the Name column from that row. Bingo!
TestDataSet testData = TestServiceAgent.GetTestData();
testData.Employee.Columns["ID"].ColumnMapping = MappingType.Hidden;
testData.Employee.Columns["CompanyID"].ColumnMapping = MappingType.Hidden;
testData.Employee.Columns.Add( "CompanyName", typeof( string ), "Parent(CompanyEmployee).Name" );
employeeGrid.DataSource = testData.Employee;
Extra advantages: this value is automatically updated if the parent changes and since it's a calculated column, you can't edit the company name in the grid. Also note that you can hide columns by setting their mapping type to Hidden. That's just a simple trick you can use on the client side.
The problem arises when you use this modified DataSet to update your entity in the backend again: you probably have some SaveTestData method on your service that accepts a TestDataSet to be saved and you just want to pass the changes of the modified DataSet along to it.
TestServiceAgent.SaveTestData( testData.GetChanges() );
Too bad, it will blow up in your face:
Exception: System.Web.Services.Protocols.SoapException: Server was unable to read request. --->
System.InvalidOperationException: There is an error in XML document (1, 12311). --->
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Data.LookupNode.Bind(DataTable table, ArrayList list)
at System.Data.DataExpression.Bind(DataTable table)
at System.Data.DataExpression..ctor(String expression, DataTable table, Type type)
at System.Data.DataColumn.set_Expression(String value)
at System.Data.Merger.MergeSchema(DataTable table)
at System.Data.Merger.MergeTableData(DataTable src)
at System.Data.Merger.MergeDataSet(DataSet source)
at System.Data.DataSet.Merge(DataSet dataSet, Boolean preserveChanges, MissingSchemaAction missingSchemaAction)
at TestProject.TestDataSet.ReadXmlSerializable(XmlReader reader) in C:\TestProject\TestDataSet.cs:line 166
at System.Data.DataSet.System.Xml.Serialization.IXmlSerializable.ReadXml(XmlReader reader)
at System.Xml.Serialization.XmlSerializationReader.ReadSerializable(IXmlSerializable serializable)
--- End of inner exception stack trace ---
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
--- End of inner exception stack trace ---
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message,
WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object parameters)
at TestProject.TestServiceProxy.SaveTestData(TestDataSet testData) in c:\testproject\testserviceagent.cs:line 291
at TestProject.TestServiceAgent.SaveTestData(TestDataSet testData) in c:\testproject\testserviceagent.cs:line 162
As you can see, the Soap/XML deserialization stack has some problems with the expression column. And this isn't just because the parent row could be missing in case you only sent the changed rows (which you should do using the
GetChanges() method on the DataSet to get the diffgram): it happens even if you send the full DataSet back. If there's an expression column that doesn't use a relation, then there's no problem.
There's a post on .NET 247 called "Serialize DataSet with ExpressionColumn" that shows somebody else running into this issue, and if you kept your foreign languages polished you can also check it out on the Russian GotDotNet site. But that's all I found on it so far, and none offer an explanation or solution.
A possible workaround would be to remove all expression columns again before submitting the DataSet back to the Service layer, but that's pretty harsh, not too transparent, and it can mess up your UI if you're still using the DataSet on-screen.
What I consider to be much better, is to create a new instance of the typed DataSet and merge the changes of the DataSet you're working with into it - making sure you ignore anything not in the DataSet's schema by using
MissingSchemaAction.Ignore. This removes all the added clutter you don't need on the backend anyway and it makes sure you get a clean and valid copy of your typed DataSet out of the door.
TestDataSet changes = new TestDataSet();
changes.Merge( testData.GetChanges(), true, MissingSchemaAction.Ignore );
TestServiceAgent.SaveTestData( changes );
This way, everything works and the universe is happy again.
And if you're worried about the cost of the extra DataSet you just created: it's on the client side so I'd consider the performance hit negligible as long as you don't constantly send this type of changes to the backend. Then again, if you are constantly doing this, then there's probably something wrong with the way you're using your service :-p