Thursday, July 19, 2007

LINQ to Reflection

One of the great things about LINQ is that it allows me to query over any object that implements IEnumerable<T>.  This includes arrays, List<T>, Collection<T>, and many others.  Since many operations involving reflection return arrays, this means I'm open to using LINQ over those operations.  Let's look at a few examples.

Loading up assemblies for searching

First, I'd like to load some assemblies for searching.  The easiest way I found to do this was to use System.Reflection.Assembly.LoadWithPartialName.  This method has been deprecated as of .NET 2.0, but it will suffice for simple operations such as searching.  I wouldn't use that method for production code, it's too unreliable.

I first came up with a list of assembly names I wanted to search and dumped them in an array:

string[] assemblyNames = { "System", 
                 "mscorlib", 
                 "System.AddIn",
                 "System.Configuration", 
                 "System.Core", 
                 "System.Data",
                 "System.Data.Entity",
                 "System.Data.Entity.Design",
                 "System.Data.Linq",
                 "System.Deployment",
                 "System.Design",
                 "System.DirectoryServices",
                 "System.DirectoryServices.Protocols",
                 "System.Drawing",
                 "System.Drawing.Design",
                 "System.EnterpriseServices",
                 "System.IdentityModel",
                 "System.IdentityModel.Selectors",
                 "System.Management",
                 "System.Management.Instrumentation",
                 "System.Messaging",
                 "System.Printing",
                 "System.Runtime.Remoting",
                 "System.Runtime.Serialization",
                 "System.Security",
                 "System.ServiceModel",
                 "System.ServiceProcess",
                 "System.Transactions",
                 "System.Web", 
                 "System.Web.Services", 
                 "System.Windows.Forms", 
                 "System.Workflow.Activities", 
                 "System.Workflow.ComponentModel", 
                 "System.Workflow.Runtime", 
                 "System.WorkflowServices", 
                 "System.Xml", 
                 "System.Xml.Linq"
             };

It's pretty much all of the .NET Framework 3.5 assemblies.  Next, I want to create a LINQ query to load up all of the assemblies, so I can perform searches over those assemblies.  Just using the extension methods, it would look like this:

var assemblies = assemblyNames.Select(name => Assembly.LoadWithPartialName(name));

With LINQ magic, here's what the same query would look like:

var assemblies = from name in assemblyNames
            select Assembly.LoadWithPartialName(name);

With the "assemblies" variable (which is actually IEnumerable<Assembly>), I can start performing queries over the loaded assemblies.

Finding generic delegates

I was experimenting with an API, and I wanted to know if a generic delegate I created already existed somewhere in .NET.  I can use some handy LINQ expressions over the loaded assemblies to do just that.  But first, I need to know how to find what I'm looking for.  I can just load up a generic delegate I'm already aware of into a Type object:

Type actionDelegate = typeof(Action<>);

When I load up a generic delegate into an instance of a Type object, I notice some key properties, such as "IsGenericType" and "IsPublic".  Both of these are true for my generic delegate type.  Unfortunately, there is no "IsDelegate" property, but it turns out that IsSubclassOf(typeof(Delegate)) will return "true".  Combining these three conditions, I have a predicate to use in my search across the assemblies.

Here's the final LINQ query:

var types = from name in assemblyNames
            select Assembly.LoadWithPartialName(name) into a
            from c in a.GetTypes()
            where c.IsGenericType && c.IsPublic && c.IsSubclassOf(typeof(Delegate))
            select c;

foreach (Type t in types)
{
    Debug.WriteLine(t);
}

This is actually two LINQ queries joined together with a continuation (the "into a" part).  The first query enumerates over the assembly string names to load the assemblies.  The second query calls "GetTypes" on each assembly to load the types, and uses the "where" clause to only pull out the generic delegates using a predicate I found earlier.  The results of this block of code shows me the generic delegates:

System.EventHandler`1[TEventArgs]
System.Action`1[T]
System.Comparison`1[T]
System.Converter`2[TInput,TOutput]
System.Predicate`1[T]
System.Linq.Func`1[TResult]
System.Linq.Func`2[TArg0,TResult]
System.Linq.Func`3[TArg0,TArg1,TResult]
System.Linq.Func`4[TArg0,TArg1,TArg2,TResult]
System.Linq.Func`5[TArg0,TArg1,TArg2,TArg3,TResult]

Not a whole lot, but it did tell me what was already there.  Specifically, I had been looking for alternate overloads to Action<T>, to see if there were multiple delegate declarations like there are for Func<TResult>.  There aren't, but it turns out there are planned overloads for Action to match Func.

Context and IDisposable

I had a discussion with a team member that centered around types with names postfixed with "Context".  I was arguing that types named "Context" implied that the type implemented IDisposable, as it was intended to create a scope.  Instead of arguing in conjectures, I sought to get some actual data, and find how many types in the .NET Framework named "Context" also implemented IDisposable.  Here's the LINQ query I came up with:

var types = from name in assemblyNames
            select Assembly.LoadWithPartialName(name) into a
            from c in a.GetTypes()
            where (c.IsClass || c.IsInterface) && c.FullName.EndsWith("Context")
            group c.FullName by c.GetInterfaces().Contains(typeof(IDisposable)) into g
            select new { IsIDisposable = g.Key, Types = g };

In this query, I want to select all types that the name ends with "Context", and group these into different bins based on whether they implement IDisposable or not.  To do this, I take advantage of both grouping in LINQ, and anonymous types to hold the data.  I output the results:

foreach (var g in types)
{
    Debug.WriteLine(string.Format("{0} types where IsIDisposable is {1}", g.Types.Count(), g.IsIDisposable));
    if (g.IsIDisposable)
    {
        foreach (string t in g.Types)
        {
            Debug.WriteLine(t);
        }
    }
}

And find that my argument doesn't hold up.  Here are the results:

144 types where IsIDisposable is False
50 types where IsIDisposable is True

I left out the output that printed out all of the Context IDisposable types, as this list was fairly long.  I decided not to filter out non-public types, as MS tends to have lots of types marked "internal".  So it turned out that only 25% of types that end with "Context" implement IDisposable, so my assumptions were incorrect.

Other applications

LINQ provides a clean syntax to search assemblies using reflection.  I've also used it to argue against read-write properties for array types (they account for only 0.6% of all collection-type properties).  The continuations and joining lower the barrier for searching for specific types, properties, etc.  Since the LINQ methods are extension methods for any IEnumerable<T> type, you'll be pleasantly surprised when IntelliSense kicks in at the options available for querying and manipulating collections and arrays.

No comments: