Binary serialization + strongly named assemblies + GAC problem

H

Harold Howe

I am having a problem deserializing objects from a library when the
following conditions exist:

1- The library is strongly named
2- The serialized file was created with version 1.0 of the assembly
3- I am trying to deserialize from an EXE that references version 2.0 of
the assembly
4- Both version 1.0 and 2.0 of the assembly reside in the GAC (no policy
redirects exist).

Note that this is not the AssemblyFormat = FormatterAssemblyStyle.Simple
problem. It is a different issue that arises because the old DLL can
still be loaded from the GAC.

object o = null;
Foo foo = null; // Foo is in the strong named assembly

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
using(FileStream stream = File.OpenRead("foo.bin"))
{
o = formatter.Deserialize(stream);
}

foo = o as Foo;
// At this point, o is not null, but foo is null

Deserialization succeeds. The problem is that the deserialized object is
a version 1 object. During deserialization, .NET finds the old version
of the library in the GAC, even though I have compiled against version
2.0. The deserialized object is essentially unusable. The cast from
object to Foo returns a null object, since the returned object is not a
2.0 Foo. The modules view in the debugger shows that both versions of
the library are loaded.

If I remove the 1.0 version of the library from the GAC, deserialization
correctly returns a 2.0 Foo object. Is there a way to make this work
that doesn't involve removing the old library from the GAC? Having the
old version available solves a different set of problems for us.


Here is a complete set of steps + code to reproduce

1- Create a simple, serializable class

//---------- MyLib.cs-----------
using System;
using System.Collections.Generic;
using System.Reflection;

[assembly: AssemblyTitle("MyLib")]
[assembly: AssemblyVersion("1.0.0.0")]

namespace MyLib
{

[Serializable]
public class Foo
{
public Foo()
{
for(int i=0;i<10; ++i)
{
Values.Add(i);
}
}

public List<int> Values = new List<int>();
}
}


2- Compile MyLib.cs into a strongly named library, and add it to the GAC:
sn -k key.snk
csc /t:library /keyfile:key.snk MyLib.cs
gacutil /i MyLib.dll


3- Create an app that serializes an instance of Foo from version 1.0 of
the library. Compile and execute

//-------Create.cs-------
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

using MyLib;

static public class Program
{
[STAThread]
public static void Main()
{
Foo foo = new Foo();

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;

using(FileStream stream = File.Create("foo.bin"))
{
formatter.Serialize(stream, foo);
}
}
}
csc /r:MyLib.dll Create.cs
Create.exe


4- Add a new field to Foo, bump the version, rebuild, and install the
new version into the GAC

//---------- MyLib.cs-----------
....<snip>
[assembly: AssemblyVersion("2.0.0.0")]

....<snip>

public class Foo
{
.... <snip>
public string Text = "hello";
}
csc /t:library /keyfile:key.snk MyLib.cs
gacutil /i MyLib.dll

5- Create an app that tries to load the file that was serialized from
version 1.0 of the DLL. Compile and execute

//-------Load.cs-------
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

using MyLib;

static public class Program
{
[STAThread]
public static void Main()
{
object o = null;
Foo foo = null;

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;

using(FileStream stream = File.OpenRead("foo.bin"))
{
o = formatter.Deserialize(stream);
}

Console.WriteLine(o.GetType().AssemblyQualifiedName);

foo = o as Foo;
if(foo == null)
Console.WriteLine("foo is null");
else
Console.WriteLine("foo is not null. Loaded correctly");
}
}
csc /r:MyLib.dll Load.cs
Load.exe

The output from Load.exe is

MyLib.Foo, MyLib, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=0b5ef2fdd6494a50

foo is null

If I remove the 1.0 version of MyLib from the GAC, deserialization
succeeds. The output is:

MyLib.Foo, MyLib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=0b5ef2fdd6494a50
foo is not null. Loaded correctly

H^2
 
N

