Advanced Basics

Extracting Data from .NET Assemblies

Ken Spencer

Code download available at:AdvancedBasics0403.exe(137 KB)

Q How can I get a list of all of the methods, properties and events in a particular assembly? For instance, I need to know this information regarding Windows® Forms and ADO.NET. Ideally, I would also like to get information on the datatype of a property and the datatype of a function's return value. I also need to have this information appear in a spreadsheet.

Q How can I get a list of all of the methods, properties and events in a particular assembly? For instance, I need to know this information regarding Windows® Forms and ADO.NET. Ideally, I would also like to get information on the datatype of a property and the datatype of a function's return value. I also need to have this information appear in a spreadsheet.

A This is an interesting question. Let me show you the application I built to do this, then I'll explain the code. Figure 1 shows the app's main interface run against the System.Windows.Forms assembly. It shows that 28,932 members were processed. I would not want to look through that list manually!

A This is an interesting question. Let me show you the application I built to do this, then I'll explain the code. Figure 1 shows the app's main interface run against the System.Windows.Forms assembly. It shows that 28,932 members were processed. I would not want to look through that list manually!

Figure 1 Assembly Info Viewer

Figure 1** Assembly Info Viewer **

Figure 2 shows a detailed list of methods, properties, and events from the assembly. It's what you would expect if you were looking at the ComboBox control. As you can see from the File menu in Figure 2, you can also save this detail to a file as comma-separated values. Then you can import the data into Microsoft® Excel or a myriad of other applications. Of course, you could also export the data as XML; maybe I'll build that into a future version of the application.

Figure 2 Save As Comma-separated Values

Figure 2** Save As Comma-separated Values **

The first form (frmMain) has a simple interface with a main menu and several label controls. The main menu has two top-level menu items, File and View. The File menu contains these items: Open Assembly, Open Assembly in GAC, and Exit.

The View menu only has one submenu: View Assembly Items.

First, I needed to figure out how to use reflection on assemblies that are in the global assembly cache (GAC). Here I load an assembly where mAssemblyName is the complete file path:

mAssembly = [Assembly].LoadFrom(mAssemblyName)

Once the assembly is loaded, I can examine its types using mAssembly. I tried this with assemblies in the GAC. I figured that I could use Load instead of LoadFrom. So far, so good. Since the Load method takes the display name of an assembly, I thought I could use the names I saw displayed in the GAC Viewer (see Figure 3).

Figure 3 GAC Viewer

Figure 3** GAC Viewer **

You can see that I have highlighted the Windows.Forms class from the Microsoft .NET Framework version 1.1. I thought I'd be able to pass "System.Windows.Forms" to Load. Wrong! For assemblies in the GAC, I must use the assembly's full name, which includes the assembly's version, culture, and public key token. So, I did some digging using the command prompt (see Figure 4).

Figure 4 Windows Forms Directory

Figure 4** Windows Forms Directory **

Figure 4 shows the GAC directory for the Windows Forms assembly. Notice the entire directory path. You can actually take the entire path and use it with LoadFrom, like so:

C:\WINNT\assembly\GAC\System.Windows.Forms\ 1.0.5000.0__b77a5c561934e089\System.Windows.Forms.dll

But that's not really the way I wanted to handle it, and the GAC folders may be in another location on the user's machine. So I decided to find out what the display name really was. It turns out it looks like this:

System.Windows.Forms, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

Clearly, the display name for strong-named assemblies contains a lot more than just the name.

At any rate, now that I know what the full names look like, let me allow the user to select them. But how do I get the display names for all assemblies in the GAC? I could figure out some cool ways, such as using interop to access the unmanaged Fusion API, but I needed the info quickly, so I took the easy way out. I simply opened the Visual Studio® command prompt and ran GACUtil.exe with the following command:

Gacutil /l > gaclist.txt

This copied a list of all assemblies in the GAC to gaclist.txt. Then I opened the text file and stripped out the header and everything after the display names. Next, I removed the tab spaces and saved the file in the application's Bin directory. At this point I added the code shown in Figure 5 to load this file into the global dtGACAssemblies datatable. This code is then loaded into the DataGrid in frmGACAssemblies.vb, thus allowing the user to select an assembly.

