Skip to content

Example Scenarios

Mike-E edited this page Jun 9, 2020 · 20 revisions

Configuration Profiles

The ExtendedXmlSerializer API allows for you to create pre-configured configuration containers, known as configuration profiles. This is useful for encapsulating re-used configurations so that you can use them across different projects, or if you have different ways you would like to configure your container and would like to easily switch between them during development.

You define your profiles and then create a configuration container through the use of the ConfiguredContainer static class to create a ConfigurationContainer that is configured via the provided configuration profile.

Let's first start with a simple configuration profile:

	sealed class Subject
	{
		public int Number { get; set; }

		public string Message { get; set; }
	}

	sealed class SimpleProfile : IConfigurationProfile
	{
		public static SimpleProfile Default { get; } = new SimpleProfile();

		SimpleProfile() {}

		public IConfigurationContainer Get(IConfigurationContainer parameter) 
			=> parameter.Type<Subject>()
		                .Member(x => x.Message)
		                .Name("NewMessage");
	}

Now let's create a configuration container with the profile. The configuration profile will apply its encapsulated configurations to the container once it has been created. We do this through the use of a ConfiguredProfile.New call:

	IConfigurationContainer container = ConfiguredContainer.New<SimpleProfile>();

	IExtendedXmlSerializer serializer = container.UseAutoFormatting()
	                          .EnableImplicitTyping(typeof(Subject))
	                          .UseOptimizedNamespaces()
	                          .Create();
	var instance = new Subject {Message = "Hello World!"};
	string document = serializer.Serialize(instance);

And the result of the document variable above is:

<?xml version="1.0" encoding="utf-8"?><Issue282Tests_Profiles-Subject Number="0" NewMessage="Hello World!" />

You can also combine configuration profiles:

	sealed class ComplexProfile : CompositeConfigurationProfile
	{
		public static ComplexProfile Default { get; } = new ComplexProfile();

		ComplexProfile() : base(SimpleProfile.Default, NumberProfile.Default) {}
	}

	sealed class NumberProfile : IConfigurationProfile
	{
		public static NumberProfile Default { get; } = new NumberProfile();

		NumberProfile() {}

		public IConfigurationContainer Get(IConfigurationContainer parameter) 
			=> parameter.Type<Subject>()
                        .Member(x => x.Number)
                        .Name("NewNumber");
	}

Create:

	IConfigurationContainer container = ConfiguredContainer.New<ComplexProfile>();

	IExtendedXmlSerializer serializer = container.UseAutoFormatting()
	                          .EnableImplicitTyping(typeof(Subject))
	                          .UseOptimizedNamespaces()
	                          .Create();
	var instance = new Subject {Message = "Hello World!", Number = 123};
	string document = serializer.Serialize(instance);

Result:

<?xml version="1.0" encoding="utf-8"?><Issue282Tests_Profiles-Subject Number="123" NewMessage="Hello World!" />

The above code is verified and can be reviewed in a passing test within our test suite here.

References

ExtendedXmlSerializer has support for capturing, storing, and re-establishing references within an object graph. There are two ways of establishing reference identity: implicit (whereby ExtendedXmlSerializer keeps track of identities for you) and explicit (whereby you explicitly specify an identity field used to define an entity identity).

Let's take a look at these two scenarios and examine their resulting document output. We will use the following classes in our sample code below:

public class Parent {
    public Child First { get; set; }
    public Child Second { get; set; }
}

public class Child {
    public Guid Id { get; set; }
    public string Message { get; set; }
}

Implicit References

Let's create our serializer and serialize a child object after assigning two values of the same instance:

var serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Parent), typeof(Child))
			                                 .UseOptimizedNamespaces()
			                                 .EnableReferences()
			                                 .Create();

var child = new Child {Id = Guid.Parse("{64D96027-94CD-4EA4-B102-4EED74BF53B0}"), Message = "Hello World!"};
var instance = new Parent {First = child, Second = child};
var document = serializer.Serialize(instance);

