Creating and Sharing .NET Assemblies For a .NET Application Projects

This article assumes the reader is aware of .NET assemblies and CLR internals. If you have any questions about this, consider reading the following articles by Microsoft

  1. http://msdn.microsoft.com/en-us/library/ms973231.aspx
  2. http://msdn.microsoft.com/en-us/library/hk5f40ct%28v=vs.90%29.aspx

In this article, we will directly discuss the creation and sharing using a simple example.

How to create assemblies?

Create a solution file using Visual Studio 2008 and add a class library project.

I named my project "TestGACRegistration" and the class "TestDLL"; see.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestGACRegistration
{
    public class TestDLL
    {
        public string mymethod()
        {
            return "M1";
        }
    }
}

Now build the class library project.

Add a new console project

Add a new console project to the solution and add a project reference to the "TestGACRegistration" project and you should find the DLL available in the references section of the console application project. The following is the code for the new console project.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            TestGACRegistration.TestDLL testdll = new TestGACRegistration.TestDLL();
            Console.WriteLine(testdll.mymethod());
            Console.ReadLine();
        }
    }
}

Now run the console application to see the output.

Console

You see the output returned was M1 and that is correct.

Normally there are many ways for developers to maintain the references to the DLLs in the development environment.

  1. Building the DLLs to a specific path and sharing all the DLLs from the path instead of referring to the class library projects.
  2. Adding references directly to the DLLs generated in the bin/release folders of each application.

The first option should be used when the DLL is being referenced by multiple projects and we do not want to duplicate the DLLs to be deployed for each project as private. The second one is good when all the DLLs are built for one project.

Another way to look at it is to imagine we have another console application in the solution which points to all the DLLs in the project. Let us call the console application "MyConsoleApp2" and the other class library "AnotherLibrary". Now, the second console project refers to both the DLLs in the solution and they have been referenced using the second option.

While I was working on the project, I configured the solution file configuration section to build all the class library projects both in debug and release mode. I had compiled the application both in debug and release mode and the DLLs were built for both modes in the respective bin/release folders of each application library and they were copied to the console application bin/release folders since they referred to these projects.

.NETAss2.jpg

Now one of the developers thinks that every time he changes the console application 1, it is building the DLL for the second class library too which is not required for the console application 1, and so he goes into the configuration settings of the solution file and he unchecks the build option for the class library 2 for both debug and release modes. While building setup projects for deployment, I simply select the output folders of the projects and I rebuild the solution in release mode without knowing that someone has changed the configuration properties of the solution.

After deploying to another environment, you may find that new changes are not elevated as the new DLLs are not built in the release mode. Unless you select the output window while building and checking if all the projects are being compiled.

So, in order to avoid this we will not allow each project to reference the class library project for the DLL but refer to the location where DLLs are copied.

This ensures that always the project is referring to the correct DLL compiled.

When we have more references to the same DLL by multiple projects or applications, the DLLs should be in shared mode rather than getting copied to each application's folder as a private assembly. The reason is that if we compile the DLL with a small change, we need to deploy all the applications referring to it. The Microsoft concept of deployment of shared assemblies in .NET helps in resolving this issue by first registering the DLL in the GAC and all the applications should be referring to the DLL available in the GAC. Also, we do not want these DLLs to be copied into the application bin/release folder. To do this, we need to set one property "Copy Local" of the DLL after referencing it.

.NETAss3.jpg

This ensures that the DLL is not copied to the local path of the application whenever you build the project.

If there is only one application that is referencing a DLL and only one or two developers are working on a project and they communicate the changes to each other, then the second option works fine.

You can test the same first without setting the property "Copy Local" to false and then setting it to false.

.NETAss4.jpg

With setting "True":

With setting as false:

.NETAss5.jpg

Again the practices differ from team to team and project to project so there are no defined practices to be followed. It should always be considered based on what is best for the team.

To deploy the shared DLLs into the GAC, we need to generate a strong name first using sn.exe. More information on this can be found in the MSDN.

http://msdn.microsoft.com/en-us/library/xwb8f617.aspx

Now we need to generate a strong name in our example for the class library "TestGACRegistration" in order to deploy it to the GAC.

Go to the Microsoft Visual Studio command prompt and run the strong name key utility as below.

