ploeh / Hyprlinkr

A URI building helper library for ASP.NET Web API
MIT License
198 stars 34 forks source link

Does not seem to be working with web api 2 properly ... #30

Open RahmanM opened 10 years ago

RahmanM commented 10 years ago

Hi,

I am using it in a web api 2 project along with the attribute routing. I have a function as below:

// GET /api/authors/1/books [Route("~/api/authors/{authorId:int}/books")] public IEnumerable GetByAuthor(int authorId) { ... }

When calling this function as:

var link=linker.GetUri(a=>a.GetByAuther(id)).ToString();

It returns as http://localhost/api/auther/1

regards,

rahman

ploeh commented 10 years ago

There's no active code in Hyprlinkr that takes attribute routing into account.

It looks as though this would need to be explicitly added. However, care must be taken to ensure that the addition of such a feature doesn't break Hyprlinkr's compatibility with ASP.NET Web API 1, so either the implementation would need to be sufficiently abstract, or it would be necessary to explicitly add a Hyprlinkr.WebApi2 extension library.

I'm going to tentatively add the jump in tag on this issue, because I think it sounds like a great task for a new contributor to take on. On the other hand, I've not performed an in-depth analysis of what this would entail, so there's no guarantee that this is an 'easy' task.

jakejscott commented 10 years ago

I agree this would be a great feature to add, I jumped straight into using Hyprlinkr, and only after playing around with it for a couple hours and I couldn't get it working with Attribute routing I decided to check if it was supported and found this issue.

Perhaps you'd be kind enough to mention that it doesn't support Attribute Routing just yet :)

Cheers Jake

ploeh commented 10 years ago

Indeed; it say so right here :)

stanshillis commented 9 years ago

There is a fairly simple solution to attribute based routing and Hyprlinkr as long as Name property on the Route attribute is set. Then it's just a matter of implementing a dispatcher that looks up that route name from the attribute at run time. Seems to work on sample routes that i tried.

MatthewRichards commented 9 years ago

I came across this limitation and did some digging. As @stanshillis says it's easy enough to write an IRouteDispatcher that grabs the Name of the Route attribute and uses that. However if you have an unnamed Route attribute it seems rather harder.

I wonder whether the WebAPI designers actually want us to generate URIs in this scenario. As far as I can tell you can't create links using UrlHelper.Link without a named route either - most attribute routes are stored in the route table within a single RouteCollectionRoute, and only the named routes are given a LinkGenerationRoute which then allows link generation code (either UrlHelper.Link or Hyprlinkr) to work.

However, giving names to all my routes felt a bit unnecessary so I wanted to find a workaround anyway. I've come up with one that dynamically adds named routes as necessary at runtime... I suspect this is as bad an idea as it sounds, but it works for me in simple cases so far :smile: Here it is in case it's of use to anyone else (it's just an implementation of Hyprlinkr's IRouteDispatcher plug-in interface):

https://gist.github.com/MatthewRichards/3177e0c5a5181de8f1e8

The only other solution I could see is to parse the Route attribute's route template in the Hyprlinkr code. This involves rather less unpleasant messing around with the routing table, but is duplicating the work of parsing the template which is likely to be fragile. Hopefully there's an even better solution I haven't spotted.

I have to say though even with my hacking about of the Route table I prefer the result to the lack of type safety you'd get by not using Hyprlinkr at all!

ploeh commented 9 years ago

@MatthewRichards Thanks for sharing your solution.

As you hint, I'm also concerned that the workaround of assigning named routes dynamically may not be a good idea. I'd really like to know what the proper way to get a URL for an unnamed route is. If anyone figures that out, I'd be interested to hear it.

The first part of your shared code, where the code uses the name from the attribute if it exists, seems to be safe to add to Hyprlinkr, if anyone fancy sending a pull request :smile:

jkodroff commented 9 years ago

Seems like this issue should be closed due to the above PR?

ploeh commented 9 years ago

@jkodroff The original issue is still unresolved, as #36 only enables named attribute routes.

It'd be nice to also get support for unnamed attribute routes, but I haven't looked into how (if at all) that would be possible to do.

dhilgarth commented 7 years ago

I have implemented this in a rather hacky way in my project. So far, it is working for my scenarios. Maybe this can be a starting point for someone who wants to implement it in Hyprlinkr:

public static class UrlHelperEx
{
    public static Uri GetLink<T, TResult>(HttpRequestMessage request, Expression<Func<T, TResult>> method)
    {
        return GetUri(request, method.GetMethodCallExpression()) ?? new RouteLinker(request).GetUri(method);
    }

    public static Uri GetLink<T>(HttpRequestMessage request, Expression<Action<T>> method)
    {
        return GetUri(request, method.GetMethodCallExpression()) ?? new RouteLinker(request).GetUri(method);
    }

    private static Uri GetBaseUri(HttpRequestMessage request)
    {
        var authority = request.RequestUri.GetLeftPart(UriPartial.Authority);
        return new Uri(authority);
    }

    private static Uri GetUri(HttpRequestMessage request, MethodCallExpression method)
    {
        var routeAttribute = method.Method.GetCustomAttribute<RouteAttribute>(false);
        if (routeAttribute == null)
            return null;
        var routePrefixAttribute = method.Object?.Type.GetCustomAttribute<RoutePrefixAttribute>(false);
        var routeTemplate = routeAttribute.Template;
        if (routePrefixAttribute != null)
            routeTemplate = $"{routePrefixAttribute.Prefix}/{routeTemplate}";

        var subRoutes = request.GetConfiguration().Routes.Where(x => x is IEnumerable<IHttpRoute>).Cast<IEnumerable<IHttpRoute>>().SelectMany(x => x);
        var matchingSubRoutes =
            subRoutes.Where(
                x =>
                    x.RouteTemplate == routeTemplate
                    && ((HttpActionDescriptor[])x.DataTokens["actions"])[0].SupportedHttpMethods.Contains(
                        request.Method));
        var routeValues = new ScalarRouteValuesQuery().GetRouteValues(method);
        routeValues.Add("httproute", null);
        var uris =
            matchingSubRoutes.Select(
                                 x =>
                                     x.GetVirtualPath(request, routeValues))
                             .ToArray();

        var relativeUri = uris.SingleOrDefault()?.VirtualPath;
        if (relativeUri == null)
            return null;

        var baseUri = GetBaseUri(request);
        return new Uri(baseUri, relativeUri);
    }
}