Note: You can also implicitly apply references specifically to a type:

var serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Parent), typeof(Child))
			                                 .UseOptimizedNamespaces()
                                             .Type<Child>()
			                                 .EnableReferences()
			                                 .Create();

In either case, the document variable above contains the following XML (formatted for convenience and readability here):

<?xml version="1.0" encoding="utf-8"?>
<Parent xmlns:exs="https://extendedxmlserializer.github.io/v2">
	<First exs:identity="1">
		<Id>64d96027-94cd-4ea4-b102-4eed74bf53b0</Id>
		<Message>Hello World!</Message>
	</First>
	<Second exs:reference="1" />
</Parent>

Note the use of exs:identity and exs:reference attributes. They are used to denote the source reference and subsequent reference pointers throughout the document, respectfully. In this case, ExtendedXmlSerializer is keeping track of the identity, and is using that by the 1 identifier. If an object graph was generated that somehow had a 2nd Child reference, its identifier would be 2, and so on.

After a reference has been established in the document, subsequent references to that original reference are denoted by the exs:reference="<id>" attribute, where <id> is the id of the reference that should returned and assigned within the object graph when it is deserialized.

Explicit References

Explicit references work much in the same way as implicit references but use different attributes. It is considered explicit as you explicitly define the identity when configuring the type on the container. Note that the resulting identity must be unique otherwise references will be lost and/or assigned incorrectly upon deserialization.

Using this code:

var serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Parent), typeof(Child))
                                             .UseOptimizedNamespaces()
                                             .Type<Child>()
                                             .EnableReferences(x => x.Id)
                                             .Create()
                                             .ForTesting();

(Note the use of EnableReferences(x => x.Id) which explicitly configures the identity field used to establish uniqueness.)

The result is this document (formatted for readability):

<?xml version="1.0" encoding="utf-8"?>
<Parent xmlns:exs="https://extendedxmlserializer.github.io/v2">
	<First Id="64d96027-94cd-4ea4-b102-4eed74bf53b0">
		<Message>Hello World!</Message>
	</First>
	<Second exs:entity="64d96027-94cd-4ea4-b102-4eed74bf53b0" />
</Parent>

Note the use of exs:identity is gone, as well as exs:reference. When explicit references are used, the original reference is serialized as usual, and the exs:entity is used on subsequent references that point back to its identity field with the provide identity reference.

Monitor Serialization Pipeline

One scenario that you might want to engage in is to monitor events during the serialization (and deserialization) process. You might want to do this for logging or storage purposes.

The ISerializationMonitor [link] has the following callbacks:

  • OnSerializing
  • OnSerialized
  • OnDeserializing
  • OnActivating
  • OnActivated
  • OnDeserialized

Let's run through a very simple example of this scenario by storing a list of strings whenever they are encountered during a serialization via the OnSerialized callback.

The code written here can be seen in a passing test defined in our test suite here.

First, define the monitor:

	sealed class Monitor : ISerializationMonitor<string>
	{
		readonly List<string> _store;

		public Monitor(List<string> store) => _store = store;

		public void OnSerializing(IFormatWriter writer, string instance) {}

		public void OnSerialized(IFormatWriter writer, string instance)
		{
			_store.Add(instance);
		}

		public void OnDeserializing(IFormatReader reader, Type instanceType) {}

		public void OnActivating(IFormatReader reader, Type instanceType) {}

		public void OnActivated(string instance) {}

		public void OnDeserialized(IFormatReader reader, string instance) {}
	}

And our subject:

	sealed class Subject
	{
		public string Message { get; set; }
	}

And finally our serializer instance:

	var instances = new List<string>();
	IExtendedXmlSerializer serializer = new ConfigurationContainer().Type<string>()
	                                                                .WithMonitor(new Monitor(instances))
	                                                                .Create();

Now let's serialize one of our subject's with a message:

	const string message = "Hello World!";
	string document = serializer.Serialize(new Subject {Message = message});

When we poll the instances variable above now, we see that it contains the Hello World! message.

