19

I'm currently having a really weird issue and I can't seem to figure out how to resolve it.

I've got a fairly complex type which I'm trying to serialize using the XmlSerializer class. This actually functions fine and the type serializes properly, but seems to take a very long time in doing so; around 5 seconds depending on the data in the object.

After a bit of profiling I've narrowed the issue down - bizarrely - to specifying an XmlRootAttribute when calling XmlSerializer.Serialize. I do this to change the name of a collection being serialized from ArrayOf to something a bit more meaningful. Once I remove the parameter the operation is almost instant!

Any thoughts or suggestions would be excellent as I'm entirely stumped on this one!

John Saunders
  • 160,644
  • 26
  • 247
  • 397
Dougc
  • 843
  • 11
  • 20
  • 1
    Okay, looks like the issue is that the serialization assembly is generated for every serializer instance if you specify anything other than a type parameter to the serializer! Which is why - I assume - I'm seeing such terrible performance. Does anyone know any reason why the default XmlSerializer would do this? I don't understand why just specifying the root node name would mean the cache couldn't be used? – Dougc Oct 09 '09 at 17:32

4 Answers4

27

Just for anyone else who runs into this problem; armed with the answer above and the example from MSDN I managed to resolve this issue using the following class:

public static class XmlSerializerCache
{
    private static readonly Dictionary<string, XmlSerializer> cache =
                            new Dictionary<string, XmlSerializer>();

    public static XmlSerializer Create(Type type, XmlRootAttribute root)
    {
        var key = String.Format(
                  CultureInfo.InvariantCulture,
                  "{0}:{1}",
                  type,
                  root.ElementName);

        if (!cache.ContainsKey(key))
        {
            cache.Add(key, new XmlSerializer(type, root));
        }

        return cache[key];
    }
}

Then instead of using the default XmlSerializer constructor which takes an XmlRootAttribute, I use the following instead:

var xmlRootAttribute = new XmlRootAttribute("ExampleElement");
var serializer = XmlSerializerCache.Create(target.GetType(), xmlRootAttribute);

My application is now performing again!

Dougc
  • 843
  • 11
  • 20
  • 6
    An ever-so-slight slight optimization on this. TryGet the value in the if-clause, if you are going to do a lot of these lookups. ContainsKey is more efficient if you only want to know if the item exists, but not get it, but since you always return the value, tryget is better: http://dotnetperls.com/dictionary-lookup – Alex K Mar 21 '10 at 15:53
  • Then you can also improve "return" statement, since you already have the instance in the variable and don't need to lookup via "cache[key]" – Sielu Sep 26 '16 at 07:19
19

As mentioned in the follow-up comment to the original question, .NET emits assemblies when creating XmlSerializers, and caches the generated assembly if it is created using one of these two constructors:

XmlSerializer(Type)
XmlSerializer(Type, String)

Assemblies generated using the other constructors are not cached, so .NET has to generate new assemblies every time.

Why? This answer probably isn't very satisfying, but peering at this in Reflector, you can see that the key used to store and access the generated XmlSerializer assemblies (TempAssemblyCacheKey) is just a simple composite key built from the serializable type and (optionally) its namespace.

Thus, there's no mechanism to tell whether a cached XmlSerializer for SomeType has a special XmlRootAttribute or the default one.

It's hard to think of a technical reason that the key couldn't accommodate more elements, so this is probably just a feature that no one had time to implement (especially since it would involve changing otherwise stable classes).

You may have seen this, but in case you haven't, the XmlSerializer class documentation discusses a workaround:

If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded, which results in a memory leak and poor performance. The easiest solution is to use one of the previously mentioned two constructors. Otherwise, you must cache the assemblies in a Hashtable,as shown in the following example.

(I've omitted the example here)

Jeff Sternal
  • 47,787
  • 8
  • 93
  • 120
  • Excellent. Thanks for your help Jeff. I have no idea why I didn't read the MSDN documentation, doh! – Dougc Oct 11 '09 at 10:58
  • Is there any examples of this HashTable being used where you dont have control over the Xml Serializer? I'm thinking specifically in a WCF Service Reference or Web Reference? I have this issue, but I cant seem to find any avenues to fix it...! – Mark H Apr 08 '15 at 08:58
2

Just had to implement something like this and used a slightly more optimized version of @Dougc's solution with a convenience overload:

public static class XmlSerializerCache {
    private static readonly Dictionary<string, XmlSerializer> cache = new Dictionary<string, XmlSerializer>();

    public static XmlSerializer Get(Type type, XmlRootAttribute root) {
        var key = String.Format("{0}:{1}", type, root.ElementName);
        XmlSerializer ser;
        if (!cache.TryGetValue(key, out ser)) {
            ser = new XmlSerializer(type, root);
            cache.Add(key, ser);
        }
        return ser;
    }

    public static XmlSerializer Get(Type type, string root) {
        return Get(type, new XmlRootAttribute(root));
    }
}
saarrrr
  • 2,754
  • 1
  • 16
  • 26
1

There is a more complex implementation explained here. However the project is no longer active.

The relevant classes are visible here: http://mvpxml.codeplex.com/SourceControl/changeset/view/64156#258382

In particular, the following function to generate a unique key can be useful:

public static string MakeKey(Type type
    , XmlAttributeOverrides overrides
    , Type[] types
    , XmlRootAttribute root
    , String defaultNamespace) {
    StringBuilder keyBuilder = new StringBuilder();
    keyBuilder.Append(type.FullName);
    keyBuilder.Append("??");
    keyBuilder.Append(SignatureExtractor.GetOverridesSignature(overrides));
    keyBuilder.Append("??");
    keyBuilder.Append(SignatureExtractor.GetTypeArraySignature(types));
    keyBuilder.Append("??");
    keyBuilder.Append(SignatureExtractor.GetXmlRootSignature(root));
    keyBuilder.Append("??");
    keyBuilder.Append(SignatureExtractor.GetDefaultNamespaceSignature(defaultNamespace));

    return keyBuilder.ToString();
}
Erwin Mayer
  • 18,076
  • 9
  • 88
  • 126