22

Is there a generally accepted way to avoid having to use KnownType attributes on WCF services? I've been doing some research, and it looks like there are two options:

  1. Data contract resolver
  2. NetDataContractSerializer

I'm not a big fan of having to statically add KnownType attributes every time I add a new type, hence wanting to avoid it.

Is there a third option that should be used? If so, what is it? If not, which of the above two options are the right way to go?

Edit - use a method

A third option would be to use reflection

[DataContract]
[KnownType("DerivedTypes")]
public abstract class FooBase
{
    private static Type[] DerivedTypes()
    {
        return typeof(FooBase).GetDerivedTypes(Assembly.GetExecutingAssembly()).ToArray();
    }
}
Community
  • 1
  • 1
Bob Horn
  • 33,387
  • 34
  • 113
  • 219

6 Answers6

30

I wanted to post what seems to be the simplest, most elegant solution that I can think of so far. If another answer comes along that's better, I'll go with that. But for now, this worked well.

The base class, with only one KnownType attribute, pointing to a method called DerivedTypes():

[KnownType("DerivedTypes")]
[DataContract]
public abstract class TaskBase : EntityBase
{
    // other class members here

    private static Type[] DerivedTypes()
    {
        return typeof(TaskBase).GetDerivedTypes(Assembly.GetExecutingAssembly()).ToArray();
    }
}

The GetDerivedTypes() method, in a separate ReflectionUtility class:

public static IEnumerable<Type> GetDerivedTypes(this Type baseType, Assembly assembly)
{
    var types = from t in assembly.GetTypes()
                where t.IsSubclassOf(baseType)
                select t;

    return types;
}
Bob Horn
  • 33,387
  • 34
  • 113
  • 219
  • 5
    If you'd rather not create the extension method, this can be turned into a one-liner. `return Assembly.GetExecutingAssembly().GetTypes().Where(_ => _.IsSubclassOf(typeof(TaskBase))).ToArray();` – x5657 Mar 16 '17 at 08:10
  • This is a great solution. Clean and simple. – kenjara Aug 03 '17 at 10:06
8

The method mentioned by Bob will work as long as all involved classes are in the same assembly.

The following method will work across assemblies:

[DataContract]
[KnownType("GetDerivedTypes")]
public class BaseClass
{
  public static List<Type> DerivedTypes = new List<Type>();

  private static IEnumerable<Type> GetDerivedTypes()
  {
    return DerivedTypes;
  }
}


[DataContract]
public class DerivedClass : BaseClass
{
  //static constructor
  static DerivedClass()
  {
    BaseClass.DerivedTypes.Add(typeof(DerivedClass)); 
  }
}
Leon van der Walt
  • 997
  • 12
  • 18
  • +1 for a nice alternative. Note that the reflection option can be made to work across assemblies as well. I like this because it's clean. The only negative I can think of is that the derived classes must remember to implement this static constructor. With reflection, nothing is left to the memory of the developer. – Bob Horn Jul 30 '14 at 13:52
  • True, but with many assemblies, performance can become a consideration. Another downfall of this approach is having to expose the public list of types. – Leon van der Walt Jul 30 '14 at 14:32
  • 2
    You also need to ensure that there's something to invoke the static constructor: the CLR runtime, I believe, does not run every static constructor in an assembly as it loads it, only the first time the class is accessed or used in some way. – Quantumplation Jul 21 '15 at 23:38
2

Here's my variant on the accepted answer:

    private static IEnumerable<Type> GetKnownTypes() {
        Type baseType = typeof(MyBaseType);
        return AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(x => x.DefinedTypes)
            .Where(x => x.IsClass && !x.IsAbstract && x.GetCustomAttribute<DataContractAttribute>() != null && baseType.IsAssignableFrom(x));
    }

