3

I'm trying to use Hyprlinkr to generate URL to the HTTP Post action. My controller looks like this:

public class MyController : ApiController {
    [HttpPost]
    public void DoSomething([FromBody]SomeDto someDto) {
        ...
    }
}

with this route:

routes.MapHttpRoute(
            name: "MyRoute",
            routeTemplate: "dosomething",
            defaults: new { controller = "My", action = "DoSomething" });

I expect to get a simple URL: http://example.com/dosomething, but it does not work. I tried two methods:

1) routeLinker.GetUri(c => c.DoSomething(null)) - throws NullReferenceException

2) routeLinker.GetUri(c => c.DoSomething(new SomeDto())) - generates invalid URL: http://example.com/dosomething?someDto=Namespace.SomeDto

Update: Issue opened at github: https://github.com/ploeh/Hyprlinkr/issues/17

Dmitry
  • 17,078
  • 2
  • 44
  • 70
  • This is almost verbatim an issue I raised on the hyprlinkr wiki: https://github.com/ploeh/Hyprlinkr/issues/28 I'm quite embarrassed now that I did not check SO first before raising. In future, I will post my issues here first! – Peter McEvoy Jan 27 '14 at 11:43

3 Answers3

3

I found a workaround, loosely based on Mark's answer. The idea is to go over every route parameter and remove those that have [FromBody] attribute applied to them. This way dispatcher does not need to be modified for every new controller or action.

public class BodyParametersRemover : IRouteDispatcher {
    private readonly IRouteDispatcher _defaultDispatcher;

    public BodyParametersRemover(String routeName) {
        if (routeName == null) {
            throw new ArgumentNullException("routeName");
        }
        _defaultDispatcher = new DefaultRouteDispatcher(routeName);
    }

    public Rouple Dispatch(
        MethodCallExpression method,
        IDictionary<string, object> routeValues) {

        var routeKeysToRemove = new HashSet<string>();
        foreach (var paramName in routeValues.Keys) {
            var parameter = method
                .Method
                .GetParameters()
                .FirstOrDefault(p => p.Name == paramName);
            if (parameter != null) {
                if (IsFromBodyParameter(parameter)) {
                    routeKeysToRemove.Add(paramName);
                }
            }
        }
        foreach (var routeKeyToRemove in routeKeysToRemove) {
            routeValues.Remove(routeKeyToRemove);
        }
        return _defaultDispatcher.Dispatch(method, routeValues);
    }

    private Boolean IsFromBodyParameter(ParameterInfo parameter) {
        var attributes = parameter.CustomAttributes;
        return attributes.Any(
            ct => ct.AttributeType == typeof (FromBodyAttribute));
    }
}
Community
  • 1
  • 1
Dmitry
  • 17,078
  • 2
  • 44
  • 70
  • 1
    +1 A convention-based approach is another alternative. Here, you don't have to edit your custom route dispatcher every time you add a new controller, but on the other hand, you have to remember to add the attribute - it's a trade-off, but if you like that better, it's fine. However, one thing I would like to point out is that you probably don't want to remove the route parameters from `routeValues` as this mutates the dictionary and you risk affecting how the overall Web API works. – Mark Seemann Apr 03 '13 at 07:38
  • Where does the ctor param "routeName" come from? I'm trying to wire this up with StructureMap and of course it's puking.... – Peter McEvoy Jan 27 '14 at 12:16
1

The second option is the way to go:

routeLinker.GetUri(c => c.DoSomething(new SomeDto()))

However, when using a POST method, you'll need to remove the model part of the generated URL. You can do that with a custom route dispatcher:

public ModelFilterRouteDispatcher : IRouteDispatcher
{
    private readonly IRouteDispatcher defaultDispatcher;

    public ModelFilterRouteDispatcher()
    {
        this.defaultDispatcher = new DefaultRouteDispatcher("DefaultApi");
    }

    public Rouple Dispatch(
        MethodCallExpression method,
        IDictionary<string, object> routeValues)
    {
        if (method.Method.ReflectedType == typeof(MyController))
        {
            var rv = new Dictionary<string, object>(routeValues);
            rv.Remove("someDto");
            return new Rouple("MyRoute", rv);
        }

        return this.defaultDispatcher.Dispatch(method, routeValues);
    }
}

Now pass that custom dispatcher into your RouteLinker instance.

Caveat: it's very late as I'm writing this and I haven't attempted to compile the above code, but I thought I'd rather throw an attempted answer here than have you wait several more days.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • I would need to modify this dispatcher every time I add or remove POST actions. Or is it suggested as a temporary workaround until Hyprlinkr would handle [HttpPost] out of the box? – Dmitry Mar 16 '13 at 15:43
  • This is what you'd have to do. It's not a bug in Hyprlinkr, unless you can define an algorithm that makes this sort of custom dispatcher redundant. – Mark Seemann Mar 16 '13 at 15:51
1

Dimitry's solution got me most of the way to where I wanted, however the routeName ctor param was a problem because StructureMap doesn't know what to put in there. Internally hyprlink is using UrlHelper to generate the URI, and that wants to know the route name to use

At that point, I see why URI generation is so tricky, because it is tied to the route names in the routing config and in order to support POST, we need to associate the method, with the correct routename and that is not known at dispatcher ctor time. Default hyprlinkr assumes there is only one route config named "DefaultRoute"

I changed Dimitry's code as follows, and adopted a convention based approach, where controller methods that start with "Get" are mapped to the route named "Get" and controller methods starting with "Add" are mapped to the route named "Add".

I wonder if there are better ways of associating a method with the proper named routeConfig?

        public class RemoveFromBodyParamsRouteDispatcher : IRouteDispatcher
{
    private static readonly ILog _log = LogManager.GetLogger(typeof (RemoveFromBodyParamsRouteDispatcher));

    public Rouple Dispatch(MethodCallExpression method,
                           IDictionary<string, object> routeValues)
    {
        var methodName = method.Method.Name;    
        DefaultRouteDispatcher defaultDispatcher;

        if (methodName.StartsWith("Get"))
            defaultDispatcher = new DefaultRouteDispatcher("Get");
        else if (methodName.StartsWith("Add"))
            defaultDispatcher = new DefaultRouteDispatcher("Add");
        else
            throw new Exception("Unable to determine correct route name for method with name " + methodName);

        _log.Debug("Dispatch methodName=" + methodName);

        //make a copy of routeValues as contract says we should not modify
        var routeValuesWithoutFromBody = new Dictionary<string, object>(routeValues);

        var routeKeysToRemove = new HashSet<string>();
        foreach (var paramName in routeValuesWithoutFromBody.Keys)
        {
            var parameter = method.Method
                                  .GetParameters()
                                  .FirstOrDefault(p => p.Name == paramName);
            if (parameter != null)
                if (IsFromBodyParameter(parameter))
                {
                    _log.Debug("Dispatch: Removing paramName=" + paramName);

                    routeKeysToRemove.Add(paramName);
                }
        }

        foreach (var routeKeyToRemove in routeKeysToRemove)
            routeValuesWithoutFromBody.Remove(routeKeyToRemove);

        return defaultDispatcher.Dispatch(method, routeValuesWithoutFromBody);
    }

    private static bool IsFromBodyParameter(ParameterInfo parameter)
    {
        //Apparently the "inherit" argument is ignored: http://msdn.microsoft.com/en-us/library/cwtf69s6(v=vs.100).aspx
        const bool msdnSaysThisArgumentIsIgnored = true;
        var attributes = parameter.GetCustomAttributes(msdnSaysThisArgumentIsIgnored);

        return attributes.Any(ct => ct is FromBodyAttribute);
    }
}
Peter McEvoy
  • 2,816
  • 19
  • 24