Monday, April 20, 2015

Extract all MVC / WebApi area/controller/actions

I did some search and created my own based on the answers found in SO.

Extract all MVC / WebApi area/controller/actions - Gist

// http://stackoverflow.com/questions/5508050/how-to-get-a-property-value-based-on-the-name
// http://forums.asp.net/t/1600843.aspx?How+list+all+of+the+Actions+in+an+MVC+application+for+security+audit+
// http://stackoverflow.com/questions/5801630/mvc-get-all-action-methods
// http://stackoverflow.com/questions/21583278/getting-all-controllers-and-actions-names-in-c-sharp
// http://stackoverflow.com/questions/15690834/asp-net-mvc4-list-of-all-areas
// http://stackoverflow.com/questions/1091853/error-message-unable-to-load-one-or-more-of-the-requested-types-retrieve-the-l
 
/// <summary>
/// Extract all MVC/WebApi area/controller/actions
/// </summary>
public class ResourceHelper
{
private const string NamespacePrefix = "github";
 
public static List<ApplicationResource> GetResources()
{
var resources = new List<ApplicationResource>();
 
try
{
resources.AddRange(MapResources<IController>()); // MVC
resources.AddRange(MapResources<IHttpController>(true)); // Web API
}
catch (ReflectionTypeLoadException ex)
{
var sb = new StringBuilder();
foreach (Exception exSub in ex.LoaderExceptions)
{
sb.AppendLine(exSub.Message);
var exFileNotFound = exSub as FileNotFoundException;
if (exFileNotFound != null)
{
if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
{
sb.AppendLine("Fusion Log:");
sb.AppendLine(exFileNotFound.FusionLog);
}
}
sb.AppendLine();
}
string errorMessage = sb.ToString();
//Display or log the error based on your application.
TraceHelper.Error(errorMessage);
}
 
return resources;
}
 
private static IEnumerable<ApplicationResource> MapResources<T>(bool api = false)
{
var cSuffix = api ? "ApiController" : "Controller";
 
var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // currently loaded assemblies
 
var areaRegistrations = GetAreaRegistrations(assemblies).ToList();
 
var controllerTypes = GetControllerTypes<T>(assemblies, cSuffix);
 
var controllerMethods = GetControllerActions(controllerTypes, api);
 
foreach (var controllerType in controllerMethods)
{
string area = GetAreaName(controllerType, areaRegistrations);
string controller = controllerType.Key.Name.Replace(cSuffix, "");
foreach (MethodInfo action in controllerType.Value)
{
string actionName = action.Name;
bool attrGet = action.GetCustomAttributes(false).Any(a => a.GetType().IsAssignableFrom(typeof(HttpPostAttribute)));
yield return new ApplicationResource(area, controller, actionName, attrGet, api);
}
}
}
 
private static string GetAreaName(
KeyValuePair<Type, IEnumerable<MethodInfo>> controllerType,
IEnumerable<Type> areaRegistrations)
{
if (controllerType.Key.Namespace != null && controllerType.Key.Namespace.Contains("Areas"))
{
var areaNamespace = controllerType.Key.Namespace;
var regArea = areaRegistrations.First(a => a.Namespace != null && areaNamespace.Contains(a.Namespace));
var areaNameProp = regArea.GetProperty("AreaName");
var instance = Activator.CreateInstance(regArea); // have to create an instance before retrieving the property value
var areaNameValue = areaNameProp.GetValue(instance);
return areaNameValue.ToString();
}
return string.Empty;
}
 
private static Dictionary<Type, IEnumerable<MethodInfo>> GetControllerActions(IEnumerable<Type> controllerTypes, bool api)
{
var controllerMethods = controllerTypes.ToDictionary(
controllerType => controllerType,
controllerType => controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
!m.IsDefined(typeof(NonActionAttribute))
// NOTE : We are using 'HttpResponseMessage' as the return in every API action method.
// if that isn't the case with your API,
// something like this will get you to a close result :
// ==> (api || typeof(ActionResult).IsAssignableFrom(m.ReturnType))
&& (api ? typeof(HttpResponseMessage).IsAssignableFrom(m.ReturnType) : typeof(ActionResult).IsAssignableFrom(m.ReturnType))
&& m.Name != "Dispose"
&& !m.IsSpecialName
&& !m.IsStatic));
return controllerMethods;
}
 
private static IEnumerable<Type> GetControllerTypes<T>(IEnumerable<Assembly> assemblies, string controllerSuffix = "Controller")
{
var controllerTypes = assemblies
.SelectMany(a => a.GetTypes())
.Where(t => t != null
&& t.IsPublic // public controllers only
&& t.Name.EndsWith(controllerSuffix, StringComparison.OrdinalIgnoreCase) // enfore naming convention
&& t.Namespace.IfNotNull(n => n.ToLowerInvariant().StartsWith(NamespacePrefix))
&& !t.IsAbstract // no abstract controllers
&& typeof(T).IsAssignableFrom(t));
// should implement T (happens automatically when you extend Controller/ApiController)
return controllerTypes;
}
 
private static IEnumerable<Type> GetAreaRegistrations(IEnumerable<Assembly> assemblies)
{
var areaRegistrations = assemblies.SelectMany(a => a.GetTypes())
.Where(t => t != null && typeof(AreaRegistration).IsAssignableFrom(t));
return areaRegistrations;
}
 
}
 
public class ApplicationResource
{
public ApplicationResource(string area, string controller, string action, bool post, bool api)
{
Area = area;
Controller = controller;
Action = action;
IsGet = !post;
IsApi = api;
}
 
public string Area { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public bool IsGet { get; set; }
public bool IsApi { get; set; }
}
Following class contains an extension method used in the helper.
public static class Extensions{
public static TInner IfNotNull<T, TInner>(this T source, Func<T, TInner> selector, bool createNew = false)
where T : class
{
return source != null
? selector(source) : (createNew ? Activator.CreateInstance<TInner>() : default(TInner));
}
}