Using custom DataContractResolver with multiple as

2019-08-09 12:02发布

问题:

I have following setup in a MEF application:

Assembly MyBaseAssembly:

namespace My.Namespace
{
    [DataContract]
    public class Container
    {
        [DataMember]
        public Data Item { get; set; }
    }

    [DataContract]
    public class Data
    {
        [DataMember]
        public string Foo { get; set; }
    }
}

Assembly SecondAssembly, references the MyBaseAssembly:

namespace My.Another.Namespace
{
    [DataContract]
    public class SecondData : Data
    {
        [DataMember]
        public string Bar { get; set; }
    }
}

Somewhere deep inside of my application I create a Container object:

Container container = new Container();
container.Item = new SecondData { Bar = "test" };

I want to serialize and de-serialize the container object. Since the SecondAssembly is a MEF-module, I need to dynamically detect and resolve the types in the data contract, so the KnownTypeAttribute is not a good solution.

I created a custom DataContractResolver, but I don't know how do I get the assembly information for de-serialization.

On serialization, I get following XML:

<d4p1:SecondData
    xmlns:d6p1="http://schemas.datacontract.org/2004/07/My.Another.Namespace"
    i:type="d7p1:My.Another.Namespace.SecondData">
...
</d4p1:SecondData>

This is the default DataContract serialization behavior: we get the type name and the type namespace, but there is no (explicit) assembly information!

Trying to de-serialize this XML, I cannot determine which assembly to use for resolving the type:

class SerializationTypeResolver : DataContractResolver
{
    ...

    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
    {
        Type result = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
        if (result == null)
        {
            // Here, I cannot rely on the declaredType parameter,
            // because it contains the declared type which is Data from MyBaseAssembly.
            // But I need the SecondData from the SecondAssembly!

            string assemblyName = ???; // How do I get this assembly name?
            string fullTypeName = typeName + ", " + assemblyName;
            result = Type.GetType(fullTypeName);
        }

        return result;
    }
}

So my question is: what is the good way to store and get assembly name while serializing and de-serializing the DataContracts?

回答1:

Why not use AssemblyQualifiedName already when serializing? Like this:

internal class SerializationTypeResolver : DataContractResolver {
    public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) {
        // not necessary to hardcode some type name of course, you can use some broader condition
        // like if type belongs to another assembly
        if (type.Name == "SecondData") {
            XmlDictionary dictionary = new XmlDictionary();
            // use assembly qualified name
            typeName = dictionary.Add(type.AssemblyQualifiedName);
            typeNamespace = dictionary.Add("http://tempuri.org"); // some namespace, does not really matter in this case
            return true;
        }
        return knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace);
    }

    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) {
        if (typeNamespace == "http://tempuri.org") {
            return Type.GetType(typeName); // assembly qualified already
        }
        return knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
    }
}


回答2:

You're going to need to go through all the referenced assemblies of the executing assembly (whether loaded or not) and look for types that are assignable from declaredType. The answer C# Reflection: Get all active assemblies in a solution? gives a starting point.

class SerializationTypeResolver : DataContractResolver
{
    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
    {
        Type result = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
        if (result == null)
        {
            foreach (var derivedType in declaredType.DerivedTypes())
            {
                XmlDictionaryString derivedTypeName;
                XmlDictionaryString derivedTypeNamespace;
                // Figure out if this derived type has the same data contract name and namespace as the incoming name & namespace.
                if (knownTypeResolver.TryResolveType(derivedType, derivedType, null, out derivedTypeName, out derivedTypeNamespace))
                {
                    if (derivedTypeName.Value == typeName && derivedTypeNamespace.Value == typeNamespace)
                    {
                        return derivedType;
                    }
                }
            }
        }

        return result;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> DerivedTypes(this Type baseType)
    {
        // TODO: Optimization: check if baseType is private or internal.
        var assemblies = baseType.Assembly.GetReferencingAssembliesAndSelf();
        Debug.Assert(assemblies.Count() == assemblies.Distinct().Count());
        return assemblies
            .SelectMany(a => a.GetTypes())
            .Where(t => baseType.IsAssignableFrom(t));
    }

    // Not sure which of the two versions of this method give better performance -- you might want to test yourself.

    public static IEnumerable<Type> DerivedTypesFromAllAssemblies(this Type baseType)
    {
        // TODO: Optimization: check if baseType is private or internal.
        var assemblies = AssemblyExtensions.GetAllAssemblies();
        Debug.Assert(assemblies.Count() == assemblies.Distinct().Count());
        return assemblies
            .SelectMany(a => a.GetTypes())
            .Where(t => baseType.IsAssignableFrom(t));
    }
}

public static class AssemblyExtensions
{
    public static IEnumerable<Assembly> GetAllAssemblies()
    {
        // Adapted from 
        // https://stackoverflow.com/questions/851248/c-sharp-reflection-get-all-active-assemblies-in-a-solution
        return Assembly.GetEntryAssembly().GetAllReferencedAssemblies();
    }