The differences are:

  1. Looks at all loaded assemblies.
  2. Checks some bits we are interested in (DataContract I think is required if you're using DataContractJsonSerializer) such as being a concrete class.
  3. You can use isSubclassOf here, I tend to prefer IsAssignableFrom in general to catch all overridden variants. In particular I think it works with generics.
  4. Take advantage of KnownTypes accepting an IEnumerable (if it matters in this case, probably not) instead of converting to an array.
user169771
  • 1,962
  • 2
  • 13
  • 11
1

If you don't like attributes everywhere then you can use configuration file.

<system.runtime.serialization>
   <dataContractSerializer>
      <declaredTypes>
         <add type = "Contact,Host,Version=1.0.0.0,Culture=neutral,
                                                              PublicKeyToken=null">
            <knownType type = "Customer,MyClassLibrary,Version=1.0.0.0,
                                             Culture=neutral,PublicKeyToken=null"/>
         </add>
      </declaredTypes>
   </dataContractSerializer>
</system.runtime.serialization>
user247702
  • 23,641
  • 15
  • 110
  • 157
Tim Phan
  • 311
  • 1
  • 11
  • 2
    Thanks for the tip, Tim. However my main goal is to not have to update a list somewhere every time there is a new type. Good to know. – Bob Horn Apr 25 '13 at 17:26
  • As well, this will require you to update your config EACH time your assembly version will changed. – evgenyl Apr 26 '13 at 04:30
1

You can implement IXmlSerializable in your custom types and handle its complexity manually. Following you can find a sample code:

[XmlRoot("ComplexTypeA")]
public class ComplexTypeA : IXmlSerializable
{
    public int Value { get; set; }

    public void WriteXml (XmlWriter writer)
    {
        writer.WriteAttributeString("Type", this.GetType().FullName);
        writer.WriteValue(this.Value.ToString());
    }

    public void ReadXml (XmlReader reader)
    {
        reader.MoveToContent();
        if (reader.HasAttributes) {
            if (reader.GetAttribute("Type") == this.GetType().FullName) {
                this.Value = int.Parse(reader.ReadString());
            }
        }
    }

    public XmlSchema GetSchema()
    {
        return(null);
    }
}

[XmlRoot("ComplexTypeB")]
public class ComplexTypeB : IXmlSerializable
{
    public string Value { get; set; }

    public void WriteXml (XmlWriter writer)
    {
        writer.WriteAttributeString("Type", this.GetType().FullName);
        writer.WriteValue(this.Value);
    }

    public void ReadXml (XmlReader reader)
    {
        reader.MoveToContent();
        if (reader.HasAttributes) {
            if (reader.GetAttribute("Type") == this.GetType().FullName) {
                this.Value = reader.ReadString();
            }
        }
    }

    public XmlSchema GetSchema()
    {
        return(null);
    }
}


[XmlRoot("ComplexTypeC")]
public class ComplexTypeC : IXmlSerializable
{
    public Object ComplexObj { get; set; }

    public void WriteXml (XmlWriter writer)
    {
        writer.WriteAttributeString("Type", this.GetType().FullName);
        if (this.ComplexObj != null)
        {
            writer.WriteAttributeString("IsNull", "False");
            writer.WriteAttributeString("SubType", this.ComplexObj.GetType().FullName);
            if (this.ComplexObj is ComplexTypeA)
            {
                writer.WriteAttributeString("HasValue", "True");
                XmlSerializer serializer = new XmlSerializer(typeof(ComplexTypeA));
                serializer.Serialize(writer, this.ComplexObj as ComplexTypeA);
            }
            else if (tthis.ComplexObj is ComplexTypeB)
            {
                writer.WriteAttributeString("HasValue", "True");
                XmlSerializer serializer = new XmlSerializer(typeof(ComplexTypeB));
                serializer.Serialize(writer, this.ComplexObj as ComplexTypeB);
            }
            else
            {
                writer.WriteAttributeString("HasValue", "False");
            }
        }
        else
        {
            writer.WriteAttributeString("IsNull", "True");
        }
    }

    public void ReadXml (XmlReader reader)
    {
        reader.MoveToContent();
        if (reader.HasAttributes) {
            if (reader.GetAttribute("Type") == this.GetType().FullName) {
                if ((reader.GetAttribute("IsNull") == "False") && (reader.GetAttribute("HasValue") == "True")) {
                    if (reader.GetAttribute("SubType") == typeof(ComplexTypeA).FullName)
                    {
                        XmlSerializer serializer = new XmlSerializer(typeof(ComplexTypeA));
                        this.ComplexObj = serializer.Deserialize(reader) as ComplexTypeA;
                    }
                    else if (reader.GetAttribute("SubType") == typeof(ComplexTypeB).FullName)
                    {
                        XmlSerializer serializer = new XmlSerializer(typeof(ComplexTypeB));
                        this.ComplexObj = serializer.Deserialize(reader) as ComplexTypeB;
                    }
                }
            }
        }
    }

    public XmlSchema GetSchema()
    {
        return(null);
    }
}

Hope it helps.

Farzan
  • 745
  • 10
  • 25
  • Thanks, @Farzan, but this seems to be more than I was looking for, as far as maintenance. My third option, in my question, allows me to implement one call, on just the base class, and save a lot of coding. Your approach is nice to see, but it's simply more code to maintain. – Bob Horn Apr 25 '13 at 19:16
0

I'd rather extract my custom types all at once and use it during serialization/deserialization. After reading this post, it took me a while to understand where to inject this list of types to be useful for serializer object. The answer was quite easy: this list is to be used as one of the input arguments of constructor of serializer object.

1- I'm using two static generic methods for serialization and deserialization, this may be more or less the way others also do the job, or at least it is very clear for making comparison with your code:

    public static byte[] Serialize<T>(T obj)
    {
        var serializer = new DataContractSerializer(typeof(T), MyGlobalObject.ResolveKnownTypes());
        var stream = new MemoryStream();
        using (var writer =
            XmlDictionaryWriter.CreateBinaryWriter(stream))
        {
            serializer.WriteObject(writer, obj);
        }
        return stream.ToArray();
    }
    public static T Deserialize<T>(byte[] data)
    {
        var serializer = new DataContractSerializer(typeof(T), MyGlobalObject.ResolveKnownTypes());
        using (var stream = new MemoryStream(data))
        using (var reader =
            XmlDictionaryReader.CreateBinaryReader(
                stream, XmlDictionaryReaderQuotas.Max))
        {
            return (T)serializer.ReadObject(reader);
        }
    }

2- Please pay attention to constructor of DataContractSerializer. We have a second argument there, which is the entry point for injecting your known types to serializer object.

3- I'm using a static method for extracting all of my own defined types from my own assemblies. your code for this static method may look like this:

    private static Type[] KnownTypes { get; set; }
    public static Type[] ResolveKnownTypes()
    {
        if (MyGlobalObject.KnownTypes == null)
        {
            List<Type> t = new List<Type>();
            List<AssemblyName> c = System.Reflection.Assembly.GetEntryAssembly().GetReferencedAssemblies().Where(b => b.Name == "DeveloperCode" | b.Name == "Library").ToList();
            foreach (AssemblyName n in c)
            {
                System.Reflection.Assembly a = System.Reflection.Assembly.Load(n);
                t.AddRange(a.GetTypes().ToList());
            }
            MyGlobalObject.KnownTypes = t.ToArray();
        }
        return IOChannel.KnownTypes;
    }

Since I was not involved in WCF (I only needed a binary serialization for file operation), my solution may not exactly address the WCF architecture, but there must be access to constructor of serializer object from somewhere.