I am using SharpSerializer to serialize/deserialize object.
I want the ability to ignore specific properties when deserializing.
SharpSerializer has an option to ignore properties by attribute or by classes and property name:
SharpSerializerSettings.AdvancedSettings.AttributesToIgnore
SharpSerializerSettings.AdvancedSettings.PropertiesToIgnore
but it seems that these settings are only used to ignore from serialization, not from deserialization (I tested with the GitHub source code and the NugetPackage).
Am I correct?
Is there any way to ignore attributes/properties from deserialization?
P.S.
I'm sure there are other great serialization libraries, but it will take a great amount of effort to change the code and all the existing serialized files.
I opened an issue on the GitHub project, but the project does not seem to be active since 2018.
The object with properties to ignore need not be the root object.
You are correct that SharpSerializer does not implement ignoring of property values when deserializing. This can be verified from the reference source for ObjectFactory.fillProperties(object obj, IEnumerable<Property> properties):
private void fillProperties(object obj, IEnumerable<Property> properties)
{
foreach (Property property in properties)
{
PropertyInfo propertyInfo = obj.GetType().GetProperty(property.Name);
if (propertyInfo == null) continue;
object value = CreateObject(property);
if (value == null) continue;
propertyInfo.SetValue(obj, value, _emptyObjectArray);
}
}
This code unconditionally sets any property read from the serialization stream into the incoming object using reflection, without checking the list of ignored attributes or properties.
Thus the only way to ignore your desired properties would seem to be to create your own versions of XmlPropertyDeserializer or BinaryPropertyDeserializer that skip or filter the unwanted properties. The following is one possible implementation for XML. This implementation reads the properties from XML into a Property hierarchy as usual, then applies a filter action to remove properties corresponding to .NET properties with a custom attribute [SharpSerializerIgnoreForDeserialize] applied, then finally creates the object tree using the pruned Property.
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class SharpSerializerIgnoreForDeserializeAttribute : System.Attribute { }
public class PropertyDeserializerDecorator : IPropertyDeserializer
{
readonly IPropertyDeserializer deserializer;
public PropertyDeserializerDecorator(IPropertyDeserializer deserializer) => this.deserializer = deserializer ?? throw new ArgumentNullException();
public virtual void Open(Stream stream) => deserializer.Open(stream);
public virtual Property Deserialize() => deserializer.Deserialize();
public virtual void Close() => deserializer.Close();
}
public class CustomPropertyDeserializer : PropertyDeserializerDecorator
{
Action<Property> deserializePropertyAction;
public CustomPropertyDeserializer(IPropertyDeserializer deserializer, Action<Property> deserializePropertyAction = default) : base(deserializer) => this.deserializePropertyAction = deserializePropertyAction;
public override Property Deserialize()
{
var property = base.Deserialize();
if (deserializePropertyAction != null)
property.WalkProperties(p => deserializePropertyAction(p));
return property;
}
}
public static partial class SharpSerializerExtensions
{
public static SharpSerializer Create(SharpSerializerXmlSettings settings, Action<Property> deserializePropertyAction = default)
{
// Adapted from https://github.com/polenter/SharpSerializer/blob/42f9a20b3934a7f2cece356cc8116a861cec0b91/SharpSerializer/SharpSerializer.cs#L139
// By https://github.com/polenter
var typeNameConverter = settings.AdvancedSettings.TypeNameConverter ??
new TypeNameConverter(
settings.IncludeAssemblyVersionInTypeName,
settings.IncludeCultureInTypeName,
settings.IncludePublicKeyTokenInTypeName);
// SimpleValueConverter
var simpleValueConverter = settings.AdvancedSettings.SimpleValueConverter ?? new SimpleValueConverter(settings.Culture, typeNameConverter);
// XmlWriterSettings
var xmlWriterSettings = new XmlWriterSettings
{
Encoding = settings.Encoding,
Indent = true,
OmitXmlDeclaration = true,
};
// XmlReaderSettings
var xmlReaderSettings = new XmlReaderSettings
{
IgnoreComments = true,
IgnoreWhitespace = true,
};
// Create Serializer and Deserializer
var reader = new DefaultXmlReader(typeNameConverter, simpleValueConverter, xmlReaderSettings);
var writer = new DefaultXmlWriter(typeNameConverter, simpleValueConverter, xmlWriterSettings);
var _serializer = new XmlPropertySerializer(writer);
var _deserializer = new CustomPropertyDeserializer(new XmlPropertyDeserializer(reader), deserializePropertyAction);
var serializer = new SharpSerializer(_serializer, _deserializer)
{
//InstanceCreator = settings.InstanceCreator ?? new DefaultInstanceCreator(), -- InstanceCreator not present in SharpSerializer 3.0.1
RootName = settings.AdvancedSettings.RootName,
};
serializer.PropertyProvider.PropertiesToIgnore = settings.AdvancedSettings.PropertiesToIgnore;
serializer.PropertyProvider.AttributesToIgnore = settings.AdvancedSettings.AttributesToIgnore;
return serializer;
}
public static void WalkProperties(this Property property, Action<Property> action)
{
if (action == null || property == null)
throw new ArgumentNullException();
// Avoid cyclic dependencies.
// Reference.IsProcessed is true only for the first reference of an object.
bool skipProperty = property is ReferenceTargetProperty refTarget
&& refTarget.Reference != null
&& !refTarget.Reference.IsProcessed;
if (skipProperty) return;
action(property);
switch (property.Art)
{
case PropertyArt.Collection:
{
foreach (var item in ((CollectionProperty)property).Items)
item.WalkProperties(action);
}
break;
case PropertyArt.Complex:
{
foreach (var item in ((ComplexProperty)property).Properties)
item.WalkProperties(action);
}
break;
case PropertyArt.Dictionary:
{
foreach (var item in ((DictionaryProperty)property).Items)
{
item.Key.WalkProperties(action);
item.Value.WalkProperties(action);
}
}
break;
case PropertyArt.MultiDimensionalArray:
{
foreach (var item in ((MultiDimensionalArrayProperty )property).Items)
item.Value.WalkProperties(action);
}
break;
case PropertyArt.Null:
case PropertyArt.Simple:
case PropertyArt.Reference:
break;
case PropertyArt.SingleDimensionalArray:
{
foreach (var item in ((SingleDimensionalArrayProperty)property).Items)
item.WalkProperties(action);
}
break;
default:
throw new NotImplementedException(property.Art.ToString());
}
}
public static void RemoveIgnoredChildProperties(Property p)
{
if (p.Art == PropertyArt.Complex)
{
var items = ((ComplexProperty)p).Properties;
for (int i = items.Count - 1; i >= 0; i--)
{
if (p.Type.GetProperty(items[i].Name)?.IsDefined(typeof(SharpSerializerIgnoreForDeserializeAttribute), true) == true)
{
items.RemoveAt(i);
}
}
}
}
}
Then, given the following models:
public class Root
{
public List<Model> Models { get; set; } = new ();
}
public class Model
{
public string Value { get; set; }
[SharpSerializerIgnoreForDeserialize]
public string IgnoreMe { get; set; }
}
You would deserialize using the customized XmlPropertyDeserializer as follows:
var settings = new SharpSerializerXmlSettings();
var customSerialzier = SharpSerializerExtensions.Create(settings, SharpSerializerExtensions.RemoveIgnoredChildProperties);
var deserialized = (Root)customSerialzier.Deserialize(stream);
If you need binary deserialization, use the following factory method to create the serializer instead:
public static partial class SharpSerializerExtensions
{
public static SharpSerializer Create(SharpSerializerBinarySettings settings, Action<Property> deserializePropertyAction = default)
{
// Adapted from https://github.com/polenter/SharpSerializer/blob/42f9a20b3934a7f2cece356cc8116a861cec0b91/SharpSerializer/SharpSerializer.cs#L168
// By https://github.com/polenter
var typeNameConverter = settings.AdvancedSettings.TypeNameConverter ??
new TypeNameConverter(
settings.IncludeAssemblyVersionInTypeName,
settings.IncludeCultureInTypeName,
settings.IncludePublicKeyTokenInTypeName);
// Create Serializer and Deserializer
Polenter.Serialization.Advanced.Binary.IBinaryReader reader;
Polenter.Serialization.Advanced.Binary.IBinaryWriter writer;
if (settings.Mode == BinarySerializationMode.Burst)
{
// Burst mode
writer = new BurstBinaryWriter(typeNameConverter, settings.Encoding);
reader = new BurstBinaryReader(typeNameConverter, settings.Encoding);
}
else
{
// Size optimized mode
writer = new SizeOptimizedBinaryWriter(typeNameConverter, settings.Encoding);
reader = new SizeOptimizedBinaryReader(typeNameConverter, settings.Encoding);
}
var _serializer = new BinaryPropertySerializer(writer);
var _deserializer = new CustomPropertyDeserializer(new BinaryPropertyDeserializer(reader), deserializePropertyAction);
var serializer = new SharpSerializer(_serializer, _deserializer)
{
//InstanceCreator = settings.InstanceCreator ?? new DefaultInstanceCreator(), -- InstanceCreator not present in SharpSerializer 3.0.1
RootName = settings.AdvancedSettings.RootName,
};
serializer.PropertyProvider.PropertiesToIgnore = settings.AdvancedSettings.PropertiesToIgnore;
serializer.PropertyProvider.AttributesToIgnore = settings.AdvancedSettings.AttributesToIgnore;
return serializer;
}
}
And do:
var settings = new SharpSerializerBinarySettings();
var customSerialzier = SharpSerializerExtensions.Create(settings, SharpSerializerExtensions.RemoveIgnoredChildProperties);
var deserialized = (Root)customSerialzier.Deserialize(stream);
Notes:
The methods SharpSerializerExtensions.Create() were modeled on SharpSerializer.initialize(SharpSerializerXmlSettings settings) and SharpSerializer.initialize(SharpSerializerBinarySettings settings) by Pawel Idzikowski
The version of SharpSerializer available on nuget, version 3.0.1, only includes commits through 10/8/2017. Submissions since then that add the ability to use Autofac as the instance creator are not available via nuget. My code is based on the version available via nuget, and thus does not initialize SharpSerializer.InstanceCreator which was added in 2018. The project appears not to have updated at all since then.
SharpSerializer.Deserialize() deserializes to the type specified in the serialization stream rather than to a type specified by the caller. It thus appears vulnerable to the sort of type injection attacks described in Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf.
For details see e.g. TypeNameHandling caution in Newtonsoft Json.
If you are willing to fork, modify and build SharpSerializer yourself, you might consider updating ObjectFactory.fillProperties(object obj, IEnumerable<Property> properties) to not set ignored properties.
Demo fiddle #1 here for XML, and #2 here for binary.
Related
I've been working on a music game and decided to add convertation of other games' levels. One of the games I decided to convert from uses JSON to store it's levels and so I'm using Newtonsoft.Json for deserializing level data. Level have can 2 object types that are stored in a single array/list with one shared property and one individual property. Keeping that in mind I made level class with it's properties, one base and two inherited classes:
class Chart
{
//Some unimportant properties
//...
public Note[] note;
class Note
{
public int[] beat;
}
class NoteSingle : Note
{
public int x;
}
class NoteRain : Note
{
public int[] endbeat;
}
}
However, when I try deserialize level, note only contains base objects. I tried creating JsonSerializerSettings with TypeNameHandling set to All and passing it to deserialization method, but it didn't worked, note still only have base classes in it.
Basically I need to load level from the file, deserialize it as Chart and make each of the notes in note be one of the types inherited from Note depending on json data. Like if note has x field then load it as NoteSingle and if it has endbeat field then load it as NoteRain.
Repres
class Note
{
public int[] beat;
}
class NoteSingle : Note
{
public int x;
}
class NoteRain : Note
{
public int[] endbeat;
}
class Chart
{
//Some unimportant properties
public Note[] note;
//Some unimportant properties
}
public static void Convert(string path)
{
string rawData = File.ReadAllText(path);
JsonSerializerSettings setts = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
Chart ch = JsonConvert.DeserializeObject<Chart>(rawData, setts);
//Level converter code
}
Example data I'm trying to load: https://pastebin.com/zgnRsgWZ
What am I doing wrong?
I try with this code:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
};
var chart = new Chart();
chart.note = new Note[]
{
new NoteSingle { x = 37 },
new NoteRain { endbeat = new[] { 9 } }
};
var json = JsonConvert.SerializeObject(chart, settings);
var chart2 = JsonConvert.DeserializeObject<Chart>(json, settings);
And it's working. json has this value:
{
"$type":"Test.Chart, SoApp",
"note":{
"$type":"Test.Chart+Note[], SoApp",
"$values":[
{
"$type":"Test.Chart+NoteSingle, SoApp",
"x":37,
"beat":null
},
{
"$type":"Test.Chart+NoteRain, SoApp",
"endbeat":{
"$type":"System.Int32[], mscorlib",
"$values":[
9
]
},
"beat":null
}
]
}
}
And chart2 has 2 notes of NoteSingle and NoteRain types. Maybe you aren't using TypeNameHandling.All in Serialize. You need to use both on Serialize and Deserialize.
UPDATE
If you haven't control of the generated JSON, you can use a Converter to deserialize it:
public class YourJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var chart = new Chart();
var notes = new List<Note>();
string name = null;
NoteRain noteRain = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;
case JsonToken.Integer:
if (name == "x")
{
var note = new NoteSingle { x = Convert.ToInt32(reader.Value) };
notes.Add(note);
}
else if (name == "endbeat")
{
if (noteRain == null)
{
noteRain = new NoteRain { endbeat = new[] { Convert.ToInt32(reader.Value) } };
notes.Add(noteRain);
}
else
{
var array = noteRain.endbeat;
noteRain.endbeat = new int[noteRain.endbeat.Length + 1];
for (int i = 0; i < array.Length; i++)
{
noteRain.endbeat[i] = array[i];
}
noteRain.endbeat[noteRain.endbeat.Length - 1] = Convert.ToInt32(reader.Value);
}
}
break;
}
}
chart.note = notes.ToArray();
return chart;
}
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return true;
}
}
This is a simple example, you must tune it but I think it's easy to do. In the property name I get the name of the property and I use later to create the correct type. If I process a x property I know that is a NoteSingle and I create it and add to notes list.
If, for example, you get a property name like beat and you don't know yet the type of the Note class, use a temporary variable to store and fill and later, when you read a property that you know is from a concrete class, create the Note instance and fill with this variable. And after that, If you read more data of this class, continue filling your instance.
Usage:
var settings = new JsonSerializerSettings();
settings.Converters.Add(new YourJsonConverter());
var chart = JsonConvert.DeserializeObject<Chart>(json, settings);
I'm trying to create my first SSIS custom source component but I can't get it to save the custom properties into the .dtsx file.
According to https://learn.microsoft.com/en-us/sql/integration-services/extending-packages-custom-objects/persisting-custom-objects , all I needed is to implement the IDTSComponentPersist interface, but this doesn't work, the LoadFromXML and SaveToXML are never called. Neither when I save the file nor when I load the package.
However, if your object has properties that use complex data types, or
if you want to perform custom processing on property values as they
are loaded and saved, you can implement the IDTSComponentPersist
interface and its LoadFromXML and SaveToXML methods. In these methods
you load from (or save to) the XML definition of the package an XML
fragment that contains the properties of your object and their current
values. The format of this XML fragment is not defined; it must only
be well-formed XML.
When I save the SSIS package and look inside the XML, I get this, no data type defined and no values :
Did I miss to set something?
To simplify, I created a small test project. The original project try to save a list of struct with 2 string and 1 integer, but both has the same "incorrect" behavior, SaveToXML and LoadFromXML are never called.
Here's my code:
using System;
using System.Collections.Generic;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Runtime;
using System.Xml;
using System.ComponentModel;
using System.Globalization;
using System.Drawing.Design;
using System.Windows.Forms.Design;
using System.Windows.Forms;
namespace TestCase
{
public class MyConverter : TypeConverter
{
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return false;
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType.Name.ToUpper() == "STRING")
return string.Join(",", ((List<string>)value).ToArray());
else
return ((string)value).Split(',');
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value.GetType().Name.ToUpper() == "STRING")
return ((string)value).Split(',');
else
return string.Join(",", ((List<string>)value).ToArray());
}
}
class FancyStringEditor : UITypeEditor
{
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
{
var svc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
List<string> vals = (List<string>)value;
string valsStr = string.Join("\r\n", vals.ToArray());
if (svc != null)
{
using (var frm = new Form { Text = "Your editor here" })
using (var txt = new TextBox { Text = valsStr, Dock = DockStyle.Fill, Multiline = true })
using (var ok = new Button { Text = "OK", Dock = DockStyle.Bottom })
{
frm.Controls.Add(txt);
frm.Controls.Add(ok);
frm.AcceptButton = ok;
ok.DialogResult = DialogResult.OK;
if (svc.ShowDialog(frm) == DialogResult.OK)
{
vals = new List<string>();
vals.AddRange(txt.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries));
value = vals;
}
}
}
return value;
}
}
[DtsPipelineComponent(ComponentType = ComponentType.SourceAdapter,
CurrentVersion = 0,
Description = "Test class for saving",
DisplayName = "Test class",
IconResource = "None",
NoEditor = false,
RequiredProductLevel = Microsoft.SqlServer.Dts.Runtime.Wrapper.DTSProductLevel.DTSPL_NONE,
SupportsBackPressure = false,
UITypeName = "None")]
public class TestSave : PipelineComponent, IDTSComponentPersist
{
private string _NbBadWordProperty = "NbBadWord";
private string _ListBadWordsProperty = "ListBadWords";
private List<string> _badWords;
public IDTSCustomProperty100 _nb;
public IDTSCustomProperty100 _list;
public TestSave()
{
_badWords = new List<string>();
_badWords.Add("Word1");
_badWords.Add("Word2");
_badWords.Add("Word3");
}
public void LoadFromXML(System.Xml.XmlElement node, IDTSInfoEvents infoEvents)
{
System.Windows.Forms.MessageBox.Show("Oh god! we're inside LoadFromXML!!");
}
public void SaveToXML(System.Xml.XmlDocument doc, IDTSInfoEvents infoEvents)
{
System.Windows.Forms.MessageBox.Show("Oh god! we're inside SaveToXML!!");
XmlElement elementRoot;
XmlNode propertyNode;
// Create a new node to persist the object and its properties.
elementRoot = doc.CreateElement(String.Empty, "NBElement", String.Empty);
XmlAttribute nbEl = doc.CreateAttribute("Nbelement");
nbEl.Value = _badWords.Count.ToString();
elementRoot.Attributes.Append(nbEl);
// Save the three properties of the object from variables into XML.
foreach (string s in _badWords)
{
propertyNode = doc.CreateNode(XmlNodeType.Element, "BadWord", String.Empty);
propertyNode.InnerText = s;
elementRoot.AppendChild(propertyNode);
}
doc.AppendChild(elementRoot);
}
private IDTSCustomProperty100 GetCustomPropertyByName(string name)
{
foreach (IDTSCustomProperty100 prop in this.ComponentMetaData.CustomPropertyCollection)
if (prop.Name.ToUpper() == name)
return prop;
return null;
}
public override DTSValidationStatus Validate()
{
return DTSValidationStatus.VS_ISVALID;
}
public override void ProvideComponentProperties()
{
try
{
base.ProvideComponentProperties();
// reset the component
this.ComponentMetaData.OutputCollection.RemoveAll();
this.ComponentMetaData.InputCollection.RemoveAll();
// Add custom properties
if (GetCustomPropertyByName(_NbBadWordProperty) == null)
{
_nb = this.ComponentMetaData.CustomPropertyCollection.New();
_nb.Name = _NbBadWordProperty;
_nb.Description = "Number of bad word to filter";
_nb.State = DTSPersistState.PS_DEFAULT;
_nb.Value = _badWords.Count;
_nb.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
}
if (GetCustomPropertyByName(_ListBadWordsProperty) == null)
{
IDTSCustomProperty100 _list = this.ComponentMetaData.CustomPropertyCollection.New();
_list.Name = _ListBadWordsProperty;
_list.Description = "List of bad words";
_list.State = DTSPersistState.PS_DEFAULT;
_list.TypeConverter = typeof(MyConverter).AssemblyQualifiedName;
_list.Value = _badWords;
_list.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
_list.UITypeEditor = typeof(FancyStringEditor).AssemblyQualifiedName;
}
// add input objects
// none
// add output objects
IDTSOutput100 o2 = this.ComponentMetaData.OutputCollection.New();
o2.Name = "Dummy output";
o2.IsSorted = false;
foreach (IDTSCustomProperty100 p in this.ComponentMetaData.CustomPropertyCollection)
{
if (p.Name == _ListBadWordsProperty)
{
MyConverter c = new MyConverter();
List<string> l = (List<string>)p.Value;
foreach (string s in l)
{
IDTSOutputColumn100 col1 = o2.OutputColumnCollection.New();
col1.Name = s.Trim();
col1.Description = "Bad word";
col1.SetDataTypeProperties(Microsoft.SqlServer.Dts.Runtime.Wrapper.DataType.DT_WSTR, 500, 0, 0, 0);
}
}
}
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show("Critical error: " + ex.Message);
}
}
}
}
Update1:
Add the TypeConverter and UITypeEditor. Still the same behavior (not saving the "complex" data type).
When I add the source component to a data flow, I got this, everything look fine:
I can edit the property, no problem
But when I save the SSIS package and look at the xml, the property still not saved and still have a datatype of System.NULL:
Thanks!
Important Note - based on Microsoft definition of IDTSComponentPersist Interface and code samples of SaveToXML found on Internet, I suspect that custom persistence can only be implemented on custom SSIS Tasks, Connection Managers and Enumerators.
Well, please choose for yourself whether do you really need to implement custom object persistence. Your custom properties seems to fit well into standard data types Int32 and String.
Important note from Microsoft -
When you implement custom persistence, you must persist all the properties of the object, including both inherited properties and custom properties that you have added.
So, you really have to do a lot of work to persist all properties of component including LocaleID from your sample - in case someone needs to alter it. I would probably do storing ListBadWords custom property as a string without custom XML persistence.
On your code -- the most possible cause of the System.Null data type problem is that ProvideComponentProperties() method is called on initialization of the component, when it is added on the Data Flow. Data type of the property is determined dynamically at this moment, the variable _badwords is not initialized yet and is a reference type, so it is defined as Null reference. The ProvideComponentProperties() method is used to define custom properties and set its default values, to solve your problem - set
if (GetCustomPropertyByName(_ListBadWordsProperty) == null)
{
IDTSCustomProperty100 _list = this.ComponentMetaData.CustomPropertyCollection.New();
_list.Name = _ListBadWordsProperty;
_list.Description = "List of bad words";
_list.State = DTSPersistState.PS_DEFAULT;
_list.TypeConverter = typeof(MyConverter).AssemblyQualifiedName;
// This is the change
_list.Value = String.Empty;
_list.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
_list.UITypeEditor = typeof(FancyStringEditor).AssemblyQualifiedName;
}
If you set yourself up on implementing custom XML persistence - please study Microsoft code sample and other sources. Saving is done a little bit other way. The main difference is that inside elementRoot of the component properties, each property is created under its own XML Node. Node's InnerText is used to store property value, and optional Node's attributes can store additional information.
I have added new fields to a persisted class, and need to ensure that they are set to sensible defaults when loading older versions of the XAML serialized file from disk. Previously, with the BinaryFormatter, I would use the OnDeserialization method to work out what defaults I should set if new fields have been added to a persisted class (using the OptionalField atttribute). E.g.:
/// <summary>
/// Runs when the entire object graph has been deserialized.
/// </summary>
/// <param name="sender">The object that initiated the callback. The functionality for this parameter is not currently implemented.</param>
public override void
OnDeserialization
(object sender)
{
I can't seem to find anything equivalent so far when writing to a XAML file, e.g:
using (TextReader reader = File.OpenText(filePath))
{
protocol = (Protocol)XamlServices.Load(reader);
}
I would like to ensure that older files which don't contain the new optional fields within the Protocol type (in the example code above) have sensible default values within. I've hunted around but can't seem to find anything obvious (e.g. https://ludovic.chabant.com/devblog/2008/06/25/almost-everything-you-need-to-know-about-xaml-serialization-part-2/) Is there any equivalent?
XamlServices internally uses a XamlObjectWriter. This type has a XamlObjectWriterSettings parameter that includes various callbacks. These aren't exposed by XamlServices, but its functionality is easily replicated.
I haven't tested this extensively, but this seems to work:
public static object LoadXaml(TextReader textReader)
{
var settings = new XamlObjectWriterSettings
{
AfterBeginInitHandler = (s, e) => Debug.Print($"Before deserializing {e.Instance}"),
AfterEndInitHandler = (s, e) => Debug.Print($"After deserializing {e.Instance}")
};
using (var xmlReader = XmlReader.Create(textReader))
using (var xamlReader = new XamlXmlReader(xmlReader))
using (var xamlWriter = new XamlObjectWriter(xamlReader.SchemaContext, settings))
{
XamlServices.Transform(xamlReader, xamlWriter);
return xamlWriter.Result;
}
}
e.Instance contains the object being deserialized. Not sure which callback is best for your purpose. They're more equivalent to the [OnDeserializing]/[OnDeserialized] attributes because they're called when the individual object is deserialized, rather than after the entire graph is complete like IDeserializationCallback.OnDeserialization.
Here is a more complete implementation of a class that provides events during serialization. XamlObjectReader doesn't support callbacks like XamlObjectWriter does, so this uses a workaround. It only raises an event before but not after serializing an object for the reasons explained in the comments.
public class CallbackXamlService
{
// Default settings that XamlService uses
public XmlWriterSettings XmlWriterSettings { get; set; }
= new XmlWriterSettings { Indent = true, OmitXmlDeclaration = true };
public event EventHandler<XamlObjectEventArgs> BeforeDeserializing;
public event EventHandler<XamlObjectEventArgs> AfterDeserializing;
public event EventHandler<XamlObjectEventArgs> BeforeSerializing;
// AfterSerializing event doesn't seem to be easily possible, see below
public object LoadXaml(TextReader textReader)
{
var settings = new XamlObjectWriterSettings
{
BeforePropertiesHandler = (s, e) => BeforeDeserializing?.Invoke(this, e),
AfterPropertiesHandler = (s, e) => AfterDeserializing?.Invoke(this, e)
};
using (var xmlReader = XmlReader.Create(textReader))
using (var xamlReader = new XamlXmlReader(xmlReader))
using (var xamlWriter = new XamlObjectWriter(xamlReader.SchemaContext, settings))
{
XamlServices.Transform(xamlReader, xamlWriter);
return xamlWriter.Result;
}
}
public string SaveXaml(object instance)
{
var stringBuilder = new StringBuilder();
using (var textWriter = new StringWriter(stringBuilder))
SaveXaml(textWriter, instance);
return stringBuilder.ToString();
}
public void SaveXaml(TextWriter textWriter, object instance)
{
Action<object> beforeSerializing = (obj) => BeforeSerializing?.Invoke(this, new XamlObjectEventArgs(obj));
// There are no equivalent callbacks on XamlObjectReaderSettings
// Using a derived XamlObjectReader to track processed objects instead
using (var xmlWriter = XmlWriter.Create(textWriter, XmlWriterSettings))
using (var xamlXmlWriter = new XamlXmlWriter(xmlWriter, new XamlSchemaContext()))
using (var xamlObjectReader = new CallbackXamlObjectReader(instance, xamlXmlWriter.SchemaContext, null, beforeSerializing))
{
XamlServices.Transform(xamlObjectReader, xamlXmlWriter);
xmlWriter.Flush();
}
}
private class CallbackXamlObjectReader : XamlObjectReader
{
public Action<object> BeforeSerializing { get; }
//private Stack<object> instanceStack = new Stack<object>();
public CallbackXamlObjectReader(object instance, XamlSchemaContext schemaContext, XamlObjectReaderSettings settings, Action<object> beforeSerializing)
: base(instance, schemaContext, settings)
{
BeforeSerializing = beforeSerializing;
}
public override bool Read()
{
if (base.Read())
{
if (NodeType == XamlNodeType.StartObject)
{
//instanceStack.Push(Instance);
BeforeSerializing?.Invoke(Instance);
}
// XamlObjectReader.Instance is not set on EndObject nodes
// EndObject nodes do not line up with StartObject nodes when types like arrays and dictionaries
// are involved, so using a stack to track the current instance doesn't always work.
// Don't know if there is a reliable way to fix this without possibly fragile special-casing,
// the XamlObjectReader internals are horrendously complex.
//else if (NodeType == XamlNodeType.EndObject)
//{
// object instance = instanceStack.Pop();
// AfterSerializing(instance);
//}
return true;
}
return false;
}
}
}
I want so serialize/deserialize the following model:
public class ReadabilitySettings
{
public ReadabilitySettings() {
}
private bool _reababilityEnabled;
public bool ReadabilityEnabled {
get {
return _reababilityEnabled;
}
set {
_reababilityEnabled = value;
}
}
private string _fontName;
public string FontName {
get {
return _fontName;
}
set {
_fontName = value;
}
}
private bool _isInverted;
public bool IsInverted {
get {
return _isInverted;
}
set {
_isInverted = value;
}
}
public enum FontSizes
{
Small = 0,
Medium = 1,
Large = 2
}
private FontSizes _fontSize;
public FontSizes FontSize { get
{
return _fontSize;
}
set
{
_fontSize = value;
}
}
}
}
I have a list containing instances of the following object:
public class CacheItem<T>
{
public string Key { get; set; }
public T Value { get; set; }
}
I populate the list like so:
list.Add(new CacheItem<ReadabilitySettings>() { Key = "key1", Value = InstanceOfReadabilitySettings };
When I want to serialize this list I call:
var json = JsonConvert.SerializeObject (list);
This works fine. No errors. It gives the following json:
[{"Key":"readbilitySettings","Value":{"ReadabilityEnabled":true,"FontName":"Lora","IsInverted":true,"FontSize":2}}]
When I want to deserialize the list I call:
var list = JsonConvert.DeserializeObject<List<CacheItem<object>>> (json);
This gives me a list of CacheItem's with it's Value property set to a JObject. No errors so far.
When I want the actual instance of ReadabilitySettings I call:
var settings = JsonConvert.DeserializeObject<ReadabilitySettings> (cacheItem.Value.ToString ());
I have to call this since the cacheItem.Value is set to a json string, not to an instance of ReadabilitySettings. The json string is:
{{ "ReadabilityEnabled": true, "FontName": "Lora", "IsInverted": true, "FontSize": 2 }} Newtonsoft.Json.Linq.JObject
Then I get this error: "Error setting value to 'ReadabilityEnabled' on 'Reflect.Mobile.Shared.State.ReadabilitySettings'."
What am I missing? Thanks!
EDIT------
This is the method that throws the error:
public T Get<T> (string key)
{
var items = GetCacheItems (); // this get the initial deserialized list of CacheItems<object> with its value property set to a JObject
if (items == null)
throw new CacheKeyNotFoundException ();
var item = items.Find (q => q.Key == key);
if (item == null)
throw new CacheKeyNotFoundException ();
var result = JsonConvert.DeserializeObject<T> (item.Value.ToString ()); //this throws the error
return result;
}
I've just tried this, which is pretty much copy/pasting your code and it works fine using Newtonsoft.Json v6.0.0.0 (from NuGet):
var list = new List<CacheItem<ReadabilitySettings>>();
list.Add(new CacheItem<ReadabilitySettings>() { Key = "key1", Value = new ReadabilitySettings() { FontName = "FontName", FontSize = ReadabilitySettings.FontSizes.Large, IsInverted = false, ReadabilityEnabled = true } });
var json = JsonConvert.SerializeObject(list);
var list2 = JsonConvert.DeserializeObject<List<CacheItem<object>>>(json);
var settings = JsonConvert.DeserializeObject<ReadabilitySettings>(list2.First().Value.ToString());
However, you don't need the last line. Simply switch List<CacheItem<object>> to List<CacheItem<ReadabilitySettings>> in your call to DeserializeObject and it automatically resolves it:
var list2 = JsonConvert.DeserializeObject<List<CacheItem<ReadabilitySettings>>>(json);
Now list2.First().Value.GetType() = ReadabilitySettings and there's no need to do any further deserialising. Is there a reason you're using object?
Edit:
I'm not sure if this helps you necessarily, but given what you're trying to do have you thought about using a custom converter? I had to do this a few days ago for similar reasons. I had an enum property coming back in my JSON (similar to your key) which gave a hint as to what type a property in my JSON and therefore my deserialised class was. The property in my class was an interface rather than an object but the same principle applies for you.
I used a custom converter to automatically handle creating the object of the correct type cleanly.
Here's a CustomConverter for your scenario. If your key is set to readbilitySettings then result.Value is initialised as ReadabilitySettings.
public class CacheItemConverter : CustomCreationConverter<CacheItem<object>>
{
public override CacheItem<object> Create(Type objectType)
{
return new CacheItem<object>();
}
public CacheItem<object> Create(Type objectType, JObject jObject)
{
var keyProperty = jObject.Property("Key");
if (keyProperty == null)
throw new ArgumentException("Key missing.");
var result = new CacheItem<object>();
var keyValue = keyProperty.First.Value<string>();
if (keyValue.Equals("readbilitySettings", StringComparison.InvariantCultureIgnoreCase))
result.Value = new ReadabilitySettings();
else
throw new ArgumentException(string.Format("Unsupported key {0}", keyValue));
return result;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
/* Here your JSON is deserialised and mapped to your object */
return target;
}
}
Usage:
var list = JsonConvert.DeserializeObject<List<CacheItem<object>>>(json, new CacheItemConverter());
Here's a link to a full working example of it:
http://pastebin.com/PJSvFDsT
Managed to solve it. Sample code did not show that ReadabilitySettings also implemented the INotifyPropertyChanged interface. The subsequent eventhandler wired-up to the PropertyChanged event of ReadabilitySettings somewhere else in the project had some errors and thus the deserializer was not able to instantiate ReadabilitySettings :-).
Not a glamorous save but its working... Thanks for your time.
I know that the same problem is faced by a lot of people in one way or another but what I'm confused about is that how come Newtonsoft JSON Serializer is able to correctly handle this case while JavaScriptSerializer fails to do so.
I'm going to use the same code sample used in one of the other stackoverflow thread (JavascriptSerializer serializing property twice when "new" used in subclass)
void Main()
{
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var json = serializer.Serialize(new Limited());
Limited status = serializer.Deserialize<Limited>(json); --> throws AmbiguousMatchException
}
public class Full
{
public String Stuff { get { return "Common things"; } }
public FullStatus Status { get; set; }
public Full(bool includestatus)
{
if(includestatus)
Status = new FullStatus();
}
}
public class Limited : Full
{
public new LimitedStatus Status { get; set; }
public Limited() : base(false)
{
Status = new LimitedStatus();
}
}
public class FullStatus
{
public String Text { get { return "Loads and loads and loads of things"; } }
}
public class LimitedStatus
{
public String Text { get { return "A few things"; } }
}
But if I use Newtonsoft Json Serializer, everythings works fine. Why? And is it possible to achieve the same using JavaScriptSerializer?
void Main()
{
var json = JsonConvert.SerializeObject(new Limited());
Limited status = JsonConvert.DeserializeObject<Limited>(json); ----> Works fine.
}
The reason this works in Json.NET is that it has specific code to handle this situation. From JsonPropertyCollection.cs:
/// <summary>
/// Adds a <see cref="JsonProperty"/> object.
/// </summary>
/// <param name="property">The property to add to the collection.</param>
public void AddProperty(JsonProperty property)
{
if (Contains(property.PropertyName))
{
// don't overwrite existing property with ignored property
if (property.Ignored)
return;
JsonProperty existingProperty = this[property.PropertyName];
bool duplicateProperty = true;
if (existingProperty.Ignored)
{
// remove ignored property so it can be replaced in collection
Remove(existingProperty);
duplicateProperty = false;
}
else
{
if (property.DeclaringType != null && existingProperty.DeclaringType != null)
{
if (property.DeclaringType.IsSubclassOf(existingProperty.DeclaringType))
{
// current property is on a derived class and hides the existing
Remove(existingProperty);
duplicateProperty = false;
}
if (existingProperty.DeclaringType.IsSubclassOf(property.DeclaringType))
{
// current property is hidden by the existing so don't add it
return;
}
}
}
if (duplicateProperty)
throw new JsonSerializationException("A member with the name '{0}' already exists on '{1}'. Use the JsonPropertyAttribute to specify another name.".FormatWith(CultureInfo.InvariantCulture, property.PropertyName, _type));
}
Add(property);
}
As you can see above, there is specific code here to prefer derived class properties over base class properties of the same name and visibility.
JavaScriptSerializer has no such logic. It simply calls Type.GetProperty(string, flags)
PropertyInfo propInfo = serverType.GetProperty(memberName,
BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
This method is documented to throw an exception in exactly this situation:
Situations in which AmbiguousMatchException occurs include the following:
A type contains two indexed properties that have the same name but different numbers of parameters. To resolve the ambiguity, use an overload of the GetProperty method that specifies parameter types.
A derived type declares a property that hides an inherited property with the same name, using the new modifier (Shadows in Visual Basic). To resolve the ambiguity, include BindingFlags.DeclaredOnly to restrict the search to members that are not inherited.
I don't know why Microsoft didn't add logic for this to JavaScriptSerializer. It's really a very simple piece of code; perhaps it got eclipsed by DataContractJsonSerializer?
You do have a workaround, which is to write a custom JavaScriptConverter:
public class LimitedConverter : JavaScriptConverter
{
const string StuffName = "Stuff";
const string StatusName = "Status";
public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
{
var limited = new Limited();
object value;
if (dictionary.TryGetValue(StuffName, out value))
{
// limited.Stuff = serializer.ConvertToType<string>(value); // Actually it's get only.
}
if (dictionary.TryGetValue(StatusName, out value))
{
limited.Status = serializer.ConvertToType<LimitedStatus>(value);
}
return limited;
}
public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
{
var limited = (Limited)obj;
if (limited == null)
return null;
var dict = new Dictionary<string, object>();
if (limited.Stuff != null)
dict.Add(StuffName, limited.Stuff);
if (limited.Status != null)
dict.Add(StatusName, limited.Status);
return dict;
}
public override IEnumerable<Type> SupportedTypes
{
get { return new [] { typeof(Limited) } ; }
}
}
And then use it like:
try
{
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
serializer.RegisterConverters(new JavaScriptConverter[] { new LimitedConverter() });
var json = serializer.Serialize(new Limited());
Debug.WriteLine(json);
var status = serializer.Deserialize<Limited>(json);
var json2 = serializer.Serialize(status);
Debug.WriteLine(json2);
}
catch (Exception ex)
{
Debug.Assert(false, ex.ToString()); // NO ASSERT.
}