how come BinaryFormatter can serialize an Action<> but Json.net cannot - c#
Trying to serialize/deserialize an Action<>.
Try #1 naive by me
JsonConvert.SerializeObject(myAction);
...
JsonConvert.Deserialize<Action>(json);
Deserialize fails saying it cannot serialize Action.
Try #2
JsonConvert.DeserializeObject<Action>(ctx.SerializedJob, new JsonSerializerSettings {ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor });
Same(ish) failure.
Try # 3
Then I found http://mikehadlow.blogspot.com/2011/04/serializing-continuations.html
This uses BinaryFormatter. I dropped this in (base64 encoding the binary to a string). Worked perfectly first time.
Try #4
I then found
https://github.com/DevrexLabs/Modules.JsonNetFormatter
Which is an IFormatter module for json.net. Wired that in, same failure - cannot deserialize.
So how come BinaryFormatter can do it but Json.net cannot?
EDIT:
The general reply is - "thats the most stupid thing to want to do". Let me show what I am trying to do
MyJobSystem.AddJob(ctx=>
{
// code to do
// ......
}, DateTime.UtcNow + TimeSpan.FromDays(2));
Ie - execute this lambda in 2 days time.
This works fine for me. Using BinaryFormatter. I was curious about why one serializing infrastructure could do it but the other could not. They both seem to have the same rules about what can and cannot be processed
The reason that BinaryFormatter is (sometimes) able to round-trip an Action<T> is that such delegates are marked as [Serializable] and implement ISerializable.
However, just because the delegate itself is marked as serializable doesn't mean that its members can be serialized successfully. In testing, I was able to serialize the following delegate:
Action<int> a1 = (a) => Console.WriteLine(a);
But attempting to serialize the following threw a SerializationException:
int i = 0;
Action<int> a2 = (a) => i = i + a;
The captured variable i apparently is placed in a non-serializable compiler-generated class thereby preventing binary serialization of the delegate from succeeding.
On the other hand, Json.NET is unable to round-trip an Action<T> despite supporting ISerializable because it does not provide support for serialization proxies configured via SerializationInfo.SetType(Type). We can confirm that Action<T> is using this mechanism with the following code:
var iSerializable = a1 as ISerializable;
if (iSerializable != null)
{
var info = new SerializationInfo(a1.GetType(), new FormatterConverter());
var initialFullTypeName = info.FullTypeName;
iSerializable.GetObjectData(info, new StreamingContext(StreamingContextStates.All));
Console.WriteLine("Initial FullTypeName = \"{0}\", final FullTypeName = \"{1}\".", initialFullTypeName, info.FullTypeName);
var enumerator = info.GetEnumerator();
while (enumerator.MoveNext())
{
Console.WriteLine(" Name = {0}, objectType = {1}, value = {2}.", enumerator.Name, enumerator.ObjectType, enumerator.Value);
}
}
When run, it outputs:
Initial FullTypeName = "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", final FullTypeName = "System.DelegateSerializationHolder".
Name = Delegate, objectType = System.DelegateSerializationHolder+DelegateEntry, value = System.DelegateSerializationHolder+DelegateEntry.
Name = method0, objectType = System.Reflection.RuntimeMethodInfo, value = Void <Test>b__0(Int32).
Notice that FullTypeName has changed to System.DelegateSerializationHolder? That's the proxy, and it's not supported by Json.NET.
This begs the question, just what is written out when a delegate is serialized? To determine this we can configure Json.NET to serialize Action<T> similarly to how BinaryFormatter would by setting
DefaultContractResolver.IgnoreSerializableAttribute = false
DefaultContractResolver.IgnoreSerializableInterface = false
JsonSerializerSettings.TypeNameHandling = TypeNameHandling.All
If I serialize a1 using these settings:
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
ContractResolver = new DefaultContractResolver
{
IgnoreSerializableInterface = false,
IgnoreSerializableAttribute = false,
},
Formatting = Formatting.Indented,
};
var json = JsonConvert.SerializeObject(a1, settings);
Console.WriteLine(json);
Then the following JSON is generated:
{
"$type": "System.Action`1[[System.Int32, mscorlib]], mscorlib",
"Delegate": {
"$type": "System.DelegateSerializationHolder+DelegateEntry, mscorlib",
"type": "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]",
"assembly": "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"target": null,
"targetTypeAssembly": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"targetTypeName": "Question49138328.TestClass",
"methodName": "<Test>b__0",
"delegateEntry": null
},
"method0": {
"$type": "System.Reflection.RuntimeMethodInfo, mscorlib",
"Name": "<Test>b__0",
"AssemblyName": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"ClassName": "Question49138328.TestClass",
"Signature": "Void <Test>b__0(Int32)",
"MemberType": 8,
"GenericArguments": null
}
}
The replacement FullTypeName is not included but everything else is. And as you can see, it's not actually storing the IL instructions of the delegate; it's storing the full signature of the method(s) to call, including the hidden, compiler-generated method name <Test>b__0 mentioned in this answer. You can see the hidden method name yourself just by printing a1.Method.Name.
Incidentally, to confirm that Json.NET is really saving the same member data as BinaryFormatter, you can serialize a1 to binary and print any embedded ASCII strings as follows:
var binary = BinaryFormatterHelper.ToBinary(a1);
var s = Regex.Replace(Encoding.ASCII.GetString(binary), #"[^\u0020-\u007E]", string.Empty);
Console.WriteLine(s);
Assert.IsTrue(s.Contains(a1.Method.Name)); // Always passes
Using the extension method:
public static partial class BinaryFormatterHelper
{
public static byte[] ToBinary<T>(T obj)
{
using (var stream = new MemoryStream())
{
new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Serialize(stream, obj);
return stream.ToArray();
}
}
}
Doing so results in the following string:
????"System.DelegateSerializationHolderDelegatemethod00System.DelegateSerializationHolder+DelegateEntry/System.Reflection.MemberInfoSerializationHolder0System.DelegateSerializationHolder+DelegateEntrytypeassemblytargettargetTypeAssemblytargetTypeNamemethodNamedelegateEntry0System.DelegateSerializationHolder+DelegateEntrylSystem.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]Kmscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nullQuestion49138328.TestClass<Test>b__0/System.Reflection.MemberInfoSerializationHolderNameAssemblyNameClassNameSignatureMemberTypeGenericArgumentsSystem.Type[]Void <Test>b__0(Int32)
And the assert never fires, indicating that the compiler-generated method name <Test>b__0 is indeed present in the binary also.
Now, here's the scary part. If I modify my c# source code to create another Action<T> before a1, like so:
// I inserted this before a1 and then recompiled:
Action<int> a0 = (a) => Debug.WriteLine(a);
Action<int> a1 = (a) => Console.WriteLine(a);
Then re-build and re-run, a1.Method.Name changes to <Test>b__1:
{
"$type": "System.Action`1[[System.Int32, mscorlib]], mscorlib",
"Delegate": {
"$type": "System.DelegateSerializationHolder+DelegateEntry, mscorlib",
"type": "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]",
"assembly": "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"target": null,
"targetTypeAssembly": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"targetTypeName": "Question49138328.TestClass",
"methodName": "<Test>b__1",
"delegateEntry": null
},
"method0": {
"$type": "System.Reflection.RuntimeMethodInfo, mscorlib",
"Name": "<Test>b__1",
"AssemblyName": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"ClassName": "Question49138328.TestClass",
"Signature": "Void <Test>b__1(Int32)",
"MemberType": 8,
"GenericArguments": null
}
}
Now if I deserialize binary data for a1 saved from the earlier version, it comes back as a0! Thus, adding another delegate somewhere in your code base, or otherwise refactoring your code in an apparently harmless way, may cause previously serialized delegate data to be corrupt and fail or even possibly execute the wrong method when deserialized into the new version of your software. Further, this is unlikely to be fixable other than by reverting all changes out of your code and never making such changes again.
To sum up, we have found that serialized delegate information is incredibly fragile to seemingly-unrelated changes in one's code base. I would strongly recommend against persisting delegates through serialization with either BinaryFormatter or Json.NET. Instead, consider maintaining a table of named delegates and serializing the names, or following the command pattern and serialize command objects.
Related
System.TypeLoadException: Could not load type 'System.Object' from assembly 'System.Private.CoreLib' because the parent does not exist
We are generating code dynamically to produce a .NET Core console application and then compiling it using: var csharpParseOptions = new CSharpParseOptions(LanguageVersion.Latest); csharpParseOptions = csharpParseOptions.WithPreprocessorSymbols(new[] { "TRACE", "DEBUG" }); var syntaxTree = CSharpSyntaxTree.ParseText(code, options: csharpParseOptions); var compilationUnitSyntax = syntaxTree.GetCompilationUnitRoot(); var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication, optimizationLevel: OptimizationLevel.Debug, platform: Platform.X64) .WithModuleName("TestConsole") .WithMetadataImportOptions(MetadataImportOptions.All) .WithDeterministic(true) .WithConcurrentBuild(true); var csharpCompilation = CSharpCompilation.Create(#"TestConsole", syntaxTrees: new[] { syntaxTree }, references: references, options: options); We can then work without any problems against the generated assembly (in memory) obtained using: using (var memoryStream = new MemoryStream()) { var emitResult = csharpCompilation.Emit(memoryStream); memoryStream.Position = 0; _assembly = Assembly.Load(memoryStream.ToArray()); } However, when we write the console.exe to disk using: csharpCompilation.Emit(fileNameOnDisk, Path.Combine(Path.GetDirectoryName(fileNameOnDisk), Path.GetFileNameWithoutExtension(fileNameOnDisk)) + ".pdb"); and try to run it from there we get the following exception: System.TypeLoadException: Could not load type 'System.Object' from assembly 'System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' because the parent does not exist. Copying the same generated code (Program.cs) into an empty Console project works perfectly but we notice that the size of the executable is significantly larger. Does anyone have any ideas how to go about fixing this problem? Thanks.
I think your problem is the lack of runtime configuration. From what I read above if you add a file named testconsole.runtimeconfig.json with the following info or similar: { "runtimeOptions": { "tfm": "net6.0", "framework": { "name": "Microsoft.NETCore.App", "version": "6.0.0" } } } You'll see that it runs. I'm also attaching a complete example with compilation.Emit that I validated in LINQPad. It does not require an additional file since it generates it in the example. Best of luck and hope it is still useful. var tree = CSharpSyntaxTree.ParseText (#"class Program { static void Main() => System.Console.WriteLine (""Hello""); }"); string trustedAssemblies = (string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"); var trustedAssemblyPaths = trustedAssemblies.Split (Path.PathSeparator); var references = trustedAssemblyPaths.Select (path => MetadataReference.CreateFromFile (path)); var compilation = CSharpCompilation .Create ("test") .WithOptions (new CSharpCompilationOptions (OutputKind.ConsoleApplication)) .AddSyntaxTrees (tree) .AddReferences (references); string outputPath = "test.dll"; Path.GetFullPath (outputPath).Dump(); EmitResult result = compilation.Emit (outputPath); Console.WriteLine (result.Success); File.WriteAllText ("test.runtimeconfig.json", #$"{{ ""runtimeOptions"": {{ ""tfm"": ""net{Environment.Version.Major}.{Environment.Version.Minor}"", ""framework"": {{ ""name"": ""Microsoft.NETCore.App"", ""version"": ""{Environment.Version.Major}.{Environment.Version.Minor}.{Environment.Version.Build}"" }} }} }}"); // Execute the program we just compiled. Util.Cmd (#"dotnet.exe", "\"" + Path.GetFullPath (outputPath) + "\"");
Get name and values of enumerator from ReflectionOnlyGetType - operation invalid in ReflectionOnlyContext
I need to be able to enumerate through values of an enumerator in a separate assembly for which I only have the assembly qualified name. I am able to determine the assembly qualified name, but I can't resolve the 'The requested operation is invalid in the ReflectionOnly context.' when I try to enumerate it. My main code, in assembly DevExample: using System; using ClassLibrary; namespace DevExample { public class Program { static void Main(string[] args) { Type myEnumType = MyEnum.One.GetType(); WriteToConsole(myEnumType); Type myEnumType_null = Type.GetType(myEnumType.FullName); // will return null string assemblyName = myEnumType.AssemblyQualifiedName; // assemblyName will be "ClassLibrary.MyEnum, ClassLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; Type myEnumTypeFromReflection = Type.ReflectionOnlyGetType(assemblyName, true, false); // Will get type succesfully WriteToConsole(myEnumTypeFromReflection); } private static void WriteToConsole(Type enumType) { foreach (int val in Enum.GetValues(enumType)) { Console.WriteLine("{0} - {0}", Enum.GetName(enumType, val)); } } } } Then in another assembly (ClassLibrary.dll) I just defined a simple enumerator namespace ClassLibrary { public enum MyEnum { Zero = 0, One = 1, Two = 2 }; } On the second write to console, I get: The requested operation is invalid in the ReflectionOnly context. I need to do this to validate an .xml that defines which enumerators a particular piece of hardware is using. Previous code has just loaded a dictionary with the name and types manually, but given that the .xml file is able to contain the assembly qualified name, it should be able to do this dynamically.
Couldn't see the wood for the trees: string assemblyString = #"ClassLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"; string name = #"ClassLibrary.MyEnum"; Type myEnumTypeDynamic = Assembly.Load(assemblyString).GetType(name);
.Net Standard 4.7.1 Could not load System.Private.CoreLib during serialization
I'm working on migrating most of my project to .Net Standard 2.0. Last part of the project in .net standard 4.7.1 is a WCF front end. This executable communicate with a .net core application throught a library Akka.net by ClientServiceReceptionist. Network communication between application by akka.net work. Except when an it try to serialize a ReadOnlyCollection. In this case the system try to load a "System.Private.CoreLib.dll" that is not available. I readed many issues with .net 4.6 using .net standard 2.0 libraries that must have been corrected in 4.7.1. I pass from package.config to PackageReference. I tryed to used NewtonSoft as a serializer in place of Hyperion without progress. Does anyone have an idea, a solution ? Edit : 07-05-2018 The issue is throw in the WCF Entry Points when i sent a ClusterClient.Send object throught the ClusterClientReceptionist. The object sent contains only boolean, string, Guid and array of string properties. Edit 08-05-2018 The object sent look like this : { (string) "Path": "/user/ProcessOrderWorkflow", "Message": { "Order": { "Data": { (string)"Sentence": "i love my name", "Options": { (boolean)"Simplify" : true, (IReadOnlyCollection<string>) LanguageAffinity : [ "FR", "EN" ] } }, "OrderQuery": { "Verb": { (string)"Verb": "Extract" }, "Arguments": [ { (string)"Argument": "Sentence" }, { (string)"Argument": "Meaning" } ] }, (Guid)"ProcessId": "0bfef4a5-c8a4-4816-81d1-6f7bf1477f65" }, (string)"ReturnTypeFullName": "Viki.Api.Server.Data.SentenceMeaningMsg,, Viki.Api.Server.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }, (boolean)"LocalAffinity": false } Each of the object used in the hierachi is builded using the constructor. All the properties are in readonly. I tryed to serialize and deserialize the result on the WCF part before it's sent and it works. var serializer = this._system.Serialization.FindSerializerFor(result); var bytes = serializer.ToBinary(result); var data = serializer.FromBinary(bytes, result.GetType()); The strange think is that it try to deserialize the object in the WCF part where it is sent and not in the LightHouse where the element should be received and transfert to a agent for processing.
This is a top result for request "Could not load System.Private.CoreLib" so I post the workaround for ASP.NET Core APIs and .NET clients. If json serializer type handling set to auto settings.TypeNameHandling = TypeNameHandling.Auto; serializer will include type information for polymorphic types. After migration to .NET Core some clients reported exception and the response body contained following type descriptor: "$type":"System.String[], System.Private.CoreLib" In the API model property type was defined as IEnumerable<string> which forced serializer to include actual type for an Array. The solution was to replace IEnumerable<string> with string[] or any other concrete type which allowed serializer to omit the type descriptor. If the above does not work, for instance when you use Dictionary<string, object> you can implement custom SerializationBinder: public class NetCoreSerializationBinder : DefaultSerializationBinder { private static readonly Regex regex = new Regex( #"System\.Private\.CoreLib(, Version=[\d\.]+)?(, Culture=[\w-]+)(, PublicKeyToken=[\w\d]+)?"); private static readonly ConcurrentDictionary<Type, (string assembly, string type)> cache = new ConcurrentDictionary<Type, (string, string)>(); public override void BindToName(Type serializedType, out string assemblyName, out string typeName) { base.BindToName(serializedType, out assemblyName, out typeName); if (cache.TryGetValue(serializedType, out var name)) { assemblyName = name.assembly; typeName = name.type; } else { if (assemblyName.AsSpan().Contains("System.Private.CoreLib".AsSpan(), StringComparison.OrdinalIgnoreCase)) assemblyName = regex.Replace(assemblyName, "mscorlib"); if (typeName.AsSpan().Contains("System.Private.CoreLib".AsSpan(), StringComparison.OrdinalIgnoreCase)) typeName = regex.Replace(typeName, "mscorlib"); cache.TryAdd(serializedType, (assemblyName, typeName)); } } } And register it in JsonSerializerSettings: settings.SerializationBinder = new NetCoreSerializationBinder(); Note: .AsSpan() for string comparison was added for backwards compatibility with .NET Framework. It doesn't harm, but not required for .NET Core 3.1+, feel free to drop it.
So this is what I thought the issue might be... The problem is that transmitting serialized content between a .NET Core application and a .NET Framework application using a polymorphic serializer like JSON.NET or Hyperion is an unsupported operation in Akka.NET at least, but also in any other runtime where the users uses CLR types as the wire format of the messages. A brief explanation as to why we don't support this in Akka.NET, from the v1.3.0 release notes: As a side note: Akka.NET on .NET 4.5 is not wire compatible with Akka.NET on .NET Core; this is due to fundamental changes made to the base types in the CLR on .NET Core. It's a common problem facing many different serializers and networking libraries in .NET at the moment. You can use a X-plat serializer we've developed here: #2947 - please comment on that thread if you're considering building hybrid .NET and .NET Core clusters. The "fundamental changes" in this case being that the namespaces for types like string are different on .NET 4.* and .NET Core, thus a polymorphic serializer that doesn't account for those differences will throw this type of exception. We do have a workaround available here if you'd like to use it: https://github.com/akkadotnet/akka.net/pull/2947 You'll need to register that serializer compat layer with JSON.NET inside Lighthouse and your .NET 4.7.1 apps.
If this issue happens during deserialization in .NET Framework application and you can't do anything about the JSON you receive, then you can extend DefaultSerializationBinder and use it in JsonSerializerSettings to deserialize JSON generated by .NET Core application: internal sealed class DotNetCompatibleSerializationBinder : DefaultSerializationBinder { private const string CoreLibAssembly = "System.Private.CoreLib"; private const string MscorlibAssembly = "mscorlib"; public override Type BindToType(string assemblyName, string typeName) { if (assemblyName == CoreLibAssembly) { assemblyName = MscorlibAssembly; typeName = typeName.Replace(CoreLibAssembly, MscorlibAssembly); } return base.BindToType(assemblyName, typeName); } } and then: var settings = new JsonSerializerSettings() { SerializationBinder = new DotNetCompatibleSerializationBinder() };
for the base Type try to use serializer.TypeNameHandling = TypeNameHandling.Auto can ignore the System.Private.CoreLib namespace in Json
What mistake am I making when serializing?
This leads to serialization exception at runtime. It's just a demo project to test the best way to do this. I included the main method and the class which im trying to serialize. Ignore: I really cant add more detail, i've described the problem, attached the code, this "please add more details" thing is the stupidest thing ever. Let me post it already. Data toSend = new Data(); toSend.Output(); ///SERIALIZE BinaryFormatter formatter = new BinaryFormatter(); Stream streamOut = File.OpenWrite("file"); formatter.Serialize(streamOut, toSend); streamOut.Close(); Console.WriteLine("----------------------------"); ///DESERIALIZE Stream streamIn = File.OpenRead("file"); Object received = formatter.Deserialize(streamIn); Data toReceive = (Data)received; toReceive.Output(); class Data : ISerializable { int integerData; string stringData; bool booleanData; int shouldnotbeserialized; public Data() { integerData = 1; stringData = "Hello"; booleanData = true; shouldnotbeserialized = 55; } //To deserialize public Data(SerializationInfo info, StreamingContext context) { integerData = info.GetInt32("theint"); stringData = info.GetString("thestring"); booleanData = info.GetBoolean("thebool"); } public void Output() { Console.WriteLine(integerData); Console.WriteLine(stringData); Console.WriteLine(booleanData); Console.WriteLine(shouldnotbeserialized); } //Implemented method to serialize public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("thestring", stringData); info.AddValue("theint", integerData); info.AddValue("thebool", booleanData); } } Exception message: Type 'SerializationDemo.Data' in Assembly 'SerializationDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.
The answer is given to you in the exception message: Type 'SerializationDemo.Data' in Assembly 'SerializationDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable. You need to mark your class with the Serializable attribute. [Serializable()] class Data : ISerializable From the context it seems like you are going to be transmitting the object down a network (deduced due to the local variable names toSend, toReceive). You need to be aware that, if you where for example, using a client-server model, an object serialized and sent from the server software will not be deserializable from the client software by default. This is because the fundamental characteristic of binary serialization is that it preserves type system fidelity. Type system fidelity denotes that type information is not lost in the serialization process. This means that when you serialize an object, is is not only the "object's state" written to the data stream, but also the name of the class and the containing assembly. Even if you defined the class Data in the assembly that is going to deserialize the data, the deserialize method will fail (throw an exception) because the deserializer will be looking for the type Program1.Namespace.Data not Program2.Namespace.Data. To remedy this you can define your data messages in a mutual assembly (class library) or by defining a SerializationBinder that will allow you to basically trick the deserialize method into thinking you are still in the same assembly.
Returning a CLR type from IronRuby
I am trying to return a CLR object from Iron Ruby. I have the following CLR type defined in C# public class BuildMetaData { public string Description { get; set; } } I have the following IronRuby file: $:.unshift(File.dirname(__FILE__) + '/../bin/Debug') require 'mscorlib' require 'Horn.Core.DSL.Domain' class MetaDataFactory def return_meta_data() meta = Horn::Core::DSL::Domain::BuildMetaData.new meta.Description = "A description of sorts" meta end end I have the following test that is failing: [Fact] public void Then_a_build_metadata_object_is_returned() { var engine = Ruby.CreateEngine(); engine.ExecuteFile("test.rb"); var code = String.Format("{0}.new.method :{1}", "MetaDataFactory", "return_meta_data"); var action = engine.CreateScriptSourceFromString(code).Execute(); var result = (BuildMetaData)engine.Operations.Call(action); Assert.Equal(result.Description, "A description of sorts"); } It fails when trying to cast the object returned from IronRuby. I get the following error message: [A]Horn.Core.DSL.Domain.BuildMetaData cannot be cast to [B]Horn.Core.DSL.Domain.BuildMetaData. Type A originates from 'Horn.Core.DSL.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'LoadNeither' at location 'C:\Projects\horn\branches\rubydsl\src\Horn.Dsl.Specificatioin\bin\Debug\Horn.Core.DSL.Domain.dll'. Type B originates from 'Horn.Core.DSL.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' in the context 'Default' at location 'C:\Users\paul.cowan\AppData\Local\Temp\1vt2usw2.rxf\Horn.Dsl.Specificatioin\assembly\dl3\1d5ed945\7c19e429_1a97c901\Horn.Core.DSL.Domain.DLL'. Is it possible to return CLR types from Iron Ruby
Rather than getting the method with a specially crafted string of Ruby, and then using C# to call the method, the preferred way to call into Ruby code from C# is the following: var engine = IronRuby.Ruby.CreateEngine() engine.ExecuteFile("test.rb") var klass = engine.Runtime.Globals.GetVariable("MetaDataFactory") var instance = engine.Operations.CreateInstance(klass) engine.Operations.InvokeMember(instance, "return_meta_data") ~Jimmy
Actually it was all down to shadow copying. My code ended up looking like this: [Fact] public void Then_the_object_should_be_accessible_in_csharp() { var engine = Ruby.CreateEngine(); engine.Runtime.LoadAssembly(typeof (BuildMetaData).Assembly); engine.ExecuteFile(buildFile); var klass = engine.Runtime.Globals.GetVariable("MetaDataFactory"); var instance = (RubyObject)engine.Operations.CreateInstance(klass); //You must have shadow-copying turned off for the next line to run and for the test to pass. //E.g. in R# "ReSharper/Options/Unit Testing/Shadow-Copy Assemblies being tested" should be un-checked. var metaData = (BuildMetaData)engine.Operations.InvokeMember(instance, "return_meta_data"); Assert.Equal(metaData.Description, "A description of sorts"); Assert.Equal(metaData.Dependencies.Count, 1); } But if I turned off shadow-copying from the R# test runner then the test now passes.