9

I serialized an instance of my class into a file (with BinaryFormatter)

After, in another project, I wanted to deserialize this file, but it did not work because my new project does not have the description of my old class. The .Deserialize() gets an exception

Unable to find assembly '*MyAssembly, Version=1.9.0.0, Culture=neutral, PublicKeyToken=null'.*".

But I have the .DLL of the assembly containing a description of the old class which I want to deserialize.

I don't want to add a reference a this DLL in the project (I want be able to deserialize a class of any kind of assembly...)

How can I inform the Serializer/Deserializer to use my dynamically loaded assembly?

dbc
  • 104,963
  • 20
  • 228
  • 340
Baud
  • 182
  • 1
  • 1
  • 8
  • 1
    As long as the assembly is loaded (be it dynamically or by adding a reference to it) the serializer should find it. You said this "How can i inform the Serializer/Deserializer to use my dynamically loaded assembly?". That implies that the assembly is already "loaded" all you need to do is "inform the Serializer/Deserializer" about the "loading". That is a contradiction. The Serializer/Deserializer needs not be informed about anything. If you've loaded the assembly then it will use it. So load the assembly. You can't escape not loading it. To load an assembly dynamically use Assembly.LoadFrom – Eduard Dumitru Sep 18 '13 at 20:29
  • 2
    https://techdigger.wordpress.com/2007/12/22/deserializing-data-into-a-dynamically-loaded-assembly/ posting this in case someone else needs it. The solution given here is to provide a custom SerializationBinder class with the BindToType method overridden to provide the deserialization Type. – Andrew Pope Oct 17 '18 at 18:46
  • You could use [AppDomain.AssemblyResolve](https://msdn.microsoft.com/en-us/library/system.appdomain.assemblyresolve.aspx) as shown in [SerializationException for dynamically loaded Type](https://stackoverflow.com/a/9162553/3744182). I think a custom `SerializationBinder` should work also, see [BinaryFormatter deserialize gives SerializationException](https://stackoverflow.com/q/2120055/3744182) and [How to create a SerializationBinder for the Binary Formatter that handles the moving of types from one assembly and namespace to another](https://stackoverflow.com/a/19927484/3744182). – dbc Dec 10 '19 at 06:51
  • Never used BinaryFormatter myself but can you go for another route instead of using BinaryFormatter like serializing yourself? E.g please see example here https://stackoverflow.com/a/1676053/2936309. The metadata mentioned here might be the same reason why you need the exact same dll while deserializing. Can you go around the issue by doing some of the work yourself? – tubakaya Dec 12 '19 at 23:00

3 Answers3

5

Assuming you are loading your assembly via Assembly.Load() or Assembly.LoadFrom(), then as explained in this answer to SerializationException for dynamically loaded Type by Chris Shain, you can use the AppDomain.AssemblyResolve event to load your dynamic assembly during deserialization. However, for security reasons you will want to prevent loading of entirely unexpected assemblies.

One possible implementation would be to introduce the following:

public class AssemblyResolver
{
    readonly string assemblyFullPath;
    readonly AssemblyName assemblyName;

    public AssemblyResolver(string assemblyName, string assemblyFullPath)
    {
        // You might want to validate here that assemblyPath really is an absolute not relative path.
        // See e.g. https://stackoverflow.com/questions/5565029/check-if-full-path-given
        this.assemblyFullPath = assemblyFullPath;
        this.assemblyName = new AssemblyName(assemblyName);
    }

    public ResolveEventHandler AssemblyResolve
    {
        get
        {
            return (o, a) =>
                {
                    var name = new AssemblyName(a.Name);
                    if (name.Name == assemblyName.Name) // Check only the name if you want to ignore version.  Otherwise you can just check string equality.
                        return Assembly.LoadFrom(assemblyFullPath);
                    return null;
                };
        }
    }
}

Then, somewhere in startup, add an appropriate ResolveEventHandler to AppDomain.CurrentDomain.AssemblyResolve e.g. as follows:

class Program
{
    const string assemblyFullPath = @"C:\Full-path-to-my-assembly\MyAssembly.dll";
    const string assemblyName = @"MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

    static Program()
    {
        AppDomain.CurrentDomain.AssemblyResolve += new AssemblyResolver(assemblyName, assemblyFullPath).AssemblyResolve;
    }

This ResolveEventHandler checks to see whether the requested assembly has your dynamic assembly's name, and if so, loads the current version from the expected full path.

An alternative would be to write a custom SerializationBinder and attach it to BinaryFormatter.Binder. In BindToType (string assemblyName, string typeName) the binder would need to check for types belonging to your dynamic assembly, and bind to them appropriately. The trick here is dealing with situations in which your dynamically loaded types are nested in a generic from another assembly, e.g. a List<MyClass>. In that case assemblyName will be the name of the assembly of List<T> not MyClass. For details on how to do this see

In comments @sgnsajgon asked, I wonder why I cannot deserialize stream the same way I would do when signed assembly is explicitly referenced in project - just formatter.Deserialize(stream) and nothing else.

While I don't know what Microsoft employees were thinking when they designed these classes (back in .Net 1.1), it might be because:

As an aside, What are the deficiencies of the built-in BinaryFormatter based .Net serialization? gives a useful overview of other problems you might encounter using BinaryFormatter.

dbc
  • 104,963
  • 20
  • 228
  • 340
3

Binary serialization has a no-nonsense attitude to DLL Hell. It records the exact assembly that contained the type when the data was serialized. And insists to find that exact assembly back when it deserializes the data. Only way to be sure that serialized data matches the type, taking any shortcuts will merely ensure you'll get exceptions when your lucky, garbage data when you are not. The odds that this will happen, sooner or later, are 100%.

So you'll need to completely scrap the idea that you can use a "dynamically loaded assembly" and get it to "deserialize a class of any kind", that's an illusion. You can spin the wheel of fortune and put a <bindingRedirect> in the app.exe.config file to force the CLR to use a different assembly version. Dealing with the mishaps are now your responsibility. Many programmers grab the opportunity, few come back from the experience without learning a new lesson. It has to be done to appreciate the consequences. So go ahead.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 2
    I check if the assembly used to Serialize the instance is the same of the assembly loaded dynamically to Deserialize. These assemblies seems the same: same name/version/culture/GUID. When i browse these Reflection.Assemly, differences found: -locations -filename (renamed dll) -evidences -# of class These assemblies are not exacly the same (i was aware about that), but have the same ident and description of the serialized class : -the number of class shouldn't be a problem because there is only one class marked [Serializable] -this class have the same description of serializable fields. – Baud Sep 19 '13 at 06:40
  • 1
    From which criteria .Net consider theses assemblies are different ? – Baud Sep 19 '13 at 06:41
  • You said `And insists to find that exact assembly back when it deserializes the data`. I have exception even for signed assembly. Cannot deserialize in any way in case of dynamic assembly. No problem in case of referenced assembly. – sgnsajgon Dec 09 '19 at 19:47
  • Assembly loaded using Assembly.LoadFrom(). – sgnsajgon Dec 09 '19 at 19:54
  • 3
    @sgnsajgon - You could use [AppDomain.AssemblyResolve](https://msdn.microsoft.com/en-us/library/system.appdomain.assemblyresolve.aspx) as shown in [SerializationException for dynamically loaded Type](https://stackoverflow.com/a/9162553/3744182). I think a custom `SerializationBinder` should work also, see [BinaryFormatter deserialize gives SerializationException](https://stackoverflow.com/q/2120055/3744182) and [How to create a SerializationBinder for the Binary Formatter that handles the moving of types from one assembly and namespace to another](https://stackoverflow.com/a/19927484). – dbc Dec 10 '19 at 06:54
  • 1
    @dbc Yes, there are workarounds, but I wonder why I cannot deserialize stream the same way I would do when signed assembly is explicitly referenced in project - just `formatter.Deserialize(stream)` and nothing else. – sgnsajgon Dec 10 '19 at 12:17
  • @sgnsajgon - *I wonder why I cannot deserialize stream the same way I would do when signed assembly is explicitly referenced in project* 1) Because, in the words of Erik Lippert, [no one ever designed, specified, implemented, tested, documented and shipped that feature.](https://blogs.msdn.microsoft.com/ericlippert/2009/06/22/why-doesnt-c-implement-top-level-methods/) 2) `BinaryFormatter` security is kind of a dumpster fire anyway, but loading any assembly by name even when not statically linked would make it *even worse* because unexpected types could get loaded by malicious payloads. – dbc Dec 10 '19 at 17:09
  • @sgnsajgon - you added a bounty here, but [SerializationException for dynamically loaded Type](https://stackoverflow.com/a/9162553/3744182) mostly looks like a legit duplicate. What do you want to do? Should I combine the comments above into an answer? – dbc Dec 10 '19 at 18:44
  • @dbc You right. The answer of Hans Passant does not clearly state that it is impossible to do it straightway, so I have asked. Then, I wonder why exception says `Unable to find assembly` rather that throwing `NotSupportedException` with proper problem description. Finally, security can be by-passed by `AppDomain.AssemblyResolve` which is very intrusive solution (affects entire AppDomain, not particular type scope). By-pass with `SerializationBinder` is IMO unclear and cumbersome. The last solution is to manually use `BinaryReader` and `BinaryWriter` but I would like to avoid it. – sgnsajgon Dec 10 '19 at 20:30
  • @dbc I think the solutions you have mentioned may work indeed, but they are not in any way more secure than solution with straightforward deserialization. – sgnsajgon Dec 10 '19 at 20:40
3

First of all, some facts about binary serialization (skip them if you are interested only in the solutions):

  • The goal of binary serialization is to make a 'bitwise' copy of an object. This often involves serialization of private fields, which may change from version to version. This is not a problem if deserialization happens always in the same process as the serialization (typical use cases: deep cloning, undo/redo, etc.).
  • Therefore binary serialization is not recommended if the deserialization can occur in a different environment (including different platform, framework version, different versions of the assemblies or even the obfuscated version of the same assembly). If you know that any of these applies your case, then consider to use a text-based serialization by public members, such as XML or JSON serialization.
  • It seems that Microsoft started to abandon BinaryFormatter. Though it will be removed/marked as obsolete only in .NET 5 (will be able to be used as a package, though), there are many types also in .NET Core 2/3 that used to be serializable in the .NET Framework but are not serializable anymore in .NET Core (eg. Type, Encoding, MemoryStream, ResourceSet, delegates, etc.).

If you are still sure you want to solve the issue by using the BinaryFormatter you have the following options:

1. Simplest case: only the assembly version has changed

You can add a simple assemblyBinding to the app.config file. Just put the actual version in the newVersion attribute.

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="MyAssembly" publicKeyToken="null" culture="neutral" />
      <bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>

2. Assembly name and/or type name has also changed (or if you prefer programmatic solutions)

IFormatter implementations (thus also BinaryFormatter) have a Binder property. You can use it to control assembly/type name resolves:

internal class MyBinder : SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        // mapping the known old type to the new one
        if (assemblyName.StartsWith("MyAssembly, ") && typeName == "MyNamespace.MyOldType")
            return typeof(MyNewType);

        // for any other type returning null to apply the default resolving logic:
        return null;
    }
}

Usage:

var formatter = new BinaryFormatter { Binder = new MyBinder() };
return (MyNewType)formatter.Deserialize(myStream);

If you just need an assembly-version insensitive resolver you can use the WeakAssemblySerializationBinder.

3. The inner structure of the new type has also changed

Since the OP did not cover this case I will not go too deep into the details. TL;DR: In this case you need to set the IFormatter.SurrogateSelector property. You can use it along with the Binder property if both the type name and the inner layout has changed. In case you are interested there are some possible sub-cases cases at the Remarks section of the CustomSerializerSurrogateSelector class.


Final thoughts:

  • The error message in the question is a hint that using BinaryFormatter is maybe not the best choice for your goals. Use the solutions above only if you are sure you want to use binary serialization. Otherwise, you can try to use XML or JSON serialization instead, which serialize types basically by public members and do not store any assembly information.
  • If you want to use the binders/surrogate selectors I linked above you can download the libraries from NuGet. It actually contains also an alternative binary serializer (disclaimer: written by me). Though it supports many simple types and collections natively (so no assembly identity is stored in the serialization stream) for custom types you may face the same issue as emerged in the question.
György Kőszeg
  • 17,093
  • 6
  • 37
  • 65