开发者

How to define which class is taken if an interface property is deserialized within a class?

开发者 https://www.devze.com 2022-12-30 07:26 出处:网络
Just imagine you have the following class [DataContract开发者_开发问答] public class NamedList { [DataMember]

Just imagine you have the following class

[DataContract开发者_开发问答]
public class NamedList
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public IList<string> Items { get; private set; }

    public DumpList(string name)
    {
        Name = name;
        Items = new List<string>();
    }
}

If you serialize this into a file, it is quite easy, cause the concrete class behind the IList is known and can be serialized.

But what happens if you try to deserialize this file back into memory?

It works without any direct error occuring.

The problem comes if you try to add or remove something from the list. In that case you'll get an exception. And the root of this exception comes from the case that the deserialized object uses as concrete implementation for the IList an Array.

To avoid this problem in this simple example is easy. Just serialize the concrete backing store instead of the public property and make the change in the constructor:

[DataMember(Name = "Items")]
private List<string> _Items;

public IList<string> Items
{
    get
    {
        return _Items;
    }
}

public DumpList(string name)
{
    Name = name;
    _Items = new List<string>();
}

But the more interesting question is:

  • Why chooses the Deserializer the Array type as concrete implementation of the IList interface?
  • Is it possible to change the settings which class should be taken for each interface?
  • If i have a self defined interface and several implementations of this interface, is it possible to tell the Deserializer which concrete class should be taken for a given interface?


You can solve this using a DataContractSurrogate for the deserialization, that replaces IList with List.

public class CustomDataContractSurrogate : IDataContractSurrogate
{
    // The only function you should care about here. The rest don't do anything, just default behavior.
    public Type GetDataContractType(Type type)
    {
        if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(ICollection<>)))
        {
            return (typeof(List<>).MakeGenericType(type.GetGenericArguments().Single()));
        }
        return type;
    }

    public object GetObjectToSerialize(object obj, Type targetType)
    {
        return obj;
    }

    public object GetDeserializedObject(object obj, Type targetType)
    {
        return obj;
    }

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

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

    public void GetKnownCustomDataTypes(Collection<Type> customDataTypes)
    {
    }

    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
    {
        return null;
    }

    public CodeTypeDeclaration ProcessImportedType(CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit)
    {
        return typeDeclaration;
    }
}

Basically that's it, you just need to create your DataContractSerializer instance with that surrogate and use it for deserialization (for serialization it won't matter), for example:

var serializer = new DataContractSerializer(type, new Type[]{}, Int32.MaxValue, false, true, new CustomDataContractSurrogate());

Or any of the other constructors that take a surrogate.

Or, (as a bonus to the answer) if you're working with app/web.config-defined services, you can define a custom behavior that creates a data contract serializer with the above surrogate:

public class CustomDataContractSerializerBehavior : DataContractSerializerOperationBehavior
{
    public CustomDataContractSerializerBehavior(OperationDescription operation)
        : base(operation)
    {
    }

    public CustomDataContractSerializerBehavior(OperationDescription operation, DataContractFormatAttribute dataContractFormatAttribute)
        : base(operation, dataContractFormatAttribute)
    {
    }

    public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns,
        IList<Type> knownTypes)
    {
        return new DataContractSerializer(type, knownTypes, Int32.MaxValue, false, true, new CustomDataContractSurrogate());
    }

    public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name,
        XmlDictionaryString ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(type, knownTypes, Int32.MaxValue, false, true, new CustomDataContractSurrogate());
    }

}

Finally you can use this behavior:

public static IMyDataServiceContract CreateService()
{
    var factory = new ChannelFactory<IMyDataServiceContract>("MyServiceName");
    SetDataContractSerializerBehavior(factory.Endpoint.Contract);
    return factory.CreateChannel();
}

private static void SetDataContractSerializerBehavior(ContractDescription contractDescription)
{
    foreach (OperationDescription operation in contractDescription.Operations)
    {
        ReplaceDataContractSerializerOperationBehavior(operation);
    }
}

private static void ReplaceDataContractSerializerOperationBehavior(OperationDescription description)
{
    DataContractSerializerOperationBehavior dcsOperationBehavior =
    description.Behaviors.Find<DataContractSerializerOperationBehavior>();

    if (dcsOperationBehavior != null)
    {
        description.Behaviors.Remove(dcsOperationBehavior);
        description.Behaviors.Add(new CustomDataContractSerializerBehavior(description));
    }
}

To finish the job, call the above CreateService somewhere to create the channel.


If you use the NetDataContractSerializer, which stores type information along with the serialized object, your problem should be solved. However, it does at the same time reduce interoperability to non-.NET clients.

0

精彩评论

暂无评论...
验证码 换一张
取 消