Search code examples
asp.net-web-api2asp.net-web-apiasp.net-web-api-routing

Unit testing attribute routing in WebApi2


I'm working with an WebApi2 attrbiute routing project and I'm trying to unit test the route (that the request is executing the correct api method in the controller). But I'm not able to make it work so far...

Seems it's not picking up the routes defined in the attribute Any idea what might be wrong or missing?

Thanks in advance! Guillermo.

This is my unit test code:

var config = new HttpConfiguration
{
    IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always,
};

config.Routes.MapHttpRoute(name: "Default", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional });

request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/homes/report");
route = config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}");
routeData = new HttpRouteData(route, new HttpRouteValueDictionary
    {
        { "controller", "homes" },
    });

config.MapHttpAttributeRoutes();
config.EnsureInitialized();

controller = new HomesController
{
    Catalog = catalog.Object,
    ControllerContext = new HttpControllerContext(config, routeData, request),
    Request = request,
};

controller.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;

var routeTester = new RouteTester(config, request, controller.ControllerContext);

Assert.IsTrue(routeTester.CompareSignatures(ReflectionHelpers.GetMethodInfo((HomesController p) => p.GetHomesReport())));

RouteTester.cs

public class RouteTester
{
    readonly HttpConfiguration config;
    readonly HttpRequestMessage request;
    readonly IHttpRouteData routeData;
    readonly IHttpControllerSelector controllerSelector;
    readonly HttpControllerContext controllerContext;

    public RouteTester(HttpConfiguration conf, HttpRequestMessage req, HttpControllerContext context)
    {
        config = conf;
        request = req;
        controllerContext = context;
        routeData = context.RouteData;

        controllerSelector = new DefaultHttpControllerSelector(config);
    }

    public string GetActionName()
    {
        if (controllerContext.ControllerDescriptor == null)
            GetControllerType();

        var actionSelector = new ApiControllerActionSelector();
        var descriptor = actionSelector.SelectAction(controllerContext);

        return descriptor.ActionName;
    }

    public Type GetControllerType()
    {
        var descriptor = controllerSelector.SelectController(request);
        controllerContext.ControllerDescriptor = descriptor;
        return descriptor.ControllerType;
    }

    public bool CompareSignatures(MethodInfo method)
    {
        if (controllerContext.ControllerDescriptor == null)
            GetControllerType();


        var actionSelector = new ApiControllerActionSelector();
        var x = actionSelector.GetActionMapping(controllerContext.ControllerDescriptor)[request.Method.ToString()];


        return x.Any(item => ((MethodBase)(((ReflectedHttpActionDescriptor)item).MethodInfo)).ToString() == ((MethodBase)method).ToString());
    }

ReflectionHelpers.cs

 public class ReflectionHelpers
{
    public static string GetMethodName<T, U>(Expression<Func<T, U>> expression)
    {
        var method = expression.Body as MethodCallExpression;

        if (method != null)
            return method.Method.Name;

        throw new ArgumentException("Expression is wrong");
    }

    public static MethodInfo GetMethodInfo<T, U>(Expression<Func<T, U>> expression)
    {
        var method = expression.Body as MethodCallExpression;
        if (method != null)
            return method.Method;


        throw new ArgumentException("Expression is wrong");
    }


    public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> expression)
    {
        var method = expression.Body as MethodCallExpression;
        if (method != null)
            return method.Method;


        throw new ArgumentException("Expression is wrong");
    }
}

Controller snippet

[Route("api/homes/homereport")]
public void GetHomesReport()
{
    var homeReportItems = HomeReport();
}

Solution

  • Not sure that it is exactly the solution for your problem, but I use this approach to keep attribute routing under control. It forces you to specify controller class and method signature but all without string literals so you get nice compile tme validation here. FunctionSignature for a simple method as public string Action(int value) it will look like this Func<int, string>.

    Test methods looks like this:

    public void TestMethod() {
        var info = GetEndpointInfo<ControllerClass, FunctionSignature>(c => c.Action);
    
        Assert.IsNotNull(info.AllowsAnonymous);
        Assert.AreEqual("data/places/{query}", info.Route);
    }
    

    And here is some lambda and reflection violence to get meta info:

    private EndpointInfo GetEndpointInfo<TController, TMethod>(Expression<Func<TController, TMethod>> expression) {
        var controllerType = typeof(TController);
        var prefix = controllerType.GetCustomAttribute<RoutePrefixAttribute>().Prefix;
        MethodInfo methodInfo = GetMemberInfo(expression);
        var template = methodInfo.GetCustomAttribute<RouteAttribute>().Template;
    
        var info = new EndpointInfo() {
            AllowsAnonymous = controllerType.GetCustomAttribute<AllowAnonymousAttribute>() != null, //etend here to check method level attr
            Route = prefix + "/" + template
        };
        return info;
    }
    
    private MethodInfo GetMemberInfo<TController, TMethod>(Expression<Func<TController, TMethod>> expression) {
        var unaryExpression = expression.Body as UnaryExpression;
        var methodCall = unaryExpression.Operand as MethodCallExpression;
        var constant = methodCall.Object as ConstantExpression;
        var methodInfo = constant.Value as MethodInfo;
        return methodInfo;
    }
    class EndpointInfo{
        public bool AllowsAnonymous { get; set; }
        public string Route { get; set; }
    }
    

    Hope this helps.