Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration / Json Serialization: Add feature to serialize interfaces #63

Closed
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.Extensions.Configuration;

namespace PiBox.Hosting.Abstractions.Extensions
Expand All @@ -10,5 +11,50 @@ public static class ConfigurationExtensions
configuration.Bind(sectionKey, config);
return config;
}

public static object GetSection(this IConfiguration configuration, string sectionKey, Type type)
{
var section = configuration.GetSection(sectionKey);
return ConfigurationSectionToObject(section).Serialize().Deserialize(type);
}
private static object ConfigurationSectionToObject(IConfigurationSection section)
{
var children = section.GetChildren().ToList();
if (children.All(child => int.TryParse(child.Key, out _)))
{
if (children.All(child => !child.GetChildren().Any()))
{
return children.Select(child => ParseValue(child.Value)).ToList();
}
return children.OrderBy(child => int.Parse(child.Key, CultureInfo.InvariantCulture))
.Select(ConfigurationSectionToObject)
.ToList();
}
var result = new Dictionary<string, object>();
foreach (var child in children)
{
result[child.Key] = child.GetChildren().Any() ? ConfigurationSectionToObject(child) : ParseValue(child.Value);
}
return result;
}

private static object ParseValue(string value)
{
if (value is null) return null;

if (bool.TryParse(value, out var boolValue))
{
return boolValue;
}
if (int.TryParse(value, out var intValue))
{
return intValue;
}
if (decimal.TryParse(value, out var decimalValue))
{
return decimalValue;
}
return value;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Reflection;
using System.Text.Encodings.Web;
Expand All @@ -15,25 +16,29 @@ public static class SerializationExtensions
{
public static readonly JsonSerializerOptions DefaultOptions = new()
{
Converters = { new JsonStringEnumConverter() },
Converters = { new JsonStringEnumConverter(), new KindSpecifierConverterFactory() },
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
IgnoreReadOnlyFields = true,
WriteIndented = false
};

private static readonly IDeserializer _deserializer = new DeserializerBuilder()
internal static DeserializerBuilder GetYamlDeserializerBuilder() => new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.WithDuplicateKeyChecking()
.WithTypeConverter(new ValueObjectYamlTypeConverter())
.Build();
.WithTypeConverter(new KindSpecifierYamlTypeConverter());

private static readonly ISerializer _serializer = new SerializerBuilder()
private static readonly IDeserializer _deserializer = GetYamlDeserializerBuilder().Build();

internal static SerializerBuilder GetYamlSerializerBuilder() => new SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeConverter(new ValueObjectYamlTypeConverter())
.Build();
.WithTypeConverter(new KindSpecifierYamlTypeConverter());

private static readonly ISerializer _serializer = GetYamlSerializerBuilder().Build();

public static string Serialize<T>(this T obj,
SerializationMethod serializationMethod = SerializationMethod.Json)
Expand Down Expand Up @@ -67,28 +72,138 @@ public static object Deserialize(this string content, Type targetType,
_ => throw new ArgumentOutOfRangeException(nameof(serializationMethod), serializationMethod, null)
};
}
private class ValueObjectYamlTypeConverter : IYamlTypeConverter

}

internal class ValueObjectYamlTypeConverter : IYamlTypeConverter
{
public bool Accepts(Type type)
{
return type.GetCustomAttributes().Any(attr => attr.GetType() == typeof(ValueObjectAttribute)
|| (attr.GetType().IsGenericType &&
attr.GetType().GetGenericTypeDefinition() == typeof(ValueObjectAttribute<>)));
}

public object ReadYaml(IParser parser, Type type)
{
var scalar = parser.Consume<Scalar>();
var valueType = type.GetProperty("Value")!.PropertyType;
return TypeDescriptor.GetConverter(valueType).ConvertFromInvariantString(scalar.Value);
}

public void WriteYaml(IEmitter emitter, object value, Type type)
{
var val = value!.GetType().GetProperty("Value")!.GetGetMethod()!
.Invoke(value, [])!
.ToString()!;
emitter.Emit(new Scalar(null, null, val, ScalarStyle.Plain, true, false));
}
}

