Link Search Menu Expand Document

2.2: Hypermedia Resources

Hypermedia Resources

We’re now going to look at the GET /api/vehicles/{registration} endpoint, which returns details of a specific vehicle. Currently, this method returns a JSON representation of the requested vehicle (or 404 Not Found if no such vehicle exists):

GET /api/vehicles/outatime

{
    "vehicleModel": {
        "manufacturer": {
            "code": "dmc",
            "name": "DMC"
        },
        "code": "dmc-delorean",
        "manufacturerCode": "dmc",
        "name": "DELOREAN"
    },
    "registration": "OUTATIME",
    "modelCode": "dmc-delorean",
    "color": "Silver",
    "year": 1985
}

By default, ASP.NET is serializing the entire object graph here, so the vehicleModel and the vehicleModel.manufacturer objects are being encoded and included in our API response. For complex object models, this could result in a lot of unnecessary information being included in every API response, so instead of encoding the data inline, we’re going to use hypermedia to handle these kinds of associations.

As with the previous example, we’re going to use the flexibility of the dynamic type to transform our strongly-typed .NET objects into hypermedia resources. We’re going to introduce another helper method here:

public static class HypermediaExtensions {
    public static dynamic ToDynamic(this object value) {
		IDictionary<string, object> expando = new ExpandoObject();
		var properties = TypeDescriptor.GetProperties(value.GetType());
		foreach (PropertyDescriptor property in properties) {
		    expando.Add(property.Name, property.GetValue(value));
    	}
		return (ExpandoObject)expando;
    }
}

There’s a lot of interesting .NET stuff going on in this method, so we’ll break it down and look at it line by line.

