0
votes

Below is a very short UWP unit test that attempts to serialize and then deserialize a class called Car using a DataContractSerializer. I plan to use this type of code in a UWP app to save session state when the app is suspended. Because I don't want to have to add every type to the KnownTypes collection, I stole a simple custom DataContractResolver from an msdn blog; it is supposed to work when serialization and deserialization happens within the same app (and thus share types and assemblies). It all works perfectly when the code is running with the full .NET Framework 4.6.2. But the WEIRD THING is that the exact same code fails it is part of a Universal Windows Project UNLESS I also turn on "Compile with .NET Native tool chain".

Why won't the exact same code work on a UWP app WITHOUT using the .NET Native tool chain? .NET Native is supposed to cause complications for serialization, so it seems very strange that my code only works on UWP when .NET Native is used. How do I get it to work on a UWP app WITHOUT using .NET Native - compilation slows dramatically in my DEBUG builds when it is turned on.

Here is a GitHub link to a full solution with both unit tests. https://github.com/jmagaram/CustomResolver

Here is the unit test code:

using System;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using System.Runtime.Serialization;
using System.Xml;
using System.Reflection;
using System.Collections.Generic;
using System.IO;

namespace ResolverTest {
    [TestClass]
    public class SerializerTestUniversal {
        [TestMethod]
        public void CanRoundtripComplexTypeWithNoKnownTypesAndCustomResolver() {
            // prepare object for serialization
            var car = new Car { Year = 2000, Model = "Ford" };
            var rootToSerialize = new Dictionary<string, object> { ["car"] = car };

            // serialize with DataContractSerializer and NO known types
            // hopefully the custom DataContractResolver will make it work
            var serializer = new DataContractSerializer(
                typeof(Dictionary<string, object>),
                new DataContractSerializerSettings { DataContractResolver = new SharedTypedResolver() });
            var memoryStream = new MemoryStream();
            serializer.WriteObject(memoryStream, rootToSerialize);

            // deserialize
            memoryStream.Position = 0;
            var output = (Dictionary<string, object>)(serializer.ReadObject(memoryStream));
            var outputCar = (Car)output["car"];

            // check that the data got roundtripped correctly
            Assert.AreEqual(car.Year, outputCar.Year);
            Assert.AreEqual(car.Model, outputCar.Model);
        }

        public class Car {
            public int Year { get; set; }
            public string Model { get; set; }
        }

        // To be used when serializing and deserializing on same machine with types defined in a shared assembly
        // Intended to used for suspend/resume serialization in UWP apps
        // Code from https://blogs.msdn.microsoft.com/youssefm/2009/06/05/configuring-known-types-dynamically-introducing-the-datacontractresolver/
        public class SharedTypedResolver : DataContractResolver {
            public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) {
                return knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null) ?? Type.GetType($"{typeName}, {typeNamespace}");
            }

            public override bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) {
                if (!knownTypeResolver.TryResolveType(dataContractType, declaredType, null, out typeName, out typeNamespace)) {
                    XmlDictionary dictionary = new XmlDictionary();
                    typeName = dictionary.Add(dataContractType.FullName);
                    typeNamespace = dictionary.Add(dataContractType.GetTypeInfo().Assembly.FullName);
                }
                return true;
            }
        }
    }
}

Here is the full rd.xml file contents required on UWP to make it work when .NET Native is turned on.

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>
    <Assembly Name="*Application*" Dynamic="Required All" />
    <Type Name="ResolverTest.SerializerTestUniversal.Car" Browse="Required Public" DataContractSerializer="Required All"/>
  </Application>
</Directives>

And finally, this is the exception that occurs when .NET Native is turned off:

Result Message: Test method ResolverTest.SerializerTestUniversal.CanRoundtripComplexTypeWithNoKnownTypesAndCustomResolver threw exception: 
System.Runtime.Serialization.SerializationException: Type 'ResolverTest.SerializerTestUniversal+Car' with data contract name 'SerializerTestUniversal.Car:http://schemas.datacontract.org/2004/07/ResolverTest' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.

== UPDATED ==

I was able to get it to work with a different DataContractResolver. See the code below. I also changed the test to use a new instance of the DataContractSerializer for deserialization, since the new resolver builds up state/information during serialization. The comments explain how it appears that the DataContractResolver is used differently by UWP and .NET 4.6.2. I still do not know why the original code failed unless .NET Native was turned on.

public class SharedTypeResolver : DataContractResolver {
    Type _mostRecentResolvedType = null;

    // When an object is serialized using the Universal Windows Platform (as of version
    // 5.2.2), the ResolveName method is called for each type it encounters immediately after
    // calling TryResolveType. The Microsoft API specification says the ResolveName method is
    // used to 'map the specified xsi:type name and namespace to a data contract type during
    // deserialization', so it is a bit surprising this method is called during
    // serialization. If ResolveName does not return a valid type during serialization,
    // serialization fails. This behavior (and the failure) seems to be unique to the UWP.
    // ResolveName is not called during serialization on the .Net Framework 4.6.2.
    //
    // During serialization it is difficult to force ResolveName to return a valid type
    // because the typeName and typeNamespace do not include the assembly, and
    // Type.GetType(string) can only find a type if it is in the currently executing assembly
    // or it if has an assembly-qualified name. Another challenge is that the typeName and
    // typeNamespace parameters are formatted differently than Type.FullName, so string
    // parsing is necessary. For example, the typeNamespace parameter looks like
    // http://schemas.datacontract.org/2004/07/namespace and the typeName parameter is
    // formatted as className+nestedClassName. Type.FullName returns a single string like
    // namespace.class+nestedClass. But even worse, generic types show up in ResolveName
    // during serialization with names like 'StackOfint'. So the HACK approach I've taken
    // here is to cache the last Type seen in the TryResolveType method. Whenever a
    // typeNamespace appears in ResolveName that does not look like a real assembly name,
    // return the cached type.
    //
    // During deserialization it is very easy for this method to generate a valid type name because the XML
    // file that was generated contains the full assembly qualified name.
    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) {
        if (typeNamespace.StartsWith("http://schemas.datacontract.org")) {
            // Should only happen on UWP when serializing, since ResolveName is called
            // immediately after TryResolveType.
            return _mostRecentResolvedType;
        }
        else {
            // Should happen when deserializing and should work with all types serialized
            // with thie resolver.
            string assemblyQualifiedTypeName = $"{typeName}, {typeNamespace}";
            return Type.GetType(assemblyQualifiedTypeName);
        }
    }

    public override bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) {
        _mostRecentResolvedType = dataContractType;
        XmlDictionary dictionary = new XmlDictionary();
        typeName = dictionary.Add(dataContractType.FullName);
        typeNamespace = dictionary.Add(dataContractType.GetTypeInfo().Assembly.FullName);
        return true;
    }
}
1

1 Answers

0
votes

This was due to a bug in .NETCore 5.2.2. I think it is fixed in 5.2.3. An engineer on the team helped me with this. It seemed to work when I downloaded the beta version of the assemblies.

https://github.com/dotnet/corefx/issues/10155