Added prototype for converting message classes into example JSON

This commit is contained in:
Mark van Renswoude 2021-11-28 17:50:17 +01:00
parent 22e6da5a57
commit 503507422d
5 changed files with 338 additions and 2 deletions

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Newtonsoft.Json.Linq;
namespace ParseTapetiMessagesPrototype
{
public static class AssemblyLoaderMessageParser
{
public static void ParseAssembly(string classLibraryFilename)
{
var loadContext = new AssemblyLoadContext(null, true);
try
{
var assembly = loadContext.LoadFromAssemblyPath(classLibraryFilename);
foreach (var assemblyType in assembly.GetTypes())
HandleType(assemblyType);
}
finally
{
loadContext.Unload();
}
}
private static void HandleType(Type type)
{
if (!type.IsClass)
return;
// For this prototype, filter out anything not ending in Message
// Might want to show a full tree in PettingZoo since this is just a convention
if (!type.Name.EndsWith("Message") || type.Name != "RelatieUpdateMessage")
return;
Console.WriteLine($"{type.Namespace}.{type.Name}");
// We can't create an instance of the type to serialize easily, as most will depend on
// assemblies not included in the NuGet package, so we'll parse the Type ourselves.
// This is still slightly easier than using MetadataReader, as we can more easily check against
// standard types like Nullable.
//
// The only external dependencies should be the attributes, like [RequiredGuid]. The messaging models
// themselves should not inherit from classes outside of their assembly, or include properties
// with types from other assemblies. With that assumption, walking the class structure should be safe.
var serialized = TypeToJObject(type);
Console.WriteLine(serialized);
Console.WriteLine("");
}
private static JObject TypeToJObject(Type type)
{
var result = new JObject();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Note: unfortunately we can not call GetCustomAttributes here, as that would
// trigger assemblies not included in the package to be loaded
var value = PropertyToJToken(propertyInfo.PropertyType);
result.Add(propertyInfo.Name, value);
}
return result;
}
private static readonly Dictionary<Type, JToken> TypeMap = new()
{
{ typeof(short), 0 },
{ typeof(ushort), 0 },
{ typeof(int), 0 },
{ typeof(uint), 0 },
{ typeof(long), 0 },
{ typeof(ulong), 0 },
{ typeof(decimal), 0.0 },
{ typeof(float), 0.0 },
{ typeof(bool), false }
};
private static JToken PropertyToJToken(Type propertyType)
{
var actualType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
// String is also a class
if (actualType == typeof(string))
return "";
if (actualType.IsClass)
{
// IEnumerable<T>
var enumerableInterface = actualType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
if (enumerableInterface != null)
return new JArray(TypeToJObject(enumerableInterface.GetGenericArguments()[0]));
return TypeToJObject(actualType);
}
if (actualType.IsArray)
return new JArray(TypeToJObject(actualType.GetElementType()));
if (actualType.IsEnum)
return Enum.GetNames(actualType).FirstOrDefault();
// Special cases for runtime generated values
if (actualType == typeof(DateTime))
{
// Strip the milliseconds for a cleaner result
var now = DateTime.UtcNow;
return new DateTime(now.Ticks - now.Ticks % TimeSpan.TicksPerSecond, now.Kind);
}
if (actualType == typeof(Guid))
return Guid.NewGuid().ToString();
return TypeMap.TryGetValue(actualType, out var mappedToken)
? mappedToken
: $"(unknown type: {actualType.Name})";
}
}
}

View File

