Как определить, какой класс берется, если свойство интерфейса десериализовано внутри класса? - PullRequest
4 голосов
/ 17 мая 2010

Только представьте, что у вас есть следующий класс

[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>();
    }
}

Если вы сериализуете это в файл, это довольно просто, потому что конкретный класс за IList известен и может быть сериализован.

Но что произойдет, если вы попытаетесь десериализовать этот файл обратно в память?
Он работает без каких-либо прямых ошибок.

Проблема возникает, если вы пытаетесь добавить или удалить что-то из списка. В этом случае вы получите исключение. И корень этого исключения происходит от случая, когда десериализованный объект использует в качестве конкретной реализации для IList a Array.

Избежать этой проблемы в этом простом примере просто. Просто сериализуйте конкретное хранилище резервных копий вместо открытого свойства и внесите изменения в конструктор:

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

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

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

Но более интересный вопрос:

  • Почему в качестве конкретной реализации интерфейса IList выбирается десериализатор типа Array?
  • Можно ли изменить настройки, какой класс следует выбрать для каждого интерфейса?
  • Если у меня есть собственный интерфейс и несколько реализаций этого интерфейса, можно ли сообщить десериализатору, какой конкретный класс следует выбрать для данного интерфейса?

Ответы [ 2 ]

3 голосов
/ 04 апреля 2011

Вы можете решить эту проблему , используя DataContractSurrogate для десериализации, который заменяет IList на 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;
    }
}

По сути, все, вам просто нужно создать экземпляр DataContractSerializer с этим суррогатом и использовать его для десериализации (для сериализации это не имеет значения), например:

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

Или любой другой конструктор, который принимает суррогат.

Или (в качестве бонуса к ответу), если вы работаете с сервисами, определенными в app / web.config, вы можете определить настраиваемое поведение, которое создает сериализатор контрактов данных с указанным выше суррогатом :

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());
    }

}

Наконец вы можете использовать это поведение:

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));
    }
}

Чтобы завершить работу , вызовите вышеупомянутый CreateService где-нибудь, чтобы создать канал.

1 голос
/ 17 мая 2010

Если вы используете NetDataContractSerializer, который хранит информацию о типе вместе с сериализованным объектом, ваша проблема должна быть решена. Однако в то же время он снижает совместимость с клиентами, не входящими в .NET.

...