internal class KindSpecifierYamlTypeConverter : IYamlTypeConverter
{
private static readonly IDeserializer _deserializer = SerializationExtensions.GetYamlDeserializerBuilder()
.WithoutTypeConverter(typeof(KindSpecifierYamlTypeConverter)).Build();
private static readonly ISerializer _serializer = SerializationExtensions.GetYamlSerializerBuilder()
.WithoutTypeConverter(typeof(KindSpecifierYamlTypeConverter)).Build();
private readonly IDictionary<Type, Type[]> _kindTypes = new ConcurrentDictionary<Type, Type[]>();
private readonly IDictionary<string, Type> _kindNames = new ConcurrentDictionary<string, Type>();

public bool Accepts(Type type)
{
return typeof(IKindSpecifier).IsAssignableFrom(type);
}

public object ReadYaml(IParser parser, Type type)
{
if (!_kindTypes.TryGetValue(type, out var possibleTypes))
{
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetTypes()).ToArray();
possibleTypes = types.Where(x => x.IsClass && type.IsAssignableFrom(x)).ToArray();
_kindTypes[type] = possibleTypes;
foreach (var pt in possibleTypes)
{
var kindName = ((IKindSpecifier)Activator.CreateInstance(pt)!).Kind.ToLowerInvariant();
_kindNames[kindName] = pt;
}
}

var yamlValue = parser.Consume<Scalar>().Value;
var kindNode = _deserializer.Deserialize<Dictionary<string, object>>(yamlValue)
.FirstOrDefault(x => x.Key.Equals(nameof(IKindSpecifier.Kind), StringComparison.InvariantCultureIgnoreCase))
.Value as string;
if (string.IsNullOrEmpty(kindNode) || !_kindNames.TryGetValue(kindNode, out var kindType))
throw new YamlException($"Unknown type for kind: {kindNode ?? "not set"}");

return _deserializer.Deserialize(yamlValue, kindType);
}

public void WriteYaml(IEmitter emitter, object value, Type type)
{
var yamlText = _serializer.Serialize(value);
emitter.Emit(new Scalar(null, null, yamlText, ScalarStyle.Plain, true, false));
}
}

internal class KindSpecifierConverterFactory : JsonConverterFactory
{
private readonly IDictionary<Type, JsonConverter> _converters = new ConcurrentDictionary<Type, JsonConverter>();
public override bool CanConvert(Type typeToConvert)
{
public bool Accepts(Type type)
return typeof(IKindSpecifier).IsAssignableFrom(typeToConvert) && typeToConvert.IsInterface;
}

public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (_converters.TryGetValue(typeToConvert, out var cachedConverter))
return cachedConverter;
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(x => x.GetTypes());
var converterType = typeof(KindSpecifierConverter<>).MakeGenericType(typeToConvert);
var converter = (JsonConverter)Activator.CreateInstance(converterType, [types])!;
_converters.Add(typeToConvert, converter);
return converter;
}

private class KindSpecifierConverter<T> : JsonConverter<T> where T : IKindSpecifier
{
private readonly IDictionary<string, Type> _types;

public KindSpecifierConverter(IEnumerable<Type> types)
{
return type.GetCustomAttributes().Any(attr => attr.GetType() == typeof(ValueObjectAttribute)
|| (attr.GetType().IsGenericType &&
attr.GetType().GetGenericTypeDefinition() == typeof(ValueObjectAttribute<>)));
_types = types
.Where(x => x is { IsClass: true, IsAbstract: false } && x.GetInterface(typeof(T).Name) != null)
.ToDictionary(x => ((T)Activator.CreateInstance(x)!).Kind.ToLowerInvariant(), x => x);
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
var typeName = FindProperty(root, nameof(IKindSpecifier.Kind)).GetString()!.ToLowerInvariant();
if (!_types.TryGetValue(typeName, out var type))
throw new JsonException($"Unknown Type: {typeName}");

public object ReadYaml(IParser parser, Type type)
var json = root.GetRawText();
return (T)JsonSerializer.Deserialize(json, type, options)!;
}

private static JsonElement FindProperty(JsonElement element, string propertyName)
{
var scalar = parser.Consume<Scalar>();
var valueType = type.GetProperty("Value")!.PropertyType;
return TypeDescriptor.GetConverter(valueType).ConvertFromInvariantString(scalar.Value);
foreach (var property in element.EnumerateObject().Where(property => string.Equals(property.Name, propertyName, StringComparison.InvariantCultureIgnoreCase)))
{
return property.Value;
}
throw new Exception("Could not find property: " + propertyName);
}

public void WriteYaml(IEmitter emitter, object value, Type type)
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var val = value!.GetType().GetProperty("Value")!.GetGetMethod()!
.Invoke(value, [])!
.ToString()!;
emitter.Emit(new Scalar(null, null, val, ScalarStyle.Plain, true, false));
var newOptions = new JsonSerializerOptions(options);
var converter = newOptions.Converters.Single(x => x.GetType() == typeof(KindSpecifierConverterFactory));
newOptions.Converters.Remove(converter);
var json = JsonSerializer.Serialize(value, newOptions);
writer.WriteRawValue(json);
}
}
}
Expand All @@ -98,4 +213,9 @@ public enum SerializationMethod
Json = 0,
Yaml = 1
}

