I'm trying to use Json.NET to serialise a class for transfer via an http request. This is a client server test program that shares some common class files. Those files are an interface (ITestCase) and the implementations of this interface (TestPicture, TestVideo).
Serialising and deserialising testCase below within the same application works fine, presumably because the Json.NET code is all contained within the one assembly. When I serialise testCase, send it to the server, then try to deserialise, I get the error
"Error resolving type specified in JSON 'com.test.testcases.TestPicture, Graphics Tester'. Path '$type', line 2, position 61"
with an Inner Exception of type JsonSerializationException with message
"Could not load assembly 'Graphics Tester'."
In the Json.NET documentation, when the Json is generated the $type value is "$type": "Newtonsoft.Json.Samples.Stockholder, Newtonsoft.Json.Tests". The second parameter seems to reference the relevant classes by namespace, rather than by project name (namely Graphics Tester) as it is happening in my instance.
Given that both projects share the requisite classes and that the files are in the same namespace, why is Json.NET looking for the assembly rather than the class itself?
Below is the skeleton of my code; details are ommited as they don't assist with the problem. Shown is the interface, and the two implementations of that interface. Also shown are the serialisation and deserialisation operations, and the resulting Json.
ITestCase testCase = new TestPicture("test.jpg")
string json = JsonConvert.SerializeObject(testCase, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
});
ITestCase instance = JsonConvert.DeserializeObject<ITestCase>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
});
//value in variable json after serialisation
//{
// "$type": "com.test.testcases.TestPicture, Graphics Tester",
// "filename": "test.jpg",
// "testPoints": null
//}
Individual class files:
namespace com.test.testcases
{
interface ITestCase
{
void Run();
bool Verify();
}
}
namespace com.test.testcases
{
class TestPicture : ITestCase {}
}
namespace com.test.testcases
{
class TestVideo : ITestCase {}
}
My Solution:
A simpler solution existed than I expected. It wasn't optimal, but it certainly works. I'd class it more as a workaround or a hack. By altering the Project Properties and setting the assembly name to be the same in both projects, it will find the classes that were already included in the project, and thus create the objects specified by the Json string.
The $type encodes the fully-qualified type name, which will differ between the Client and Server if they're not using the same project to define these types (i.e. if each defines the type by itself rather than referencing a common assembly).
In your case, in the client the fully-qualified name is com.test.testcases.TestPicture, Graphics Tester, but when this gets to the server the server can't find any assembly called Graphics Tester.
You can solve this in one of two ways:
Define a common assembly for serializable types, and reference it from both client and server.
Define the types separately both in the client and the server, and customize Json.NET's type resolving by providing a custom SerializationBinder to JsonSerializerSettings.Binder, where you could implement your own logic.
Related
I am primarily asking this question to document a problem that took me several hours to solve where I couldn't find a solution online.
I am using the XmlSerializer to serialize and deserialize a base class where there are several inherited classes:
public abstract class WorkRequest
{}
[WorkRequest]
public class A : WorkRequest
{}
[WorkRequest]
public class B : WorkRequest
{}
The types for the derived classes are loaded via reflection. The key point is how the references for the DLLs are found and loaded:
string path;
Assembly main = Assembly.GetEntryAssembly();
if (main == null) // IIS
path = AppDomain.CurrentDomain.RelativeSearchPath;
else // Standard .NET Service
{
path = main.Location;
path = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar));
}
string[] fileList = Directory.GetFiles(path, "*.dll");
if (main != null)
RegisterInterfaces(main);
foreach (string file in fileList)
{
try
{
Assembly asm = Assembly.LoadFile(file);
RegisterInterfaces(asm);
}
catch (Exception e)
{
}
}
When the application is running in asp.net, AppDomain.CurrentDomain.RelativeSearchPath returns the bin directory of the web application in IIS. When running as a standard .NET service, the entry assembly Location is used to load referenced assemblies to find the request types. The classes in the DLLs are probed for all the types with the WorkRequestAttribute.
The serialization is done as follows:
var settings = new XmlWriterSettings()
{
//Encoding = Encoding.UTF8,
Indent = false,
OmitXmlDeclaration = true
};
using (var tw = new StringWriter())
using (var xmlWriter = XmlWriter.Create(tw, settings))
{
var serializer = GetSerializer(typeof(WorkRequest));
serializer.Serialize(xmlWriter, input);
tw.Flush();
return tw.ToString();
}
The serializer is loaded as follows:
var serializer = new XmlSerializer(theType, _knownTypes.Value);
As recommended, the serializer is allocated once and cached, and the type is typeof(WorkRequest). The _knownTypes are the classes inherited from the WorkRequest found by reflection using the WorkRequest attribute by the above code.
This code runs fine as a .NET service and the referenced types loaded as above. When running within IIS, when the XmlSerializer.Serialize() function is called, the following exception was reported:
The type A was not expected. Use the XmlInclude or SoapInclude
attribute to specify types that are not known statically.
I also tried using a serializer for the inherited type A and the same known types list, but the following error was reported:
Types A' and 'A' both use the
XML type name, 'MyClass', from namespace ''. Use XML attributes to
specify a unique XML name and/or namespace for the type.
It appears that the IIS app is loading the DLLs differently then how I'm loading them, and the known types don't match and the serializer can't find the type. Are there any suggestions what I'm doing wrong to load the types when running in IIS?
Is there another way I could address this to resolve the problem?
I solved the problem by using the type of the request passed in as the explicit known type and still using a serializer for typeof(WorkRequest). The reference to the known types is correct. Note that this does not solve the issue for deserialization within IIS, but I don't have a need for that at this time.
It seems the core problem is loading the assemblies in .NET, the types, even though they're the same, .NET does not think they're the same types and is not using the loaded known types.
I have written a simple COM object in C# with only one method, which is called GetMac. I can't get it to work. I am trying to access it from a legacy Borland C++ Builder 4 (BCB4) application, which I know is old, and not used much anymore, but I am able to access other COM objects from it fine.
The Borland development machine is running Windows XP, so I make the C# COM object target the .NET 4.0 framework. I copied the DLL and PDB file over from the C# Visual Studio machine to the XP machine. I registered it via the following command:
"%WINDIR%\Microsoft.NET\Framework\v4.0.30319\regasm.exe" TRSDotNetCOM.dll /tlb /nologo /codebase
I am able to instantiate the COM object (class) fine via the following line of code:
Variant TDN = CreateOleObject("TRSDotNetCOM.TRSCOM_Class");
If I change the name string, it doesn't work, so I know I have this part correct.
However, when I try to call the method as follows:
MacV = TDN.OleFunction(funcNameV,counterV,macKeyV);
... I get a runtime exception (unfortunately, there's an issue with BCB4's exception handling for OLE calls, so the only info the debugger gives me is "Exception Occurred").
Since I am able to call other COM objects from the same BCB4 application in the same manner, I don't think the problem is with my C++ code. I think it is an issue with either the C#-created COM DLL, or the registration thereof.
To explore this, I used Microsoft OLE/COM Object Viewer to browse my system for the OLE object. I was able to find my object as "TRSDotNetCOM.TRSCOM_Class", as expected.
I'm brand new at using the OLE/COM Object Viewer, so I hope I am looking at the right things below:
When I expand the class, I see the following:
I right-clicked on _Object and chose "View", then "View Type Info". Then, the pane on the right shows:
[ uuid(65074F7F-63C0-304E-AF0A-D51741CB4A8D), hidden, dual, nonextensible,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "System.Object")
] dispinterface _Object {
properties:
methods:
[id(00000000), propget,
custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")]
BSTR ToString();
[id(0x60020001)]
VARIANT_BOOL Equals([in] VARIANT obj);
[id(0x60020002)]
long GetHashCode();
[id(0x60020003)]
_Type* GetType(); };
When I expand the tree on the left, this is what I see:
I do not see my method "GetMac" listed anywhere in there. So, I'm thinking that somehow the method is not visible to COM, or that it's not getting registered via regasm.
Here is the source for the COM object:
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace TRSDotNetCOM
{
[Guid("80ef9acd-3a75-4fcd-b841-11199d827e8f")]
public interface TRSCOM_Interface
{
[DispId(1)]
string GetMac(string counter, string macKey);
}
// Events interface Database_COMObjectEvents
[Guid("67bd8422-9641-4675-acda-3dfc3c911a07"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TRSCOM_Events
{
}
[Guid("854dee72-83a7-4902-ab50-5c7a73a7e17d"),
ClassInterface(ClassInterfaceType.None),
ComVisible(true),
ComSourceInterfaces(typeof(TRSCOM_Events))]
public class TRSCOM_Class : TRSCOM_Interface
{
public TRSCOM_Class()
{
}
[ComVisible(true)]
public string GetMac(string counter, string macKey)
{
// convert counter to bytes
var counterBytes = Encoding.UTF8.GetBytes(counter);
// import AES 128 MAC_KEY
byte[] macKeyBytes = Convert.FromBase64String(macKey);
var hmac = new HMACSHA256(macKeyBytes);
var macBytes = hmac.ComputeHash(counterBytes);
var retval = Convert.ToBase64String(macBytes);
return retval;
}
}
}
I did make sure and go into the project properties and check the "Register for COM interop" checkbox. I also generated a Secure Name file with the "sn" utility, and loaded the file in the Signing section of settings.
So...
1) Am I looking in the correct place in the OLE/COM Object Viewer for my method?
2) If so, why would my method not be visible or not get registered?
3) Any ideas of what else could be wrong?
UPDATE: Here is the updated code with Joe W's and Paulo's suggestions. (It still does not work however)
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace TRSDotNetCOM
{
[Guid("80ef9acd-3a75-4fcd-b841-11199d827e8f"),
ComVisible(true)]
public interface TRSCOM_Interface
{
[DispId(1)]
string GetMac(string counter, string macKey);
}
// Events interface Database_COMObjectEvents
[Guid("67bd8422-9641-4675-acda-3dfc3c911a07"),
ComImport,
ComVisible(true),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TRSCOM_Events
{
}
[Guid("854dee72-83a7-4902-ab50-5c7a73a7e17d"),
ClassInterface(ClassInterfaceType.None),
ComDefaultInterface(typeof(TRSCOM_Interface)),
ComVisible(true),
ComSourceInterfaces(typeof(TRSCOM_Events))]
public class TRSCOM_Class : TRSCOM_Interface
{
public TRSCOM_Class()
{
}
public string GetMac(string counter, string macKey)
{
// convert counter to bytes
var counterBytes = Encoding.UTF8.GetBytes(counter);
// import AES 128 MAC_KEY
byte[] macKeyBytes = Convert.FromBase64String(macKey);
var hmac = new HMACSHA256(macKeyBytes);
var macBytes = hmac.ComputeHash(counterBytes);
var retval = Convert.ToBase64String(macBytes);
return retval;
}
}
}
You're missing just a few bits.
Declare your interfaces as ComVisible:
[ComVisible(true)]
public interface TRSCOM_Interface
If your assembly is already COM visible by default (you can check this in the project's properties or typically in AssemblyInfo.cs), you don't need to do this, but it does no harm and it'll keep the interface available for regasm.exe and tlbexp.exe in case you revert this configuration.
Declare the events interface as ComImport:
[ComImport]
public interface TRSCOM_Events
My guess here is that this interface is defined outside your C# project, probably by the BCB4 application or one of its modules.
If my guess is wrong and your C# project is the one defining this interface, then [ComVisible(true)].
If this interface has event methods, you then implement then as events in the class.
Finally, to avoid having another interface exported for your class, you may want to add the ClassInterface attribute:
[ClassInterface(ClassInterfaceType.None)]
public class TRSCOM_Class : TRSCOM_Interface
This way, you're telling that TRSCOM_Interface is your class default interface, as it is the first one you implement, and regasm.exe /tlb won't generate a class interface.
Depending on the order of implemented interfaces is not reassuring, so you can also use the ComDefaultInterface attribute:
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(TRSCOM_Interface))]
public class TRSCOM_Class : TRSCOM_Interface
Now, you can have any order in the implemented interfaces list without worrying about changing the default class interface.
That is the first time I have ever seen a method declared ComVisible. I would forgo that and instead declare the TRSCOM_Interface interface ComVisible.
In my solution I have three projects like this.
I copy Common.dll and Service.dll in a folder like d:\libs and use the below code with type scan
ObjectFactory.Initialize(x =>
{
x.Scan(xx =>
{
xx.AssembliesFromPath(#"d:\libs");
xx.LookForRegistries();
});
});
//I have PersonService that implement IPersonService
namespace Common
{
public interface IPersonService
{
string SayHello(string name);
}
}
namespace Services
{
public class PersonService : IPersonService
{
public string SayHello(string name)
{
return string.Format("Hello {0}", name);
}
}
}
After Initialize my dependencies when I want get instance from IPerson I get this error
var personService = ObjectFactory.GetInstance<IPersonService>();
{"No default Instance is registered and cannot be automatically determined for type 'Common.IPersonService'\r\n\r\nThere is no configuration specified for Common.IPersonService\r\n\r\n1.) Container.GetInstance(Common.IPersonService)\r\n"}
Add xx.WithDefaultConventions(); as well.
And when you are designing a plugin system by using StructureMap, the host project shouldn't have a reference to any of the plugins. Only the interface/contract plugin should be referenced and this assembly shouldn't be copied to d:\libs folder. In other words, the current application domain shouldn't have 2 instances of any assemblies. So if you want to use IPersonService interface in the host program directly, add a reference to Common.dll and don't copy it to d:\libs folder to avoid duplication. And now the host project shouldn't have a reference to Services.dll too.
You can use the WithDefaultConventions() in your call to Scan which will internally use a DefaultConventionScanner.
You can read the source code or look at the documentation but it does this:
var interfaceName = "I" + concreteType.Name;
return concreteType.GetInterfaces().FirstOrDefault(t => t.Name == interfaceName);
During a default convention scan, each concrete type scanned will look for the first interface of the class name preceded by the letter "I". Therefore, while scanning, IFooService with be automatically registered and assigned if FooService is found during the scan, therefore allowing you to not have to explicitly say
For<IFooService>().Use<FooService>()
The exception you are seeing will always be thrown when there is no registered concrete implementation for the type you requested.
The types you expected to be registered are not because you are scaning with xx.LookForRegistries() which looks for StructureMap registries (you can read more about registries here - http://structuremap.github.io/registration/registry-dsl/). If you create such registries with proper registrations then SM will find them and registeres.
As already mentioned the altrernative is to use xx.WithDefaultConventions(). Take a look at this edge case when using default convention the expected type was not registered - https://stackoverflow.com/a/27449018/4336786
I am encountering a problem with Json.NET (version 6.0.5) that leaves me a bit puzzled.
One of my classes that gets to be serialized looks something like this:
[JsonConstructor]
public MyContainerClass(IEnumerable<AbstractBaseClass> myDerivedUnitClasses)
{
if (myDerivedUnitClasses == null)
{
Units = ImmutableHashSet.Create<object>();
}
else
{
Units = myDerivedUnitClasses.ToImmutableHashSet();
}
}
public IEnumerable<AbstractBaseClass> Units { get; private set; }
Using Json.Convert with TypeNameHandling set to TypeNameHandling.Auto serializes this without problems. The serialized JSON includes the expected $type-qualifier for the property: "System.Collections.Immutable.ImmutableHashSet`1[[AbstractBaseClass, MyLibrary]], System.Collections.Immutable"
I got one project in my Solution where I serialize the data structure and another one where I deserialize it using Json.Convert (deserialization also using automatic type name handling). Deserialization fails with this error: Error resolving type specified in JSON System.Collections.Immutable.ImmutableHashSet`1[[AbstractBaseClass, MyLibrary]], System.Collections.Immutable
Using the source of Json.NET I traced the error back to the DefaultSerializationBinder calling assembly.GetType(string name) and getting null as result.
So far so bad. Here comes the part that leaves me especially puzzled right now: When I deserialize the JSON in the same code block where I serialize my data structure everything works perfectly fine (using the same code that I use in the other project).
Thank you for your help.
Turns out there was an assembly binding problem regarding the assembly containing AbstractBaseClass in the one project. I used fuslogvw.exe of the Visual Studio Tools while debugging to check for errors and noted that the directories that were searched were not the ones I was expecting and did not contain the assembly file.
My solution was to subscribe to the AppDomain.CurrentDomain.AssemblyResolve-event prior to deserialization and then manually load the assembly from the correct path via Assembly.LoadFrom in the event handler.
I have created a phonebook application and it works fine after a awhile i liked to make an upgrade for my application and i started from scratch i didn't inherit it from my old class,and i successes too ,my request
"I want to migrate my contacts from the old application to the
new one"
,so i made an adapter class for this reason in my new application with the following code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace PhoneBook
{
class Adapter
{
PhoneRecord PhRecord; //the new application object
CTeleRecord TelRecord; //the old application object
string fileName;
public Adapter(string filename)
{
fileName = filename;
}
public void convert()
{
PhRecord = new PhoneRecord();
TelRecord = new CTeleRecord();
FileStream OpFileSt = new FileStream(fileName, FileMode.Open,FileAccess.Read);
BinaryFormatter readBin = new BinaryFormatter();
for (; ; )
{
try
{
TelRecord.ResetTheObject();
TelRecord = (CTeleRecord)readBin.Deserialize(OpFileSt);
PhRecord.SetName = TelRecord.GetName;
PhRecord.SetHomeNumber = TelRecord.GetHomeNumber;
PhRecord.SetMobileNumber = TelRecord.GetMobileNumber;
PhRecord.SetWorkNumber = TelRecord.GetWorkNumber;
PhRecord.SetSpecialNumber = TelRecord.GetSpecialNumber;
PhRecord.SetEmail = TelRecord.GetEmail;
PhRecord.SetNotes = TelRecord.GetNotes;
PhBookContainer.phBookItems.Add(PhRecord);
}
catch (IOException xxx)
{
MessageBox.Show(xxx.Message);
}
catch (ArgumentException tt)
{
MessageBox.Show(tt.Message);
}
//if end of file is reached
catch (SerializationException x)
{
MessageBox.Show(x.Message + x.Source);
break;
}
}
OpFileSt.Close();
PhBookContainer.Save(#"d:\MyPhBook.pbf");
}
}
}
the problem is when i try to read the file ctreated by my old application i receive serialization exception with this message
"Unalel to find assembly 'PhoneBook,Version=1.0.0.0,Culture=neutral,PublicK eyToken=null"
and the source of exception is mscorlib.
when i read the same file with my old application (Which is the origin of the file) i have no problem and i don't know what to do to make my adapter class work.
When the class is serialised, it includes the assembly information of the class.
It does this so the deserializer knows what type of class to create with the serialised data.
The problem is that while the two classes may seem to be identical, they are not because they are in different assemblies.
The recommended way to do this is to always put serializable classes in a class library. Then in your situation V2.0 of your application can reference the V1.0 assembly, and then you can deserialize the objects.
If your V1.0 classes aren't in a class library (e.g. they're embedded in an executable), you can build your V2.0 classes in a class library, and add functionality to your V1.0 app to transform classes to V2.0 classes.
Post any questions you might have as comments.
Hope this helps.
BinaryFormatter is not very tolerant to assembly changes. I long ago reached the conclusion that it is OK (just about) for transport, but not good for any kind of storage - it is just too brittle.
In short, I would use another serializer - but contract-based, not type-based (so any type with the same cnotract can share the data):
in many cases XmlSerializer will do; it has some limitations (public types and members), but it works generally
with .NET 3.0, DataContractSerializer is useful
or if you want something outside of the code libs, protobuf-net is very fast and efficient
Of those, only DataContractSerializer will currently support "graph" mode (rather than trees).
If you have existing data that you're fighting, I would be sorely tempted to use the old code (or something very close to it) to re-write the data in a contract-based form. Although you say you've only just created it, so maybe this isn't a problem.
As previously stated the file contains the fully qualified assembly name of your class, which has changed in your new project. If you your assembly, class name and namespaces match, you can set the Assembly format to simple on the formatter:
BinaryFormatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
This use LoadWithPartialName when the formatter tries to load this type. See MSDN for more info.
You could also write a serialization binder to resolve the differences.