-
Notifications
You must be signed in to change notification settings - Fork 5
Home
The goal of this project is to provide a way to model resources. The essential considerations are
- what data should a resource provide in its representations
- how are resources related to each other
This should be easy to model as simple classes without having to consider what the format of representations actually look like.
The following walks through the evolution of a very simple resource model. This will demonstrate how this project improves already cool out of the box stuff that Web Api comes with. This is just an illustration, with lots of details like database access, exception handling left out.
Our goal: when the system receives a GET request for a Person resource, we want to respond with a representation formatted according to the HAL specification. For example, if John Doe has a Mustang, we may see get a representation like this:
{
"name": "John Doe",
"address": "123 Main St.",
"_links": {
"car": {
"href": "http://example.org/api/car/1"
},
"self": {
"href": "http://example.org/api/person/1"
}
}
}
Following the "car" link relation will give us details about his car, which happens to be a Mustang.
That's where we're going. Let's get there step by step.
Let's start really simply. I've got one resource Person
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
}
and one controller PersonController which has one method Get
public class PersonController : ApiController
{
private Dictionary<int, Person> _people = new Dictionary<int, Person>
{
{ 1, new Person { Name = "John Doe", Address = "123 Main St." } },
{ 2, new Person { Name = "Sally Smith", Address = "321 Main St.", Phone = "555-123-4567"} }
};
[Route("api/person/{personId}", Name = "GetPersonById")]
public IHttpActionResult Get(int personId)
{
if (! _people.ContainsKey(personId))
{
return NotFound();
}
var person = _people[personId];
return Ok(person);
}
}
I run this, do a GET request on /api/person/1, which gives the following:
{"Name":"John Doe","Address":"123 Main St.","Phone":null}
Nice, but there are a couple observations:
- JSON is traditionally camelCase. I wish it was "name":"John Doe"
- This person doesn't have a phone. I would like to omit null fields.
- How do I provide link relations?
Moving on... A person may own a car (just one for now to keep it simple). So I define a Car as:
public class Car
{
public string Make { get; set; }
public string Model { get; set; }
public string SerialNumber { get; set; }
}
and (again to keep it simple) here is the CarController
public class CarController : ApiController
{
private Dictionary<int, Car> _cars = new Dictionary<int, Car>
{
{ 1, new Car { Make = "Ford", Model = "Mustang" } },
{ 2, new Car { Make = "Chevrolet", Model = "Corvette"} }
};
[Route("api/car/{carId}", Name = "GetCarById")]
public IHttpActionResult Get(int carId)
{
if (!_cars.ContainsKey(carId))
{
return NotFound();
}
return Ok(_cars[carId]);
}
}
I can now add car ownership to the Person model:
public class Person
{
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
public int MyCarId { get; set; }
}
And for the sake of this demo, adjust the PersonController dictionary:
private Dictionary<int, Person> _people = new Dictionary<int, Person>
{
{ 1, new Person { Name = "John Doe", Address = "123 Main St.", MyCarId = 1 } },
{ 2, new Person { Name = "Sally Smith", Address = "321 Main St.", Phone="555-123-4567", MyCarId = 2} }
};
Predictably, doing a GET on, say /api/person/1 now looks like this:
{"Name":"John Doe","Address":"123 Main St.","Phone":null,"MyCarId":1}
As of this writing, this project contains only one media type formatter: HAL Let's see how this project takes the Person / Car model, makes it more RESTful, and streams out respresentations in the HAL format.
After adding PointW.WebApi.ResourceModel to your web api project, add the HAL formatter to your config section.
config.MapHttpAttributeRoutes();
config.Formatters.Clear();
config.Formatters.Add(new HalJsonMediaTypeFormatter { Indent = true });
config.EnsureInitialized();
(Note: in practice I set Indent = true only #if DEBUG)
Now have Person and Car inherit from Resource (I know, I know, inheritance. This will likely be changed to composition in a future release).
public class Person : Resource
{
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
public int MyCarId { get; set; }
}
... public class Car : Resource { public string Make { get; set; } public string Model { get; set; } public string SerialNumber { get; set; } }
When we deliver a Person resource, we want the representation to contain, not the ID of the Car the person owns, but a link to GET that car ourselves. This is easy. In the PersonController class, change the Get method as follows:
[Route("api/person/{personId}", Name = "GetPersonById")]
public IHttpActionResult Get(int personId)
{
if (! _people.ContainsKey(personId))
{
return NotFound();
}
var person = _people[personId];
person.Relations.Add("car",
new Link
{
Href = Url.Link("GetCarById", new { carId = person.MyCarId })
});
return Ok(person);
}
The key to the Resource model is the Relations property. Each Resource can have a list of other resources related to it. In this case a Person is related to a Car, and we have just added a link from Person to the resource of Car she owns.
If we do a GET on /api/person/1 now, we get this:
{
"name": "John Doe",
"address": "123 Main St.",
"myCarId": 1,
"_links": {
"car": {
"href": "http://example.org/api/car/1"
}
}
}
This is much nicer:
- JSON is in camelCase
- representation is in HAL format
- null values do not appear in the representation (you can override this with the [AlwaysShow] attribute)
- The client can RESTfully navigate from this resource to the Car resource by dereferencing the car link relation
We can tidy things up a bit: We don't really need or want "myCarId" to appear in the representation. We can model this by applying an attribute to that property:
public class Person : Resource
{
public string Name { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
[NeverShow]
public int MyCarId { get; set; }
}
Then we should add a "self" link relation to the Get() method of the PersonController:
person.Relations.Add("self",
new Link
{
Href = Url.Link("GetPersonById", new { personId = personId })
});
And now we do a GET on /api/person/1 and see that we have arrived at our goal:
{
"name": "John Doe",
"address": "123 Main St.",
"_links": {
"car": {
"href": "http://example.org/api/car/1"
},
"self": {
"href": "http://example.org/api/person/1"
}
}
}