Generally accepted way to avoid KnownType attribute for every derived class
Asked Answered
C

6

23

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();
    }
}
Coaler answered 25/4, 2013 at 16:38 Comment(0)
C
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;
}
Coaler answered 25/4, 2013 at 19:39 Comment(2)
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();Abydos
This is a great solution. Clean and simple.Troposphere
P
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)); 
  }
}
Paris answered 30/7, 2014 at 13:4 Comment(3)
+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.Coaler
True, but with many assemblies, performance can become a consideration. Another downfall of this approach is having to expose the public list of types.Paris
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.Alcazar
P
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.
Penthea answered 20/12, 2018 at 21:32 Comment(0)
A
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>
Autolysin answered 25/4, 2013 at 17:22 Comment(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.Coaler
As well, this will require you to update your config EACH time your assembly version will changed.Murry
M
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.

Moiety answered 25/4, 2013 at 17:40 Comment(1)
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.Coaler
C
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.

Catalepsy answered 25/11, 2016 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.