Figure 5 GetGACItems

Sub GetGACItems() Dim [Assembly] As [Assembly] Dim dc As DataColumn Dim FileName As String Dim FilePath As String Dim rw As DataRow dtGACAssemblies = New DataTable dc = New DataColumn dc.ColumnName = "DisplayName" dtGACAssemblies.Columns.Add(dc) FileName = "gaclist.txt" FilePath = Environment.CurrentDirectory() FileName = FilePath & "\" & FileName If Not File.Exists(FileName) Then MsgBox("File does not exist.", FileName) Return End If Dim sr As StreamReader = File.OpenText(FileName) Dim input As String input = sr.ReadLine() While Not input Is Nothing If input <> "" Then rw = dtGACAssemblies.NewRow rw("DisplayName") = Trim(input) dtGACAssemblies.Rows.Add(rw) End If input = sr.ReadLine() End While sr.Close() End Sub

Now, let's see how to pull information from the assemblies. I used the same approach I used in my February 2003 column (see Figure 6). The LoadClassesMemberInfo method takes two parameters. The first is the AssemblyFullFileName, which can be either the assembly file name or the display name. If the display name is used, the second parameter should be set to True. This parameter is checked in the first part of the method's code to determine if Load or LoadFrom should be called:

If Not UseAsDisplayName Then mAssembly = [Assembly].LoadFrom(mAssemblyName) Else mAssembly = [Assembly].Load(mAssemblyName) End If

Now the application has an assembly loaded, so you can work with it using reflection.

Figure 6 LoadClassesMemberInfo

Function LoadClassesMemberInfo(ByVal AssemblyFullFileName _ As String, ByVal UseAsDisplayName As Boolean) As DataTable Dim t As Type Dim othis As Object Dim mAssembly As [Assembly] Dim marrayOfTypes() As Type Dim mAssemblyName As String 'Dim mPropInfo As PropertyInfo Dim rw As DataRow Dim OutputThisOne As Boolean Dim tmpName, tmpHeader As String Dim sOutput As String Dim iOutputClasses As Integer Dim ds As New DataSet StatusBar1.Text = "" Me.Cursor.Current = Cursors.WaitCursor mAssemblyName = AssemblyFullFileName sOutput = "" Try If Not UseAsDisplayName Then mAssembly = [Assembly].LoadFrom(mAssemblyName) Else mAssembly = [Assembly].Load(mAssemblyName) End If marrayOfTypes = mAssembly.GetExportedTypes() sOutput = mAssembly.FullName For Each t In marrayOfTypes OutputThisOne = False If t Is Nothing Then MsgBox("Type was not found — exiting") Exit Function End If If Not t.Name.EndsWith("base") Then 'output if class does not end with base OutputThisOne = True End If tmpHeader = "" tmpName = "" If OutputThisOne Then iOutputClasses += 1 If ds.Tables.Count = 0 Then ds.Tables.Add(GetMemberInfo(t)) ElseIf ds.Tables.Count > 0 Then ds.Merge(GetMemberInfo(t)) End If End If Next Catch exc As Exception StatusBar1.Text = mAssemblyName & "error occurred. " & exc.Message Finally mAssembly = Nothing GC.Collect() End Try Me.Cursor.Current = Cursors.Default Return ds.Tables(0) End Function

Now, let's analyze the assembly. I use a For Next loop to iterate over all exported types from the assembly (only public classes are exported). This loop calls the GetMemberInfo method (shown in Figure 7) and passes it the reference to the current type (t). The first bit of Figure 7 calls GetMembers to pull the members from the type, returning an array of MemberInfo objects:

lMemberInfo = t.GetMembers((BindingFlags.Public Or _ BindingFlags.Instance Or BindingFlags.InvokeMethod))

Now you can loop through this array and process the data for each member. All finished, right? Not so fast.

Figure 7 GetMemberInfo