public interface IKindSpecifier
{
string Kind { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using NUnit.Framework;
using PiBox.Hosting.Abstractions.Extensions;

namespace PiBox.Hosting.Abstractions.Tests.Extensions
{
public class ConfigurationExtensionTests
{
[Test]
public void CanGetConfigurationSectionAsTypedObject()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "conf:key1", "value1" },
{ "conf:key2:name", "name" },
{ "conf:key2:active", "true" },
{ "conf:key2:age", "13" },
{ "conf:key2:price", "123.321" },
{ "conf:key3:0:kind", "test" },
{ "conf:key3:0:name", "some-name" },
{ "conf:key3:1:kind", "test" },
{ "conf:key3:1:name", "some-name2" },
{ "conf:key4:0", "1" },
{ "conf:key4:1", "2" },
{ "conf:key4:2", "3" },

})
.Build();
var result = configuration.GetSection("conf", typeof(TestConfiguration));
var config = result.Should().NotBeNull().And.BeOfType<TestConfiguration>().Which;
config.Key1.Should().Be("value1");
config.Key2.Name.Should().Be("name");
config.Key2.Active.Should().BeTrue();
config.Key2.Age.Should().Be(13);
config.Key2.Price.Should().Be(123.321m);
config.Key3.Should().HaveCount(2);
var conf1 = config.Key3[0].Should().BeOfType<TestKindConfiguration>().Which;
var conf2 = config.Key3[1].Should().BeOfType<TestKindConfiguration>().Which;
conf1.Kind.Should().Be("Test");
conf1.Name.Should().Be("some-name");
conf2.Kind.Should().Be("Test");
conf2.Name.Should().Be("some-name2");

config.Key4.Should().HaveCount(3)
.And.BeEquivalentTo([1, 2, 3]);
config.Key5.Should().BeNull();

configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "conf:key1", "value1" },
{ "conf:key2:name", "name" },
{ "conf:key2:active", "true" },
{ "conf:key2:age", "13" },
{ "conf:key2:price", "123.321" },
// { "conf:key3:0:kind", "test" },
{ "conf:key3:0:name", "some-name" },
{ "conf:key3:1:kind", "test" },
{ "conf:key3:1:name", "some-name2" },
{ "conf:key4:0", "1" },
{ "conf:key4:1", "2" },
{ "conf:key4:2", "3" },

})
.Build();
configuration.Invoking(x => x.GetSection("conf", typeof(TestConfiguration)))
.Should().Throw<Exception>().And.Message.Should().Contain("Could not find property");

configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "conf:key1", "value1" },
{ "conf:key2:name", "name" },
{ "conf:key2:active", "true" },
{ "conf:key2:age", "13" },
{ "conf:key2:price", "123.321" },
{ "conf:key3:0:kind", "failure" },
{ "conf:key3:0:name", "some-name" },
{ "conf:key3:1:kind", "test" },
{ "conf:key3:1:name", "some-name2" },
{ "conf:key4:0", "1" },
{ "conf:key4:1", "2" },
{ "conf:key4:2", "3" },

})
.Build();
configuration.Invoking(x => x.GetSection("conf", typeof(TestConfiguration)))
.Should().Throw<JsonException>().And.Message.Should().Contain("Unknown Type: failure");
}

private class TestConfiguration
{
public string Key1 { get; set; }
public TestNestConfiguration Key2 { get; set; }
public IList<IInterfaceConfiguration> Key3 { get; set; }
public IList<int> Key4 { get; set; }
public string Key5 { get; set; }
}

private class TestNestConfiguration
{
public string Name { get; set; }
public int Age { get; set; }
public bool Active { get; set; }
public decimal Price { get; set; }
}

private interface IInterfaceConfiguration : IKindSpecifier;

private class TestKindConfiguration : IInterfaceConfiguration
{
public string Kind => "Test";
public string Name { get; set; }
}
}
}
Loading
Loading