Edit: root cause of the question
I'm working on an application, that uses System.Messaging
and XML serialisation via XmlMessageFormatter
.
I would like to send an object, let's say Class1
, having an ID field:
public class Class1{
public long Id1;
}
I would also like to send another object, let's say Class16
, having another ID field:
public class Class16{
public long Id16;
}
In XML, both need to look like:
<HM>Human_Message
<ID>Own_Identifier</ID>
</HM>
In order to achieve this, I'm working with following [Xml]
-like configurations:
Class1:
[XmlRoot(ElementName = "HM")]
public class Class1{
[XmlElement(ElementName = "ID")]
public long Id1;
}
Class16:
[XmlRoot(ElementName = "HM")]
public class Class16{
[XmlElement(ElementName = "ID")]
public long Id16;
}
As you see, the XML body will indeed be equal for both classes.
Is this even possible?
Edit: original question
I have a basic class (simple class), from which there are several subclasses (about 27 of them), inheriting from it.
I'm using standard C# System.Messaging
system for sending objects back and forth.
Very simplified:
Sending side:
I have a MessageQueue, doing:
subClass1 Obj1 = subClass1(...);
...
Basic_Class Obj_To_Be_Sent = Basic_Class(Obj1);
System.Messaging.Message message = new System.Messaging.Message(Obj_To_Be_Sent);
obj_MessageQueue.Send(message, ...);
When checking Obj_To_Be_Sent
, the type is correct.
Once this is sent, when I have a look at Computer Management, Services and Applications, Message Queuing, ..., Properties, I see the message, but I can't verify if the type is still correct.
Receiving side:
I have an _xmlMessageFormatter
, containing (amongst others):
System.Type[] messageTypes = new System.Type[27];
messageTypes[0] = typeof(SubClass1);
...
messageTypes[15] = typeof(SubClass16);
...
message = this._receiveQueue.Receive();
Basic_Class general = (Basic_Class)this._xmlMessageFormatter.Read(message);
Type objectType= general.GetType();
To my surprise, objectType
is wrong (it is believed to be SubClass16
).
This application has worked fine before, but now something seems to fail. The biggest problem I have is that I don't know how to check the steps between sending the message and getting the type of the received message.
Does anybody have knowledge on Computer Management, Services and Applications, Message Queuing, ..., how can I check if the object type on the sending side is ok?
Does anybody have knowledge on _xmlFormatter.Read()
and GetType()
? (Already after the Read()
, the watch-window mentions the Type of general
to be wrong)
Thanks in advance
Edit after more investigation
I delete my own answer as the problem is not completely solved.
Meanwhile I've discovered that [XmlRoot]
entries are causing the mentioned issues: I've been using the same [XmlRoot]
entry for different classes.
Is there a way to differentiate?
For your information, I've already tried the following but it did not work:
Class1:
[XmlRoot(ElementName = "HM", DataType = "subClass1", Namespace="Namespace")]
public class subClass1 : Basic_Class
Class2:
[XmlRoot(ElementName = "HM", DataType = "subClass16", Namespace="Namespace")]
public class subClass16 : Basic_Class
while the _xmlFormatter.TargetTypes
contained entries like:
Name = "subClass1" FullName="Namespace.Class1"
Name = "subClass16" FullName="Namespace.Class16"
Does anybody have any ideas?
TL/DR
XmlMessageFormatter
recognizes the type of object received by inspecting the XML root element name and namespace URI and finding a compatible type among the provided TargetTypes
:
Specifies the set of possible types that will be deserialized by the formatter from the message provided.
Thus you need to specify distinct XmlRootAttribute.ElementName
root element names for each of your derived types rather than using the same name "HM"
for all of them. Having done so, you should be able to send and receive them using XmlMessageFormatter
without loss of type information.
If for whatever reason this cannot be done and you need to use the same root name "HM" on all your classes, you will need to implement your own custom IMessageFormatter
based on XmlMessageFormatter
that encodes the type via some other mechanism such as an xsi:type
attribute.
Explanation
You are using XmlMessageFormatter
to send and receive instances of a polymorphic type hierarchy formatted as XML. Internally this serializer uses XmlSerializer
to serialize to and deserialize from XML. This serializer supports two different mechanisms for exchanging polymorphic types:
Type information may be specified by using different (root) element names for each type.
Since the root element name is given by default by the class name, you may not need to indicate different root element names via metadata, but if you do, use XmlRootAttribute.ElementName
:
The name of the XML root element that is generated and recognized in an XML-document instance. The default is the name of the serialized class.
If you were to opt for this mechanism, your classes would look something like:
[XmlRoot(ElementName = "Basic_Class", Namespace="Namespace")]
public class Basic_Class
{
}
[XmlRoot(ElementName = "Class1", Namespace="Namespace")]
public class Class1 : Basic_Class
{
}
[XmlRoot(ElementName = "Class16", Namespace="Namespace")]
public class Class16 : Basic_Class
{
}
When using this mechanism, construct an XmlSerializer
for the concrete type to be serialized.
If a common root element is emitted for all subtypes, type information may be specified via an xsi:type
attribute.
This attribute, short for {http://www.w3.org/2001/XMLSchema-instance}type
, is a w3c standard attribute that allows an element to explicitly assert its type, e.g. when it is a polymorphic subtype of the expected element type. XmlSerializer
supports this attribute and will use it to determine the actual type of object to deserialize for such a polymorphic type.
If you were to opt for this mechanism, your classes would look something like:
[XmlRoot(ElementName = "Basic_Class", Namespace="Namespace")]
[XmlInclude(typeof(Class1)), XmlInclude(typeof(Class16))] // Include all subtypes here!
public class Basic_Class
{
}
[XmlRoot(ElementName = "Class1", Namespace="Namespace")]
public class Class1 : Basic_Class
{
}
[XmlRoot(ElementName = "Class16", Namespace="Namespace")]
public class Class16 : Basic_Class
{
}
When using this mechanism, construct a serializer for the shared base type Basic_Class
rather than the concrete type.
But, of these two mechanisms, only the first is supported by XmlMessageFormatter
, as can be seen from the reference source. The Write(Message message, object obj)
simply constructs or uses a default serializer for the concrete type of the incoming object:
Type serializedType = obj.GetType(); XmlSerializer serializer = null; if (this.targetSerializerTable.ContainsKey(serializedType)) serializer = (XmlSerializer)this.targetSerializerTable[serializedType]; else { serializer = new XmlSerializer(serializedType); this.targetSerializerTable[serializedType] = serializer; } serializer.Serialize(stream, obj);
As the serializer is constructed using obj.GetType()
, the root element name will be the name specified for the derived, concrete class, and xsi:type
information will not be included.
The Read(Message message)
method cycles through default serializers constructed for the TargetTypes
and uses the first for which XmlSerializer.CanDeserialize(XmlReader)
returns true
:
foreach (XmlSerializer serializer in targetSerializerTable.Values) { if (serializer.CanDeserialize(reader)) return serializer.Deserialize(reader); }
This method in turn simply checks to see whether the root element name and namespace are the compatible with the type to be deserialized. And this is why you need to use different root element names for each concrete type.
Implementing your own IMessageFormatter
.
As explained above, XmlMessageFormatter
only supports polymorphism via different root element names. If this is not acceptable, you will need to implement your IMessageFormatter
that encodes the type via some other mechanism such as the xsi:type
attribute described above.
For instance, the following IMessageFormatter
supports serializing and deserializing via a supplied XmlSerializer
:
public class OverrideXmlMessageFormatter : IMessageFormatter
{
readonly XmlSerializer serializer;
public OverrideXmlMessageFormatter(XmlSerializer serializer)
{
if (serializer == null)
throw new ArgumentNullException();
this.serializer = serializer;
}
#region IMessageFormatter Members
public bool CanRead(Message message)
{
if (message == null)
throw new ArgumentNullException();
var stream = message.BodyStream;
bool canRead;
try
{
using (var reader = XmlReader.Create(message.BodyStream))
canRead = serializer.CanDeserialize(reader);
}
catch (Exception)
{
canRead = false;
}
message.BodyStream.Position = 0; // reset stream in case CanRead is followed by Deserialize
return canRead;
}
public object Read(Message message)
{
if (message == null)
throw new ArgumentNullException();
using (var reader = XmlReader.Create(message.BodyStream))
return serializer.Deserialize(reader);
}
public void Write(Message message, object obj)
{
if (message == null || obj == null)
throw new ArgumentNullException();
var stream = new MemoryStream();
serializer.Serialize(stream, obj);
message.BodyStream = stream;
message.BodyType = 0;
}
#endregion
#region ICloneable Members
public object Clone()
{
return new OverrideXmlMessageFormatter(serializer);
}
#endregion
}
Then to send and receive messages of type Class1
and/or Class16
, define the following XmlSerializerCache.MessagingSerializer
:
public static class XmlSerializerCache
{
static XmlSerializer CreateSharedSerializer(Type baseType, Type[] extraTypes, string rootName, string rootNamespace)
{
// baseType could be typeof(object) if there is no common base type.
return new XmlSerializer(baseType,
null,
extraTypes,
new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace },
null);
}
static XmlSerializer serializer = CreateSharedSerializer(
// The base type for the classes you want to send. Could be object if there is no more derived base type.
typeof(object),
// Add all the derived types of the classes you want to send
new[] { typeof(Class1), typeof(Class16) },
// Your required root element name.
"MH",
// Your required root element namespace.
"");
// The serializer must be statically cached to avoid a severe memory leak, see https://stackoverflow.com/questions/23897145/memory-leak-using-streamreader-and-xmlserializer
public static XmlSerializer MessagingSerializer { get { return serializer; } }
}
And set
IMessageFormatter _xmlMessageFormatter = new OverrideXmlMessageFormatter(XmlSerializerCache.MessagingSerializer);
You will need to use this formatter on both the sending and receiving sides.
The XML sent will look like (for Class1
):
<MH xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xsi:type="Class1">
<ID>10101</ID>
</MH>
Notes
XmlRootAttribute.DataType
is not used in specifying polymorphic type information. Instead it can be used to indicate that the element contains a value of some specific XML primitive type such as dateTime
, duration
, etc:
An XSD (XML Schema Document) data type.
As such setting this value is not helpful in your application.
Some related questions about XmlSerializer
and polymorphism include:
When serializing to XML with an XmlSerializer
with attribute overrides, you must statically cache and reuse the serializer to avoid a severe memory leak. See Memory Leak using StreamReader and XmlSerializer for why.
If you do implement your own IMessageFormatter
you could hypothetically implement your own messaging format using a different serializer such as Json.NET.