For a full working test demonstrating this scenario, see the test described here.

Create an Extension

Creating an extension is at the heart of ExtendedXmlSerializer's extension model.

In this very simple example, we are going to extend the configuration container that creates our root serializer to create a content serializer so that it always add the number 42 whenever it encounters number during both serialization and deserialization.

Here's our subject:

	sealed class Subject
	{
		public int Number { get; set; }
	}

If we create an instance of this subject with its Number set to 10, it will serialize that value with 52 and then deserialize with a

Now, why on earth would we ever want to do this?! For demonstration purposes, of course! 😁

All of the code provided in this article can be found in the form of a passing test as defined here.

Let's begin by defining our extension:

    sealed class Extension : ISerializerExtension
	{
		public static Extension Default { get; } = new Extension();

		Extension() {}

		public IServiceRepository Get(IServiceRepository parameter) => parameter.DecorateContentsWith<Contents>()
		                                                                        .Then();

		void ICommand<IServices>.Execute(IServices parameter) {}

		sealed class Contents : IContents
		{
			readonly IContents        _previous;
			readonly ISerializer<int> _number;

			public Contents(IContents previous)
				: this(previous, new AnswerToEverythingSerializer(previous.Get(typeof(int)).For<int>())) {}

			public Contents(IContents previous, ISerializer<int> number)
			{
				_previous = previous;
				_number   = number;
			}

			public ISerializer Get(TypeInfo parameter)
				=> parameter == typeof(int) ? _number.Adapt() : _previous.Get(parameter);
		}

		sealed class AnswerToEverythingSerializer : ISerializer<int>
		{
			readonly ISerializer<int> _previous;

			public AnswerToEverythingSerializer(ISerializer<int> previous) => _previous = previous;

			public int Get(IFormatReader parameter) => _previous.Get(parameter) + 42;

			public void Write(IFormatWriter writer, int instance)
			{
				_previous.Write(writer, instance + 42);
			}
		}
	}

Here this extension is used to register the AnswerToEverythingSerializer whenever a content serializer is requested for the type of int. The AnswerToEverythingSerializer adds 42 to a number when it serializes and then 42 again to a value when it deserializes it.

Now let's create a container that is extended with this extension:

	IExtendedXmlSerializer serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Subject))
	                                                                .Extend(Extension.Default)
	                                                                .Create();
	string document = serializer.Serialize(new Subject(){Number = 10});

When we inspect the document variable above, we get the following XML:

<?xml version=""1.0"" encoding=""utf-8""?>
<Subject><Number>52</Number></Subject>

Now let's deserialize it:

var number = serializer.Deserialize<Subject>(document).Number;

Inspecting the number variable reveals 94 as it added 42 to the serialized value of 52.

This is a very contrived example but hopefully it communicates the power of the extension model that is available in ExtendedXmlSerializer. For a working example of the code above, check out the passing test that is in our test suite that demonstrates the above scenario.

Register Custom Serializer

You have full control over the serialization of a particular type. Consider the following class structure:

	public class Subject
	{
		public Subject(string text, int number)
		{
			Text   = text;
			Number = number;
		}

		public string Text { get; }
		public int Number { get; }
	}

Now, while it is possible to use EnableParameterizedContent [feature, API] to serialize and deserialize the above contract, what we want to demonstrate here is the ability to have full control over how the above is stored and retrieved.

The following code can be found in a passing test stored here.

First, let's create a serializer:

	sealed class SubjectSerializer : ISerializer<Subject>
	{
		public static SubjectSerializer Default { get; } = new SubjectSerializer();

		SubjectSerializer() {}

		public Subject Get(IFormatReader parameter)
		{
			var parts  = parameter.Content().Split('|');
			var result = new Subject(parts[0], int.Parse(parts[1]));
			return result;
		}

		public void Write(IFormatWriter writer, Subject instance)
		{
			writer.Content($"{instance.Text}|{instance.Number}");
		}
	}

