0

I am using .NET 4.52. I am programming in VB.NET, but if you have a solution in C#, I can transpose.

I have an entire class library, which has a bunch of complex types, etc which represent different messages in our system which I can't change. Depending on the Message_Type (an attribute in the XMLRoot) different attributes and elements are required. If I try to deserialize an object that has the wrong info, it does not throw an exception, and I want it to. XSD validation does not work because often the element name is the same for two different types, but each type requires different stuff. Using XMLAttribute and XMLElement tags on my classes, there is no "Required" property. Even though there is a "IsNullable" property on Elements (but not Attributes), the XMLSerializer seems to pay no attention to this during deserialization.

So, I decided that I would try to create an additional "Required" attribute:

<AttributeUsage(AttributeTargets.Property, Inherited:=False, AllowMultiple:=False)>
Public Class XMLPlusElementAttribute
      Inherits XmlElementAttribute

    Public Sub New()
        MyBase.ElementName = ElementName
        Me.m_Required = False
    End Sub
    Private m_Required As Boolean

    Public Overridable Property Required() As Boolean
        Get
            Return m_Required
        End Get
        Set(value As Boolean)
            m_Required = value
        End Set
    End Property
End Class

<AttributeUsage(AttributeTargets.Property, Inherited:=False, AllowMultiple:=False)>
Public Class XMLPlusAttributeAttribute
    Inherits XmlAttributeAttribute

    Public Sub New()
        MyBase.AttributeName = AttributeName
        Me.m_Required = False
    End Sub
    Private m_Required As Boolean

    Public Overridable Property Required() As Boolean
        Get
            Return m_Required
        End Get
        Set(value As Boolean)
            m_Required = value
        End Set
    End Property
End Class

I can now decorate my classes with them:

<Serializable>
<XmlRoot("INTERFACE")>
Public MustInherit Class WM_Interface

    Private m_Message_Type As String
    Private m_Event_DTTM As String
    Private m_Business_Unit As String

    <XMLPlusAttribute(AttributeName:="MESSAGE_TYPE", Required:=True)>
    Public Property Message_Type() As String
        Get
            Return m_Message_Type
        End Get
        Set(value As String)
            m_Message_Type = value
        End Set
    End Property

    <XMLPlusAttribute(AttributeName:="EVENT_DTTM", Required:=True)>
    Public Property Event_DTTM() As String
        Get
            Return m_Event_DTTM
        End Get
        Set(value As String)
            m_Event_DTTM = value
        End Set
    End Property

    <XMLPlusAttribute(AttributeName:="BUSINESS_UNIT", Required:=True)>
    Public Property Business_Unit() As String
        Get
            Return m_Business_Unit
        End Get
        Set(value As String)
            m_Business_Unit = value
        End Set
    End Property
End Class

<Serializable>
<XmlRoot("INTERFACE")>
Public Class WM_Interface_BOX
    Inherits WM_Interface

    Private m_Container As WM_Container_BOX

    <XMLPlusElement(ElementName:="CONTAINER", IsNullable:=False, Required:=True)>
    Public Property Container() As WM_Container_BOX
        Get
            Return m_Container
        End Get
        Set(value As WM_Container_BOX)
            m_Container = value
        End Set
    End Property
End Class

<Serializable>
<XmlRoot("INTERFACE")>
Public Class WM_Interface_FIB
    Inherits WM_Interface

    Private m_Fiber As WM_Fiber

    <XMLPlusElement(ElementName:="FIBER", IsNullable:=False, Required:=True)>
    Public Property Fiber() As WM_Fiber
        Get
            Return m_Fiber
        End Get
        Set(value As WM_Fiber)
            m_Fiber = value
        End Set
    End Property
End Class

So the question now is how to customize the serialization / deserialization process to make use of this new "Required" attribute. If I inherit XMLSerializer, I can seemingly override the methods, but I am not sure what to put in there:

Public Class XMLPlusSerializer
    Inherits XmlSerializer

    Protected Overrides Function Deserialize(reader As XmlSerializationReader) As Object
        Return MyBase.Deserialize(reader)
    End Function

    Protected Overrides Sub Serialize(o As Object, writer As XmlSerializationWriter)
        MyBase.Serialize(o, writer)
    End Sub
End Class

I know I can also implement ISerializable and write custom ReadXML() and WriteXML() methods for each, but I want something way more generic. Any help or guidance you can suggest will be greatly appreciated!

djv
  • 15,168
  • 7
  • 48
  • 72
