15

I'm trying to load a tree of objects via XML serialisation, and at the moment it will load the objects in, and create the tree quite happily. My issue revolves around the fact that these classes support a level of auditing. What I'd like to be able to do is call some method on each object after it has finished being loaded.

For the sake of argument, assume I have a fairly generic object tree with differing classes at different levels, like:

 <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>

Is there any way using the default serialisers (In the similar way that you can create methods like ShouldSerializeFoo) to determine when loading has finished for each object?

Edit: I should point out that the obvious case of exposing something akin to an OnLoaded() method that I could call after deserialising, strikes me as being a "bad thing to do".

Edit2: For the sake of discussion this is my current hack "approach", which works for the basic level, but the child City node still thinks it needs to be saved with changes (in the real world the object model is a lot more complex, but this will at least compile, without the need for full source)

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;
    }
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Rowland Shaw
  • 37,700
  • 14
  • 97
  • 166
  • Not a real answer, but why can't you do the auditing in the property get accessor? – weiqure Aug 12 '09 at 14:36
  • The auditing is in the property set, so it keeps a record internally that it has been changed since it was loaded (and thus needs saving, which would then say that the values all changed from their defaults to the real values that where loaded) – Rowland Shaw Aug 12 '09 at 14:39
  • Well, in that case, it's reassuring that we agree on how it should be done :) – Rowland Shaw Aug 12 '09 at 15:02

10 Answers10

15

Hmmm... it's still not pretty but you could refactor your deserialization logic into a dedicated class which could notify the deserialized object that it originated from XML before returning it to the caller.

Update: I think this should be fairly easy to do without straying too far from the patterns laid by the framework... you'd just need to ensure that you use the CustomXmlSerializer. Classes that need this notification just need to implement 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;
        }
    }
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
STW
  • 44,917
  • 17
  • 105
  • 161
  • I think this option makes the most sense. Post-process the objects after deserialization. – mackenir Aug 12 '09 at 15:04
  • Playing around with it I still think it's a little cludgy, but if the class handling the Deserialization uses generics and notifies the deserialized objects via an interface then it's a little better and not too bad to code against. – STW Aug 12 '09 at 15:12
  • That's effectively my best option "at the moment". I'm struggling with an elegant option to pass that message down the tree - exposing an `OnLoaded()` type of method seems "wrong" – Rowland Shaw Aug 12 '09 at 15:13
  • Would the updated suggestion look to see if the interface is implemented on the differing layers in the tree? For instance, Say I care about it for Office nodes and State nodes, but not Town nodes? – Rowland Shaw Aug 12 '09 at 16:17
  • @Rowland: Unfortunately not; it will call it on the root type being deserialized but it doesn't appear to work on nested types. – STW Aug 12 '09 at 17:10
  • I cheated a little, and used `internal virtual OnXmlDeserialized()` on a base class, and for the moment, override to iterate through the collections on each object. Less than elegant, but at least it works. – Rowland Shaw Aug 26 '09 at 12:33
  • 4
    I'm trying to get this to work and the Deserialize override never gets called. Am I missing some other part to this? – Adam Lear Mar 26 '10 at 18:33
  • 11
    Yeah this doesn't work for me either. CustomXmlSerializer.Deserialize is never called. I feel like this answer should be modded down unless somebody can explain what some of us are doing wrong. Otherwise this is just getting peoples hopes up and then wasting their time when it doesn't work. I'm using .NET 4.0. – eodabash Jan 25 '11 at 06:57
  • 1
    @eodabash it worked for me - perhaps you should ask a new question explaining what you've tried, and what does happen instead of what you expect. – Rowland Shaw Jun 17 '15 at 08:02
  • @eodabash, see my answer for a working solution as well as a potential explanation for why this one doesn't work. – HotN Jun 17 '15 at 14:23
5