The above is very rudimentary and extremely error-prone (particularly the int.Parse call), but again what we are demonstrating here is full control over the serialization and deserialization process for this particular type. If you feel so inclined (and maybe a little crazy 😅), you could even modify the serializer to send and retrieve data from a database.

That is the point (and power!) of this scenario, as well as demonstrating how to register a serializer for a particular type.

Now that we have the serializer created, let's register it and create a root container:

	IExtendedXmlSerializer serializer = new ConfigurationContainer().Type<Subject>()
	                                                                .Register()
	                                                                .Serializer()
	                                                                .Using(SubjectSerializer.Default)
	                                                                .Create();
    Subject instance = new Subject("Hello World!", 123);
	string document = serializer.Serialize(instance);

The above will create the following XML:

<?xml version="1.0" encoding="utf-8"?>
<Subject xmlns="clr-namespace:Namespace;assembly=Assembly">Hello World!|123</Subject>

The above code can be viewed as a passing test here.

Register a Converter

While registering a serializer is meant for more complex serialization scenarios, you can use an IConverter<T> for simple string-based conversion implementations. Let's walk through a simple example here.

First, start by defining your converter for a type:

	sealed class Subject
	{
		public string Message { get; set; }
	}

	sealed class SubjectConverter : ConverterBase<Subject>
	{
		public static SubjectConverter Default { get; } = new SubjectConverter();

		SubjectConverter() {}

		public override Subject Parse(string data) => new Subject {Message = data};

		public override string Format(Subject instance) => instance.Message;
	}

Register it and serialize:

	IExtendedXmlSerializer serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Subject))
	                                                                .Type<Subject>()
	                                                                .Register()
	                                                                .Converter()
	                                                                .Using(SubjectConverter.Default)
	                                                                .Create();

	var instance = new Subject {Message = "Hello World!"};
	string document = serializer.Serialize(instance);

Result:

<?xml version=""1.0"" encoding=""utf-8""?><Subject>Hello World!</Subject>

The above code can be reviewed in a passing test within our test suite here.

Serialization of Dictionary

You can serialize generic dictionary, that can store any type.

    public class TestClass
    {
        public Dictionary<int, string> Dictionary { get; set; }
    }
    TestClass obj = new TestClass
    {
        Dictionary = new Dictionary<int, string>
        {
            {1, "First"},
            {2, "Second"},
            {3, "Other"},
        }
    };

Output XML will look like:

    <?xml version="1.0" encoding="utf-8"?>
    <TestClass xmlns="clr-namespace:ExtendedXmlSerializer.Samples.Dictianary;assembly=ExtendedXmlSerializer.Samples">
      <Dictionary>
        <Item xmlns="https://extendedxmlserializer.github.io/system">
          <Key>1</Key>
          <Value>First</Value>
        </Item>
        <Item xmlns="https://extendedxmlserializer.github.io/system">
          <Key>2</Key>
          <Value>Second</Value>
        </Item>
        <Item xmlns="https://extendedxmlserializer.github.io/system">
          <Key>3</Key>
          <Value>Other</Value>
        </Item>
      </Dictionary>
    </TestClass>

If you use UseOptimizedNamespaces function xml will look like:

    <?xml version="1.0" encoding="utf-8"?>
    <TestClass xmlns:sys="https://extendedxmlserializer.github.io/system" xmlns:exs="https://extendedxmlserializer.github.io/v2" xmlns="clr-namespace:ExtendedXmlSerializer.Samples.Dictianary;assembly=ExtendedXmlSerializer.Samples">
      <Dictionary>
        <sys:Item>
          <Key>1</Key>
          <Value>First</Value>
        </sys:Item>
        <sys:Item>
          <Key>2</Key>
          <Value>Second</Value>
        </sys:Item>
        <sys:Item>
          <Key>3</Key>
          <Value>Other</Value>
        </sys:Item>
      </Dictionary>
    </TestClass>

Migrate XML Based on Older Class Model

