Reusing .NET Assemblies in Silverlight
Table of Contents
Introduction
Long before Silverlight 1.0 was released, it was actually called WPF/E or WPF Everywhere. The idea was to allow you to create WPF like interfaces in your web browser. This can be seen in a very small way in Silverlight 1.0. All it provided was very basic primitive objects with the ability for interact with client-side technologies like JavaScript. However, with Silverlight 2.0, Silverlight is actually more than what was originally promised with the term "WPF/E". Silverlight is now far much more than a graphical technology. All this stuff about Silverlight being "WPF for the Web" is more to make the marketing folks happy than anything else.
As a technology parallel to .NET, Silverlight is not part of the .NET family. Rather, it essentially mirrors the .NET platform to create a new platform inside of a web browser where you have a mini-CLR and mini-Framework Class Library (FCL). However, even though they are parallel technologies, you would suspect that Microsoft would allow some level of reuse between the two. As it turns out, most topics are completely reusable. Among other things, Silverlight has delegates, reference types, value types, a System namespace, and the ability to write code in both C# and VB.
Furthermore, despite the rumors, Silverlight also shares the exact same module and assembly format as .NET. This may seem completely shocking to some people given the fact that Visual Studio 2008 doesn't allow you to reference a .NET assembly in a Silverlight project. In reality, however, there's no technical reason for this prohibition. There isn't a single byte difference between a Silverlight and .NET assembly. One way to see this is by referencing a Silverlight assembly in a .NET project. Just try it. It works great. So, why doesn't Visual Studio allow .NET assemblies in Silverlight projects?
To answer this, we need to understand that just because an optional helper tool (i.e. Visual Studio) doesn't allow something, that doesn't mean the technology itself doesn't. In this case, the reason why Visual Studio allows a .NET project to reference Silverlight assemblies, but not the other way around is probably because .NET assemblies can normally do more. For example, .NET has all kinds of XML related entities in its System.Xml assembly. If Silverlight were to try to use this, it would blow up at runtime. However, both Silverlight and .NET have an mscorlib assembly thus giving them a sense of brotherhood. Having said that, Silverlight has the System.Windows.Browser assembly which, upon access in .NET, would make your .NET application explode! Thus, the Visual Studio restriction laws are flawed.
Fortunately, there are ways around Visual Studio's fascist regime. I'm going to talk about two different ways of reusing .NET assemblies and code in Silverlight. The first technique is the more powerful assembly-level technique, while the second is more flexible file-level technique. Each technique is useful for its own particular scenarios. Please keep naive comments of "I'm ALWAYS going to..." and "I'm NEVER going to..." to yourself. You need to make decisions of which of these techniques or possibly another technique to use on a case by case basis.
The Assembly-Level Technique
For this technique, you need to understand what's going on under the covers when you try to add a .NET reference to your Silverlight application in Visual Studio. It's actually incredibly simple. Visual Studio isn't a monolith that controls all your code from a centralized location; sometimes it uses plug-ins to do it's dirty work.
In this case, Visual Studio 2008 uses the Microsoft.VisualStudio.Silverlight .NET assembly. In this assembly is the Microsoft.VisualStudio.Silverlight.SLUtil class which contains the IsSilverlightAssembly method. When you add an assembly to a Silverlight Project, this method is called internally to see if your assembly is Silverlight. If it is, it will add it. If not, it won't. It's just that simple. But, given that the Silverlight and .NET assembly format is the same, how can it know?
You may be shocked to find out that the reason behind this is completely artificial: if the assembly references the 2.0.5.X version of the mscorlib assembly, then Visual Studio says that it's a Silverlight assembly! This test is essentially all the IsSilverlightAssembly does. Therefore, if you take your .NET 2.x/3.x assembly and change the version of mscorlib that your assembly references from 2.0.0.0 to 2.0.5.0, you may then add the assembly as a reference. Now let's talk about this with a more hands on approach.
Below is the sample code we will be working with for this part of the discussion. Say this code is placed in an empty .NET project. When it is compiled, we will have an assembly. Let's call it DotNet.dll.
using System; //+ namespace DotNet { public class Test { public String GetText() { return String.Format(" ", "This", "is", "a", "test"); } } }
Before we go any further, lets' discuss the state of the universe at this point. If you ever try to solve a problem without understanding how the system works, you will at best be hacking the system. Professionals don't do this. Therefore, let's try to understand what's going on.
The first thing you need to know is that when you add an assembly to a project in Visual Studio, you are simply telling Visual Studio to tell the compiler what reference you have so that when the compiler translates your code into IL, it knows what assemblies to include as "extern assembly" sections. Even then, only the assemblies that are actually used in your code will have "extern assembly" sections. Thus, even if you added reference every single assembly in your entire system but only use two, the IL will only have two extern sections (i.e. references assemblies). The second thing you need to know is that no matter what, your assemblies will always have a reference to mscorlib. This is the root of all things and is where System.Object is stored.
To help you understand this, let's take a look at the IL produced by this class. To look at this IL, we are going to use .NET's ILDasm utility. Reflector will not be your tool of choice here. Reflector is awesome for referencing code, but not for working with it. It's more about form than function. With ILDasm we are going to run the below command:
ILDasm DotNet.dll /out:DotNet.il
For the sake of your sanity, use the Visual Studio command prompt for this. Otherwise you will need to either state the absolute path of ILDasm or set the path.
This command will produce two files: DotNet.il and DotNet.res. The res file is completely meaningless for our discussion and, therefore, will be ignored. Here is the IL code in DotNet.il:
.assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 2:0:0:0 } .assembly DotNet { /** a lot of assembly level attributes have been left out **/ .hash algorithm 0x00008004 .ver 1:0:0:0 } .module DotNet.dll .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 .corflags 0x00000001 .class public auto ansi beforefieldinit DotNet.Test extends [mscorlib]System.Object { .method public hidebysig instance string GetText() cil managed { .maxstack 4 .locals init ([0] object[] CS$0$0000) IL_0000: ldstr " " IL_0005: ldc.i4.4 IL_0006: newarr [mscorlib]System.Object IL_000b: stloc.0 IL_000c: ldloc.0 IL_000d: ldc.i4.0 IL_000e: ldstr "This" IL_0013: stelem.ref IL_0014: ldloc.0 IL_0015: ldc.i4.1 IL_0016: ldstr "is" IL_001b: stelem.ref IL_001c: ldloc.0 IL_001d: ldc.i4.2 IL_001e: ldstr "a" IL_0023: stelem.ref IL_0024: ldloc.0 IL_0025: ldc.i4.3 IL_0026: ldstr "test" IL_002b: stelem.ref IL_002c: ldloc.0 IL_002d: call string [mscorlib]System.String::Format(string, object[]) IL_0032: ret } .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret } }
Right now we only care about the first section:
.assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver 2:0:0:0 }
This ".assembly extern ASSEMBLYNAME" pattern is how your assembly references are stored in your assembly. In this case, you can see that mscorlib is referenced using both it's version and it's public key. For our current mission, all we need to do is change the second 0 to a 5. The public key tokens used in Silverlight are completely different from the ones in .NET, but we are trying to fool Visual Studio, not Silverlight. This is a compile-time issue, not a runtime-issue. Speaking more technically, we don't care about the public key token because this information is only used when an assembly is to be loaded. The correct mscorlib assembly will have already loaded by the Silverlight application itself long before our assembly comes on the scene. So, in our case, this entire mscorlib reference is really just to make the assembly legal and to fool Visual Studio.
Once you make the change from 2:0:0:0 to 2:0:5:0, all you need to do is use ILAsm to restore the state of the universe (unlike Reflector with C#, ILAsm can put humpty dumpty back together again). Here's our command for doing this (in this case the resource part is completely optional, but let's add it for completeness):
ilasm DotNet.il /dll /resource:DotNet.res /out:DotNet2.dll
You are now free to reference your .NET assembly in your Silverlight project or application. As I've already mentioned, Silverlight and .NET have the same assembly format. There's nothing in Silverlight that stops us from referencing .NET assemblies, it was only Visual Studio stopping us.
At this point you have just the basics of this topic. However, it's not the end of the story. As you should be aware, .NET's core assemblies use four-part names. That is, they have a strong name. This is used to disambiguate them from other assemblies. That is, instead of the System assembly being called merely "System", which can easily conflict with other assemblies (obviously written by non-.NET developers who don't realize that System should be reserved), it's actually named "System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089". When you reference an assembly, you need to make sure to match the name, version, culture, and public key token. When it comes to using .NET assemblies in Silverlight, this is critically important.
Let's say, for instance, that you created a .NET project which referenced and used entities from the System, System.ServiceModel, and System.Runtime.Serialization assemblies. In this case, the IL produced by the .NET compiler will create the following three extern assembly sections:
.assembly extern System { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 2:0:0:0 } .assembly extern System.ServiceModel { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 3:0:0:0 } .assembly extern System.Runtime.Serialization { .publickeytoken = (B7 7A 5C 56 19 34 E0 89) .ver 3:0:0:0 }
Notice the public key token on each. Here all three are the same, but for other .NET assemblies they may be different. What's important here, though, is that the keys are used to identity the assemblies for .NET, not Silverlight. Thus, even though you did add your .NET assembly to your Silverlight application, an exception would be thrown in runtime at the point where your application tries to access something in one of these assemblies.
The following shows you what would happen in the extreme case of trying to use the System.Web assembly in your Silverlight. You would get the same error if you tried to access something in one of the above assemblies as well.
As it stands, though, we can fix this just as easily as we fixed the mscorlib problem in Visual Studio. All we need to do is open our IL and change the public keys and versions to the Silverlight versions. Below is a list of the common Silverlight assemblies each with their public key token and version:
.assembly extern mscorlib { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System.Core { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System.Net { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System.Runtime.Serialization { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System.Windows { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } .assembly extern System.Windows.Browser { .publickeytoken = (7C EC 85 D7 BE A7 79 8E) .ver 2:0:5:0 } //+ note the different public key token in the following .assembly extern System.ServiceModel { .publickeytoken = (31 BF 38 56 AD 36 4E 35) .ver 2:0:5:0 } .assembly extern System.Json { .publickeytoken = (31 BF 38 56 AD 36 4E 35) .ver 2:0:5:0 }
Just use the same ILDasm/Edit/ILAsm procedure already mentioned to tell the assembly to use the appropriate Silverlight assemblies instead of the .NET assemblies. This is an extremely simple procedure consisting of nothing more than a replace, a procedure that could easily be automated with very minimal effort. It shouldn't take you much time at all to write a simple .NET application to do this for you. It would just be a simple .NET to Silverlight converter and validator (to test for assemblies not supported in Silverlight). Put that application in your Post Build Events (one of the top 5 greatest features of Visual Studio!) and you're done. No special binary hex value searching necessary. All you're doing is changing two well documented settings (the public key token and version).
For certain assemblies, this isn't the end of the story. If your .NET assembly has a strong name, then by modifying it's IL, you have effectively rendered it useless. Aside from disambiguation, strong names are also used for tamper protection. You can sort of think of them as a CRC32 in this sense. If you were to modify the IL of an assembly with a strong name, you would get a compile-time error like the following:
However, as you know by the fact that we have looked at the raw text of the source code with our own eyes, the strong name does absolutely no encryption of the IL. That's one of the most common misconceptions of strong names. They are not used as for public key encryption of the assembly. Therefore, we are able to get around this by removing the public key from our assembly before using ILAsm. Below is what the public key will look like in your IL file. Just delete this section and run ILAsm.
.publickey = (00 24 00 00 04 80 00 00 94 00 00 00 06 02 00 00 00 24 00 00 52 53 41 31 00 04 00 00 01 00 01 00 37 3C 5A 7F 6D B6 3F 30 D8 3F DE E3 17 FE E5 2E 68 43 16 A9 7C 42 69 5A 05 52 E6 73 C5 AC 58 7E B0 00 9F DC 1B 0A 78 57 79 12 79 53 E1 60 EB C9 ED 49 7C 8C 73 1B 01 A7 BA 57 79 B5 53 83 8B CA 8D F8 6F 3B BD A5 E4 BA 6A 12 B9 52 F2 E9 A3 FC 42 17 E4 33 97 92 DC 21 30 57 B9 D3 63 7A F2 43 73 42 70 18 89 8B 44 B9 D4 5A BA A9 21 A3 D9 E0 86 20 3C 30 01 A9 B9 BB F4 D8 79 B7 7D 56 5A A9)
Upon using ILAsm to create the binary version of the same IL, you will be able to add your assembly, compile and run your application without a problem. However, you can take this one step further by telling ILAsm to sign the assembly using your original strong name key. To do this, just use the key command line option to specify the strong name key you would like to use. Below is the new syntax for re-signing your assembly:
ILAsm DotNet.il /dll /resource:DotNet.res /out:DotNet11.dll /key=..\..\MyStrongNameKey.snk
At this point you have a strongly-named Silverlight assembly createdrom your existing .NET assembly.
Now, before moving on to explain a more flexible method of reuse, I want to cover a few miscellaneous topics. First, for those of you who know some IL and are trying to be clever to make this process even simpler, you may think you could just do the following:
.assembly extern mscorlib { auto }
This won't work as ILAsm will look for "auto" and place the 2.0.0.0 version in it's place, thus leaving you right where you started. Also, don't even think about leaving the entire mscorlib part off either. That won't fool anyone since ILAsm will detect that it's missing and add it before continuing the assembly process. You need to explicitly state that you want assembly version 2.0.5.0.
Second, you need to think twice before you add a Silverlight assembly to a .NET application. In the Visual Studio world, if you add a .NET assembly, you add only that assembly. But, in the that assembly is a Silverlgiht assembly, then you will see all of the associated Silverlight assemblies added for each culture you have. When I did this on my system, exactly 100 extra files were added to my Bin folder! That's insane. So, perhaps the Visual Studio team put a "Add Reference" block in the wrong place!
The File-Level Technique
Now all of this is great. You can easily access your .NET assemblies in Silverlight. But, many times this isn't even what you need. You need to remember that every time you reference an assembly in Silverlight, you increase the size of your Silverlight XAP package. Whereas .NET and Silverlight will only register assembly references in IL when they are actually used, Silverlight will package referenced assemblies in the XAP file regardless of use. They assemblies will also be registered in the AppManifest.xaml file as an assembly part. Though the XAP file is nothing more than a ZIP file, thereby shrinking the size of the assembly, this still spells "bloat" if all you need is just a few basic types from an assembly that's within your control. For situations like this, there's a much simpler and much more flexible solution.
The solution to this again deals with understanding the internals of your system: whenever you add a file to your project in Visual Studio, all you are really doing is adding a file to an ItemGroup XML section in the .NET project file. This is just a basic text file that describes the project. As you may have guessed, the ItemGroup section simply contains groups of items. In the case of compilation files (i.e. classes, structs, enums, etc...), they are Compile items. Here's an example of a snippet from a .NET project:
<ItemGroup> <Compile Include="Client\PersonClient.cs" /> <Compile Include="Agent\PersonAgent.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Configuration.cs" /> <Compile Include="Information.cs" /> <Compile Include="_DataContract\Person.cs" /> <Compile Include="_ServiceContract\IPersonService.cs" /> </ItemGroup>
Given this information, all you need to do is (1) create a Silverlight version of this assembly, (2) open the project file and (3) copy/paste in the parts you want to use in your Silverlight project with the appropriate relative paths changed. This will create a link from the Silverlight project's items to the physical items. No copying is done. They are pointing to the exact same file. When they are compiled, there is no need to do any IL changes in your assemblies at all since the Silverlight assembly will be Silverlight and the .NET assembly will be .NET.
Now that you know about this under-the-covers approach, you should be aware that this is actually a fully supported option in Visual Studio. Just go to add an existing item to your project and instead of clicking add or just hitting enter, hit the little arrow next to add and select "Add As Link". This will do the exact same thing as what we did in our bulk copy/paste method in the project file. Here's a screen shot of the option in Visual Studio:
What may be more interesting to you is that this feature may be used anywhere in .NET. You can use this to reuse any files in your entire system. It's a very powerful technique to reuse specific items in assemblies. It comes it very handy when two assemblies need to share classes and creating a third assembly which both may access leads to needless complexity.
Conclusion
Given these two techniques, you should be able to effectively architect a solution that scales to virtually any number of developers. The first technique is easy to deploy using a custom utility and post build events, while the second is natively supported by any good version control systems. Keep in mind though, that when using the first technique you may not always need to do this on every build. The best approach I've seen for this is to have a centralized location on a network share that contains nightly (or whatever) builds of core assemblies. Then, a login script will copy each of the assemblies to each developers machine. This will cut down on the complexity of compilation and dramatically lower the time to compile any solution.
Regardless of which technique you use, you should feel a sense of freedom knowing of their existence. This is especially true if all you are doing is trying to share data contracts between .NET and Silverlight. As I've mentioned in my popular 70+ page "Understanding WCF in Silverlight 2" document, the "Add Service Reference" feature is not something that should be used in production. In fact, it's painful in development as well. Using the techniques described here, you can easily share your data contracts between your .NET server and the Silverlight client without the FrontPage/Word 95 style code generation. For more information on this specific topics, see the aforementioned document.
Links
- Understanding WCF in Silverlight 2 - a complete document on using WCF in Silverlight. If you will ever do WCF in either .NET or Silverlight, you may want to study this.
- Expert .NET 2.0 IL Assembler - THE source for information on Microsoft IL, the language of .NET. The more you know, the more you can control. Buy this book.
- Strong-Named Assemblies (MSDN)
- Using Strong Name Signatures (MSDN)
- ILDasm (MSDN)
- ILAsm (MSDN)