    public static IEnumerable<Assembly> GetAllReferencedAssemblies(this Assembly root)
    {
        // WARNING: Assembly.GetAllReferencedAssemblies() will optimize away any reference if there
        // is not an explicit use of a type in that assembly from the referring assembly --
        // And simply adding an attribute like [XmlInclude(typeof(T))] seems not to do
        // the trick.  See
        // https://social.msdn.microsoft.com/Forums/vstudio/en-US/17f89058-5780-48c5-a43a-dbb4edab43ed/getreferencedassemblies-not-returning-complete-list?forum=netfxbcl
        // Thus if you are using this to, say, discover all derived types of a base type, the assembly
        // of the derived types MUST contain at least one type that is referenced explicitly from the 
        // root assembly, directly or indirectly.

        var list = new HashSet<string>();
        var stack = new Stack<Assembly>();

        stack.Push(root);

        do
        {
            var asm = stack.Pop();

            yield return asm;

            foreach (var reference in asm.GetReferencedAssemblies())
                if (!list.Contains(reference.FullName))
                {
                    stack.Push(Assembly.Load(reference));
                    list.Add(reference.FullName);
                }

        }
        while (stack.Count > 0);
    }

    public static IEnumerable<Assembly> GetReferencingAssemblies(this Assembly target)
    {
        if (target == null)
            throw new ArgumentNullException();
        // Assemblies can have circular references:
        // https://stackoverflow.com/questions/1316518/how-did-microsoft-create-assemblies-that-have-circular-references
        // So a naive algorithm isn't going to work.

        var done = new HashSet<Assembly>();

        var root = Assembly.GetEntryAssembly();
        var allAssemblies = root.GetAllReferencedAssemblies().ToList();

        foreach (var assembly in GetAllAssemblies())
        {
            if (target == assembly)
                continue;
            if (done.Contains(assembly))
                continue;
            var refersTo = (assembly == root ? allAssemblies : assembly.GetAllReferencedAssemblies()).Contains(target);
            done.Add(assembly);
            if (refersTo)
                yield return assembly;
        }
    }

    public static IEnumerable<Assembly> GetReferencingAssembliesAndSelf(this Assembly target)
    {
        return new[] { target }.Concat(target.GetReferencingAssemblies());
    }
}

Incidentally, instead of a contract resolver, you could use the DataContractSerializer(Type, IEnumerable<Type>) constructor.

Honestly, the performance is not so good since the code loads all assemblies referenced by the root assembly, including Microsoft DLLs and 3rd party DLLs. You might want to develop some way to cut down on the number of assemblies to load by checking the name before loading, for instance by skipping Microsoft assemblies if the base class comes from your own codebase.



回答3:

Hundreds of years ago, I had a similar situation - it wasn't MEF but a MEF-like architecture we hand-rolled. (MEF had a bad rep in them days.) We had real well-centralized governance over the creation of our data contract serializers, and so it was easy to insert a surrogate provider.

It's not a contract resolver per se - but ends up working in a similar way and is inserted into the serialization pipeline at the same place and time as a resolver.

I'm outlining this from memory, which is highly fallible all these years later, but it went something like this. One detail I don't remember is whether the AssemblyAwareSurrogate serialized a byte array or a string. Could go either way, I suppose.

public class AssembyAwareSurrogateProvider: IDataContractSurrogate
{

  [DataContract]
  class AssemblyAwareSurrogate
  {
    [DataMember]
    public string AssemblyName { get; set; }
    [DataMember]
    public string TypeName { get; set; }

    [DataMember]
    public byte[ ] Object { get; set; }

    public AssemblyAwareSurrogate( object obj )
    {
      this.AssemblyName = obj.GetType( ).Assembly.FullName;
      this.TypeName = obj.GetType( ).FullName;

      var serializer = new DataContractSerializer( obj.GetType( ) );
      using ( var stream = new MemoryStream( ) )
      {
        serializer.WriteObject( stream, obj );
        stream.Flush( );
        Object = stream.ToArray( );
      }
    }
  }

  public Type GetDataContractType( Type type )
  {
    if ( SatisifesConstraints( type ) ) return typeof( AssemblyAwareSurrogate );
    return type;
  }
  private bool SatisifesConstraints( Type type )
  {
    //--> er - whatever types you're insterested in...
    return type != typeof( AssemblyAwareSurrogate );
  }

  public object GetDeserializedObject( object obj, Type targetType )
  {
    var surrogate = obj as AssemblyAwareSurrogate;
    if ( surrogate != null )
    {
      var assy = Assembly.Load( new AssemblyName( surrogate.AssemblyName ) );
      var serializer = new DataContractSerializer( assy.GetType( surrogate.TypeName ) );
      using ( var stream = new MemoryStream( surrogate.Object ) )
      {
        return serializer.ReadObject( stream );
      }
    }
    return obj;
  }

  public object GetObjectToSerialize( object obj, Type targetType )
  {
    if ( SatisifesConstraints( obj.GetType( ) ) )
    {
      return new AssemblyAwareSurrogate( obj );
    }
    return obj;
  }

  public object GetCustomDataToExport( Type clrType, Type dataContractType )
  {
    return null;
  }

  public object GetCustomDataToExport( MemberInfo memberInfo, Type dataContractType )
  {
    return null;
  }

  public void GetKnownCustomDataTypes( Collection<Type> customDataTypes )
  {
    throw new NotImplementedException( );
  }


  public Type GetReferencedTypeOnImport( string typeName, string typeNamespace, object customData )
  {
    throw new NotImplementedException( );
  }

  public CodeTypeDeclaration ProcessImportedType( CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit )
  {
    throw new NotImplementedException( );
  }
}