Как узнать, когда вы загрузились через XML-сериализацию?

Я пытаюсь загрузить дерево объектов через сериализацию XML, и на данный момент он будет загружать объекты и создавать дерево довольно счастливо. Моя проблема связана с тем, что эти классы поддерживают уровень аудита. То, что я хотел бы сделать, это вызвать некоторый метод для каждого объекта после его загрузки.

Для аргумента предположим, что у меня есть довольно общее дерево объектов с разными классами на разных уровнях, например:

 <Customer name="Foo Bar Inc.">
   <Office IsHq="True">
     <Street>123 Any Street</Street>
     <Town name="Anytown">
       <State name="Anystate">
         <Country name="My Country" />
       </State>
     </Town>
   </Office>
   <Office IsHq="False">
     <Street>456 High Street</Street>
     <Town name="Anycity">
       <State name="Anystate">
         <Country name="My Country" />
       </State>
     </Town>
   </Office>
 </Customer>

Можно ли использовать сериализаторы по умолчанию (аналогично тому, как вы можете создавать такие методы, как ShouldSerializeFoo), чтобы определить, когда загрузка завершена для каждого объекта?

Edit: Я должен указать, что очевидный случай разоблачения чего-то, похожего на метод OnLoaded(), который я мог бы назвать после десериализации, нападает на меня как на "плохую вещь".

Edit2: Для обсуждения это мой текущий hack "подход", который работает на базовом уровне, но ребенок City node все еще считает, что он должен быть сохранен с изменениями (в реальном мире объектная модель намного сложнее, но это, по крайней мере, будет скомпилировано без необходимости полного источника)

public class Office
{
    [XmlAttribute("IsHq")]
    public bool IsHeadquarters { get; set; }

    [XmlElement]
    public string Street { get; set; }

    [XmlElement]
    public Town Town { get; set; }

    protected virtual void OnLoaded() {}

    public static OfficeCollection Search()
    {
        OfficeCollection retval = new OfficeCollection();
        string xmlString = @"
                    <Office IsHq='True'>
                        <Street>123 Any Street</Street>
                        <Town name='Anytown'>
                            <State name='Anystate'>
                                <Country name='My Country' />
                            </State>
                        </Town>
                    </Office>";

        XmlSerializer xs = new XmlSerializer(retval.GetType());
        XmlReader xr = new XmlTextReader(xmlString);
        retval = (OfficeCollection)xs.Deserialize(xr);

        foreach (Office thisOffice in retval)
        {
            thisOffice.OnLoaded();
        }
        return retval;
    }
}

Ответы

Ответ 1

Хммм... это все еще не очень хорошо, но вы можете реорганизовать свою логику десериализации в выделенный класс, который мог бы уведомить десериализованный объект, что он возник из XML, прежде чем вернуть его вызывающему.

Обновление: Я думаю, что это должно быть довольно легко обойтись без слишком большого отклонения от шаблонов, установленных инфраструктурой... вам просто нужно убедиться, что вы используете CustomXmlSerializer. Классам, которые нуждаются в этом уведомлении, просто нужно реализовать IXmlDeserializationCallback

using System.Xml.Serialization;

namespace Custom.Xml.Serialization
{
    public interface IXmlDeserializationCallback
    {
        void OnXmlDeserialization(object sender);
    }

    public class CustomXmlSerializer : XmlSerializer
    {
        protected override object Deserialize(XmlSerializationReader reader)
        {
            var result = base.Deserialize(reader);

            var deserializedCallback = result as IXmlDeserializationCallback;
            if (deserializedCallback != null)
            {
                deserializedCallback.OnXmlDeserialization(this);
            }

            return result;
        }
    }
}

Ответ 2

Принятое решение для меня не совсем сработало. Переопределенный метод Deserialize() никогда не вызывался. Я считаю, что это потому, что этот метод не является общедоступным и поэтому вызывается одним (или более) общедоступными методами Deserialize(), но не всеми из них.

Здесь реализация, которая работает путем скрытия метода и использует существующий интерфейс IDeserializationCallback, поэтому любая десериализация с использованием методов, отличных от xml, может по-прежнему запускать метод OnDeserialization() этого интерфейса. Он также использует отражение для перемещения дочерних свойств, чтобы увидеть, реализуют ли они также IDeserializationCallback и соответственно их называет.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml;
using System.Xml.Serialization;

namespace Xml.Serialization
{
    class XmlCallbackSerializer : XmlSerializer
    {
        public XmlCallbackSerializer(Type type) : base(type)
        {
        }

        public XmlCallbackSerializer(XmlTypeMapping xmlTypeMapping) : base(xmlTypeMapping)
        {
        }

        public XmlCallbackSerializer(Type type, string defaultNamespace) : base(type, defaultNamespace)
        {
        }

        public XmlCallbackSerializer(Type type, Type[] extraTypes) : base(type, extraTypes)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides) : base(type, overrides)
        {
        }