Function GetMemberInfo(ByVal t As Type) As DataTable Dim dc As DataColumn Dim dt As DataTable Dim rw As DataRow Dim i As Integer Dim lMemberInfo() As MemberInfo Dim lAssemblyName As String Dim lPropertyInfo As PropertyInfo Dim lMethodInfo As MethodInfo Dim lEventInvo As EventInfo dt = New DataTable dt.TableName = "MemberInfo" dc = New DataColumn dc.ColumnName = "ClassName" dt.Columns.Add(dc) dc = New DataColumn dc.ColumnName = "ItemName" dt.Columns.Add(dc) dc = New DataColumn dc.ColumnName = "ItemType" dt.Columns.Add(dc) dc = New DataColumn dc.ColumnName = "ItemDataType" dt.Columns.Add(dc) Try lMemberInfo = t.GetMembers(( BindingFlags.Public Or _ BindingFlags.Instance Or BindingFlags.InvokeMethod)) lAssemblyName = t.Name For i = 0 To lMemberInfo.Length - 1 rw = dt.NewRow If lMemberInfo(i).MemberType <> MemberTypes.Method _ And lMemberInfo(i).MemberType <> MemberTypes.Property _ And lMemberInfo(i).MemberType <> MemberTypes.Event Then rw("ClassName") = lAssemblyName rw("ItemName") = lMemberInfo(i).Name rw("ItemType") = lMemberInfo(i).MemberType dt.Rows.Add(rw) End If Next i 'add methods For Each lMethodInfo In t.GetMethods(( _ BindingFlags.Public Or _ BindingFlags.Instance Or BindingFlags.InvokeMethod)) rw = dt.NewRow rw("ClassName") = t.Name rw("ItemName") = lMethodInfo.Name rw("ItemDataType") = lMethodInfo.ReturnType.ToString rw("ItemType") = "Method" dt.Rows.Add(rw) Next 'add properties For Each lPropertyInfo In t.GetProperties((BindingFlags.Public Or _ BindingFlags.Instance Or BindingFlags.InvokeMethod)) rw = dt.NewRow rw("ClassName") = t.Name rw("ItemName") = lPropertyInfo.Name rw("ItemDataType") = lPropertyInfo.PropertyType.ToString rw("ItemType") = "property" dt.Rows.Add(rw) Next 'add events For Each lEventInvo In t.GetEvents(( _ BindingFlags.Public Or BindingFlags.Instance _ Or BindingFlags.InvokeMethod)) rw = dt.NewRow rw("ClassName") = t.Name rw("ItemName") = lEventInvo.Name rw("ItemDataType") = "" rw("ItemType") = "event" dt.Rows.Add(rw) Next Catch e As Exception msgbox(("Exception : " + e.Message.ToString())) End Try Return dt End Function

I also wanted to extract the DataType for each member; while it's possible to get this from MemberInfo by casting it to the appropriate derived type, I took another approach. First, in the loop which processes members, I filter out methods, properties, and events with the If block so that only other members, such as fields and constructors, remain. Second, I added three more For Each loops, one each for methods, properties, and events.

Each one of these member types has a corresponding info class, all of which are derived from MemberInfo.

MethodsMethodInfo EventsEventInfo PropertiesPropertyInfo

Each of these classes exposes the attributes of the corresponding member and provides access to its metadata. And just as I was able to use Type's GetMembers to retrieve all members of a type, I can use Type's GetMethods, GetProperties, and GetEvents methods to return arrays of the corresponding info objects. Thus, adding the other three For Each loops to process these items separately adds the information to the datatable returned from GetMemberInfo. I still need to pull out the parameters for each method, but that's pretty easy to do. In fact, if you look at my code in the aforementioned February 2003 installment, you will see this in action.

The code in the download is pretty simple, so I will leave it to you to explore (see the link at the top of this article). Check out the MouseUp event in frmGACAssemblies.vb. This event shows an alternate way of handling selection events in a DataGrid. There are many times when handling the MouseUp event is preferable to using another event, such as Click, because MouseUp provides more information about the performed action.

Send your questions and comments for Ken to  basics@microsoft.com.

Ken Spencer works for 32X Tech, where he provides training, software development, and consulting services on Microsoft technologies.