eflection is truly a paradigm shift for developers using Microsoft’s line of development tools. The runtime availability of assembly metadata provides developers with new flexibility and the ability to develop interesting software tools. Developers often use reflection to load assemblies and instantiate classes dynamically, or to invoke methods, but you can also use reflection to inspect assembly metadata.
That capability leads to a way to solve an often-experienced problem?application deployment failures due to missing project dependencies on a machine. When you deploy a new .NET DLL or executable, knowing that a dependency is missing would aid greatly in fixing the problem before customers report the problem. Using reflection, you can easily build a tool to validate an assembly’s dependencies and report on which ones are missing.
This article describes a tool you can use in your own projects to report missing dependencies. For server or middle-tier applications, you can call the validation code directly from the application. For client-deployed applications, you can include the code as a diagnostic tool for troubleshooting problems in the application. Before building the tool, here’s a quick tour of what reflection is and how it works.
At its most basic level, reflection provides a way to inspect assembly metadata, including the classes exposed by the assembly, the methods and types found therein, attributes assigned to various objects, and other related information, such as referenced assemblies and assembly version numbers. Using this information, reflection also provides a way to dynamically instantiate classes and invoke methods on those classes in a more-or-less late-bound manner. Reflection has been available since the original release of the .NET framework.
In this article, you’ll see how to use reflection to expose assembly metadata. Specifically, to build a dependency validation tool, you need to discover which assemblies an application (which itself is an assembly) depends on for proper operation. The validation tool verifies that all a project’s primary dependencies are available, and that any secondary dependencies (those required by primary dependencies) are available, and so on, until the entire dependency hierarchy has been traversed. As you will see, this process is surprisingly fast, even for projects with deep dependency levels.
How .NET Resolves Assembly References
To get started on the development of the validation tool, you must first understand what it means for an assembly to be available?more specifically, how you can know for sure that an assembly can be found and loaded successfully at runtime. Unlike the registration model used by COM, whereby components are registered in the machine’s registry, .NET by default does not register its components in a centralized fashion (with some exceptions, which aren’t relevant to this tool). Instead, the .NET Framework depends on a well-defined process to find referenced assemblies. Unless assemblies are registered in a configuration file or stored in the GAC, the framework uses a probing process to find referenced assemblies.
Because the probing process traverses certain directories in and hierarchically beneath the directory where the hosting application is located, assemblies not stored in the GAC or registered in a config file must be located in either the hosting application’s directory or in a particular directory below the application root directory. This process is also documented at MSDN.
|Author’s Note: Most of the functionality described in this article is exposed through the System.Reflection namespace. You should add an Imports statement (if you use VB.NET) referencing this assembly in each of the code files you create that use reflection.
Building the AssemblyValidator
The first step in validating the currently executing assembly’s dependencies is to retrieve a handle to the currently executing assembly, because you need a handle to the topmost assembly for the process in which the validation code is running. The System.Reflection namespace’s Assembly class provides the GetEntryAssembly() function that you can use to obtain a handle to this assembly. The function returns an Assembly object, which represents an assembly that has been loaded into memory.
Dim objAssembly_Self As Assembly ObjAssembly_Self = Assembly.GetEntryAssembly()
For reference, the Assembly class provides access to the metadata exposed by a particular instance of an assembly. It is important to note that the Assembly class is tied to an instance of an assembly loaded into memory because it is possible, especially with the Xcopy deployment methods promulgated by Microsoft, to have many identical assemblies that differ only by their locations. If you are interested in only the generic information about an assembly, use the AssemblyName class.
The AssemblyName class stores enough information about an assembly to enable you to load an instance into memory?more specifically, it provides enough information for the .NET Framework to find and load it for you. One key detail used by .NET is the assembly’s FullName property, which holds an assembly’s name, version, culture, and public key. This combination of attributes ensures that .NET loads the exact assembly you intend?no two assemblies should ever have an identical FullName. When you query the assembly metadata for referenced assemblies the Assembly class returns a list of referenced assemblies as AssemblyName objects.
So, after you get a reference to the entry assembly, you can request the list of assemblies it references, returned as an array of AssemblyName objects. You can then iterate through the array, passing each AssemblyName object to a recursive method named ValidateAssembly. The recursive nature of this function ensures that the AssemblyValidator validates all the dependencies that exist in the hierarchical assembly dependency structure.
Dim objDepAssembly As AssemblyName For Each objDepAssembly in _ objAssembly_Self.GetReferencedAssemblies() ValidateAssembly(objDepAssembly) Next
Internally, the ValidateAssembly method uses an Assembly_List object defined in the validation tool to keep track of which assemblies have been referenced and to maintain details about each of the referenced assemblies. Other than keeping track of assembly details, the most important role of the Assembly_List object is to avoid repeatedly validating assemblies that have already been validated. Worse than the small amount of additional time required to re-verify assemblies, you could quickly cause a stack overflow if the recursive calls exhausted your application’s memory resources. So, before adding another assembly to the Assembly_List object, the AssemblyValidator first checks the list to see if it’s already been verified. If so (it exists in the list), the tool stops the recursion and returns from the ValidateAssembly method.
The ValidateAssembly method first attempts to load the assembly using the AssemblyName object provided as a parameter. The Assembly class provides a shared overloaded Load method; the sample application uses the overload version that accepts an assembly name object. The CreateAssembly method shown below demonstrates how to use the AssemblyName object to load assemblies. Note the possible common exceptions raised by the Load method.
'attempt to create the assembly using the assembly name object Private Function CreateAssembly( _ ByVal p_objAssemblyName As AssemblyName, _ ByRef p_strError As String) As Assembly Dim objAssembly As System.Reflection.Assembly '---- try to create the assembly Try objAssembly = System.Reflection.Assembly.Load( _ p_objAssemblyName) p_strError = "" Catch exSystem_BadImageFormatException As _ System.BadImageFormatException p_strError = "File is not a .NET assembly" objAssembly = Nothing Catch exSystem_IO_FileNotFoundException As _ System.IO.FileNotFoundException p_strError = "Could not load assembly -- " & _ "file not found" objAssembly = Nothing Catch ex As Exception p_strError = "An error occurred loading the assembly" objAssembly = Nothing End Try Return objAssembly End Function
If the assembly cannot be loaded, then recursion stops at this level, and the AssemblyValidator logs an error in the Assembly_List indicating why the assembly could not be loaded. When the assembly loads successfully, the AssemblyValidator adds the assembly details to the Assembly_List object, and recursively verifies each of this assembly’s referenced assemblies. This process continues until all the dependencies have been verified. Listing 1 shows the complete ValidateAssembly method.
At the end of the process, the Assembly_List class provides a FormatList method used to produce a string representation of the list of referenced assemblies. By default, the AssemblyValidator displays only assemblies that could not be loaded, because it’s far too difficult to scroll through the lists of dependencies manually, looking for problems?even simple “Hello World” WinForms projects produce long lists of dependencies.
As an exercise, I recommend that you modify the sample code to instruct the Assembly_List to display all dependencies (without duplicates), including those assemblies that loaded successfully, and observe the list that is produced. To make this modification, open the Assembly_Validator class in the AssemblyDependencyValidator project, and modify the last line of the ValidateEntryAssembly method, changing the parameter to the FormatList method to be False, as shown below:
m_strResults = strValidatorResults & vbCrLf & _ m_objBindingInfo.FormatList(False)
How to Include This Validation Tool in Your Project
To use the AssemblyValidator tool provided in the sample code in your project, you will need to build the AssemblyDependencyValidator project, and add a reference to the resulting AssemblyDependencyValidator.dll file in your application project. The sample code includes a “big button” style window to call the validation code, but in a deployed project, I recommend either calling the validation code from a menu item or via a command line switch. If called via a command line switch, the application could dump the results of the validation test out to a text file, letting you manage the whole process using your deployment tool.
After setting a reference to the AssemblyDependencyValidator.dll assembly, create an object of type Assembly_Validator, and call its ValidateEntryAssembly method. When this process completes, you can instruct the Assembly_Validator class to display the results in a window by calling the DisplayValidationResults method, or you can direct the results to an output file by obtaining the results of the validation process in string format from the ValidationResults property, for example:
Dim objAssemblyValidator As New Assembly_Validator objAssemblyValidator.ValidateEntryAssembly() objAssemblyValidator.DisplayValidationResults()
Could I Implement This as an External Tool?
You’ve probably noticed that, as written, the AssemblyValidator code must be included with your project, but you may come to the conclusion that a validation tool would be better built as an external tool. Such an external tool would let users select an assembly (exe or dll). The tool would load the specified assembly and then recursively validate its referenced assemblies as described above.
While this is possible, it will cause problems due to the probing rules used by the .NET Framework. For the sample validation application discussed in this article, the implication of this probing process is that it cannot attempt to verify assembly availability remotely. If the validation tool were written as a separate executable and run from a different directory than the executable whose dependencies we wish to verify (target executable), .NET’s probing process would traverse a different set of directories than if the probing process were running from the target executable’s directory. Therefore, to accurately validate the availability of referenced assemblies, the validation code must run from within the target executable.
For example, if the specified assembly references DLLs that aren’t registered in a config file and aren’t in the GAC, then when the AssemblyValidator attempts to load the specified assembly (using .NET’s probing process), it likely won’t find it.
One solution to this probing problem is to implement your own probing logic, based on the same rules used by the .NET Framework. However, that could prove problematic if future releases of the .NET Framework alter the probing rules. Further, you would also have to manually parse the application and machine config files for information related to assembly registration. In addition, you would need to probe the GAC for the assembly. With that said, it’s certainly possible to create a custom assembly loader that mimics the process used by .NET, and doing so might result in a more generally useful tool.
Another solution to the probing problem would be copy the validation application to the same directory as the target application, and run it from there. While that’s certainly possible, there’s a hint of a lack of elegance in this solution, and it hardly seems worth the effort, even if you automated the process of copying the validation tool and running it in the appropriate directory.
It is important to understand that just because an assembly is referenced does not necessarily mean it will ever be used by your application. The assembly may contain only classes that your application does not use. In many such cases, your application would run correctly in the absence of these referenced assemblies. Keep this in mind as you run this tool.
Another potential problem arises when an application uses reflection to late-bind to assemblies. In this case, the assembly metadata cannot publish information about those assemblies. If your applications make frequent use of late-bound assemblies, remember that because the AssemblyValidator relies on metadata created at compile time, those assemblies will not be inspected.
It has always been a bothersome task to verify the availability of a newly deployed assembly’s referenced assemblies. In fact, this very issue has caused countless problems for many software groups. The problem isn’t new for .NET assemblies?it was a problem for COM deployments as well. However, through reflection, .NET has provided the technology for validating these dependencies easily.
I recommend that you include a validation tool similar to the one described in this article in all your executables. When included as a piece of a larger application framework, it serves as a great tool to help make software more robust and manageable.