Generic Data Contracts in WCF

by Larry Spencer Friday, June 3, 2011 10:15 PM

I don’t know about you, but I’m addicted to generics.  So I was very disappointed to discover that a WCF data contract must not be an open generic.

It makes sense when you think about it.  How can you promise the world that this is what you’re going to serialize, when you haven’t even said what data type you’ll be working with.

Happily, there is at least one work-around.

The Ideal

Consider the data contract below.  It’s what I’d like to be able to do.  I want a List whose contents are guaranteed to all derive from the same type.  That’s exactly what a generic List<T> is for.

[DataContract]
public class MyDataContract<T>
{
    [DataMember]
    public List<T> MyList { get; private set; }

    public MyDataContract(params T[] list)
    {
        MyList = new List<T>(list);
    }
}

Here is a possible service contract that uses the generic data contract.

[ServiceContract]
public interface IMyServiceContract
{
    [OperationContract]
    MyDataContract<T> ReverseList<T>(MyDataContract<T> list);
}

If I attempt to use this data contract, I get the following error when I generate or update the Service Reference:

 

Type … cannot be exported as a schema type because it is an open generic type. You can only export a generic type if all its generic parameter types are actual types.

 

It’s really true: I can’t have a generic type in my data contract.

The Work-Around

How can I get the strong-typing of generics, yet abide by the constraints of a data contract?  How about this:

[DataContract]
public class MyDataContract
{
    [DataMember]
    private List<object> MyList;

    static public MyDataContract Create<T>(IEnumerable<T> list)
    {
        var dc = new MyDataContract();
        dc.MyList = new List<object>(list.Cast<object>());
        return dc;
    }

    public T[] GetList<T>()
    {
        return MyList.Cast<T>().ToArray();
    }

    public void ReverseList()
    {
        MyList.Reverse();
    }
}

Although the serialized field is a non-type-safe List<object>, there’s no way a developer can manipulate it as such. Here’s the pattern.

  1. The field itself is private.
  2. There is only one way to set the field: with the generic Create<T> method.  The type parameter, T, gets used in the IEnumerable<T> parameter.  Since Create<T> is the only way to set the field, I can now be sure that my field is sound.
  3. The static Create<T> method is used instead of a constructor because the constructor of a non-generic class cannot have a generic type parameter.
  4. There is only one way to get data out of the field: with the generic GetList<T> method. This returns a copy of the list; to emphasize that it's only a copy, an array is returned, not a List. The array is, of course, guaranteed to be uniformly of type T. If the method is called with the wrong type parameter, then Cast<T> will throw an exception.

If it were important, I could add other generic methods to add items to the list, etc.  In each, I could check the type parameter against what’s already in the array.  One way to do this is with Code Analysis and a ContractInvariant attribute, but that’s a subject for another post.

Here are the important modules in their final form.

 

Program.cs

using System;
using GenericDataContract.Types;

namespace GenericDataContract.Consumer
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press Enter when the service is ready.");
            Console.ReadLine();

            var client = new MyServiceReference.MyServiceContractClient();
            var channel = client.ChannelFactory.CreateChannel();

            var original = MyDataContract.Create(
                new string[] { "I", "am", "Sam" });

            var reversed = channel.ReverseList(original);

            Console.WriteLine("Original:");
            WriteList(original);
            Console.WriteLine();
            Console.WriteLine("Reversed:");
            WriteList(reversed);
            Console.WriteLine();

            client.Close();

            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }

        static void WriteList(MyDataContract data)
        {
            foreach (string s in data.GetList<string>())
                Console.Write("{0} ",s);
            Console.WriteLine();
        }
    }
}

IMyServiceContract.cs

using System.ServiceModel;

namespace GenericDataContract.Types
{
    [ServiceContract]
    public interface IMyServiceContract
    {
        [OperationContract]
        MyDataContract ReverseList(MyDataContract list);
    }
}

MyDataContract.cs

using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;

namespace GenericDataContract.Types
{
    [DataContract]
    public class MyDataContract
    {
        [DataMember]
        private List<object> MyList;

        static public MyDataContract Create<T>(IEnumerable<T> list)
        {
            var dc = new MyDataContract();
            dc.MyList = new List<object>(list.Cast<object>());
            return dc;
        }

        public T[] GetList<T>()
        {
            return MyList.Cast<T>().ToArray();
        }

        public void ReverseList()
        {
            MyList.Reverse();
        }
    }
}

MyServiceSvc.cs

using GenericDataContract.Types;

namespace GenericDataContract.WcfServiceApp
{
    public class Service1 : IMyServiceContract
    {
        public MyDataContract ReverseList(MyDataContract list)
        {
            list.ReverseList();
            return list;
        }
    }
}

...and the output:

Tags:

All | WCF

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.