tags:

views:

1932

answers:

5

I'm a bit new to WCF and I don't think I completely understand what the deal is with DataContracts. I have this 'RequestArray' class:

[DataContract]
public class RequestArray
{
  private int m_TotalRecords;
  private RequestRecord[] m_Record;

  [System.Xml.Serialization.XmlElement]
  [DataMember]
  public RequestRecord[] Record
  { 
    get { return m_Record; }
  }

  [DataMember]
  public int TotalRecords
  {
    get { return m_TotalRecords; }
    set {
      if (value > 0 && value <= 100) {
        m_TotalRecords = value;
    m_Record = new RequestRecord[value];
    for (int i = 0; i < m_TotalRecords; i++)
      m_Record[i] = new RequestRecord();
      }
    }
  }
}

The idea is when the client says requestArray.TotalRecords=6; the Record array will be allocated and initialized (I realize that I'm hiding an implementation behind an assignment, this is out of my control).

The problem is that when the client does this, the TotalRecord's set code is not called, breakpoints in the service confirm this as well. Rather, some sort of generic setter has been generated that is called instead. How do I get the client to use my setter instead?

EDIT: It looks like I didn't quite get how the [DataContract] works, but it makes sense that the client wouldn't be executing this code. Like I mentioned in a comment, if I 'manually' do the work of the setter, I see that the set code does get executed right when I call the service function.

The serialization I'm still uncertain on. The contents of the RequestRecord[] array are not getting carried over. The Record class has setters/getters, I feel like I need a helper function somewhere to help it serialize the whole class.

Thanks all for your help!

+1  A: 

Sounds like you may be confusing server side and client side functionality. If you generated the client side classes via Visual Studio then your Set logic is not going to be carried over. You can check this by opening the ProjectFolder\Service References\SomeServiceName\References.cs file and looking at the defenition of the object VS generated for you.

If your client and server are sharing a common dll with your Set logic, then that would be really strange. I would need to see more of the code calling it.

edit: As an addendum, if you want your server and client to share the same logic on their contracted objects, the best thing to do is to set aside a separate dll with all your datacontracts and servicecontracts, then include this dll in both projects. I have also found that letting VS generate the proxy classes for you is nice and fast, but in the long run will cause major headaches.

Mike_G
You're correct. I manually performed the actions on the client side, and noticed that the setter was actually getting called - once the object was passed to the server side.I'm not sure if the DLL solution will be practical. I think I was making a bad assumption about DataContracts.
Marc Bernier
I went with autogenerated classes at first thinking it would be fine, but found that there were too many nuances that VS introduced. With a separate dll, you know exactly how the serialization will look on both sides. It also reduces the amount of maintenance required for on both sides.
Mike_G
I will add this here since I cant add it to jonathan.fenwick's post: I wrote this generic WCF client in hoping to simplify the shutting down the channnel:http://aintisaword.wordpress.com/2008/11/04/generic-client-for-wcf/
Mike_G
+2  A: 

I just ran a little test and it called the setter. How are you deserializing the objects?

class Program
{
    static void Main(string[] args)
    {
        var serializer = new DataContractSerializer(typeof(Employee));

        var employee = new Employee() { Name="Joe" };
        using (var ms = new MemoryStream())
        {
            serializer.WriteObject(ms, employee);

            ms.Flush();
            ms.Position = 0;

            var newE = serializer.ReadObject(ms) as Employee;
        }

        Console.ReadKey();

    }
}
[DataContract]
public class Employee
{
    private string _name;
    [DataMember]
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}
bendewey
+1 because your code inspired me to write some test code and post my forthcoming answer...
EnocNRoll
I had never previously bothered to test the serialization of my objects like this because generally I treat my DataContract classes like property bags that hold nothing but bits, which I believe is the intention, mostly. But, now I am incorporating this test into my object validation code.
EnocNRoll
ok, I posted the solution based on your code...
EnocNRoll
+1  A: 

The client would have to be using the same class implementation. When you add a service reference to a specific application, the implementation details behind the properties for a data contract are not added to the class when VS generates the client-side class. Compile the exposed classes into a separate class library, reference that class library in the client project, and when you're adding the service reference to your project, be sure that under the Advanced... tab, "Reuse types in referenced assemblies" is checked.

David Morton
+1  A: 

Assuming that you are creating a Service Reference to the WCF service I believe that another comment is correct in that on the client you are actually accessing a generated class that has been created as part of the proxy class generation.

There is a way to accomplish what you are trying to achieve. I would question it from an architectural perspective and it will only work with .NET clients.

  • Isolate your service interface, parameter types and return types (i.e. your DataContracts) into an assembly. I call this the Contract assembly

  • Move you implementation of the service into a separate assembly, I call this the Implementation Assembly, and reference the Contract Assembly

  • In the client create a reference to the Contract Assembly

  • In the client manually code the instantiation of the channel ... something like this (you'll have to look up the exact syntax): IMyService myService = new ChannelFactory< IMyService>().CreateChannel();

The post condition should be that the behaviour written in the DataContract class is invoked successfully on the client because the local type reference is used.

Happy coding!

+4  A: 

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:

  1. Group all data contracts by themselves (100%) into one or more assemblies, no exceptions.

  2. Group all service contracts into one or more assemblies.

  3. Group all service types, i.e., the classes that implement the service contract, into one ore more assemblies.

  4. Group all client channel proxies, i.e., the class that invokes the methods defined on the service contract interface, into one or more assemblies.

  5. 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...

  1. 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.

  2. 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.

  3. 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
EnocNRoll
Actually, all of the answers were very helpful, but this was the most complete. Thanks guys, it's working like a charm now!
Marc Bernier