        public XmlCallbackSerializer(Type type, XmlRootAttribute root) : base(type, root)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes,
            XmlRootAttribute root, string defaultNamespace) : base(type, overrides, extraTypes, root, defaultNamespace)
        {
        }

        public XmlCallbackSerializer(Type type, XmlAttributeOverrides overrides, Type[] extraTypes,
            XmlRootAttribute root, string defaultNamespace, string location)
            : base(type, overrides, extraTypes, root, defaultNamespace, location)
        {
        }

        public new object Deserialize(Stream stream)
        {
            var result = base.Deserialize(stream);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(TextReader textReader)
        {
            var result = base.Deserialize(textReader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader)
        {
            var result = base.Deserialize(xmlReader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlSerializationReader reader)
        {
            var result = base.Deserialize(reader);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, string encodingStyle)
        {
            var result = base.Deserialize(xmlReader, encodingStyle);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, XmlDeserializationEvents events)
        {
            var result = base.Deserialize(xmlReader, events);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        public new object Deserialize(XmlReader xmlReader, string encodingStyle, XmlDeserializationEvents events)
        {
            var result = base.Deserialize(xmlReader, encodingStyle, events);

            CheckForDeserializationCallbacks(result);

            return result;
        }

        private void CheckForDeserializationCallbacks(object deserializedObject)
        {
            var deserializationCallback = deserializedObject as IDeserializationCallback;

            if (deserializationCallback != null)
            {
                deserializationCallback.OnDeserialization(this);
            }

            var properties = deserializedObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var propertyInfo in properties)
            {
                if (propertyInfo.PropertyType.GetInterface(typeof(IEnumerable<>).FullName) != null)
                {
                    var collection = propertyInfo.GetValue(deserializedObject) as IEnumerable;

                    if (collection != null)
                    {
                        foreach (var item in collection)
                        {
                            CheckForDeserializationCallbacks(item);
                        }
                    }
                }
                else
                {
                    CheckForDeserializationCallbacks(propertyInfo.GetValue(deserializedObject));
                }
            }
        }
    }
}

Ответ 3

Я попробовал решение, предоставленное абатищевым, но, как указано в комментариях ниже его ответа, метод Deserialize в пользовательском сериализаторе никогда не будет вызван.

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

protected object Deserialize(System.IO.StringReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.IO.TextReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.Xml.XmlReader reader)
{
    var result = base.Deserialize(reader);

    CallBack(result);

    return result;
}

protected object Deserialize(System.IO.Stream stream)
{
    var result = base.Deserialize(stream);

    CallBack(result);

    return result;
}

private void CallBack(object result)
{
    var deserializedCallback = result as IXmlDeserializationCallback;
    if (deserializedCallback != null)
    {
        deserializedCallback.OnXmlDeserialization(this);
    }
}

Таким образом, я действительно вижу способ вызова Deserialize.

Ответ 4

Жесткий, поскольку XmlSerializer не поддерживает события обратного вызова сериализации. Можно ли использовать DataContractSerializer? Это делает, но не позволяет атрибуты (например, @name выше).

В противном случае; вы можете реализовать IXmlSerializable, но это очень много работы и очень подвержено ошибкам.

В противном случае - возможно, проверка вызывающего абонента через стек, но это очень хрупкий и запах зрелый.

Ответ 5

После некоторого времени с первым ответом я принял код из сообщения HotN, за исключением CheckForDeserializationCallbacks:

private static void ProcessOnDeserialize(object _result) {
  var type = _result != null ? _result.GetType() : null;
  var methods = type != null ? type.GetMethods().Where(_ => _.GetCustomAttributes(true).Any(_m => _m is OnDeserializedAttribute)) : null;
  if (methods != null) {
    foreach (var mi in methods) {
      mi.Invoke(_result, null);
    }
  }
  var properties = type != null ? type.GetProperties().Where(_ => _.GetCustomAttributes(true).Any(_m => _m is XmlElementAttribute || _m is XmlAttributeAttribute)) : null;
  if (properties != null) {
    foreach (var prop in properties) {
      var obj = prop.GetValue(_result, null);
      var enumeration = obj as IEnumerable;
      if (obj is IEnumerable) {
        foreach (var item in enumeration) {
          ProcessOnDeserialize(item);
        }
      } else {
        ProcessOnDeserialize(obj);
      }
    }
  }
}

Это позволяет использовать стандартный [OnDeserialized].

UPD. Обновлено сообщение для рекурсивного перехода по дереву объектов.

Ответ 6

Я использую метод factory, который добавляет больше логики после того, как структурированный объект XML был десериализован. Такая логика включает восстановление внутренних отношений (дочернего родителя, родного брата..) между членами объекта.

Ответ 7

В моем случае это была коллекция объектов, поэтому использовалось исключенное решение, чтобы немного изменить его

  private static void PostDeserializedProcess<T>(T deserializedObj)
    {
        var deserializedCallback = deserializedObj as IXmlPostDeserializationCallback;
        if (deserializedCallback != null)
        {
            deserializedCallback.OnXmlDeserialized(deserializedObj);
        }
        else
        {
            // it could be a List of objects 
            // and we need to check for every object in the list
            var collection = deserializedObj as System.Collections.IEnumerable;
            if (collection != null)
            {
                foreach (var item in collection)
                {
                    PostDeserializedProcess(item);
                }
            }
        }
    }

И тогда все отлично работает

Ответ 8

Я тоже несколько старался заставить эти решения работать. Я нашел простейшее решение, чтобы мой огонь OnDeserialization() вызывал обратный вызов, а XmlSerializer - это цепочка вызова BinaryFormatter впоследствии. У моего класса уже был метод GetClone(), поэтому он был довольно простым и сбрасывал все мои попытки переопределить XmlSerializer

public static Foo Deserialize(string path) {
    Foo foo;
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(Foo));
    using (StreamReader textReader = new StreamReader(path)) {
        foo = (Foo)xmlSerializer.Deserialize(textReader); // this does NOT fire the OnDeserialization callbacks
    }
    return foo.GetClone();
}

public Foo GetClone() {
    using (var ms = new MemoryStream()) {
        var formatter = new BinaryFormatter();
        formatter.Serialize(ms, this);
        ms.Position = 0;
        return (Foo)formatter.Deserialize(ms); // this DOES fire the OnDeserialization callbacks
    }
}