Sunday, 23 August 2015

Code As Data With Reflection

Other than occasional use, working with reflection using C# isn't something I've had that much need to do. Recently though I was working on an additional feature for the Umbraco package The Dashboard. It provides a nice first screen for editors, showing their recent edits and other work carried out on the site content.

The idea for the new feature was to create a developer version of this, that displayed various details about the code that had been implemented on the project. The use case being in the case of inheriting or coming back to a site, being able to have a quick overview of the techniques that have been used for route hijacking, surface controllers, application events and the like.

In order to implement this it was going to be necessary to make quite heavy use of reflection and this post highlights a few of the methods used.

Getting instances of types

In the case of Umbraco route hijacking using custom controllers it's necessary to implement an MVC controller that implements an interface (IRenderMvcController). Similarly for surface controllers, you need to inherit from a base class (SurfaceController). So to find all the instances of these I needed to search all the assemblies of the project and find all non-abstract, classes that implemented a given interface or inherited a given base type. I also wanted to exclude anything that came with the Umbraco core itself as that wasn't useful to display.

Here's the code I used:

public class ControllerDetailDto
{
    public string Name { get; set; }

    public string Namespace { get; set; }
}

public IEnumerable<ControllerDetailDto> GetControllersAssignableFrom(Type baseType)
{
    return GetNonCoreTypesAssignableFrom(baseType)
        .Select(x => new ControllerDetailDto
        {
            Name = x.Name,
            Namespace = x.Namespace,
        });
}

private IEnumerable<Type> GetNonCoreTypesAssignableFrom(Type baseType)
{
    return AppDomain.CurrentDomain.GetAssemblies()
        .SelectMany(s => s.GetTypes())
        .Where(p => baseType.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract && !p.Namespace.ToLower().StartsWith("umbraco."));
}

Getting method details

In the case of custom controllers used for route hijacking, I also wanted to get the list of action methods. This was done by finding all public, non-static methods direct declared on the controller and filtering by those that returned ActionResult or something inheriting from it. I was only interested in the GET ones so obtained all those by excluding any that had an [HttpPost] attribute.

The crucial part of the code for doing this was as follows:

public IEnumerable<string> GetActionMethodsOnController(Type controllerType)
{
    return controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
        .Where(x => typeof (ActionResult).IsAssignableFrom(x.ReturnType) &&
            !x.Name.StartsWith("get_") &&
            (!x.GetCustomAttributes(typeof (HttpPostAttribute), false).Any()))
        .Select(x => x.Name)
        .OrderBy(x => x);
}

Getting event details

The last task I had was to get details of the application event handlers that had been registered. For a given Umbraco service, e.g. ContentService, there are a number of methods that may have handlers registered on it - to carry out actions on Saving of content for example.

This wasn't particularly straightforward but I got some help from this blog post by David Kowalski and a chapter he referenced from C# in Depth. The code I used to get this information was as follows:

public class ReflectedClassDto
{
    public string Name { get; set; }

    public string Namespace { get; set; }
}

public class CustomEventDto
{
    public string EventName { get; set; }

    public IEnumerable<ReflectedClassDto> Handlers { get; set; }
}

public IEnumerable<CustomEventDto> GetCustomEvents(IService serviceInstance)
{
    return serviceInstance.GetType().GetEvents()
        .Select(x => new CustomEventDto
        {
            EventName = x.Name,
            Handlers = GetCustomEventHandlers(serviceInstance, x)
        })
        .Where(x => x.Handlers != null && x.Handlers.Any());
}

private static IEnumerable GetCustomEventHandlers(IService serviceInstance, EventInfo eventInfo)
{
    var fi = GetEventField(serviceInstance, eventInfo);
    if (fi != null)
    {
        var del = fi.GetValue(serviceInstance) as Delegate;
        if (del != null)
        {
            return del.GetInvocationList()
                .Where(x => !x.Method.DeclaringType.FullName.ToLower().StartsWith("umbraco."))
                .Select(x => new ReflectedClassDto
                {
                    Name = x.Method.Name,
                    Namespace = x.Method.DeclaringType.FullName,
                });
        }
    }

    return null;
}

private static FieldInfo GetEventField(IService serviceInstance, EventInfo eventInfo)
{
    return serviceInstance.GetType()
        .GetField(eventInfo.Name,
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public |
            BindingFlags.FlattenHierarchy);
}

The first step is to get the list of events defined on the service instance - these are things like OnSaving etc - that have at least one handler defined. To get the event handlers we then extract the private field that backs the event and obtain the value of that field as a delegate (of type MulticastDelegate). With that there's a method called GetInvocationList() that allows you to retrieve all the defined handlers for the event. Again I remove all the ones that come with the Umbraco Core.

To see this in context see the open-source repository for the package and specifically this file.

No comments:

Post a Comment