RSS

Populating Url Link properties in RESTful asp.net WebAPI projects

07 Sep

I recently started developing a WebApi solution that needed to return Json in the response and discovered what seems to be a common problem with populating urls in the response.

Basically it’s good practice to send urls to help clients navigate around your API. In my example, we have two entities, SoftwarePackage and Version:

SoftwarePackage:
Id : string
Name : string
CreatedOn : DateTime
Versions : IEnumerable

Version:
Name : string
Package : SoftwarePackage
Number : string
Publisher : string
CreatedOn : DateTime
Hash : string

So we populate the SoftwarePackage respository, and a GET request to /api/packages should return:

[
    {
         "id": "package-1",
          "name": "Package 1",
          "createdOn": "2017-09-08T11:32:10.1990243+01:00",
          "versions": [{
              "name": "0-0-4-5",
               "number": "0.0.4.5",
               "publisher": "John Coleman",
               "createdOn": "2017-07-09T09:42:50.1990243+01:00",
               "hash": "12345",
               "link": {
                   "title": "0.0.4.5",
                   "hRef": "http://packages.local/api/packages/package-1/0-0-4-5"
               }
           },
          {
               "name": "0-0-4-3",
               "number": "0.0.4.3",
               "publisher": "Pete Dierdon",
               "createdOn": "2017-07-26T10:46:50.1990243+01:00",
               "hash": "1223",
               "link": {
                   "title": "0.0.4.3",
                   "hRef": "http://packages.local/api/packages/package-1/0-0-4-3"
               }
           },
           {
               "name": "0-0-4-2",
               "number": "0.0.4.2",
               "publisher": "Aliyah Patel",
               "createdOn": "2017-08-19T14:17:30.1990243+01:00",
               "hash": "434243",
               "link": {
                   "title": "0.0.4.2",
                   "hRef": "http://packages.local/api/packages/package-1/0-0-4-2"
               }
           }
         ],
         "link": {
         "title": "Package 1",
         "hRef": "http://packages.local/api/packages/package-1"
     }
 },

You can see that each entity has a Link property containing a title and an hRef property to help clients consume the api. We could quite easily populate these in code but I though it would be nice to have some declarative mechanism that uses attributes and the MVC route table to generate them prior to returning the response.

I therefore created the following attributes to help us generate the urls:

RouteNameAttribute – A class attribute that tells us which route to use – the route is specified by its name
RouteParentAttribute – A property attribute that indicates that the property value should be used to generate the url for this entity
RouteParameterAttribute – This will tell our method which property to use as a route value, along with the route parameter name

I realise this may be a bit confusing so we’ll use the following as an example:

The package route is called “Package” and is as follows:
“api/packages/{id}/{v}” whereby {id} is the package id and {v} is the optional version to use.

We therefore decorate our SoftwarePackage class with the following attributes:

[RouteName("Package")]
public class SoftwarePackage : IEquatable, ILinkedResource
{
[RouteParameter("id")]
public string Id { get; set; }</code>

[LinkTitle]
public string Name { get; set; }
public DateTime CreatedOn { get; set; }
public IList Versions { get; set; }...

The Version entity uses the same route but with the {v} parameter populated, so we can specify the attributes on our Version class like this:

[RouteName("Package")]
public class PackageVersion : IEquatable, ILinkedResource
{
[RouteParameter("v")]
public string Name { get { return Number.Replace(".", "-"); } }</code>

[RouteParent]
[JsonIgnore]
public SoftwarePackage Package { get; set; }

[LinkTitle]
public string Number { get; set; }
public string Publisher { get; set; }...

So now we need a method that can use reflection to analyse our new attributes and construct urls by first getting the route name, and then constructing a dictionary of route values using those attributes, then passing this data to the ApiControllers’s Url.Link method and using the result to populate the relevant property. I’ve implemented this method as an ApiController extension.

The controller extension will accept an instance of ILinkedResource that specifies that an entity has a Link property of type ResourceLink. It is this property that our method will fill with a title and url.

    public static class ApiControllerExtensions
    {
        public static void PopulateLinks(this ApiController controller, ILinkedResource resource, int depth = 0)
        {
            var t = resource.GetType();

            // Get dictionary containing route values
            var routeVals = GetRouteValues(resource);

            // Get route
            var routeNameAtt = t.GetCustomAttributes(typeof(RouteNameAttribute), false).Cast<RouteNameAttribute>();
            var titleAtts = t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(LinkTitleAttribute)));

            if (depth > 0)
            {
                var childProps = t.GetProperties().Where(prop => typeof(IEnumerable<ILinkedResource>).IsAssignableFrom(prop.PropertyType));

                foreach (var childProp in childProps)
                {
                    var childResources = (IEnumerable<ILinkedResource>)childProp.GetValue(resource) ?? new List<ILinkedResource>();

                    foreach (var childResource in childResources)
                    {
                        controller.PopulateLinks(childResource, depth - 1);
                    }
                }
            }

            if (routeNameAtt.Count() != 1)
            {
                throw new InvalidOperationException(string.Format("Type {0} doesn't specify a route with the RouteName attribute, or specifies more than one route name", t.FullName));
            }

            if (titleAtts.Count() == 0)
            {
                throw new InvalidOperationException(string.Format("Type {0} doesn't have a property with LinkTitle attribute assigned", t.FullName));
            }

            if (titleAtts.Count() > 1)
            {
                throw new InvalidOperationException(string.Format("Type {0} has more than one property with LinkTitle attribute assigned", t.FullName));
            }

            // populate
            PopulateLink(controller, resource, routeNameAtt.First().RouteName, titleAtts.First().GetValue(resource).ToString(), routeVals);
        }

        private static IDictionary<string, object> GetRouteValues(ILinkedResource resource)
        {
            var results = new Dictionary<string, object>();

            var t = resource.GetType();

            // Get properties decorated with RouteParameter attribute and populate dictionary
            var props = t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(RouteParameterAttribute)));
            var parentProp = t.GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(RouteParentAttribute))).SingleOrDefault();

            foreach (var prop in props)
            {
                var val = prop.GetValue(resource);
                var pars = prop.GetCustomAttributes(typeof(RouteParameterAttribute), false).Cast<RouteParameterAttribute>();

                foreach (var par in pars)
                {
                    results.Add(par.ParameterName, val);
                }
            }

            if (parentProp != null)
            {
                var r = (ILinkedResource) parentProp.GetValue(resource);

                if (r != null)
                {
                    foreach (var kv in GetRouteValues(r))
                    {
                        results.Add(kv.Key, kv.Value);
                    }
                }
                else
                {
                    throw new ArgumentException("Property {0} of {1} must be populated in order to generate route parameteres for links");
                }
            }

            return results;
        }

        private static void PopulateLink(ApiController controller, ILinkedResource resource, string routeName, string title, IDictionary<string, object> routeVals)
        {
            try
            {
                resource.Link = new ResourceLink()
                {
                    HRef = controller.Url.Link(routeName, routeVals),
                    Title = title
                };
            }
            catch (ArgumentException ex)
            {
                throw ex;
            }
        }

    }

…and to call it from our controller:

// GET: api/Packages
public IHttpActionResult Get()
{
_repository.ToList().ForEach(i =&gt; this.PopulateLinks(i, 1) );
return Json(_repository, JsonConvert.DefaultSettings());
}

I have included the source code here – please feel free to adapt it and let me know if you see any potential improvements that could be made. This is a different approach to the one taken by Ben foster below which aims to solve the same problem. This is not a finished product and the example site give s rough idea of how this could be used. If you run the solution and go to /api/packages you will see the ‘root’ page.

References:

http://benfoster.io/blog/generating-hypermedia-links-in-aspnet-web-api

 

 
Leave a comment

Posted by on September 7, 2017 in asp.net, asp.net mvc, dotnet, Json, RESTful, WebAPI

 

Leave a comment