public static dynamic ToDynamic(this object value) {

This defines an extension method on object, which is the fundamental base class of the entire .NET type system. Everything in .NET derives from object, so an extension method on object will be available on any object anywhere in our application.

IDictionary<string, object> expando = new ExpandoObject();

There are three different ways to interact with dynamic types in .NET. One is to use the C# dynamic keyword directly, but when we declare an object as dynamic, .NET actually instantiates an ExpandoObject – and we can manipulate the properties of this object via the IDictionary<string, object> interface.

Next, we’re going to use reflection to obtain a type descriptor of the object we’re working with, so we can map the property names and values from our source object onto our new dynamic object. Notice that we checking each property to see if it’s decorated with the [JsonIgnore] attribute, and skip any properties that are; this ensures that properties marked as [JsonIgnore] don’t get included in our dynamic object:

var properties = TypeDescriptor.GetProperties(value.GetType());
foreach (PropertyDescriptor property in properties) {
	expando.Add(property.Name, property.GetValue(value));
}

Finally, we cast our IDictionary<string,object> back to an ExpandoObject and return it:

return (ExpandoObject)expando;

Once we’ve added our ToDynamic method to our codebase, we can use it in any of our controller actions:

[HttpGet("{id}")]
[Produces("application/hal+json")]
public IActionResult Get(string id) {
	var vehicle = db.FindVehicle(id);
	if (vehicle == default) return NotFound();
	var json = vehicle.ToDynamic();
	return Ok(json);
}

We need to make a few more tweaks, though. Here’s what we’ll get back if we run this code:

{
    "VehicleModel": {
        "manufacturer": {
            "code": "dmc",
            "name": "DMC"
        },
        "code": "dmc-delorean",
        "manufacturerCode": "dmc",
        "name": "DELOREAN"
    },
    "LazyLoader": {},
    "Registration": "OUTATIME",
    "ModelCode": "dmc-delorean",
    "Color": "Silver",
    "Year": 1985
}

First, we want to exclude the VehicleModel property – we’re going to use hypermedia references instead of returning this inline. To do this, we’re going to decorate the VehicleModel property on our Vehicle object with the [JsonIgnore] attribute, and then modify our ToDynamic implementation to exclude any properties which have this attribute:

using Newtonsoft.Json; // see note below about namespaces!

namespace Autobarn.Data.Entities {
	public partial class Vehicle {
		...
		[JsonIgnore]
		public virtual Model VehicleModel { get; set; }
	}
}

ℹ Watch out for namespaces here. There’s a System.Text.Json.Serialization.JsonIgnoreAttribute and a Newtonsoft.Json.JsonIgnoreAttribute - you’ll need to use the attribute from whichever JSON serializer you’re using in your project. Autobarn uses the Newtonsoft.Json serializer, so make sure add using Newtonsoft.Json directive - and be careful using tools like ReSharper that will automatically reference missing namespaces for you, in case they pick the wrong one.

Now we’ll modify our ToDynamic method to filter out these attributes:

public static class HypermediaExtensions {
	public static dynamic ToDynamic(this object value) {
		IDictionary<string, object> expando = new ExpandoObject();
		var properties = TypeDescriptor.GetProperties(value.GetType());
		foreach (PropertyDescriptor property in properties) {
			if (Ignore(property)) continue;
			expando.Add(property.Name, property.GetValue(value));
		}
		return (ExpandoObject)expando;
	}

    private static bool Ignore(PropertyDescriptor property) {
		return property.Attributes.OfType<Newtonsoft.Json.JsonIgnoreAttribute>().Any();
	}
}

We’re going to make two more improvements here. First, if you’re using the Entity Framework data provider, you’ll notice a property called LazyLoader appearing on JSON objects. This is part of Entity Framework’s internal plumbing (it’s used to enable lazy loading of our database entities), and we don’t really want it appearing on our API responses, so we’ll add a line to the HyperMediaExtensions.Ignore method to filter this property out:

private static bool Ignore(PropertyDescriptor property) {
	if (property.Name == "LazyLoader") return(true);
	return property.Attributes.OfType<Newtonsoft.Json.JsonIgnoreAttribute>().Any();
}

Finally, the JSON serializer creates camel-case property values by default, so when we serialize SomeProperty it’ll come out as someProperty, but this doesn’t apply to properties which are dictionary keys – and because dynamic objects are serialized via their IDictionary interface, we need to explicitly enable camel-casing for these property names.

Find the line in Startup.cs where we add NewtonsoftJson to the project:

services.AddControllersWithViews().AddNewtonsoftJson();

The AddNewtonsoftJson() method takes an optional Action<MvcNewtonsoftJsonOptions> parameter, which we can use to override the default serialization support by setting processDictionaryKeys to true:

services
	.AddControllersWithViews()
	.AddNewtonsoftJson(options => options.UseCamelCasing(processDictionaryKeys: true));

That’s it - when we hit our GET /api/vehicles/outatime endpoint now, we’ll get back a nice, clean JSON representation:

{
    "registration": "OUTATIME",
    "modelCode": "dmc-delorean",
    "color": "Silver",
    "year": 1985
}

Now, we’re going to add some hypermedia properties to include the resource’s own URL (via the _links.self.href property) and a link to the vehicle model:

[HttpGet("{id}")]
public IActionResult Get(string id) {
	var vehicle = db.FindVehicle(id);
	if (vehicle == default) return NotFound();
	var json = vehicle.ToDynamic();
	json._links = new {
		self = new { href = $"/api/vehicles/{id}" },
		vehicleModel = new {href = $"/api/manufacturers/{vehicle.Model.Manufacturer.Code}/models/{vehicle.Model.Code}"}
	};
	return Ok(json);
}

When we hit that endpoint now, we get this – a JSON resource representing a vehicle, including hypermedia links to related resources.

{
    "registration": "OUTATIME",
    "modelCode": "dmc-delorean",
    "color": "Silver",
    "year": 1985,
    "_links": {
        "self": {
            "href": "/api/vehicles/outatime"
        },
        "model": {
            "href": "/api/manufacturers/dmc/models/delorean"
        }
    }
}	

Exercise: Hypermedia Resources

In this exercise, we’ll create a set of resource endpoints that return information about vehicle manufacturers and models

  • Create an endpoint at /api/manufacturers that returns a list of vehicle manufacturers.

  • Create an endpoint at /api/manufacturers/{code} that returns details of a specific manufacturer

Each manufacturer should include hypermedia links for self and models.

Exercise Part 2

Create an endpoint at /api/models/{code}/vehicles which lists all the vehicles listed for sale matching a specified manufacturer and model.

There is already an endpoint at /api/models/{code} which returns information about a particular vehicle model. Extend this resource to include hypermedia links for self, manufacturer, and vehicles. The manufacturer and vehicles links should point to the endpoints you created in this exercise.