The accepted solution didn't quite work for me. The overridden Deserialize() method never got called. I believe this is because that method is not public and is therefore called by one (or more) of the public Deserialize() methods, but not all of them.

Here's an implementation that works by method hiding and makes use of the existing IDeserializationCallback interface so any deserialization using non-xml methods can still trigger the OnDeserialization() method of that interface. It also uses reflection to traverse child properties to see if they also implement IDeserializationCallback and calls them accordingly.

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));
                }
            }
        }
    }
}
HotN
  • 4,216
  • 3
  • 40
  • 51
2

I tried the solution provided by abatishchev but as pointed out by the comments below his answer, the Deserialize method in the custom serializer never seems to get called.

I was able to get this working by overloading all the different Deserialize overloads I would need so that it would always call the custom method.

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

This way I actually see the Deserialize method getting called.

Freek Buurman
  • 1,279
  • 1
  • 9
  • 15
1

A toughie, since XmlSerializer doesn't support serialization callback events. Is there any way you could use DataContractSerializer? That does, but doesn't allow attributes (like @name above).

Otherwise; you could implement IXmlSerializable, but that is lots of work, and very error-prone.

Otherwise - perhaps checking the caller via the stack, but that is very brittle, and smells ripe.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

After wasting some time with first answer, I adopted code from HotN's post, except for 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);
      }
    }
  }
}

This allows using of standard [OnDeserialized].

UPD. Updated post for recursive walk on object tree.

Norritt
  • 113
  • 1
  • 6
0

I use a factory method which add more logic after the XML structured object has been deserialized. Such logic includes restoring the internal relationship (child-parent, sibling..) between object members.

Khanh Hua
  • 1,086
  • 1
  • 14
  • 22
0

In my case it was a collection of objects, so used an excepted solution having to modify it a bit

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

And then everything is working perfectly

0

I struggled somewhat as well getting the above solutions to work. I found the simplest solution to have my OnDeserialization() callbacks fire while using XmlSerializer was to chain a call to BinaryFormatter afterwards. My class already had a GetClone() method so it was rather straightforward and negated all my attempts at overriding 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
    }
}
user2921789
  • 135
  • 1
  • 8
0

The accepted solution didn't work for me either.

In order to make it finally work, I needed to modify HotN's solution a little bit. In particular I added the

propertyInfo.GetIndexParameters().Length

check (according to: https://learn.microsoft.com/en-us/dotnet/api/system.reflection.propertyinfo.getvalue), in order to avoid a missmatching parameters exception.

Also I have some properties that are not supposed to get mapped. I attributed them with [XmlIgnore], but the provided solution still processed them. But adding a check check for whether the passed parameter object is null did the trick.

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

    public class CustomXmlSerializer : XmlSerializer
    {
        public CustomXmlSerializer(Type type) : base(type) { }

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

            CheckForDeserializationCallbacks(result);

            return result;
        }

        private void CheckForDeserializationCallbacks(object deserializedObject)
        {
            if (deserializedObject == null)
                return;

            var deserializationCallback = deserializedObject as IXmlDeserializationCallback;

            if (deserializationCallback != null)
            {
                deserializationCallback.OnXmlDeserialization(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
                {
                    if (propertyInfo.GetIndexParameters().Length == 0)
                        CheckForDeserializationCallbacks(propertyInfo.GetValue(deserializedObject));
                }
            }
        }
    }
}
ltsstar
  • 832
  • 1
  • 8
  • 24
0

I haven't tested this yet, but I wanted to build upon HotN's answer with support for OnSerializing and OnSerialized. I don't believe there is a corresponding ISerializationCallback so I implemented it my own way.

Update (1)

My original solution was flawed. The following is tested and works.

Update (2)

IDeserializationCallback doesn't always appear to work so I removed it.

XmlCallbackSerializer

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;

    public 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);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(TextReader textReader)
    {
        var result = base.Deserialize(textReader);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(XmlReader xmlReader)
    {
        var result = base.Deserialize(xmlReader);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(XmlSerializationReader reader)
    {
        var result = base.Deserialize(reader);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(XmlReader xmlReader, string encodingStyle)
    {
        var result = base.Deserialize(xmlReader, encodingStyle);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(XmlReader xmlReader, XmlDeserializationEvents events)
    {
        var result = base.Deserialize(xmlReader, events);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    public new object Deserialize(XmlReader xmlReader, string encodingStyle, XmlDeserializationEvents events)
    {
        var result = base.Deserialize(xmlReader, encodingStyle, events);
        EachWith<OnDeserializedAttribute>(result);
        return result;
    }

    //...

    public new void Serialize(object o, XmlSerializationWriter xmlSerializationWriter)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(o, xmlSerializationWriter);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(Stream stream, object o)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(stream, o);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(TextWriter textWriter, object o)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(textWriter, o);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(XmlWriter xmlWriter, object o)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(xmlWriter, o);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(Stream stream, object o, XmlSerializerNamespaces namespaces)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(stream, o, namespaces);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(TextWriter textWriter, object o, XmlSerializerNamespaces namespaces)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(textWriter, o, namespaces);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(xmlWriter, o, namespaces);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces, string encodingStyle)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(xmlWriter, o, namespaces, encodingStyle);
        EachWith<OnSerializedAttribute>(o);
    }

    public new void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces, string encodingStyle, string id)
    {
        EachWith<OnSerializingAttribute>(o);
        base.Serialize(xmlWriter, o, namespaces, encodingStyle, id);
        EachWith<OnSerializedAttribute>(o);
    }

    //...

    public static bool HasAttribute<T>(MemberInfo input)
    {
        foreach (var i in input.GetCustomAttributes(true))
        {
            if (i is T)
                return true;
        }
        return false;
    }

    //...

    bool IsValidType(Type i)
        => i is not null 
        && !i.Equals(typeof(string)) 
        && !i.IsArray 
        && i.IsClass 
        && !i.IsEnum 
        && !i.IsImport 
        && !i.IsInterface 
        && !i.IsPrimitive 
        && i.IsPublic 
        && i.IsSerializable 
        && !i.IsValueType 
        && i.IsVisible;

    //...

    void Each(object input, Action<object> action) 
    {
        var type = input?.GetType();
        if (IsValidType(type))
        {
            action(input);
            if (input is IEnumerable j)
            {
                foreach (var i in j)
                    Each(i, action);
            }

            foreach (var i in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (IsValidType(i.PropertyType))
                {
                    object temp = default;
                    Try.Invoke(() => temp = i.GetValue(input, null));
                    if (temp != null)
                        Each(temp, action);
                }
            }
        }
    }

    void EachWith<T>(object input) where T : Attribute
    {
        Each(input, i =>
        {
            var methods = i.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            if (methods?.Length > 0)
            {
                foreach (var j in methods)
                {
                    if (HasAttribute<T>(j))
                        j.Invoke(i, new object[] { new StreamingContext(StreamingContextStates.Other) });
                }
            }
        });
    }
}

Try

using System;

public static class Try
{
    public static bool Invoke(Action @try, Action<Exception> @catch = null)
    {
        try
        {
            @try();
            return true;
        }
        catch (Exception e)
        {
            @catch?.Invoke(e);
            return false;
        }
    }
}

The method must

  • Include the OnSerializing or OnSerialized attribute
  • And define a StreamingContext parameter

To use interchangeably with BinaryFormatter, the StreamingContext parameter is required. When executing the method during XML serialization, a new instance of StreamingContext is created.

Examples:

[OnDeserialized]
internal/private/protected/public void OnDeserialized(StreamingContext context) { }

[OnSerializing]
internal/private/protected/public void OnSerializing(StreamingContext context) { }

[OnSerialized]
internal/private/protected/public void OnSerialized(StreamingContext context) { }
Imagin
  • 121
  • 3