In standard XMLSerializer you can't deserialize XML in case you change model. In ExtendedXMLSerializer you can create migrator for each class separately. E.g.: If you have big class, that uses small class and this small class will be changed you can create migrator only for this small class. You don't have to modify whole big XML. Now I will show you a simple example.

If you had a class:

    public class TestClass
    {
        public int Id { get; set; }
        public string Type { get; set; }
    }

and generated XML look like:

    <?xml version="1.0" encoding="utf-8"?>
    <TestClass xmlns="clr-namespace:ExtendedXmlSerialization.Samples.MigrationMap;assembly=ExtendedXmlSerializer.Samples">
      <Id>1</Id>
      <Type>Type</Type>
    </TestClass>

Then you renamed property:

    public class TestClass
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

and generated XML look like:

    <?xml version="1.0" encoding="utf-8"?>
    <TestClass xmlns:exs="https://extendedxmlserializer.github.io/v2" exs:version="1" xmlns="clr-namespace:ExtendedXmlSerialization.Samples.MigrationMap;assembly=ExtendedXmlSerializer.Samples">
      <Id>1</Id>
      <Name>Type</Name>
    </TestClass>

Then, you added new property and you wanted to calculate a new value during deserialization.

    public class TestClass
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Value { get; set; }
    }

and new XML should look like:

    <?xml version="1.0" encoding="utf-8"?>
    <TestClass xmlns:exs="https://extendedxmlserializer.github.io/v2" exs:version="2" xmlns="clr-namespace:ExtendedXmlSerializer.Samples.MigrationMap;assembly=ExtendedXmlSerializer.Samples">
      <Id>1</Id>
      <Name>Type</Name>
      <Value>Calculated</Value>
    </TestClass>

You can migrate (read) old version of XML using migrations:

    public class TestClassMigrations : IEnumerable<Action<XElement>>
    {
        public static void MigrationV0(XElement node)
        {
            XElement typeElement = node.Member("Type");
            // Add new node
            node.Add(new XElement("Name", typeElement.Value));
            // Remove old node
            typeElement.Remove();
        }
    
        public static void MigrationV1(XElement node)
        {
            // Add new node
            node.Add(new XElement("Value", "Calculated"));
        }
    
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    
        public IEnumerator<Action<XElement>> GetEnumerator()
        {
            yield return MigrationV0;
            yield return MigrationV1;
        }
    }

Then, you must register your TestClassMigrations class in configuration

    IExtendedXmlSerializer serializer = new ConfigurationContainer().ConfigureType<TestClass>()
                                                                    .AddMigration(new TestClassMigrations())
                                                                    .Create();

Unknown Content

Consider the following class:

class Subject {
    public string Message { get; set; }
}

And the following Xml:

<Subject>
	<Unknown>Unexpected content will abort.</Unknown>
    <Message>Hello World!</Message>
</Subject>

By default as of 3.x functionality, ExtendedXmlSerializer aborts its reading of the XML document when it reaches any content that it cannot deserialize into the target object. In this case, this is the Unknown element in the provided XML document.

The result of this aborted operation is an incomplete object that is not fully deserialized, with values not applied to object properties that were not reached before the encountered invalid content in the XML document.

It is fair to say that this is not exactly ideal functionality and it should rather throw to promote better visibility of an invalid document. In a future breaking change version this will be done after considering community feedback.

However, in the meantime, ExtendedXmlSerializer does provide some control over this scenario for v3.x, via the WithUnknownContent method call:

var serializer = new ConfigurationContainer().EnableImplicitTyping(typeof(Subject))
                                             .WithUnknownContent()
                                             .Continue()
                                             .Create();
...

The WithUnknownContent has 3 different options:

  1. Continue. This will ignore unknown content and continue reading the document, attempting to process the next element.
  2. Throw. This will throw an exception whenever unknown content is encountered.
  3. Call. Provide a delegate to invoke whenever unknown content is encountered (for logging, etc.)

More discussion around this can be found at the following issue: https://github.com/ExtendedXmlSerializer/home/issues/393