We have a complex issue in our system regarding retaining polymorphism after we have saved a config file to the database.
We have simplified the problem for the sake of this question. See code below.
Here is the code:
class Wheel
{
public virtual string GetWidth()
{
return "Normal";
}
}
class SmallWheel : Wheel
{
public override string GetWidth()
{
return "Small";
}
}
class BigWheel : Wheel
{
public override string GetWidth()
{
return "Big";
}
}
public static async Task Main(string[] args)
{
//Build list of wheels
var wheels = new List<Wheel>()
{
new Wheel(),
new SmallWheel(),
new BigWheel(),
};
//We print wheels to check it works
foreach (var x in wheels)
{
Console.WriteLine(x.GetWidth());
}
//Save the list to the db as a string
var file = JsonConvert.SerializeObject(wheels);
//We just use file instead of db for simplictiydf
//Later we read the config file from the DB
var wheelsFromDb = JsonConvert.DeserializeObject<List<Wheel>>(file);
//We now want to print out our wheels.
Console.WriteLine("Printing WHeels from Db");
foreach (var x in wheelsFromDb)
{
Console.WriteLine(x.GetWidth());
}
}
Results when I run it:
Normal
Small
Big
Printing WHeels from Db
Normal
Normal
Normal
Now as you can see we are losing what type the wheel is after de-serialisation.
How can we go about solving this issue?
In essence, we need to store a config file with multiple children classes each with overrides functions. I realise that when Json deserializes the raw string it has no context of what type the class was before. Is there a way we can store that context or use a different library or database?
CodePudding user response:
I am using this code for a list of derived classes. You can use a base class instead of an interface as well
IAnimal[] animals = new IAnimal[] {
new Cat{CatName="Tom"},
new Dog{DogName="Scoopy"},
new Rabbit{RabitName="Honey"}
};
var jsonSerializerSettings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.All
};
var json = JsonConvert.SerializeObject(animals, jsonSerializerSettings);
List<IAnimal> animalsBack = ((JArray)JsonConvert.DeserializeObject(json)).Select(o => (IAnimal)JsonConvert.DeserializeObject(o.ToString(), Type.GetType((string)o["$type"]))).ToList();
classes
public interface IAnimal
{
}
public class Animal : IAnimal
{
}
public class Cat : IAnimal { public string CatName { get; set; } }
public class Dog : IAnimal { public string DogName { get; set; } }
public class Rabbit : IAnimal { public string RabitName { get; set; } }
CodePudding user response:
JsonConvert
creates property $type
under the hood when TypeNameHandling.All
is used.
So we can give a clue to compiler about what type actually is by creating Size
property in classes:
public class Wheel
{
public virtual string Size { get; set; } = WheelSize.NORMAL;
public virtual string GetWidth() => Size;
}
public class SmallWheel : Wheel
{
public override string Size { get; set; } = WheelSize.SMALL;
}
public class BigWheel : Wheel
{
override public string Size { get; set; } = WheelSize.BIG;
}
This is a class which contains size of wheels:
public class WheelSize
{
public static string SMALL = "small";
public static string NORMAL = "normal";
public static string BIG = "big";
}
So far, so good. But how we can deserialize json based on size wheel? We can write custom deserializer:
internal class WheelJsonConverter : JsonConverter
{
private readonly Type[] _types;
public WheelJsonConverter(params Type[] types)
{
_types = types;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
List<Wheel> wheels = new List<Wheel>();
WheelToSize wheelToSize = new WheelToSize();
JArray jArray = JArray.Load(reader);
foreach (JToken token in jArray)
{
string wheelSize = token["size"].ToString();
Wheel wheel = wheelToSize.WheelBySize[wheelSize];
wheels.Add(wheel);
}
return wheels;
}
public override bool CanConvert(Type objectType)
{
if (objectType.IsGenericType
&& IsGenericTypeArgumentTheSame(objectType))
return true;
return false;
}
private bool IsGenericTypeArgumentTheSame(Type objectType) =>
objectType.GenericTypeArguments[0] == _types[0];
public override void WriteJson(JsonWriter writer, object? value,
JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
o.WriteTo(writer);
}
}
}
This is a class to create json in lower case:
public class LowercaseContractResolver : DefaultContractResolver
{
protected override string ResolvePropertyName(string propertyName)
{
return propertyName.ToLower();
}
}
This is a factory which creates an object based on size:
public class WheelToSize
{
public Dictionary<string, Wheel> WheelBySize { get; private set; } = new()
{
{ WheelSize.NORMAL, new Wheel() },
{ WheelSize.BIG, new BigWheel() },
{ WheelSize.SMALL, new SmallWheel() }
};
}
And then you can run code like this:
List<Wheel> wheels = new List<Wheel>()
{
new Wheel(),
new SmallWheel(),
new BigWheel(),
};
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new LowercaseContractResolver();
var file = JsonConvert.SerializeObject(wheels, Formatting.Indented, settings);
var wheelsFromDb = JsonConvert.DeserializeObject<List<Wheel>>(file,
new WheelJsonConverter(typeof(Wheel)));
//We now want to print out our wheels.
Console.WriteLine("Printing WHeels from Db");
foreach (var x in wheelsFromDb)
Console.WriteLine(x.GetWidth());