.NETAss6.jpg

The key should be specified in the AssemblyInfo.cs of the class library. It will be available in the properties folder of the Class library project. Normally, the key would be copied into the organization's secured folder path and will be referenced.

Add the key attribute as below.

If you forget to add the key file attribute you would get an error during deployment.

Deployment

Now, after the coding is completed how can I deploy the shared DLLs in the GAC of the production server? The answer is we can do in two ways:

  • Using a tool for registering the DLL in the GAC i.e., GACUTIL.exe
  • Using a Microsoft setup project

We use the setup project option since we can add all the required DLLs in one shot and we can build and run the MSI file. The MSI file deploys the DLLs into the GAC without the GACUTIL.exe as it is intelligent enough to sense based on where the files are to be copied.

Add a new setup project to the solution and name it "SetupDLLs".

.NETAss7.jpg

After the project is created right-click on the project and click "View | File System" and you see 3 project folders already present and so add another folder by right-clicking on the "File System On Target Machine" and select "Add Special Folder" and select the "Global Assembly Cache" folder. By default, any files that are copied to the "Global Assembly Cache Folder" will be deployed automatically by the setup project.

.NETAss8.jpg

.NETAss9.jpg

Now add the DLLs to be deployed to this special folder. In our case, we add only one. Now build the setup project. Let us see what happens if the strong name key was not added to the assembly. Just for testing purposes, I remove the key attribute in the "AssemblyInfo.cs" file and try to build the setup project again.

.NETAss10.jpg

You would see an error if a shared assembly does not have a strong name specified. Now, revert back to the change and run the setup project again. You would see the following content in the output window now: ------ Pre-build validation for project 'SetupDLLs' completed ------

Build started: Project: SetupDLLs, Configuration: Debug

Building file 'C:\TestGACRegistration\SetupDLLs\Debug\SetupDLLs.msi'... Packaging file 'TestGACRegistration.dll'.

Build 1. succeeded or up-to-date, 0 failed, 0 skipped

Now you will see the two files are copied into the bin/release folder based on the mode you have built the setup project.

.NETAss11.jpg

Now run the setup.exe and deploy the DLLs. You can manually run the exe file or run it from the command prompt. We do it manually, but if you would like to learn about silent installation, refer to the following URL:

http://msdn.microsoft.com/en-us/library/windows/desktop/aa372024%28v=vs.85%29.aspx

After the installation is successful, you can go and check if the DLLs are successfully deployed to the GAC. The location of the GAC is normally C:\Winnt\assembly or C:\Windows\assembly.

.NETAss12.jpg

You see now the DLL is successfully registered in the GAC.

Now we can test if this works. One question to ask is: If I change the class library code locally in my solution and build the DLL, does my console application refer to the new DLL?

The answer is No. If you have a DLL registered in the GAC with the same version as you have in your local application, always the one in the GAC is referenced. If you want to test the new code, you have to change the version of the DLL in your local application by changing the version in the "AssemblyInfo.cs" class located in the property folder of the class library. I have compiled the assembly with version 1.1.0.0 and deployed it in GAC for our example.

If you would like to test it, let us go and change the code in the method as below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestGACRegistration
{
    public class TestDLL
    {
        public string mymethod()
        {
            return "M2";
        }
    }
}

Now my method should return "M2" instead of "M1" if the console application is referencing the local DLL. Now build the class library and run the console application to see the result.

Still, you see the output as "M1" only. If you want to test it more thoroughly you can delete the class library reference and add it to the console application project to test it and you see it always returns "M1" only.

Now I will change the version and test it again.

// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

Now build the class library and add the reference to the new DLL/project for clean testing. You will now see the value displayed as "M2".

One drawback observed while using the setup project is you cannot uninstall the assembly from the GAC directly unless you uninstall the setup project instance in the computer. This helps in uninstalling the previous version and installing the new one. If we make new changes to the DLL and these code changes are made for only one of the application's purposes, then we would like the other applications with our new version deployment. If we want to do that, we need to change the upgrade code of the setup project and install another version of the same DLL. We all know GAC supports side-by-side execution of multiple DLLs having the same name and different versions.

In real-time, you might find more flexibility in deploying shared DLLs using the setup project.