@ -0,0 +1,164 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using Newtonsoft.Json.Linq;
namespace ParseTapetiMessagesPrototype
{
public static class MetadataReaderMessageParser
{
public static void ParseAssembly(string classLibraryFilename)
{
try
{
using var fileStream = new FileStream(classLibraryFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var peReader = new PEReader(fileStream);
var metadataReader = peReader.GetMetadataReader();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var typeDefinitionHandle in metadataReader.TypeDefinitions)
{
var typeDefinition = metadataReader.GetTypeDefinition(typeDefinitionHandle);
HandleTypeDefinition(metadataReader, typeDefinition);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
private static void HandleTypeDefinition(MetadataReader metadataReader, TypeDefinition typeDefinition)
{
var typeNamespace = metadataReader.GetString(typeDefinition.Namespace);
var typeName = metadataReader.GetString(typeDefinition.Name);
// For this prototype, filter out anything not ending in Message
// Might want to show a full tree in PettingZoo since this is just a convention
if (!typeName.EndsWith("Message"))
return;
Console.WriteLine($"{typeNamespace}.{typeName}");
var example = new JObject();
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var propertyDefinitionHandle in typeDefinition.GetProperties())
{
// TODO get properties from base class
var propertyDefinition = metadataReader.GetPropertyDefinition(propertyDefinitionHandle);
HandlePropertyDefinition(metadataReader, propertyDefinition, example);
}
Console.WriteLine(example.ToString());
Console.WriteLine();
}
private static void HandlePropertyDefinition(MetadataReader metadataReader, PropertyDefinition propertyDefinition, JObject targetObject)
{
var fieldName = metadataReader.GetString(propertyDefinition.Name);
var signature = propertyDefinition.DecodeSignature(new JsonSignatureProvider(), null);
targetObject.Add(fieldName, signature.ReturnType);
}
private class JsonSignatureProvider : ISignatureTypeProvider<JToken, object>
{
public JToken GetPrimitiveType(PrimitiveTypeCode typeCode)
{
return typeCode switch
{
PrimitiveTypeCode.Boolean => false,
PrimitiveTypeCode.Byte or
PrimitiveTypeCode.Int16 or
PrimitiveTypeCode.Int32 or
PrimitiveTypeCode.Int64 or
PrimitiveTypeCode.IntPtr or
PrimitiveTypeCode.SByte or
PrimitiveTypeCode.UInt16 or
PrimitiveTypeCode.UInt32 or
PrimitiveTypeCode.UInt64 or
PrimitiveTypeCode.UIntPtr => 0,
PrimitiveTypeCode.Char or
PrimitiveTypeCode.String => "",
PrimitiveTypeCode.Double or
PrimitiveTypeCode.Single => 0.0,
// TODO recurse
PrimitiveTypeCode.Object => "OBJECT",
_ => $"Unsupported primitive type code: {typeCode}"
};
}
public JToken GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind = 0) => "typedef";
public JToken GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind = 0)
{
var typeReference = reader.GetTypeReference(handle);
var typeName = reader.GetString(typeReference.Name);
return typeName;
}
public JToken GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind = 0) => "typespec";
public JToken GetSZArrayType(JToken elementType) => new JValue(elementType + "[]");
public JToken GetPointerType(JToken elementType) => null;
public JToken GetByReferenceType(JToken elementType) => null;
public JToken GetGenericMethodParameter(object genericContext, int index) => "!!" + index;
public JToken GetGenericTypeParameter(object genericContext, int index) => "!" + index;
public JToken GetPinnedType(JToken elementType) => elementType + " pinned";
public JToken GetGenericInstantiation(JToken genericType, ImmutableArray<JToken> typeArguments) => genericType + "<" + string.Join(",", typeArguments) + ">";
public JToken GetModifiedType(JToken modifierType, JToken unmodifiedType, bool isRequired) => unmodifiedType + (isRequired ? " modreq(" : " modopt(") + modifierType + ")";
public JToken GetArrayType(JToken elementType, ArrayShape shape)
{
var builder = new StringBuilder();
builder.Append(elementType);
builder.Append('[');
for (int i = 0; i < shape.Rank; i++)
{
int lowerBound = 0;
if (i < shape.LowerBounds.Length)
{
lowerBound = shape.LowerBounds[i];
builder.Append(lowerBound);
}
builder.Append("...");
if (i < shape.Sizes.Length)
{
builder.Append(lowerBound + shape.Sizes[i] - 1);
}
if (i < shape.Rank - 1)
{
builder.Append(',');
}
}
builder.Append(']');
return builder.ToString();
}
public JToken GetFunctionPointerType(MethodSignature<JToken> signature) => "methodptr(something)";
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
namespace ParseTapetiMessagesPrototype
{
public class Program
{
public static void Main()
{
const string classLibraryFilename = "D:\\Temp\\lib\\netstandard2.0\\Messaging.Relatie.dll";
// There are advantages to using the MetadataReader, for example no code is run (LoadAssemblyForReflection is no longer
// supported in .NET Core) and the assembly is not locked at all. This comes at the cost of complexity however, so
// this prototype explores both options.
//
// In the final version perhaps we can work around loading the assembly into our own process by spawning a new process
// to convert it into metadata used by the main process.
//MetadataReaderMessageParser.ParseAssembly(classLibraryFilename);
AssemblyLoaderMessageParser.ParseAssembly(classLibraryFilename);
}
}
}

View File

@ -10,9 +10,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParseTapetiMessagesPrototype", "ParseTapetiMessagesPrototype\ParseTapetiMessagesPrototype.csproj", "{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -32,6 +34,10 @@ Global
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B06DDB4F-04D1-4325-9F7B-5FBA0AAE47E7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE