Created
December 28, 2024 17:22
-
-
Save nicknow/c5320f8544118f5a5b4baea5ade56324 to your computer and use it in GitHub Desktop.
A model for doing YAML serialization/deserialization using YamlDotNet where the object graph list arrays that use an interface, i.e., List<IInterface>, that need to be converted to/from concrete classes that implement the interface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
MIT License | |
Copyright (c) 2024 Nicolas A. Nowinski ([email protected]) | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
void Main() | |
{ | |
var yamlDocument = | |
@" | |
ConfigName: TestConfiguration | |
Version: 1 | |
Tables: | |
- Table1 | |
- Table2 | |
- Table3 | |
Concepts: | |
- Concept1: | |
Name: ConceptOne | |
InnerConcepts: &ic0001 | |
- Concept2: | |
Descriptions: Ow wow too much... | |
Value: 92 | |
- Concept2: | |
Descriptions: This is getting too big | |
Value: 343 | |
- Concept1: | |
Name: Building stuff deeper and deeper | |
- Concept2: | |
Descriptions: A description of Concept Two | |
Value: 42 | |
SingleConcept: | |
Concept2: | |
Descriptions: Deep Descriptions | |
Value: 2432 | |
- Concept1: | |
Name: Another Concept One | |
InnerConcepts: *ic0001 | |
- Concept2: | |
Descriptions: Another description for Concept Two | |
Value: 99 | |
- Concept3: | |
Descriptions: description for Concept Three | |
Value: 867 | |
Details: | |
Detail: Some special details | |
Rating: 874 | |
"; | |
//This will configure a mapper to let the Polymorhpic serializers handle interfaces and concrete classes | |
var mapper = new PolymorphicObjectMapper(); | |
mapper.Add(typeof(IConcept), typeof(Concept1)); | |
mapper.Add(typeof(IConcept), typeof(Concept2)); | |
mapper.Add(typeof(IConcept), typeof(Concept3)); | |
var deserializer = new DeserializerBuilder() | |
.WithNodeDeserializer(inner => new PolymorphicObjectDeserializer(inner, mapper), syntax => syntax.InsteadOf<ObjectNodeDeserializer>()) | |
.Build(); | |
var root = deserializer.Deserialize<Root>(yamlDocument); | |
var polymorhpicObjectConverter = new PolymorphicObjectConverter(mapper); | |
var rootSerializerBuilder = new SerializerBuilder() | |
.WithTypeConverter(polymorhpicObjectConverter); | |
polymorhpicObjectConverter.ValueSerializer = rootSerializerBuilder.BuildValueSerializer(); | |
var rootSerializer = rootSerializerBuilder.Build(); | |
var rootDoc = rootSerializer.Serialize(root); | |
string seperator = "------------------------------"; | |
Console.WriteLine("Original YAML Document"); | |
Console.WriteLine(seperator); | |
yamlDocument.Dump(); | |
Console.WriteLine(seperator); | |
Console.WriteLine("Deserialized Object Graph"); | |
root.Dump(); | |
Console.WriteLine(seperator); | |
Console.WriteLine("Serialized Document from Object Graph"); | |
Console.WriteLine(seperator); | |
rootDoc.Dump(); | |
Console.WriteLine(seperator); | |
} | |
public class PolymorphicObjectDeserializer : YamlDotNet.Serialization.INodeDeserializer | |
{ | |
private readonly INodeDeserializer _innerDeserializer; | |
private PolymorphicObjectMapper _map; | |
public PolymorphicObjectDeserializer(INodeDeserializer innerDeserializer, PolymorphicObjectMapper polymorphicObjectMapper) | |
{ | |
_innerDeserializer = innerDeserializer; | |
_map = polymorphicObjectMapper; | |
} | |
public bool Deserialize( | |
IParser reader, | |
Type expectedType, | |
Func<IParser, Type, object> nestedObjectDeserializer, | |
out object value, ObjectDeserializer rootDeserializer) | |
{ | |
// Check if we are deserializing an IConcept type | |
if (_map.CanHandleInterface(expectedType)) | |
{ | |
var current = reader.Current as MappingStart; | |
if (current != null) | |
{ | |
// Read the key (e.g., Concept1, Concept2) | |
reader.MoveNext(); | |
var scalar = reader.Current as Scalar; | |
if (scalar != null) | |
{ | |
Type concreteType = _map.GetConcreteType(scalar.Value, expectedType); | |
// Move to the object value | |
reader.MoveNext(); | |
value = nestedObjectDeserializer(reader, concreteType); | |
reader.MoveNext(); // Move past the end of the mapping node | |
return true; | |
} | |
} | |
} | |
// Fallback to the default deserializer | |
return _innerDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer); | |
} | |
} | |
public sealed class PolymorphicObjectConverter : IYamlTypeConverter | |
{ | |
//Credit where credit is due: | |
// https://stackoverflow.com/questions/64242023/yamldotnet-custom-serialization | |
// and https://stackoverflow.com/questions/78211029/convert-list-to-objects-with-custom-names-in-yamldotnet | |
//These two Stackoverflow posts got me to this solution. | |
// Unfortunately the API does not provide those in the ReadYaml and WriteYaml | |
// methods, so we are forced to set them after creation. | |
public IValueSerializer ValueSerializer { get; set; } | |
public IValueDeserializer ValueDeserializer { get; set; } | |
private PolymorphicObjectMapper _mapper; | |
public PolymorphicObjectConverter (PolymorphicObjectMapper polymorphicObjectMapper) | |
{ | |
_mapper = polymorphicObjectMapper; | |
} | |
public bool Accepts(Type type) | |
{ | |
//return typeof(IConcept).IsAssignableFrom(type); | |
return _mapper.CanHandleType(type); | |
} | |
public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) | |
{ | |
return rootDeserializer(type); | |
} | |
public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer serializer) | |
{ | |
emitter.Emit(new MappingStart()); | |
var component = value; | |
var otherPropertisMap = type | |
.GetProperties() | |
.Where(p => p.GetValue(component) != null) | |
.ToDictionary(p => p.Name, p => p.GetValue(component)); | |
ValueSerializer.SerializeValue(emitter, type.Name, typeof(string)); | |
ValueSerializer.SerializeValue(emitter, otherPropertisMap, typeof(Dictionary<string, object>)); | |
emitter.Emit(new MappingEnd()); | |
} | |
} | |
public class PolymorphicObjectMapper | |
{ | |
private Dictionary<string, InterfaceMap> interfaces = new Dictionary<string, InterfaceMap>(); | |
private HashSet<string> handledTypes = new HashSet<string>(); | |
public void Add(Type InterfaceClass, Type ConcreteClass) | |
{ | |
if (handledTypes.Contains(ConcreteClass.Name)) return; | |
if (!interfaces.ContainsKey(InterfaceClass.Name)) interfaces.Add(InterfaceClass.Name, new InterfaceMap()); | |
handledTypes.Add(ConcreteClass.Name); | |
interfaces[InterfaceClass.Name].classMappings.Add(ConcreteClass.Name, ConcreteClass); | |
} | |
public bool CanHandleInterface(Type interfaceType) | |
{ | |
return interfaces.ContainsKey(interfaceType.Name); | |
} | |
public bool CanHandleType(Type type) | |
{ | |
return handledTypes.Contains(type.Name); | |
} | |
public Type GetConcreteType(string typeName, Type interfaceType) | |
{ | |
if (interfaces.ContainsKey(interfaceType.Name) && interfaces[interfaceType.Name].classMappings.ContainsKey(typeName)) | |
return interfaces[interfaceType.Name].classMappings[typeName]; | |
else throw new Exception("Cannot Find Type for Specificed Type Name and Interface"); | |
} | |
} | |
public class InterfaceMap | |
{ | |
public string interfaceName { get; set;} | |
public Dictionary<string, Type> classMappings { get; set;} | |
public InterfaceMap() | |
{ | |
classMappings = new Dictionary<string, Type>(); | |
} | |
} | |
public class Root | |
{ | |
public string ConfigName {get; set;} | |
public int Version {get; set;} | |
public List<string> Tables {get; set;} | |
public List<IConcept> Concepts {get; set;} | |
} | |
public interface IConcept | |
{} | |
public class Concept1 : IConcept | |
{ | |
public string Name {get; set;} | |
public List<IConcept> InnerConcepts {get; set;} | |
} | |
public class Concept2 : IConcept | |
{ | |
public string Descriptions { get; set; } | |
public int? Value { get; set; } | |
public IConcept SingleConcept {get; set;} | |
} | |
public class Concept3 : IConcept | |
{ | |
public string Descriptions { get; set; } | |
public int Value { get; set; } | |
public ConceptDetails Details {get; set;} | |
} | |
public class ConceptDetails | |
{ | |
public string Detail {get; set;} | |
public int? Rating {get; set;} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment