The premise that the client proxy will execute the same code on the client and on the server is flawed because WCF is based on interfaces. I explain this point in bullet #2 below.
Rules of Sharing WCF Interfaces & Implementation
If you want to share the implementation of the data contract you will need to factor the RequestArray class into a class library that holds NOTHING BUT the data contract classes, including presumably also the RequestRecord class.
Rules I live by:
Group all data contracts by themselves (100%) into one or more assemblies, no exceptions.
Group all service contracts into one or more assemblies.
Group all service types, i.e., the classes that implement the service contract, into one ore more assemblies.
Group all client channel proxies, i.e., the class that invokes the methods defined on the service contract interface, into one or more assemblies.
In a generic framework where all client software operates as a WCF service (I avoid duplex connections), it is safe to merge rules 2, 3 & 4 so that service contracts, service types and channel proxies are grouped together into one assembly.
The main reason for separating the interfaces into a more flexible dependency chain is that it is possible to deploy a limited set of assemblies to the client without exposing unnecessary and potentially proprietary implementation details. Another reason is that it makes refactoring so much easier, especially in cases where you want to implement or extend a generic framework through inheritance or delegation.
Examining the Code
There are a few BIG problems with the code for RequestArray...
The setter logic will overwrite any of the modified elements of the m_Record array variable when the DataContract instance is deserialized. This violates deserialization principles.
The Record property will not be able to be deserialized because the Record property on the RequestArray class is Read-only (since it lacks a setter). Generally, I find that for DataContract classes the best approach for read-only properties is simply a method. It is a bad idea to get into the habit of treating data contracts like they are anything more than bit buckets. What the attributes are basically doing is dynamically creating an interface definition specifically used for data serialization and deserialization. I believe that it is a mistake to think of the data on the wire as objects. Rather, it is an opt-in way of specifying the relevant data parts of an object that need to persist over the wire.
The TotalRecords property becomes dangerous if it ends up (correctly) just allowing the m_TotalRecords variable to be set, since it will be totally independent of the internal array. In order for me to get this to work acceptably in my sample code (below), I had to shield the set with if (m_TotalRecords == 0)
. In the sample code I have saved for future use, I comment out the TotalRecords property altogether, but I leave m_TotalRecords just to illustrate that the private object is in fact preserved over the wire.
Fixed Code
I adapted bendewey's sample code (thanks!) and came up with this complete test. Note: I had to define RequestRecord. Also, please see the code comments. If there are any bugs or anything that is unclear, please let me know.
#region WCFDataContractTest
[DataContract] // The enclosed type needs to also be attributed for WCF
public class RequestRecord
{
public RequestRecord() { }
[DataMember] // This is CRUCIAL, otherwise the Name property will not be preserved.
public string Name { get; set; }
}
[DataContract] // Encloses the RequestRecord type
public class RequestArray
{
private int m_TotalRecords; // should be for internal bookkeeping only
private RequestRecord[] m_Record;
[System.Xml.Serialization.XmlElement]
[DataMember]
public RequestRecord[] Record
{
get { return m_Record; }
// deserialization will not work without the set
set { m_Record = value; }
}
[DataMember] // is not really needed
public int TotalRecords
{
get { return m_TotalRecords; }
set
{
if (m_TotalRecords == 0)
m_TotalRecords = value;
}
}
// The constructor is not called by the deserialization mechanism,
// therefore this is the right place to specify the array size and to
// perform the array initialization.
public RequestArray(int totalRecords)
{
if (totalRecords > 0 && totalRecords <= 100)
{
m_TotalRecords = totalRecords;
m_Record = new RequestRecord[totalRecords];
for (int i = 0; i < m_TotalRecords; i++)
m_Record[i] = new RequestRecord() { Name = "Record #" + i.ToString() };
m_TotalRecords = totalRecords;
}
else
m_TotalRecords = 0;
}
}
public static void TestWCFDataContract()
{
var serializer = new DataContractSerializer(typeof(RequestArray));
var test = new RequestArray(6);
Trace.WriteLine("Array contents after 'new':");
for (int i = 0; i < test.Record.Length; i++)
Trace.WriteLine("\tRecord #" + i.ToString() + " .Name = " + test.Record[i].Name);
//Modify the record values...
for (int i = 0; i < test.Record.Length; i++)
test.Record[i].Name = "Record (Altered) #" + i.ToString();
Trace.WriteLine("Array contents after modification:");
for (int i = 0; i < test.Record.Length; i++)
Trace.WriteLine("\tRecord #" + i.ToString() + " .Name = " + test.Record[i].Name);
using (var ms = new MemoryStream())
{
serializer.WriteObject(ms, test);
ms.Flush();
ms.Position = 0;
var newE = serializer.ReadObject(ms) as RequestArray;
Trace.WriteLine("Array contents upon deserialization:");
for (int i = 0; i < newE.Record.Length; i++)
Trace.WriteLine("\tRecord #" + i.ToString() + " .Name = " + newE.Record[i].Name);
}
}
#endregion
The listing for this sample program, after running TestWCFDataContract is:
Array contents after 'new':
Record #0 .Name = Record #0
Record #1 .Name = Record #1
Record #2 .Name = Record #2
Record #3 .Name = Record #3
Record #4 .Name = Record #4
Record #5 .Name = Record #5
Array contents after modification:
Record #0 .Name = Record (Altered) #0
Record #1 .Name = Record (Altered) #1
Record #2 .Name = Record (Altered) #2
Record #3 .Name = Record (Altered) #3
Record #4 .Name = Record (Altered) #4
Record #5 .Name = Record (Altered) #5
Array contents upon deserialization:
Record #0 .Name = Record (Altered) #0
Record #1 .Name = Record (Altered) #1
Record #2 .Name = Record (Altered) #2
Record #3 .Name = Record (Altered) #3
Record #4 .Name = Record (Altered) #4
Record #5 .Name = Record (Altered) #5