Reputation: 10227
I am communicating between a .NET Core 3.1 (+ .NET Standard 2.1) app and a .NET Framework 4.6 app, both of which I have total control over. The .NET Core launches the .NET Framework process, and these two processes talk to each other over a redirected stdout/stdin.
My .NET Core app (Core) wants to tell my .NET Framework app (FW) to run a method given some data.
The API it uses is something like SendMessage("FW_MethodName", new List<object> { param1, param2 });
(This operation needs to happen in the FW process and FW doesn't know to do it until Core tells it to do it).
I use Json.NET for serialization and serialize the above message like so:
public class ATypeOfMessage : BaseMessage
{
public string Name { get; set; }
public List<object> Args { get; set; }
public Message(string name, List<object> args)
{
Name = name;
Args = args;
}
}
ATypeOfMessage message = new ATypeOfMessage("FW_MethodName", new List<object> { param1, param2 });
JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandline = TypeNameHandling.All };
string serializedMessage = JsonConvert.SerializeObject(message, typeof(BaseMessage), settings);
// the idea being that I have multiple types of messages, so I serialize/deserialize BaseMessages and
// save off $type metadata, so I can deserialize the string into the correct derived class (ATypeOfMessage)
myFWProcess.StandardInput.WriteLine(serializedMessage);
FW then reads the message:
string serializedMessage = Console.In.ReadLine();
JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandline = TypeNameHandling.All };
BaseMessage message = JsonConvert.DeserializeObject<BaseMessage>(serializedMessage, settings);
switch (message)
{
case ATypeOfMessage myMessage:
// ATypeOfMessage is doubly-defined in FW and Core
// get the methodInfo for method myMessage.Name and do...
object answer = methodInfo.Invoke(this, myMessage.Args.ToArray());
break;
}
This works fine for simple types (e.g. int, string), but the moment I pass in something more complex like byte[]
, deserialization crashes.
This happens because the $type
field that Json.NET saves off contains the assembly System.Private.CoreLib
, which can't be access from FW.
I tried working around this by doing some assembly name redirecting using Json.NET's SerializationBinder
:
JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandline = TypeNameHandling.All, SerializationBinder = new MySerializationBinder() };
class MySerializationBinder : DefaultSerializationBinder
{
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
base.BindToName(serializedType, out assemblyName, out typeName);
if (assemblyName.StartsWith("System.Private.CoreLib"))
{
assemblyName = "mscorlib";
}
}
}
This is a bit hacky, and I'd prefer not to do it, but it works fine for more complex types, so I can do SendMessage("FW_MethodName", new List<object> { byteArray, stringArray });
However, even more complex types like Dictionary<string, List<string>>
break. The $type
data that's saved off is:
\"$type\":\"System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib]], mscorlib\",\"k1\":[\"a\",\"b\",\"c\"],\"k2\":[\"a\",\"b\",\"c\"],\"k3\":[\"a\",\"b\",\"c\"]
which contains CoreLib again, so FW is unable to deserialize it.
Another thing that I've tried is to amend ATypeOfMessage
to accept a list of strings instead of objects, and then I do double serialization.
Core Serialization:
public SendMessage(string methodName, List<object> args)
{
List<string> serializedArgs = new List<string>();
foreach (object arg in args)
{
serializedArgs.Add(JsonConvert.SerializeObject(arg);
}
// ... same logic as before to create ATypeOfMessage, serialize the whole thing, and send it to FW
}
FW Deserialization:
// ... deserializes the object as before, but before invoking the method, we deserialize the specific args
MethodInfo methodInfo = typeof(ClassWithMethods).GetMethod(myMessage.Name);
ParameterInfo[] paramInfo = methodInfo.GetParameters();
object[] finalParams = new object[myMessage.Args.Count];
for (int i = 0 ; i < myMessage.Args.Count; i++)
{
object arg = myMessage.Args[i];
finalParameters[i] = JsonConvert.DeserializeObject(arg, paramInfo[i].ParameterType);
}
object answer = methodInfo.Invoke(this, finalParameters);
This seems to work fine, but I'd prefer a method that doesn't involve double serializing each arg into a string. Is there such a method?
The last thing I tried was to not save off $type
information for my args:
[JsonProperty(ItemTypeNameHandling = TypeNameHandling.None)]
public List<object> Args { get; set; }
Then when deserializing, the args become JObjects, JArrays, or JValues (all types of JTokens) as per the "Untyped Objects" section here.
And then when I deserialize and prepare to call the method, I convert my args like so:
for (int i = 0 ; i < myMessage.Args.Count; i++)
{
object arg = msg.Args[i];
if (arg is JToken)
{
arg = (arg as JToken).ToObject(paramInfo[i].ParameterType);
}
finalParameters[i] = arg;
}
Now, I have the opposite problem as before. The complex types (string[]
, Dictionary<string, List<string>>
) deserialize correctly, but a type like byte[]
doesn't, because Json.NET turns the byte array into a base64-encoded string, and so when FW gets it, it doesn't know that the string should really be a byte array.
How can I cleanly send data like this between Core and FW? Maybe I need to use JsonConverters
for something like this?
Upvotes: 3
Views: 2420
Reputation: 10227
I ended up doing the double serialization over the args that I mentioned in my question. Didn't find any other reliable way of handling this:
.NET Core:
public SendMessage(string methodName, List<object> args)
{
List<string> serializedArgs = new List<string>();
foreach (object arg in args)
{
serializedArgs.Add(JsonConvert.SerializeObject(arg);
}
// ... same logic as before to create ATypeOfMessage, serialize the whole thing, and send it to FW
}
.NET Framework:
// ... deserializes the object as before, but before invoking the method, we deserialize the specific args
MethodInfo methodInfo = typeof(ClassWithMethods).GetMethod(myMessage.Name);
ParameterInfo[] paramInfo = methodInfo.GetParameters();
object[] finalParams = new object[myMessage.Args.Count];
for (int i = 0 ; i < myMessage.Args.Count; i++)
{
object arg = myMessage.Args[i];
finalParameters[i] = JsonConvert.DeserializeObject(arg, paramInfo[i].ParameterType);
}
object answer = methodInfo.Invoke(this, finalParameters);
Upvotes: 0
Reputation: 18023
It seems that JSON.NET ignores the the [TypeForwardedFrom]
attributes of relocated types, which is exactly for compatibility reason when types are serialized. In .NET Core every classic serializable framework type has this attribute with the old framework identity. For example: https://source.dot.net/#System.Private.CoreLib/Dictionary.cs,35
Solution 1:
In your MySerializationBinder
override the BindToName
method:
public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
if (Attribute.GetCustomAttribute(serializedType, typeof(TypeForwardedFromAttribute), false) is TypeForwardedFromAttribute attr)
assemblyName = attr.AssemblyFullName;
// ...
}
Resolving types with old identity should work automatically by Type.GetType
also in .NET Core/Standard
Solution 2:
If you communicate between .NET applications and are not enforced to use JSON serialization you can try this XmlSerializer
, which ignores assembly names by default but if used with assembly qualified names, then it can consider the TypeForwardedFromAttribute
(nuget)
Upvotes: 2
Reputation: 9824
I can not help with serialisation itself, only with way to get around needing it.
Since we have a case of one process (Core) starting the other (FW), they could actually communicate with Console Input Redirection. If you start a programm via the Process class (wich is the primary way anyway) and the other programm is a console or at least checks the streams, you can have them communicate that way: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardinput#remarks
While easier then Serialisation, it might not be as "future proof" as it. Later you may want something totally else being able to call the FrameWork App. Or you generally want it to be able to communicate via Networking.
Upvotes: -1