David P
  • 2,027
  • 3
  • 15
  • 27
  • `XmlSerializer` doesn't actually serialize and deserialize directly. Instead it uses code generation techniques to dynamically generate assemblies that do the actual serialization and deserialization. And, there's no way to subclass and override the code generation engine. On the other hand, if you can make an XSD for your types, you can validate while deserializing, as shown in [this answer](https://stackoverflow.com/a/259969/3744182). – dbc Mar 22 '17 at 18:01
  • As I stated, XSD will not work for me. See how I have two classes that both inherit from WM_Interface. Both have an XML Element called "INTERFACE" but require completely different sets of elements inside them in order for them to be valid. XSD, however, will not allow you to duplicate the name of an – David P Mar 22 '17 at 18:09
  • Then it sounds like you have the following options. 1) Implement `IXmlSerializable` (which you don't want to do) and drop in your own generic deserialization engine. 2) Preprocess and postprocess your XML attribute `"Message_Type"` to [`"xsi:type"`](https://msdn.microsoft.com/en-us/library/ca1ks327.aspx) and deserialize as a class hierarchy. 3) Manually validate after deserialization in an `OnDeserialized` event as shown in [How do you find out when you've been loaded via XML Serialization?](https://stackoverflow.com/q/1266547/3744182). – dbc Mar 22 '17 at 18:19
  • Thanks - I ended up going with manually validating after deserialization. I am posting my own answer for posterity's sake. – David P Mar 23 '17 at 16:14

1 Answers1

1

Following dbc's advice, I went for the following solution. Any suggestions on how to optimize it any more would greatly be appreciated:

Public Class XMLPlusSerializer
    Inherits XmlSerializer

    Public Sub New()
        MyBase.New()
    End Sub
    Public Sub New(type As Type)
        MyBase.New(type)
    End Sub
    Public Sub New(xmlTypeMapping As XmlTypeMapping)
        MyBase.New(xmlTypeMapping)
    End Sub
    Public Sub New(type As Type, defaultNamespace As String)
        MyBase.New(type, defaultNamespace)
    End Sub
    Public Sub New(type As Type, extraTypes() As Type)
        MyBase.New(type, extraTypes)
    End Sub
    Public Sub New(type As Type, objOverrides As XmlAttributeOverrides)
        MyBase.New(type, objOverrides)
    End Sub
    Public Sub New(type As Type, root As XmlRootAttribute)
        MyBase.New(type, root)
    End Sub
    Public Sub New(type As Type, objOverrides As XmlAttributeOverrides, extraTypes() As Type, root As XmlRootAttribute, defaultNamespace As String)
        MyBase.New(type, objOverrides, extraTypes, root, defaultNamespace)
    End Sub
    Public Sub New(type As Type, objOverrides As XmlAttributeOverrides, extraTypes() As Type, root As XmlRootAttribute, defaultNamespace As String, location As String)
        MyBase.New(type, objOverrides, extraTypes, root, defaultNamespace, location)
    End Sub
    Public Shadows Function Deserialize(stream As Stream) As Object
        Dim ret = MyBase.Deserialize(stream)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function
    Public Shadows Function Deserialize(textReader As TextReader) As Object
        Dim ret = MyBase.Deserialize(textReader)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function
    Public Shadows Function Deserialize(reader As XmlSerializationReader) As Object
        Dim ret = MyBase.Deserialize(reader)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function
    Public Shadows Function Deserialize(xmlReader As XmlReader, encodingStyle As String) As Object
        Dim ret = MyBase.Deserialize(xmlReader, encodingStyle)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function
    Public Shadows Function Deserialize(xmlReader As XmlReader, events As XmlDeserializationEvents) As Object
        Dim ret = MyBase.Deserialize(xmlReader, events)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function
    Public Shadows Function Deserialize(xmlReader As XmlReader, encodingStyle As String, events As XmlDeserializationEvents) As Object
        Dim ret = MyBase.Deserialize(xmlReader, encodingStyle, events)
        Dim result As XMLPlusValidateRequiredResult = ValidateRequired(ret)
        If result.IsValid = False Then
            Throw New Exception(result.ExceptionMessage)
        End If
        Return ret
    End Function

    Private Function ValidateRequired(obj As Object) As XMLPlusValidateRequiredResult
        Dim ret As New XMLPlusValidateRequiredResult()

        Try
            Dim arrPI() As PropertyInfo = obj.GetType().GetProperties(BindingFlags.Public Or BindingFlags.Instance)
            For Each pi As PropertyInfo In arrPI
                Dim xmlAttributeRequired As Attribute = pi.GetCustomAttribute(GetType(XMLPlusAttributeAttribute))
                If xmlAttributeRequired IsNot Nothing Then
                    ' If its an attribute and is required, make sure there is a value
                    Dim xmlAttributeRequiredInst As XMLPlusAttributeAttribute = DirectCast(xmlAttributeRequired, XMLPlusAttributeAttribute)
                    If xmlAttributeRequiredInst.Required = True Then
                        If IsNothing(pi.GetValue(obj)) = True Then
                            Throw New Exception(String.Format("XML Deserialization Exception, Message = Property '{0}' can't be null or empty. Attribute '{1}' must be defined.", pi.Name, xmlAttributeRequiredInst.AttributeName))
                        Else
                            If pi.PropertyType = GetType(String) Then
                                If DirectCast(pi.GetValue(obj), String) = String.Empty Then
                                    Throw New Exception(String.Format("XML Deserialization Exception, Message = Property '{0}' can't be null or empty. Attribute '{1}' must be defined.", pi.Name, xmlAttributeRequiredInst.AttributeName))
                                End If
                            End If
                        End If
                    End If
                Else
                    ' If its an element and is required, make sure there is a value
                    Dim xmlElementRequired As Attribute = pi.GetCustomAttribute(GetType(XMLPlusElementAttribute))
                    If xmlElementRequired IsNot Nothing Then
                        Dim xmlElementRequiredInst As XMLPlusElementAttribute = DirectCast(xmlElementRequired, XMLPlusElementAttribute)
                        If xmlElementRequiredInst.Required = True Then
                            Dim objElem As Object = pi.GetValue(obj)
                            If IsNothing(objElem) Then
                                'If its null, immediately throw an exception
                                Throw New Exception(String.Format("XML Deserialization Exception, Message = Element '{0}' can't be null or empty. Must contain 1 or more instances of &lt;{1}&gt;", pi.Name, xmlElementRequiredInst.ElementName))
                            Else
                                Dim objType As Type = objElem.GetType()
                                If objType.IsGenericType And (objType.GetGenericTypeDefinition() = GetType(List(Of ))) Then
                                    'If its a list, make sure Count > 0
                                    Dim objList As IList = DirectCast(objElem, IList)
                                    If objList.Count = 0 Then
                                        Throw New Exception(String.Format("XML Deserialization Exception, Message = Element '{0}' can't be null or empty. Must contain 1 or more instances of &lt;{1}&gt;", pi.Name, xmlElementRequiredInst.ElementName))
                                    Else
                                        'Iterate through each list item and validate the object of each
                                        For i As Int32 = 0 To objList.Count - 1
                                            Dim objItem As Object = objList(i)
                                            Dim result As XMLPlusValidateRequiredResult = ValidateRequired(objItem)
                                            If result.IsValid = False Then
                                                Throw New Exception(result.ExceptionMessage)
                                            End If
                                        Next
                                    End If
                                Else
                                    'If its a standard singleton object, validate the object
                                    Dim result As XMLPlusValidateRequiredResult = ValidateRequired(objElem)
                                    If result.IsValid = False Then
                                        Throw New Exception(result.ExceptionMessage)
                                    End If
                                End If
                            End If
                        End If
                    End If
                End If
            Next
            ret.IsValid = True
            ret.ExceptionMessage = String.Empty
        Catch ex As Exception
            ret.IsValid = False
            ret.ExceptionMessage = ex.ToString()
        End Try
        Return ret
    End Function

    Private Class XMLPlusValidateRequiredResult
        Private m_IsValid As Boolean
        Private m_ExceptionMessage As String

        Public Property IsValid() As Boolean
            Get
                Return m_IsValid
            End Get
            Set(value As Boolean)
                m_IsValid = value
            End Set
        End Property

        Public Property ExceptionMessage() As String
            Get
                Return m_ExceptionMessage
            End Get
            Set(value As String)
                m_ExceptionMessage = value
            End Set
        End Property
    End Class

End Class

<AttributeUsage(AttributeTargets.Property, Inherited:=False, AllowMultiple:=False)>
Public Class XMLPlusElementAttribute
    Inherits XmlElementAttribute

    Public Sub New()
        MyBase.ElementName = ElementName
        Me.m_Required = False
    End Sub
    Private m_Required As Boolean

    Public Overridable Property Required() As Boolean
        Get
            Return m_Required
        End Get
        Set(value As Boolean)
            m_Required = value
        End Set
    End Property
End Class

<AttributeUsage(AttributeTargets.Property, Inherited:=False, AllowMultiple:=False)>
Public Class XMLPlusAttributeAttribute
    Inherits XmlAttributeAttribute

    Public Sub New()
        MyBase.AttributeName = AttributeName
        Me.m_Required = False
    End Sub
    Private m_Required As Boolean

    Public Overridable Property Required() As Boolean
        Get
            Return m_Required
        End Get
        Set(value As Boolean)
            m_Required = value
        End Set
    End Property
End Class
David P
  • 2,027
  • 3
  • 15
  • 27