Nicholas Paldino [.NET/C# MVP]

Harald,

Why not specify a redirection for your application? You can specify
that when the old version of the library is specified, you use the new
version.

If that doesn't work, I would set a reference to both dlls and then set
up aliases for them so that you can use both versions in your application.
Of course, this would require you to switch based on which version you are
working with, providing a shim to make it easier to use, most likely (so you
don't have to switch on every call).

Hope this helps.


--
- Nicholas Paldino [.NET/C# MVP]
- (e-mail address removed)

Harold Howe said:
I am having a problem deserializing objects from a library when the
following conditions exist:

1- The library is strongly named
2- The serialized file was created with version 1.0 of the assembly
3- I am trying to deserialize from an EXE that references version 2.0 of
the assembly
4- Both version 1.0 and 2.0 of the assembly reside in the GAC (no policy
redirects exist).

Note that this is not the AssemblyFormat = FormatterAssemblyStyle.Simple
problem. It is a different issue that arises because the old DLL can still
be loaded from the GAC.

object o = null;
Foo foo = null; // Foo is in the strong named assembly

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
using(FileStream stream = File.OpenRead("foo.bin"))
{
o = formatter.Deserialize(stream);
}

foo = o as Foo;
// At this point, o is not null, but foo is null

Deserialization succeeds. The problem is that the deserialized object is a
version 1 object. During deserialization, .NET finds the old version of
the library in the GAC, even though I have compiled against version 2.0.
The deserialized object is essentially unusable. The cast from object to
Foo returns a null object, since the returned object is not a 2.0 Foo. The
modules view in the debugger shows that both versions of the library are
loaded.

If I remove the 1.0 version of the library from the GAC, deserialization
correctly returns a 2.0 Foo object. Is there a way to make this work that
doesn't involve removing the old library from the GAC? Having the old
version available solves a different set of problems for us.


Here is a complete set of steps + code to reproduce

1- Create a simple, serializable class

//---------- MyLib.cs-----------
using System;
using System.Collections.Generic;
using System.Reflection;

[assembly: AssemblyTitle("MyLib")]
[assembly: AssemblyVersion("1.0.0.0")]

namespace MyLib
{

[Serializable]
public class Foo
{
public Foo()
{
for(int i=0;i<10; ++i)
{
Values.Add(i);
}
}

public List<int> Values = new List<int>();
}
}


2- Compile MyLib.cs into a strongly named library, and add it to the GAC:
sn -k key.snk
csc /t:library /keyfile:key.snk MyLib.cs
gacutil /i MyLib.dll


3- Create an app that serializes an instance of Foo from version 1.0 of
the library. Compile and execute

//-------Create.cs-------
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

using MyLib;

static public class Program
{
[STAThread]
public static void Main()
{
Foo foo = new Foo();

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;

using(FileStream stream = File.Create("foo.bin"))
{
formatter.Serialize(stream, foo);
}
}
}
csc /r:MyLib.dll Create.cs
Create.exe


4- Add a new field to Foo, bump the version, rebuild, and install the new
version into the GAC

//---------- MyLib.cs-----------
...<snip>
[assembly: AssemblyVersion("2.0.0.0")]

...<snip>

public class Foo
{
... <snip>
public string Text = "hello";
}
csc /t:library /keyfile:key.snk MyLib.cs
gacutil /i MyLib.dll

5- Create an app that tries to load the file that was serialized from
version 1.0 of the DLL. Compile and execute

//-------Load.cs-------
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;

using MyLib;

static public class Program
{
[STAThread]
public static void Main()
{
object o = null;
Foo foo = null;

BinaryFormatter formatter = new BinaryFormatter();
formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;

using(FileStream stream = File.OpenRead("foo.bin"))
{
o = formatter.Deserialize(stream);
}

Console.WriteLine(o.GetType().AssemblyQualifiedName);

foo = o as Foo;
if(foo == null)
Console.WriteLine("foo is null");
else
Console.WriteLine("foo is not null. Loaded correctly");
}
}
csc /r:MyLib.dll Load.cs
Load.exe

The output from Load.exe is

MyLib.Foo, MyLib, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=0b5ef2fdd6494a50

foo is null

If I remove the 1.0 version of MyLib from the GAC, deserialization
succeeds. The output is:

MyLib.Foo, MyLib, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=0b5ef2fdd6494a50
foo is not null. Loaded correctly

H^2
 
T

Thomas T. Veldhouse

Harold Howe said:
I am having a problem deserializing objects from a library when the
following conditions exist:

1- The library is strongly named
2- The serialized file was created with version 1.0 of the assembly
3- I am trying to deserialize from an EXE that references version 2.0 of
the assembly
4- Both version 1.0 and 2.0 of the assembly reside in the GAC (no policy
redirects exist).

Yeah, it's a PITA isn't it? A real Microsoft Faux Paux if you ask me.

Try using this binder when you deserialize (attach it to the binary
formatter). You should be able to set the AssemblyFormat to Simple but that
doesn't seem to do it across assembly boundries.

sealed class SimpleDeserializationBinder : SerializationBinder
{
private Regex _assemRegex
= new Regex("(?<assembly>^.*?),.*");
private Regex _typeRegex
= new Regex("(?<type>.*?),(?<assembly>.*?),.*(?<end>]])");

public override Type BindToType(string assemblyName, string typeName)
{
// remove strong name from assembly
Match match = _assemRegex.Match(assemblyName);
if (match.Success)
{
assemblyName = match.Groups["assembly"].Value;
}

// remove strong name from any generic collections
match = _typeRegex.Match(typeName);
if (match.Success)
{
typeName = string.Format("{0},{1}{2}",
match.Groups["type"].Value,
match.Groups["assembly"].Value,
match.Groups["end"].Value);
}

// replace assembly name with the simple assembly
// name - strip the strong name off of the name
string type = string.Format("{0}, {1}", typeName,
assemblyName);

// The following line of code returns the type.
return Type.GetType(type);
}
}
 
T

Thomas T. Veldhouse

Thomas T. Veldhouse said:
Try using this binder when you deserialize (attach it to the binary
formatter). You should be able to set the AssemblyFormat to Simple but that
doesn't seem to do it across assembly boundries.

BTW ... I utilize it as follows:

BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
binaryFormatter.Binder = new SimpleDeserializationBinder();
 
H

Harold Howe

Why not specify a redirection for your application? You can specify
that when the old version of the library is specified, you use the new
version.

Thanks for the suggestion. Initially we couldn't use a redirect for
technical reasons. Our application is a shell that loads various
plugins. The plugins all run in the same app domain, which means they
share a common app configuration. But the plugins have varying DLL
dependendencies. For example, plugin A that relies on version 1 of
MyLib, and plugin B that relies on version 2. One set of redirects for
all plugins would not be adequate.

However, after further testing, a redirect seems to be the only reliable
solution to my serialization problem. It also solves a couple of other
problems that I didn't post, namely, the deserialization of generics
where the type parameter is a user defined type. So we are working on
redesigning our system so that each plugin runs in its own appdomain.
That is something we have wanted to do for a long time anyway, but never
got around to working out all the kinks. Now we have no choice.

H^2
 
H

Harold Howe

Thomas said:
Yeah, it's a PITA isn't it? A real Microsoft Faux Paux if you ask me.

Try using this binder when you deserialize (attach it to the binary
formatter).
....

Thanks for the code. I tried using your binder. I was already using a
custom binder, so it was easy to incorporate your code into it.

At first, it seemed to work just fine. Then in started failing. What I
discovered is that calling

Type.GetType("MyLib.Foo, MyLib");

succeeds when MyLib.dll is in the same directory as the executable. It
doesn't matter that MyLib is strongly named and was already loaded from
the GAC. It isn't loaded again, and type resolution succeeds.

However, if I nuke the local DLL, then type resolution fails, and I am
back to square one. I also experimented with just nuking the version
information from the fully qualified assembly name. This resulted in the
same behavior.

H^2
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top